Skip to content

Site coordinator role (issue #18, PR 2/2) - #41

Merged
matbrgz merged 2 commits into
masterfrom
feat/issue-18-site-coordinator-role
Sep 10, 2026
Merged

matbrgz merged 2 commits into
masterfrom
feat/issue-18-site-coordinator-role

Conversation

@matbrgz

@matbrgz matbrgz commented Sep 10, 2026

Copy link
Copy Markdown
Member

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 to auth()->user()->site_id, mirroring the authorize*Access() pattern already used by JudgeController/StaffController.
  • 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.
  • New /site/* routes gated by role:site,admin (same pattern as /judge/* and /staff/*), and a new "Meu Site" sidebar section.
  • 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 Bump axios from 0.15.3 to 0.18.1 #1 see the wrong (or no) questions.

Test plan

  • New 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.
  • ClarificationControllerTest addition covering the site-tagging fix.
  • Full suite: 542 passed, 0 failed.
  • Manual browser pass: created a site-coordinator account via the admin Users page, logged in as it, confirmed all four pages render correctly and are scoped to that site.

🤖 Generated with Claude Code

https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn

matbrgz and others added 2 commits September 10, 2026 20:20
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
@matbrgz

matbrgz commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Review follow-up

Fixed 4 real bugs from code review (see latest commit): a soft-deleted-problem crash on the site dashboard, answered status detection missing broadcast statuses (was letting a resubmit silently overwrite a broadcast answer), null-contest crashes in completeTask/answerClarification (also backported the same defensive fix to the pre-existing, identical pattern in StaffController/JudgeController), and a multi-active-contest edge case in ClarificationController::store().

Two findings are documented but not fixed here (architectural, not bugs): SiteController::storeTeam() duplicates Backend\UserController's user-creation logic, and authorizeSiteAccess() duplicates the same authorization-check shape already copy-pasted across JudgeController/StaffController. Both are consistent with an existing pattern in the codebase (Judge and Staff already each have their own copy) rather than something this PR introduced fresh -- worth consolidating into a shared helper as a follow-up, but out of scope here.

All 545 tests passing. Merging.

@matbrgz
matbrgz merged commit 06960b4 into master Sep 10, 2026
matbrgz added a commit that referenced this pull request Sep 15, 2026
…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>
@matbrgz
matbrgz deleted the feat/issue-18-site-coordinator-role branch September 17, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant