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:
2026-10-18 17:10:36 +01:00
co-authored by Claude Opus 5.5
parent 81e4a8aecc
commit 3286b80029
340 changed files with 3161 additions and 12060 deletions
+2 -2
View File
@@ -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,
-121
View File
@@ -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 });
}