fix(04-06): five post-deploy fixes

- Middleware (proxy.ts): switch internal API calls to localhost instead of
  request.url — avoids Docker hairpin NAT issues where the container can't
  reach its own external hostname via Traefik
- ProjectRow timer: pass projectId={project.id} to TimerCell so it calls
  startTimer(projectId) directly instead of startTimerForClient(project.id)
- Admin client list: add slug field to ClientWithPayments + show slug link
  when available (falls back to token) — so admins see the human-readable URL
- Analytics contracted: sum projects.accepted_total (authoritative) instead
  of clients.accepted_total (always 0 in multi-project architecture)
- createClient: auto-generate slug from client name at creation time
  (e.g. "Mario Rossi" → "mario-rossi"), with -2/-3 suffix on conflict

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 13:42:38 +02:00
parent e6a9774cfe
commit eab88c9f63
6 changed files with 42 additions and 14 deletions
+25
View File
@@ -3,9 +3,30 @@
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import { clients, projects, payments } from "@/db/schema";
function toSlug(name: string): string {
return name
.toLowerCase()
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 50);
}
async function uniqueSlug(base: string): Promise<string | null> {
if (base.length < 3) return null;
const candidates = [base, ...Array.from({ length: 8 }, (_, i) => `${base.slice(0, 47)}-${i + 2}`)];
for (const candidate of candidates) {
const rows = await db.select({ id: clients.id }).from(clients).where(eq(clients.slug, candidate)).limit(1);
if (rows.length === 0) return candidate;
}
return null;
}
const createClientSchema = z.object({
name: z.string().min(1, "Nome richiesto"),
brand_name: z.string().min(1, "Nome brand richiesto"),
@@ -26,6 +47,9 @@ export async function createClient(formData: FormData) {
);
}
// Auto-generate slug from name (e.g. "Mario Rossi" → "mario-rossi")
const slug = await uniqueSlug(toSlug(parsed.data.name));
// Insert client — token and id are auto-generated by $defaultFn(() => nanoid())
const [newClient] = await db
.insert(clients)
@@ -33,6 +57,7 @@ export async function createClient(formData: FormData) {
name: parsed.data.name,
brand_name: parsed.data.brand_name,
brief: parsed.data.brief,
slug,
})
.returning({ id: clients.id, token: clients.token, brand_name: clients.brand_name });