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,4 +1,5 @@
import userAuth from "@/src/functions/backend/auth/user-auth";
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio";
import grabUserPasswordHash from "@/src/functions/backend/auth/grab-user-password-hash";
@@ -26,7 +27,7 @@ export default async function grabSettingsServerProps({ req }: Params) {
if (!user_res.singleRes) {
return {
redirect: {
destination: "/admin",
destination: AdminHomePath,
},
};
}
@@ -1,85 +0,0 @@
import type { BUN_SQLITE_COBBLE_USERS } from "@/db/types/db";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import clientDeleteMediaHandler from "@/src/functions/frontend/client-delete-media-handler";
import clientUploadMediaHandler from "@/src/functions/frontend/client-upload-media-handler";
import type useFormInit from "@/src/hooks/use-form-init";
import type { UserFormObject } from "@/src/types";
import _ from "lodash";
export default async function submitAccountForm({
setLoading,
setStatus,
form,
existing_full,
image,
}: ReturnType<typeof useFormInit<UserFormObject>>) {
try {
const user_id = existing_full?.id;
if (!user_id) {
return;
}
if (!window.confirm("Update your account details?")) {
return;
}
const new_user_data: BUN_SQLITE_COBBLE_USERS = _.pick<
UserFormObject,
keyof UserFormObject
>(form, ["first_name", "last_name", "email", "username", "bio"]);
setLoading(true);
const update_user_res = await adminCrudHandler({
action: "update",
table: "users",
update_data: new_user_data,
id: user_id,
});
if (!update_user_res.success) {
throw new Error(update_user_res.msg || "Account Update Failed");
}
if (image?.imageBase64Full) {
const delete_media_res = await clientDeleteMediaHandler({
user_id: user_id,
media_paradigm: "user-profile-image",
});
if (!delete_media_res.success) {
throw new Error(
delete_media_res.msg ||
delete_media_res.error ||
`Couldn't delete old media`,
);
}
const upload_image_res = await clientUploadMediaHandler({
user_id: user_id,
media_base_64_data_url: image.imageBase64Full,
media_paradigms: ["user-profile-image"],
media_type: "image",
is_media_primary: true,
update_on_duplicate: true,
reauth: true,
});
if (!upload_image_res.success) {
throw new Error(
upload_image_res.msg || "Couldn't upload Image",
);
}
}
window.location.reload();
} catch (error: any) {
setStatus({
error: true,
msg: error.message || "Account Form Error",
});
setLoading(false);
}
}