Skip to content
Open
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ VPS / Node server notes:
- Start with `npm run start`.
- Set `PORT` if needed, for example `PORT=3000 npm run start`.
- Use a reverse proxy such as Nginx or Caddy if exposing publicly.
- No database is required for this prototype.
- Data is local to the user’s browser.
- No database is required for participant-only local prototype testing.
- Data is local to the user’s browser unless optional server submission is explicitly configured.

Docker server test:

Expand Down Expand Up @@ -123,7 +123,7 @@ The prototype stores data locally in the browser. The JSON export can include:
- Computed analytical metrics.
- Completeness flags: `hasParticipantProfile`, `completedGame`, `completedGameRounds`, `hasPreRevealSurvey`, `hasSeenReveal`, `hasPostRevealSurvey`, and `isComplete`.

No backend is configured in this prototype. Data leaves the browser only if a user copies or downloads the export and shares it elsewhere.
Without optional server submission, data leaves the browser only if a user copies or downloads the export and shares it elsewhere. When server submission is enabled, completed exports are posted to the configured database for researcher download.

## Analytical metrics

Expand All @@ -146,6 +146,12 @@ The metrics are simple derived values for prototype analysis. They should not be

Protected admin export endpoints let a researcher retrieve server-submitted study data when `ENABLE_SERVER_SUBMISSION=true`, `DATABASE_URL`, and `ADMIN_EXPORT_TOKEN` are configured on the server. Keep `ADMIN_EXPORT_TOKEN` secret; requests must send it as a bearer token. These endpoints are intended for researcher use only and are not linked from the participant UI.

### Admin dashboard

The browser dashboard is available at `/admin`. It requires `ADMIN_EXPORT_TOKEN` and asks the researcher to enter the token in the browser before loading data. By default, the dashboard keeps the entered token in `sessionStorage`; the optional “Remember for this browser” checkbox stores it in `localStorage` only when explicitly selected.

The dashboard shows submission counts, first/last submission timestamps, high/low coverage counts, average computed metrics, and a recent-submissions table. It also allows JSON and CSV downloads from the browser. This is intentionally not a full authentication system: use it only behind HTTPS, and put `/admin` behind reverse proxy basic auth, a VPN, or another stronger access-control layer for real deployments.

JSON export example:

```bash
Expand Down
31 changes: 31 additions & 0 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import Link from "next/link";
import { AdminDashboard } from "@/components/admin/AdminDashboard";

export const metadata: Metadata = {
title: "Admin dashboard | Hidden Cost Game",
robots: {
index: false,
follow: false,
},
};

export default function AdminPage() {
return (
<main className="mx-auto flex min-h-screen w-full max-w-7xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
<header className="space-y-3">
<Link href="/" className="text-sm font-semibold text-research-700 hover:text-research-900">
← Back to home
</Link>
<div className="space-y-2">
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-research-700">Research admin</p>
<h1 className="text-3xl font-bold tracking-tight text-ink md:text-4xl">Admin dashboard</h1>
<p className="max-w-3xl leading-7 text-slate-600">
Enter the server-side export token to view submission counts, recent records, and browser downloads. This page does not expose the configured token in source and is not linked from the participant flow.
</p>
</div>
</header>
<AdminDashboard />
</main>
);
}
24 changes: 24 additions & 0 deletions app/api/admin/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { validateAdminRequest } from "@/lib/adminAuth.server";
import { AdminSubmissionError, adminStatsJson, getAdminStats } from "@/lib/adminSubmissions";

export const runtime = "nodejs";

export async function GET(request: NextRequest) {
const auth = validateAdminRequest(request);
if (!auth.ok) {
return auth.response;
}

try {
const stats = await getAdminStats();
return NextResponse.json(adminStatsJson(stats));
} catch (error) {
if (error instanceof AdminSubmissionError) {
return NextResponse.json({ ok: false, error: error.message }, { status: error.status });
}

return NextResponse.json({ ok: false, error: "Unable to retrieve admin stats." }, { status: 500 });
}
}
Loading