Imported from dcsergio/televoto (
AGENTS.md). Install upstream withnpx skills add dcsergio/televoto. Copyright stays with the author.
AI Agent Instructions for Voto Subito
Scope
This file contains only project-specific guidance for AI coding agents. For generic setup and template-level details, see README.md.
Runbook
| Task | Command |
|---|---|
| Full dev (frontend + backend) | npm run dev |
| Frontend only | npm run dev:client |
| Backend only | npm run dev:server |
| Build | npm run build |
| Lint (backend only) | npm run lint |
| Frontend smoke tests | npm run test:client |
| DB seed | npm run db:seed |
| DB migration | npm run db:migrate |
| DB migration (via pooler) | npm run db:migrate:pooler |
| DB push (no migration history) | npm run db:push |
| Prisma Studio | npm run db:studio |
| E2E tests | npm run test:e2e |
Notes:
npm run devstarts the Angular dev server (ng serve, port 8080) and Express (port 3001) concurrently. The Angular dev server proxies/api/*to Express (client/proxy.conf.json) — required because the backend disables CORS.- Port 8080 conflicts will fail startup outright.
npm run buildruns the backend TypeScript build (tsc -b) thenng buildinsideclient/; the Angular output is configured to land in repo-rootdist/, notclient/dist/.
Architecture Boundaries
- Frontend app:
client/src/app/(Angular standalone components, signals, Angular Material, Tailwind). Pages:admin-shell(root-only,/),event-manager-shell(single-event ops,/manager),voting-shell(public voting,/vote); Classifica lives inclient/src/app/components/score/(/score). - Frontend API layer:
client/src/app/api/*.api.ts(one Angular service per resource area; use these, avoid rawHttpClientcalls in components/pages). - Backend:
server/index.tsonly bootstraps the Express app (helmet/cors/json middleware, SPA static fallback) and mounts routers — it does not hold route logic itself. Route handlers live inserver/routes/*.tssplit by resource (auth,events,candidates,judge-tokens,votes,rankings); each delegates toserver/services/*.ts, which useserver/repositories/*.tsfor Prisma access. Auth helpers live inserver/middleware/auth.middleware.tsandserver/lib/. - Vercel adapter:
api/[...path].tslazily imports the compiledserver/index.jsand rewrites the request path — keep it a thin pass-through, never fork route logic between it andserver/index.ts. - DB schema/migrations:
prisma/schema.prismaandprisma/migrations/. - Generated Prisma client:
src/generated/prisma/(do not edit manually) — this is the only thing left under the repo-rootsrc/directory; the former React app there has been fully replaced byclient/.
Backend API Surface
Routes are split across server/routes/*.ts (32 routes total). Key groups:
- Auth (
auth.routes.ts):POST /api/auth/root/login,POST /api/auth/root/password,POST /api/auth/event/login,POST /api/auth/event/:eventId/password(event-manager self-service rotation, requires current password) - Events (
events.routes.ts):GET/POST /api/events,GET /api/events/by-code/:eventCode,GET/PUT /api/events/:eventId,PUT /api/events/:eventId/manager-password(root-only reset, no current password needed),PUT /api/events/:eventId/manager-settings(event-manager self-service, name only),GET /api/events/:eventId/voting-progress,PUT /api/events/:eventId/voting-state,POST /api/events/:eventId/start,DELETE /api/events/:eventId/votes,PUT /api/events/:eventId/archive-state(root-only),POST /api/events/:eventId/clone(root-only) - Voting (
votes.routes.ts):POST /api/vote(judge-token gated, not device-based, rate-limited) - Candidates (
candidates.routes.ts):GET /api/candidates/:eventId,POST /api/candidates,PUT /api/candidates/:id,DELETE /api/candidates/:id - Judge tokens (
judge-tokens.routes.ts):GET/POST /api/events/:eventId/judge-tokens,GET /api/events/:eventId/judge-tokens/stream(SSE),POST /api/judge-tokens/validate,POST /api/judge-tokens/finalize,POST /api/judge-tokens/:id/reissue(lost-judge-code recovery),POST /api/judge-tokens/:id/revoke - Rankings (
rankings.routes.ts):GET /api/rankings/:eventId,GET /api/events/:eventId/partial-rankings
When adding/changing endpoints:
- Update the relevant
server/routes/*.ts(and itsservice/repositoryif logic or data access changes). - Add/update the matching method in the relevant
client/src/app/api/*.api.ts. - Wire usage into the page/component.
- If touching judge-token or voting-progress logic, keep it in sync with the
judge-tokens/streamSSE push (server/services/voting-progress.service.ts).
Project Conventions
- UI text and server errors are primarily Italian. Keep language consistent.
- There is no device-fingerprinting anywhere in this codebase. Voting identity is judge-token-based: a vote is unique per
(candidateId, judgeTokenId), gated by the opaquejudgeTokensent in thePOST /api/votebody — not a device ID. - Vote score must be integer 1-10, or
nullfor an abstention (validated server-side,server/validation/vote.schemas.ts). - Types used by the UI are in
client/src/app/models/types.ts(EventData,CandidateData); API-only shapes likeRankingEntrylive alongside their*.api.tsfile. - Root/event-manager bearer tokens are held as signals in
client/src/app/state/auth-state.service.tsand persisted tosessionStorage. - Two auth layers, both password-based, no user-account system: root (
requireRootAuth, one global password, gates the admin page (/) + cross-event endpoints,/api/auth/root/login) and event-manager (requireEventManagerAuth, one password per event, gates/manager+ candidate/judge-token/voting-lifecycle endpoints,/api/auth/event/login). A root token is also accepted wherever an event-manager token is expected (deliberate superuser bypass) —/managertakes advantage of this so root can open any event without a manager password;/scoredeliberately does not. - Judges never use the root/manager auth system — they get single-use opaque
JudgeTokens (link/QR), stored only as a hash + preview, carrying aVoterType(QUALIFICATAweighted vsPOPOLAREgeneral public). - Event archiving reuses
Event.active(previously alwaystrue) as the archived flag:false= archived. Archived events are excluded from the admin event selector and active lists; the admin "Archiviati" section (admin.util.ts'sAdminSection = 'archived') lists them for unarchiving or cloning.POST /api/events/:eventId/cloneduplicates the event, its candidates, weights, and manager credential into a new event with a freshly generated code and" (copia)"appended to the name. handleManageEvent/openManagerinadmin-shell.tsnavigate to/managervia the AngularRouterin the same tab (notwindow.open), so root'ssessionStoragetoken is simply still there — no opener/noopenerconcerns like the still-new-tab/voteand/scorelinks.
Known Pitfalls
DELETE /api/candidates/:idreorders remaining candidate numbers to keep them sequential.POST /api/events/:eventId/startperforms a transaction that renumbers candidates, clears votes, resets all non-revoked judge tokens back toACTIVE(clearingusedAt/finalizedAt), and reopens voting — treat as a destructive reset, not an incremental update. It also triggers ajudge-tokens/streamSSE broadcast afterward so the admin progress dashboard reflects the reset tokens.- Do not hand-edit generated Prisma files in
src/generated/prisma/; regenerate withnpx prisma generateif needed. - After schema changes, use
npm run db:migrate(not onlyprisma db push) to preserve migration history. - The backend only serves the SPA's
index.htmlas a fallback for the exact pathsGET /,GET /vote,GET /manager,GET /score(no wildcard route) — the Angular app must not introduce real nested routes under any of them (it uses query params instead, see?adminSection=, shared by both admin andmanager). Adding a fifth top-level route means updatingclient/src/app/app.routes.ts, this fallback list inserver/index.ts, andvercel.json'srewritestogether. - Ranking math (
server/services/ranking.service.ts, used by bothGET /api/rankings/:eventIdand the partial-rankings endpoint) divides the qualified-judge average by the count of eligible non-revokedQUALIFICATAtokens, not just judges who voted — abstentions pull the average down. Popular-vote average can use a trimmed mean (Event.enableTrimmedMean/trimmedMeanPercentage). Final score blends both pools viaweightQualificata/weightPopolare(default 70/30).
Key Files
- Frontend routes:
client/src/app/app.routes.ts - Frontend composition:
client/src/app/pages/,client/src/app/components/ - API client wrappers:
client/src/app/api/*.api.ts - Backend bootstrap:
server/index.ts; route/service/repository logic:server/routes/,server/services/,server/repositories/,server/middleware/ - Vercel serverless adapter:
api/[...path].ts - Dev-server proxy/ports:
client/proxy.conf.json,client/angular.json - Prisma schema:
prisma/schema.prisma