Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions apps/server/src/auth/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,29 @@ export function validateAuthRuntimeConfig(): void {
export async function ensureAdminUserExists(): Promise<void> {
const email = config.admin.email;

await prisma.user.upsert({
const existing = await prisma.user.findUnique({
where: { email },
update: {
name: "Admin",
},
create: {
email,
name: "Admin",
},
select: { id: true, name: true },
});

logger.info("Admin user ensured for auth", { email });
if (!existing) {
await prisma.user.create({
data: {
email,
name: "Admin",
},
});

logger.info("Admin user created for auth", { email });
return;
}

if (existing.name !== "Admin") {
await prisma.user.update({
where: { id: existing.id },
data: { name: "Admin" },
});

logger.info("Admin user normalized for auth", { email });
}
}
23 changes: 20 additions & 3 deletions apps/server/src/controllers/healthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ import { createErrorResponse, createSuccessResponse } from "#utils/errors";

export class HealthController {
/**
* Comprehensive health check endpoint.
* Verifies connectivity to the database and Redis.
* Lightweight liveness endpoint.
* Does not touch external dependencies, so uptime checks do not wake database compute.
*/

static async check(_req: Request, res: Response) {
res.json(
createSuccessResponse(
{
status: "ok",
timestamp: new Date().toISOString(),
},
"Server is healthy",
),
);
}

/**
* Comprehensive readiness endpoint.
* Verifies connectivity to the database and Redis for manual/deploy-time checks.
*
* @param req Express request
* @param res Express response
* @param next Express next function
*/

static async check(req: Request, res: Response) {
static async ready(_req: Request, res: Response) {
try {
await prisma.$queryRaw`SELECT 1`;

Expand Down
1 change: 1 addition & 0 deletions apps/server/src/routes/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import { HealthController } from "#controllers/healthController";
const router = Router();

router.get("/", HealthController.check);
router.get("/ready", HealthController.ready);

export default router;
18 changes: 16 additions & 2 deletions apps/server/src/utils/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import pg from "pg";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import pg from "pg";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
function parsePositiveInt(value: string | undefined, fallback: number) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

const isProduction = process.env.NODE_ENV === "production";

const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: parsePositiveInt(process.env.DB_POOL_MAX, isProduction ? 1 : 5),
idleTimeoutMillis: parsePositiveInt(process.env.DB_POOL_IDLE_TIMEOUT_MS, 10_000),
connectionTimeoutMillis: parsePositiveInt(process.env.DB_POOL_CONNECTION_TIMEOUT_MS, 10_000),
allowExitOnIdle: true,
});

const adapter = new PrismaPg(pool);

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
Expand Down
98 changes: 98 additions & 0 deletions apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import type { LucideIcon } from "lucide-react";

import Link from "next/link";
import { useSyncExternalStore } from "react";
import { BookOpen, FolderOpen, ArrowRight, BriefcaseBusiness } from "lucide-react";

import { Card } from "@veriworkly/ui";

import {
getResumeWorkspaceSnapshot,
subscribeToResumeWorkspace,
RESUME_WORKSPACE_SERVER_SNAPSHOT,
} from "@/features/documents/services/resume-workspace";

import RecentCard from "./RecentCard";
import OverviewHomeHeader from "./OverviewHomeHeader";
import OverviewReferenceCard from "./OverviewReferenceCard";

function MiniLink({ href, icon: Icon, label }: { href: string; icon: LucideIcon; label: string }) {
return (
<Link
href={href}
className="border-border hover:bg-card bg-background/70 flex items-center gap-3 rounded-xl border p-3 text-sm font-bold transition"
>
<Icon className="text-accent h-4 w-4" />
<span className="min-w-0 flex-1 truncate">{label}</span>
<ArrowRight className="text-muted h-4 w-4" />
</Link>
);
}

const OverviewHome = () => {
const snapshot = useSyncExternalStore(
subscribeToResumeWorkspace,
() => getResumeWorkspaceSnapshot(),
() => RESUME_WORKSPACE_SERVER_SNAPSHOT,
);

const totalCount = snapshot.counts.RESUME;
const resumeCount = snapshot.counts.RESUME;
const recentDocs = snapshot.docs.slice(0, 6);

return (
<section className="space-y-7" aria-label="Studio overview">
<OverviewHomeHeader totalCount={totalCount} resumeCount={resumeCount} />

<OverviewReferenceCard />

<Card className="overflow-hidden rounded-2xl p-0">
<div className="grid gap-0 lg:grid-cols-[minmax(0,1fr)_20rem]">
<div className="border-border/70 border-b p-5 sm:p-6 lg:border-r lg:border-b-0">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-black">Recently opened</h2>
<p className="text-muted text-sm">Compact view, not another full resume page.</p>
</div>

<Link href="/documents" className="text-accent text-sm font-bold">
All resumes
</Link>
</div>

<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{recentDocs.length > 0 ? (
recentDocs.map((doc) => <RecentCard key={`${doc.type}-${doc.id}`} doc={doc} />)
) : (
<div className="border-border bg-background/70 col-span-full rounded-xl border p-5">
<p className="font-bold">No files yet</p>

<p className="text-muted mt-1 text-sm">
Use New Document in sidebar to create first resume.
</p>
</div>
)}
</div>
</div>

<aside className="space-y-4 p-5 sm:p-6">
<div>
<h2 className="text-sm font-black">Workspace shortcuts</h2>
<p className="text-muted mt-1 text-xs">Profile data, roadmap, and resume library.</p>
</div>

<div className="grid gap-2">
<MiniLink href="/profile" icon={BriefcaseBusiness} label="Profile workspace" />
<MiniLink href="/documents" icon={FolderOpen} label="Resume library" />
<MiniLink href="/admin/roadmap" icon={BookOpen} label="Roadmap" />
</div>
</aside>
</div>
</Card>
</section>
);
};

export default OverviewHome;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { useMemo } from "react";

import { useUserStore } from "@/store/useUserStore";

function Stat({ label, value }: { label: string; value: number }) {
return (
<div className="border-border bg-background/70 min-w-20 rounded-xl border px-3 py-2">
<p className="text-lg font-black">{value}</p>
<p className="text-muted text-[11px]">{label}</p>
</div>
);
}

const OverviewHomeHeader = ({
totalCount,
resumeCount,
}: {
totalCount: number;
resumeCount: number;
}) => {
const user = useUserStore((state) => state.user);

const firstName = useMemo(() => {
const name = user?.name || user?.email?.split("@")[0] || "builder";
return name.split(" ")[0] || "builder";
}, [user]);

return (
<header className="border-border bg-card rounded-2xl border p-5 shadow-sm sm:p-6">
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-accent text-xs font-bold tracking-[0.2em] uppercase">Overview</p>

<h1 className="mt-3 text-3xl font-black tracking-tight sm:text-4xl">
Good morning, {firstName}.
</h1>

<p className="text-muted mt-2 max-w-2xl text-base">
Recent resumes, useful references, and account shortcuts without duplicating the full
library page.
</p>
</div>

<div className="grid grid-cols-3 gap-2 text-center">
<Stat label="Files" value={totalCount} />
<Stat label="Resumes" value={resumeCount} />
<Stat label="Shared" value={0} />
</div>
</div>
</header>
);
};

export default OverviewHomeHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Link from "next/link";
import { BookOpen, FolderOpen, HelpCircle, ArrowRight } from "lucide-react";

import { siteConfig } from "@/config/site";

const referenceCards = [
{
href: `${siteConfig.links.docs}/docs`,
title: "Studio docs",
text: "Export, sharing, sync, and editor basics.",
icon: BookOpen,
},

{
href: "/documents",
title: "Resume library",
text: "Open saved resumes with sync, sharing, and list view.",
icon: FolderOpen,
},

{
href: `${siteConfig.links.main}/faq`,
title: "FAQ",
text: "Fast answers for account and document workflow.",
icon: HelpCircle,
},
];

const OverviewReferenceCard = () => {
return (
<div className="grid gap-4 lg:grid-cols-3">
{referenceCards.map((item) => {
const Icon = item.icon;

return (
<Link
key={item.title}
href={item.href}
target={item.href.startsWith("http") ? "_blank" : undefined}
rel={item.href.startsWith("http") ? "noreferrer" : undefined}
className="group border-border bg-card hover:border-accent/50 min-h-40 overflow-hidden rounded-2xl border p-5 text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
>
<div className="flex items-start justify-between gap-4">
<span className="bg-accent/10 text-accent flex h-11 w-11 items-center justify-center rounded-xl">
<Icon className="h-5 w-5" />
</span>

<ArrowRight className="text-muted group-hover:text-accent h-5 w-5 transition group-hover:translate-x-1" />
</div>

<h2 className="mt-7 text-xl font-black tracking-tight">{item.title}</h2>

<p className="text-muted mt-2 max-w-xs text-sm leading-6">{item.text}</p>
</Link>
);
})}
</div>
);
};

export default OverviewReferenceCard;
42 changes: 42 additions & 0 deletions apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import Link from "next/link";
import Image from "next/image";
import { FileText } from "lucide-react";

import { Badge } from "@veriworkly/ui";

import { getDocumentDefinition } from "@/features/documents/core/registry";
import { type ResumeWorkspaceDoc } from "@/features/documents/services/resume-workspace";

const RecentCard = ({ doc }: { doc: ResumeWorkspaceDoc }) => {
const definition = getDocumentDefinition(doc.type);

return (
<Link
href={`/editor/${doc.type.toLowerCase()}/${doc.id}`}
className="border-border bg-background/70 group hover:border-accent/40 hover:bg-card overflow-hidden rounded-xl border transition"
>
<div className="border-border/70 relative h-28 border-b bg-[color-mix(in_oklab,var(--card)_78%,var(--background))]">
{doc.previewImage ? (
<Image src={doc.previewImage} alt="" fill className="object-contain p-2" sizes="20vw" />
) : (
<div className="flex h-full items-center justify-center">
<FileText className="text-accent h-8 w-8" />
</div>
)}
</div>

<div className="p-3">
<div className="flex items-center gap-2">
<h3 className="min-w-0 flex-1 truncate text-sm font-bold">{doc.title}</h3>
<Badge className="px-2 py-0.5 text-[10px]">{definition.label}</Badge>
</div>

<p className="text-muted mt-1 truncate text-xs">{doc.description || doc.templateName}</p>
</div>
</Link>
);
};

export default RecentCard;
Loading
Loading