Build Cobble project structure from the handoff
- Remove the preset document suite (letters, emails, invoices, quotes, PDF generation, line items) with its routes, API handlers and tables - Rebrand to Cobble: site data, Lato, handoff design tokens (light and dark) in main.css, brand assets in public/brand, ember stone logo and regenerated favicons - Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots) - Add the chat shell at /admin/bots: rail with pinned coordinator and user popover, bottom-anchored conversation with composer, working-on panel, and a send-message API scoped to the session user - Add BotStone and Wordmark components, crew status and relative time utils with tests, and update AGENTS.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams, ClientAdminCrudParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminCrudHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
>(params: ClientAdminCrudParams<T>) {
|
||||
const {
|
||||
action,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BUN_SQLITE_BSQLITE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { AdminFetchParams, ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function adminFetchHandler<
|
||||
T extends {} = BUN_SQLITE_BSQLITE_ALL_TYPEDEFS,
|
||||
T extends {} = BUN_SQLITE_COBBLE_ALL_TYPEDEFS,
|
||||
>({ url: passed_url, method = "GET", query, body }: AdminFetchParams<T>) {
|
||||
let url = passed_url;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BUN_SQLITE_BSQLITE_MEDIA } from "@/db/types/db";
|
||||
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
@@ -6,7 +6,7 @@ import type { APIResponseObject } from "@moduletrace/bun-sqlite";
|
||||
export default async function clientUploadMediaHandler(params: ApiReqParams) {
|
||||
const res = await fetchApi<
|
||||
ApiReqParams,
|
||||
APIResponseObject<BUN_SQLITE_BSQLITE_MEDIA>
|
||||
APIResponseObject<BUN_SQLITE_COBBLE_MEDIA>
|
||||
>(`/api/admin/upload-media`, {
|
||||
method: "POST",
|
||||
body: params,
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* # Client-side actions for document/invoice generation & emailing.
|
||||
* PDF endpoints return binary PDFs (not JSON), so plain fetch is used
|
||||
* here instead of `fetchApi`.
|
||||
*/
|
||||
|
||||
async function postAdminAction({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Request failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadGeneratedPdf({
|
||||
url,
|
||||
body,
|
||||
fallback_filename,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
fallback_filename?: string;
|
||||
}): Promise<{ success: boolean; msg?: string }> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
|
||||
const content_type = res.headers.get("Content-Type") || "";
|
||||
|
||||
if (content_type.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
return {
|
||||
success: Boolean(json.success),
|
||||
msg: json.msg,
|
||||
};
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
|
||||
if (blob.size == 0) {
|
||||
throw new Error(`Received an empty PDF`);
|
||||
}
|
||||
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
const filename_match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
const filename =
|
||||
filename_match?.[1] || fallback_filename || "document.pdf";
|
||||
|
||||
const object_url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
anchor.href = object_url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(object_url);
|
||||
}, 1000);
|
||||
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message || "Couldn't download document",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailRecord({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string;
|
||||
body: any;
|
||||
}) {
|
||||
return await postAdminAction({ url, body });
|
||||
}
|
||||
Reference in New Issue
Block a user