claude-fix: settings.ts rewrite (appSettings + fields match), Nav theme path, mount settings route

This commit is contained in:
Dennis (via Claude+Gemma) 2026-05-23 05:27:54 +02:00
parent a2ce92f0db
commit 573e2c9680
7 changed files with 373 additions and 72 deletions

View File

@ -8,6 +8,7 @@
"started_at": "2026-05-23T05:20:11.407108",
"attempted_features": [
"settings-page",
"api-client-phase5"
"api-client-phase5",
"router-phase5"
]
}

View File

@ -621,3 +621,29 @@ src/routes/settings.ts(3,10): error TS2305: Module '"../db/schema"' has no expor
undefined
/home/dark/Developer/EmberClone/apps/api:
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
- `05:26:14` **INFO** Committed feature router-phase5
- `05:26:14` **INFO** Pushed: rc=0
## Phase-5 Run beendet (2026-05-23 05:26:14)
- `05:26:14` **INFO** OK: 3, Attempted: 3, Total: 6
- `05:26:14` **INFO** Running db:generate + db:migrate for schema changes…
- `05:26:15` **INFO** db:generate rc=0: /EmberClone/apps/api/drizzle.config.ts'
5 tables
app_settings 5 columns 0 indexes 0 fks
customers 4 columns 0 indexes 0 fks
projects 5 columns 0 indexes 1 fks
time_entries 7 columns 0 indexes 2 fks
users 6 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/0001_minor_fabian_cortez.sql 🚀
- `05:26:16` **INFO** db:migrate rc=0:
> @emberclone/api@0.0.1 db:migrate /home/dark/Developer/EmberClone/apps/api
> tsx src/db/migrate.ts
Running migrations...
Migrations completed successfully
Checking for admin user...
Admin user already exists

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS "app_settings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"workspace_name" text DEFAULT 'EmberClone' NOT NULL,
"default_billable" boolean DEFAULT true NOT NULL,
"week_start" integer DEFAULT 1 NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);

View File

@ -0,0 +1,310 @@
{
"id": "fda40693-a479-48ca-b84b-678a33a4d995",
"prevId": "cf31898f-9a96-4dfd-ac9e-23326daf55fb",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.app_settings": {
"name": "app_settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workspace_name": {
"name": "workspace_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'EmberClone'"
},
"default_billable": {
"name": "default_billable",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"week_start": {
"name": "week_start",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.customers": {
"name": "customers",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"customer_id": {
"name": "customer_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"projects_customer_id_customers_id_fk": {
"name": "projects_customer_id_customers_id_fk",
"tableFrom": "projects",
"tableTo": "customers",
"columnsFrom": [
"customer_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.time_entries": {
"name": "time_entries",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"project_id": {
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"end_time": {
"name": "end_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"time_entries_user_id_users_id_fk": {
"name": "time_entries_user_id_users_id_fk",
"tableFrom": "time_entries",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"time_entries_project_id_projects_id_fk": {
"name": "time_entries_project_id_projects_id_fk",
"tableFrom": "time_entries",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -8,6 +8,13 @@
"when": 1779503479059,
"tag": "0000_empty_talon",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1779506775428,
"tag": "0001_minor_fabian_cortez",
"breakpoints": true
}
]
}

View File

@ -1,95 +1,45 @@
import { FastifyInstance } from "fastify"
import { db } from "../db"
import { settings } from "../db/schema"
import { eq } from "drizzle-orm"
import { z } from "zod"
import { db } from "../db"
import { appSettings } from "../db/schema"
const SettingsUpdateSchema = z.object({
siteName: z.string().min(1).optional(),
maintenanceMode: z.boolean().optional(),
contactEmail: z.string().email().optional(),
allowRegistration: z.boolean().optional()
workspaceName: z.string().min(1).optional(),
defaultBillable: z.boolean().optional(),
weekStart: z.number().int().min(0).max(6).optional(),
})
export default async function settingsRoutes(fastify: FastifyInstance) {
fastify.addHook("preHandler", async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
} catch {
return reply.code(401).send({ message: "Unauthorized" })
}
})
const isAdmin = (request: any) => {
return (request.user as { sub: string, role: string })?.role === "admin"
async function getOrCreate() {
const rows = await db.select().from(appSettings).limit(1)
if (rows.length > 0) return rows[0]
const [created] = await db.insert(appSettings).values({}).returning()
return created
}
fastify.get("/", async (request, reply) => {
const [currentSettings] = await db
.select()
.from(settings)
.limit(1)
if (!currentSettings) {
// Lazy-init default settings if none exist
const [defaults] = await db
.insert(settings)
.values({
siteName: "EmberClone",
maintenanceMode: false,
allowRegistration: true,
contactEmail: "admin@example.com"
})
.returning()
return defaults
}
return currentSettings
})
fastify.get("/", async () => getOrCreate())
fastify.patch("/", async (request, reply) => {
if (!isAdmin(request)) {
return reply.code(403).send({ message: "Forbidden: Admin role required" })
const payload = request.user as { sub: string; role: string }
if (payload.role !== "admin") {
return reply.code(403).send({ message: "Admin only" })
}
const body = SettingsUpdateSchema.parse(request.body)
// We assume there is only one settings row (singleton pattern)
const current = await getOrCreate()
const [updated] = await db
.update(settings)
.set(body)
.where(eq(settings.id, "global")) // Assuming a fixed ID for global settings
.update(appSettings)
.set({ ...body, updatedAt: new Date() })
.where(eq(appSettings.id, current.id))
.returning()
if (!updated) {
// If no row with ID 'global' exists, we create it or update the first available
const [existing] = await db.select().from(settings).limit(1)
if (!existing) {
const [created] = await db
.insert(settings)
.values({
id: "global",
...body,
siteName: body.siteName ?? "EmberClone",
maintenanceMode: body.maintenanceMode ?? false,
allowRegistration: body.allowRegistration ?? true,
contactEmail: body.contactEmail ?? "admin@example.com"
})
.returning()
return created
}
const [finalUpdate] = await db
.update(settings)
.set(body)
.where(eq(settings.id, existing.id))
.returning()
return finalUpdate
}
return updated
})
}
}

View File

@ -14,7 +14,7 @@ import {
} from "lucide-react"
import { useQuery } from "@tanstack/react-query"
import { api } from "../lib/api"
import { useTheme } from "../hooks/useTheme"
import { useTheme } from "../lib/theme"
export default function Nav() {
const location = useLocation()