Make Cobble a local install: setup flow, bots-first admin, settings

Local, password-only auth:
- No public pages; everything outside /admin and /auth redirects in
- First run (no users) sends every page to /auth/signup, which creates
  the super admin (user #1) once and then locks
- Login is email/username + password; removed the emailed-code flow,
  /auth/sso and email password reset
- Google login kept for existing accounts only, never creates them
- seed-super-admin now requires SEED_SUPER_ADMIN_PASSWORD

Bots-first admin:
- /admin and closed sections (people, logs, backups) redirect to
  /admin/bots; only bots and settings are live (AdminSections)
- Removed the old admin layout; every /admin page uses the bots rail
- Coordinator bot created at signup/seed and by /admin/bots
  (ensureCoordinatorBot), which redirects to its chat
- bots.system_prompt (markdown) with a default coordinator prompt
- seed-bots reuses the existing coordinator

Settings:
- Sub pages general (default), profile, change-password, listed in the
  rail with a back link; unknown settings paths redirect to general
- Redesigned settings UI: pane header, grouped rows, sticky save bar
- General: System/Light/Dark theme, saved per device

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 17:49:25 +01:00
co-authored by Claude Opus 5.5
parent 3286b80029
commit 694c83346f
159 changed files with 1891 additions and 2964 deletions
@@ -1,17 +0,0 @@
import { AppData } from "@/src/data/app-data";
import type { SSOPendingLogin } from "@/src/types";
import EJSON from "@/src/utils/ejson";
export default function readSSOPendingLogin(): SSOPendingLogin | undefined {
try {
const stored = sessionStorage.getItem(
AppData["SSOPendingLoginStorageKey"],
);
return stored
? (EJSON.parse(stored) as SSOPendingLogin | undefined)
: undefined;
} catch {
return undefined;
}
}
@@ -1,34 +0,0 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type useFormInit from "@/src/hooks/use-form-init";
import type { ApiReqParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
type Params = Pick<ReturnType<typeof useFormInit>, "setLoading" | "setStatus">;
export default async function resendLoginCode({
setLoading,
setStatus,
}: Params) {
setLoading(true);
const res = await fetchApi<ApiReqParams, APIResponseObject>(
`/api/auth/resend-login-code`,
{
method: "POST",
body: {},
},
);
setStatus(
res.success
? {
success: true,
msg: "A new code is on its way. Previous codes no longer work.",
}
: {
error: true,
msg: res.msg || "Couldn't resend the code. Please try again.",
},
);
setLoading(false);
}
@@ -1,16 +0,0 @@
import { AppData } from "@/src/data/app-data";
import type { SSOPendingLogin } from "@/src/types";
import EJSON from "@/src/utils/ejson";
/**
* Remembers which login is waiting for a code, so the SSO page can word
* itself to match. Best effort only.
*/
export default function saveSSOPendingLogin(pending: SSOPendingLogin) {
try {
sessionStorage.setItem(
AppData["SSOPendingLoginStorageKey"],
EJSON.stringify(pending) || "",
);
} catch {}
}