Repository navigation
Site coordinator role (issue #18, PR 2/2) - #41
Conversation
Adds the 'site' user type as a real role: a coordinator stationed at one physical site gets a read-only dashboard of their site's submissions/teams, plus site-scoped versions of staff tasks, team creation, and clarification answering -- everything scoped to auth()->user()->site_id, mirroring the access-scoping pattern already used by JudgeController/StaffController (authorize*Access() methods, one route per contest-scoped resource). - SiteController (new, top-level): dashboard(), tasks()/completeTask(), teams()/storeTeam(), clarifications()/answerClarification() -- each scoped to the coordinator's own site. An admin with no site_id of their own can spot-check a specific site via ?site_id=; a coordinator is always locked to their own site regardless of that query param (can't view/act on another site by editing the URL). - New /site/* routes, gated by role:site,admin (same middleware pattern as /judge/* and /staff/*). - New "Meu Site" sidebar section, visible to isSite()/isAdmin(), mirroring the existing Judge/Staff sections. - Fixed a real bug found while wiring this up: ClarificationController::store() always tagged a new clarification with "the first site associated with the contest," regardless of the asking user's actual site -- which would have made every site coordinator except whoever owns site #1 see the wrong (or no) questions. Now uses the user's own site_id when set. Verified in-browser end to end: created a site-coordinator account via the admin Users page (from PR 1's site_id assignment fix), logged in as that account, and confirmed the dashboard, tasks, teams, and clarifications pages all render correctly and are scoped to that site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn
- site/dashboard.blade.php: guard $run->problem access -- Problem uses SoftDeletes, so a run's problem can resolve to null after deletion, which would 500 the whole dashboard instead of showing one row as "Problema removido". - SiteController::clarifications(): compute `answered` the same way Clarification::isAnswered() does (answered/broadcast_site/ broadcast_all), not a raw status === 'answered' check. The raw check showed an already-broadcast question as pending and still rendered the answer form, so resubmitting would have silently overwritten the broadcast answer. - SiteController::completeTask()/answerClarification(), StaffController::complete(), JudgeController::judge(): guard ->contest->getContestTime() with ?-> -- Contest uses SoftDeletes, so a Task/Run/Clarification can outlive its contest being soft-deleted, which would fatal instead of just recording contest_time as 0. - ClarificationController::store(): nothing deactivates other contests when one is activated, so more than one can have is_active=true. The fix from the previous commit picked the user's own site without checking it belongs to the same contest as the one resolved here -- now falls back to that contest's own site if it doesn't, so contest_id and site_id can never disagree on which contest a new clarification belongs to. Not fixed, documented instead (architectural, not a bug): SiteController duplicates Backend\UserController's user-creation logic and JudgeController/ StaffController's authorize*Access() pattern rather than sharing it. This already existed as a precedent (Judge and Staff each have their own copy) before this PR added a third; consolidating all of them into a shared helper is a reasonable follow-up but out of scope here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn
Review follow-upFixed 4 real bugs from code review (see latest commit): a soft-deleted-problem crash on the site dashboard, Two findings are documented but not fixed here (architectural, not bugs): All 545 tests passing. Merging. |
…ckend (#191) (#204) `/judge/health` respondia 200 e `/api/frontend/judging-health` respondia 404, entao a pagina renderizava a mensagem de indisponivel do api.js. Era o unico dos dezesseis endpoints de feature que nao resolvia -- e o docs/specs/README.md ja registrava a divida: "faltam o endpoint de health e dados de atividade/recuperacao deste contrato". O watchdog funciona desde o #45; ninguem conseguia ver o que ele fazia. Duas decisoes valem registro. Primeira: o endpoint NAO inventa dado. A spec do #45 e explicita que `last_judge_activity_at`, token de lease e timestamp de requeue sao extensoes propostas que o Run nao tem, e que o backend deve "omitir capacidades/retornar null sem inventar atividade". Entao `last_activity_at` e `next_check_at` voltam null. Derivar do `created_at` seria apresentar suposicao como medicao, e tela que mostra um "visto por ultimo" errado com confianca e pior do que tela que diz nao saber. Segunda, e e a que mais rende: o watchdog passa a registrar que rodou. Scheduler parado e prova sem nada travado sao situacoes opostas que hoje ficam identicas em qualquer tela -- as duas mostram lista vazia. `enabled` deixa de ser "o comando esta registrado" e passa a ser "rodou nos ultimos quinze minutos" (tres vezes a cadencia de cinco), com `last_checked_at` para o operador ver quando esteve vivo pela ultima vez. Nunca rodou e rodou faz uma hora sao reportados diferente, porque pedem respostas diferentes. O `summary` conta tudo no horizonte independentemente do filtro de status, como a spec pede: resumo que encolhe junto com o filtro diria ao operador que o problema passou quando ele so estava olhando uma fatia. Run julgado normalmente nao entra na lista -- nao faz parte da historia de recuperacao, e lista-lo enterraria o punhado que faz. Escopo por sede igual ao do JudgeController: juiz de sede ve a propria mais o que foi roteado para ele (#41); admin ve o contest. Fecha #191. Claude-Session: https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Second half of issue #18 (multi-site coordination), on top of #40's foundation. Adds the 'site' user type as a real, working role: a coordinator stationed at one physical site gets a read-only dashboard of their site's submissions/teams, plus site-scoped versions of staff tasks, team creation, and clarification answering.
SiteController(new, top-level):dashboard(),tasks()/completeTask(),teams()/storeTeam(),clarifications()/answerClarification()-- each scoped toauth()->user()->site_id, mirroring theauthorize*Access()pattern already used byJudgeController/StaffController.site_idof their own can spot-check a specific site via?site_id=; a coordinator is always locked to their own site regardless of that query param./site/*routes gated byrole:site,admin(same pattern as/judge/*and/staff/*), and a new "Meu Site" sidebar section.ClarificationController::store()always tagged a new clarification with "the first site associated with the contest," regardless of the asking user's actual site -- which would have made every site coordinator except whoever owns site Bump axios from 0.15.3 to 0.18.1 #1 see the wrong (or no) questions.Test plan
tests/Feature/SiteControllerTest.php: access denial for non-site/non-admin, dashboard/tasks/teams/clarifications scoped correctly, cross-site 403s on completing a task or answering a clarification, team creation scoped to the coordinator's site, admin spot-check via query param, coordinator can't escape their own site via that same query param.ClarificationControllerTestaddition covering the site-tagging fix.🤖 Generated with Claude Code
https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn