Skip to content

Latest commit

 

History

History
379 lines (352 loc) · 30.5 KB

File metadata and controls

379 lines (352 loc) · 30.5 KB

MOJ — Visão geral (comece por aqui)

O MOJ (Melhor/Meta Online Judge, moj.naquadah.com.br) é um juiz online escrito em bash. Este repositório é a v2 API-first: nginx + backend bash (fcgiwrap) + frontend estático modular, lendo o mesmo contests/<id>/ de sempre (sem migração de dados).

Fluxo de submissão/julgamento e como os daemons conversam: ver FLOW.md. Contrato de rotas: API.md (+ web/api/openapi.json). Formato do pacote de problema (orgs, coleções, metadados): PACOTE.md. Placar: SCOREBOARD.md. Deploy: DEPLOY.md. Plano original: PLAN.md.

Convenção de commit: mensagens em português, no presente, prefixadas pelo componente (ex.: problemas: …). O rodapé leva apenas Co-Authored-By:nunca uma linha Claude-Session: (é ruído no histórico). Vale também p/ assistentes de IA.

Documentação junto com o código (doc atrasada = bug): ao mudar comportamento ou contrato, atualize a doc no mesmo commit — rotas/campos em API.md e em ../web/api/openapi.json (mantenha os dois em sincronia); arquitetura/fluxo aqui e em FLOW.md. bash docs/build-html.sh refaz o HTML.

Estrutura

moj/
  server/          backend bash sob nginx + fcgiwrap
    api/v1/
      router.sh    front-controller único: PATH_INFO -> handlers/<segmentos>.sh
      lib/         common.sh (resposta/JSON/validação/audit), params.sh, auth.sh, profile.sh, contest-create.sh
      handlers/    auth/ index/ treino/ contest/ submission/ admin/ ops/  (1 arquivo por rota)
    daemons/       judged.sh (consumidor do spool, inotify)
    judge-gw/      sched-lib.sh (escalonador pull: registro+fila+claim) + judge.sh (mock/local, dev) + PULL.md
    score/         build.sh (recalcula placar), stats-gen.sh (gera o cache de estatísticas), jplag-run.sh
    etc/           common.conf, nginx/, systemd/
  web/             frontend vanilla (ES modules, sem build), servido estático
    shared/        api.js auth.js ui.js editor.js charts(/lib) flags.js sonic.js contest-host/guard/shell.js contest-config/
    index/ contests/ status/ treino/ contest/  (home, arquivo de encerrados, status público, treino, contest)
  judge/           agente pull (moj-agent@pos/gpu/cm/hu, puxa job no heartbeat) + mojtools (sandbox bubblewrap)
  mojinho-bot/     bot do Telegram (vira cliente da API)
  contests/<id>/   DADOS (conf, users/<login>/ (account.json+history+metrics+submissions), var/placar.txt, …) — fonte da verdade
  run/             estado de runtime (sessions/, spool/, results/, registry/, sockets)  [não versionado]
  docs/            esta documentação

Camada de API

  • Roteador único router.sh: sanitiza os segmentos (sem traversal), mapeia /a/b/chandlers/a/b/c.sh e faz source do handler (que usa $REQUEST_METHOD).
  • Envelope JSON consistente: {success:true, …} ou {success:false,error:{message,code}}, sempre com o status HTTP correto (fail/ok_json em lib/common.sh). Horários em EPOCH. Histórico e placar são TXT cru (eficiência + é o que o front parseia).
  • Auth Authorization: Bearer <token> → sessões em run/sessions/ (modo 700), gravadas com printf %q (o arquivo é sourced; nada de injeção). Papéis por sufixo no login: .admin / .judge / .cjudge / .staff / .cstaff / .mon (.cstaff = chefe de sede: vê etiquetas de credenciais, fila do staff em leitura e a cerimônia da sede — não herda .staff).
  • Isolamento por subdomínio: em <id>.moj.<base> o nginx injeta CONTEST_HOST; o router.sh só serve aquele contest (auth/contest/submit/submission) e o frontend redireciona o resto para /contest/ (shared/contest-guard.js). Única exceção: /treino/ajuda/ (o "como enviar": E/S, extensões, template por linguagem) — página ESTÁTICA de instrução, sem dado de treino/problema/contest, liberada porque o competidor precisa dela na LAN isolada; lá dentro ela veste o topbar do contest e obedece o LOCALE dele. Página nova só entra nessa lista se não falar com a API do treino.
  • jq para todo JSON; contests/<c>/conf é sourced — por isso a criação de contest escreve tudo com printf %q.

Frontend

Vanilla ES modules, sem build, servido estático. shared/ concentra o cliente de API (fetch + Bearer + envelope), auth/token (localStorage), ui.js (el(), avatares, i18n pt/en), o editor CodeMirror 6 (bundle vendorizado em shared/vendor/codemirror/ — sem CDN, contest roda em LAN isolada; fallback textarea), os gráficos SVG build-free (/lib/charts.js), e os assets offline: bandeiras locais (shared/flags/, 271 países + 27 estados do BR) e GIFs do Sonic (shared/assets/sonic/). Editores de configuração de contest reaproveitáveis em shared/contest-config/ (cores, países/escolas, regiões, básico, settings/toggles (settings-editor.js), seletor de linguagens (lang-picker.js) e o painel de busca+sorteio do banco por coleção/tag/dificuldade (bank-panel.js)) — os mesmos na criação e no admin do contest.

O que existe (funcional)

Home & treino livre

Home com notícias, contests (abertos/por vir/encerrados; abre cada um pelo subdomínio), top10 e destaques; página pública /status/ (health: fila por lista, máquinas julgando, daemons). Treino livre (/treino/, UMA página com DOIS estados client-side após 1 fetch): hub (default) com omnibusca central de sugestões agrupadas (coleções/tags/problemas, com status pessoal), atalhos (aleatório/fáceis/não resolvidos), "Para você" (continue de onde parou + sugestão, do /treino/history-full), carrossel de coleções em destaque com barra de progresso pessoal (dedup por sobreposição de conjunto — "obi" × "Olimpíada Brasileira…") e "mais enviados na semana" (/treino/trending); busca avançada (?browse=1 ou qualquer filtro, deep-links ?searchcol= antigos continuam valendo) com trilho de facetas — status, dificuldade (derivada da taxa), árvore de coleções com multi-seleção OR (checkbox; searchcol aceita CSV) e progresso por nó, tags AND com contagens vivas — chips removíveis, ordenação (mais resolvidos/A–Z/dificuldade) e tabela sempre visível paginada; página do problema com enunciado + editor CodeMirror + upload + histórico com polling (o veredicto exibido é sempre o canônico; cada submissão julgada mostra um resumo abaixo — "Passou em X/Y testes (Z%)", pontos + grupos, ou Score heurístico — via /submission/summary, redigido por modo em contests: lib/verdict.sh), perfil público por usuário (/treino/stat/?user= — cabeçalho com membro desde/editor/último envio, cartões, curva de resolvidos, heatmap 26 semanas + punchcard dia×hora, veredictos/linguagens/dificuldade/tags em barras, progresso por coleção, problemas em aberto, conquistas derivadas de um REGISTRO gerível (aba 🏅 do admin → var/achievements.json; default embarcado; ver docs/PERFIL.md) e histórico paginado com filtros; tudo client-side de poucos fetches paralelos; perfil privado = cadeado, e o Bearer vai sempre que logado — admin vê o que a API já lhe permitia), stats por problema (cache, linguagens, editores, nuvem de avatares), e painel admin do treino (sessões/logs com UA+IP, busca/regex, bulk logout/lock, notícias, auditoria, máquinas, e — em abas com índice/TOCFila & tempo de resposta (contadores de submissão + calibração, o que cada máquina roda agora (calibração vs submissão), tempo de veredito e mapas de calor de volume de submissões e calibrações) e Estatísticas (usuários, sessões, problemas: total/públicos/privados, quebra por autor, mapa de calor de entrada de públicos e atividade diária). Fontes: /treino/admin/{queue,judges,response-stats,calib-activity,stats}.

Gestão de problemas (MOJ-nativo por org, keyless) & painel de status

Formato do pacote, .moj-meta.json, orgs e coleções: ver PACOTE.md (fonte única). Roteiro de montar um pacote e o que faz cada script: mojtools/README.md.

Autoria/edição em /problemas/ (storage: repo git LOCAL por problema em <org>/<prob>; acesso por ORG — membros escrevem, a trava public_allowed barra vazamento; só o login do MOJ). A aba Painel (GET /problems/status) dá a visão agregada dos problemas de que o login é dono, colaborador ou membro da org: quantos/quais calibrando, validados, calibrados, precisam recalibrar (time-limit desatualizado após mudança no pacote) e com erro, mais a planilha de time-limits. O acesso é cortado na API (owners_visible): problema privado de terceiro não aparece. Staleness vem do checksum do pacote carimbado no índice de donos (barato; ≤30 min de atraso); /problems/tl recomputa na hora p/ 1 problema. A aba Análise (GET /problems/my-stats) dá o panorama de submissões dos seus problemas agregado em toda a plataforma (treino + as turmas): tentativas, acertos, erros mais comuns, linguagens, nº de contests e o mais popular — cache precomputado que reconcilia o namespace do history (problemas-apc#…) com o índice de donos (apc#…) via collections; só agregados (sem logins, sem nomes de contests) — não vaza prova privada.

Store por-usuário, cadastro por Telegram e alertas

Todo contest guarda um diretório por conta (contests/<c>/users/<login>/: account.json + history/metrics.json/submissões/logs/results/photo.png próprios). Não existe passwd: auth (verify_password), placar (sc_users), perfis e listagens leem os account.json direto (USERS_FROM=<src> cai para o users/ do contest-fonte — participantes compartilhados têm dir local sem account.json). Perfil (universidade/editor/privacidade) e metadados de time (.team{name,univ_short,univ_full,flag}) vivem no próprio account.json. Ganhos: trocar de username = mv do diretório e a maioria dos scripts de conta/julgamento só muda o caminho (lib/users.sh, emit_history_stream). Os handlers de usuário do admin do contest (user-add/user-disable/user-remove/users-set-password) escrevem no account.json (fonte da verdade); remover = mv do diretório p/ .removed-users/ (submissões preservadas). Migração de contest pré-reforma (arquivado em contests-legado/): server/bin/store-migrate.sh <c> [--apply] [--from <dir>] — dry-run por padrão; canonicaliza probids (offset/a.borg#prob), leva team/perfil ao account.json, roteia os flat files, gera metrics, move os resíduos p/ .legacy-store/ e só publica em contests/ após verificação (contas, spot-check de senhas, soma do history, metrics, placar). server/bin/store-cleanup.sh <c> limpa resíduos de contest já migrado. Migração vinda do MOJ ANTIGO (backup em contests-backup/, probid já em <repo>#<slug>) é outro caminho — o store-migrate.sh não serve porque aborta se o destino existe e faz mv -T do contest: server/bin/treino-map-gen.sh decide <repo legado>#<slug><org>#<prob> (alias repo/collections, casefold, slug, título e, por último, o TEXTO do enunciado) e emite um TSV auditável; server/bin/treino-migrate.sh {stage|verify|install|audit} consome o TSV (recusa qualquer linha ?) e funde num contest vivo — conta nova entra por mv do diretório, conta que já existe só ganha history (dedup por subid) e submissões, senha do prod prevalece, telegram via tg_link. stage/verify não tocam o destino; audit confere o instalado contra o legado. Contas GERIDAS (menores, SEM Telegram — docs/CONTAS-GERIDAS.md): criadas pelo admin (aba 🧒; rotas /treino/admin/managed-*), marca .managed{by,note,birthdate,expires_at} no account.json; menor de 18 = perfil sempre privado (gate no profile_is_public) + Telegram bloqueado (link-start 403) — caem sozinhos aos 18; expiração opcional corta o login; senhas geradas mostradas UMA vez ao admin. O treino ganha um overlay de Telegram (lib/telegram.sh): cadastro web-first (/treino/cadastro/) confirmado por deep-link no bot, 1 Telegram = 1 conta (anti-duplicata), recuperação de senha pelo vínculo, e senha entregue só por DM. O mojinho-bot virou transporte fino (bot-token mojb_, sem .admin/GODS) e entrega alertas de incidente que a API decide (lib/alerts.sh + GET /ops/alerts: juiz offline+fila, fila grande, daemon caído, com histerese/cooldown) aos .admin com Telegram vinculado + grupo.

Criação de contest (/treino/criar/) — wizard multi-etapa

Permissão por lista do admin OU threshold de problemas resolvidos. Wizard em 8 passos (shell criar.js + steps/*.js; um objeto draft único — ir-e-voltar não perde nada): 0 Começar (em branco / template salvo / duplicar contest meu / importar .tar.gz / baixar template JSON / salvar template de contest existente), 1 Dados (nome/id/modo/datas), 2 Problemas (painel compartilhado de busca+sorteio por coleção/tag/dificuldade, add por ID, enunciado custom HTML e PDF por problema), 3 Usuários (compartilhados do treino ou próprios, com colagem fluida + senhas legíveis + CSV), 4 Admin (obrigatório), 5 Opções (o MESMO settings-editor da aba Configurações do admin — paridade total, + prioridade de julgamento), 6 Visual (cores/Sonic, países/escolas, regiões), 7 Revisão (resumo + validações + Criar/Criar vazio + salvar como template). Templates nomeados ficam no servidor por criador (/treino/contest-create/templates); duplicar/exportar usam /treino/contest-create/{mine,export,duplicate} (só dono/admin — 404 p/ terceiros).

Ambiente de contest (<id>.moj.<base>)

Login (com gate opcional por substring de User-Agent), página principal (problemas + submissão + editor que o admin pode desligar), placar multi-modo (icpc/obi/treino/ heurístico/outro) com bandeiras locais, filtro por país/escola, modo anônimo (agregado/ quartis), freeze (esconde resultados após o horário; build.sh gera placar.txt público congelado e placar-full.txt completo — .admin/.judge/.cjudge + allowlist SCORE_FULL_USERS veem o completo) com cerimônia de revelação nativa (/contest/score/reveal.html, estilo ICPC resolver: delta frozen→full revelado de baixo p/ cima, passo/auto, botão "descongelar tudo" = settings freeze:0, só admin; o placar aceita &view=public p/ o privilegiado obter a visão congelada; link na aba Situação do admin). O .cstaff conduz a cerimônia POR SEDE: a mesma página com &scope=mine — a API recorta frozen+full aos usuários do escopo dele (staff-filters) e só libera o full quando o contest terminou p/ todas as sedes (contest_over_for_all: fim do conf + o maior end de time-overrides.json; admin pode antecipar via allowlist SCORE_FULL_USERS), tempo de solução relativo ao início (não EPOCH), e nav por papel. Contest 🕵️ SUPER SECRETO (conf SECRET=1, marcável na criação e no admin): fora das listagens públicas (home, arquivo /contests/, /status/) e o placar deixa de ser públicoscore/balloons/regions/ teams-meta exigem sessão daquele contest (401 secret_login_required). A tela de login/ countdown continua funcionando p/ quem tem o link (/contest/basic segue público). Desmarcar exige digitar o id. Usuários comuns têm no menu uma página própria de Backup de arquivos (/contest/backup/) p/ guardar versões de solução (não polui a home); o admin vê/baixa todos na aba Backups (zip por usuário). Quando há usuário .staff no contest, os alunos ganham também a página Impressão (/contest/print/): enviam um arquivo (PDF/imagem/texto/código) e acompanham o status (pendente→processada→entregue) — ver Impressão (.staff) abaixo. Os problemas usam o id canônico coleção#problema (igual ao treino — é o que o juiz usa p/ achar o pacote); o editor é o CodeMirror compartilhado (shared/editor.js, com tela cheia e nova janela) e a seleção de linguagens é a lista inteira do MOJ (shared/languages.js), reduzida à whitelist do conf LANGUAGES= quando definida. O placar é gerado de users/*/metrics.json (mantidos incrementais pelo daemon; score/build.sh + sc_cells — ver SCOREBOARD.md), sem varrer history. O aluno recebe aviso de novidades (notícias + clarifications respondidas, com badge de não lidas — poll de /contest/updates) e vê o tempo-limite por linguagem no detalhe do problema (ocultável pelo admin). Acesso por fase+papel (forçado pela API, não só no front): .admin/.judge veem os problemas e submetem a qualquer momento (antes/durante/depois); o usuário normal só vê os problemas após o início (antes disso, ao logar, recebe uma tela de contagem regressiva) e só submete durante a janela (/contest/problems devolve locked:"not_started" e /submit recusa com 403 fora da janela — contest_not_started/contest_ended); .staff/.cstaff não veem problemas nem submetem; .mon submete só na janela (como o normal) mas fica fora do placar. A janela tem prorrogação por sede/grupo (time-overrides.json, regras regex no login — 1ª que casa estende o fim SÓ daquele grupo, ex.: queda de energia numa sede; contest_end_effective em lib/contest-gate.sh vale no /submit e no countdown do /contest/basic autenticado; editável na aba Configurações do admin e por moj-contest extend --group, auditado). Telas internas:

  • /contest/admin/ — hub com 10 sub-abas (abas antigas viram alias: #staff→Tarefas, #log/#backups→Usuários & sessões): 👥 Times (gerência POR-USUÁRIO da identidade dos times — o NOME é campo único: fullname = nome do time, pois usuário de contest É o time; + país, sede, universidade (.team do account.json, que placar/crachás/impressão leem), brasão (logo.png, máx 128) e foto (photo.png, máx 1000, 📷 clicável no placar); carga única via CSV com cabeçalho (login,nome,pais,sede,univ,univ_nome, ordem livre; time/ equipe = aliases de nome — o MESMO formato serve na carga em lote de credenciais), fotos/brasões em LOTE (arquivos <login>.<ext>, 1 POST por arquivo) e o botão "Materializar matches" que aplica as regras regex (teams-meta/regiões) aos campos vazios de uma vez — /contest/{teams,team-photo,team-logo} + /contest/admin/{teams,team-assets}; contest com users_from fica só no overlay regex); ✅ Pré-prova (checklist verde/amarelo/vermelho de véspera: janela, SHOWLOG oculto (anti- vazamento em icpc), freeze, juízes online, toolchain das linguagens permitidas, TL calibrado por problema + cache nos juízes, staff de impressão, contas, spool travado — /contest/admin/preflight); Situação (painel ao vivo e acionável: logados + alerta de multi-sessão, ações sugeridas, saúde por juiz (online/offline/cache/linguagens), fila, pendentes com tempo de espera, submissões recentes, por problema, métricas avg/p95, timeline com picos e cards de tarefas do staff (impressões/balões pendentes) — /contest/admin/dashboard, auto-refresh); Configurações (tempos, login on/off, abertura, freeze, toggles editor/log/código/tempo-limite/anônimo/ 🕵️ SUPER SECRETO, gate de UA, linguagens permitidas do contest — o MESMO settings-editor do wizard; desmarcar o secreto exige digitar o id); Problemas (busca no banco público + os privados do dono do contest com badges, sorteio por coleção/tag/dificuldade, add/remover/reordenar/renomear — sem "add por id"); Aparência (cores/Sonic, países/escolas com preview de matches + import/export JSON + template dos sem match, regiões, básico); Usuários & sessões (add/reset/remover/deslogar/desabilitar/troca de senha geral, filtros (busca + ativos/desabilitados/privilegiados, teto de 300 p/ contest 1000+) e carga em lote (colar/arquivo → POST /contest/admin/users-bulk, skip/update, CSV das credenciais — subir competidores depois de criar o contest só com contas administrativas)
    • sessões com alerta de UA/IP diferente, deslogar por UA, log de acessos + backups dos usuários); Tarefas do staff (web/contest/admin/tasks.js — panorama e AÇÃO: resumo em cards, a fila completa de impressão + balões de /contest/staff/queue com filtros/idade/CSV, o admin pode abrir o PDF e marcar processada/entregue, desempenho por staff e o escopo por regex de cada .staff — semeável das regiões — /contest/admin/staff-filters); Tarefas do judge (shared/review-board.js — o MESMO board da Situação do chief: cards da correção manual, a fila completa com filtros/idade/quem pegou/votos (o review/list só manda os votos p/ admin/chefe — anti-anchoring p/ o juiz comum), ação Decidir/Resolver = review/resolve, o override auditado que libera o veredicto ao aluno na hora, desempenho por juiz e a config de opções/matriz no fim); Auditoria (feed cronológico unificado no instante exato de cada evento: ações de admin + logins + submissões (no sub_epoch) + veredictos (no finalized_at, com o juiz) — cada submissão gera 2 entradas, submissão e correção, p/ o trace completo; /contest/admin/audit-log, filtrável + download CSV). Problemas também edita as linguagens permitidas por problema (problem-langs.json), que o editor do aluno e a tabela de tempo-limite respeitam, e o pool de juízes por problema (problem-judges.json — herda o CONTEST_JUDGES do conf quando vazio; o escalonador é ESTRITO: só o pool julga, e o TL exibido é só dos hosts do pool efetivo). Rejulgar (aba "todas submissões") agora reconstrói a fonte arquivada e re-julga de fato (marca como pendente na Situação). Criação não sobrescreve a conta admin já existente (senha digitada respeitada; em modo compartilhado o <login>.admin existente é reutilizado).
  • /contest/allsubmissions/ — todas as submissões (ver código/log, filtrar, marcar grupo/todos, rejulgar em lote).
  • /contest/statistics/ — estatísticas ricas (totais, por problema, quartis, distribuição, tentativas, veredicto×problema, balões, linha do tempo).
  • /contest/clarification/ — perguntas (por problema/geral); admin/judge/mon respondem (pública/privada) e publicam notícias do contest. O asker é anônimo p/ os juízes (tratamento isonômico; recuperável só pelo admin via auditoria); responder exige reserva (clarification-claim, TTL 5 min) p/ dois juízes não pegarem a mesma; o juiz manda aviso oficial (Q+A público, autor oculto) e o juiz-chefe/admin editam respostas/notícias já dadas.
  • /contest/judge/ — área de avaliação. /contest/jplag/ — similaridade das soluções aceitas (roda o jar, mostra pares + comparação lado-a-lado).
  • /contest/chief/painel do juiz-chefe (.cjudge) e do admin: Situação da avaliação usa o mesmo board da aba "Tarefas do judge" do admin (shared/review-board.js: cards, fila completa com idade/quem pegou/votos, ação Decidir/Resolver e desempenho por juiz, via review/{list,stats,resolve}), Conflitos e a config do veredicto manual (opções + matriz). O alerta de conflito (banner + bip) é global (shared/chief-alert.js): segue o chief/admin em qualquer página do contest e abre a fila já filtrada em conflitos.

Juiz .judge, juiz-chefe .cjudge & veredicto manual

Papéis (sufixo no login; ver lib/auth.sh): .judge submete a qualquer hora (fora do placar/estatísticas), responde clarifications e cria avisos; .cjudge (juiz-chefe) herda o juiz (is_judge vale p/ ele) + extras escopados (is_chief): editar notícias/respostas já dadas, ver Situação e Todas Submissões (mesmas ops do admin), resolver conflitos e editar a config de auto-veredicto — não é admin pleno. .cjudge está nas quatro listas de sufixo (auth/score-common/stats-gen/login) p/ ficar fora do placar e isento da janela de login.

Veredicto manual (opt-in por contest, MANUAL_VERDICT): quando ligado, o daemon segura o veredicto computado p/ revisão humana — grava contests/<c>/review/<id>.json e deixa o history provisório (o aluno segue vendo "julgando"); a exceção é a matriz auto-verdicts.json (problema × linguagem × veredicto, editável por admin/chief) que libera combinações automáticas. O casamento da matriz é pelo veredicto canônico (verdict_canon, sem o sufixo de score ,Np que o juiz embute), e erros de juiz (Judge Error/No_Servers) também são segurados — o competidor vê só Not Answered Yet (nenhuma mensagem de erro vaza); o juiz vê o erro no painel e re-julga. Dois .judge pegam a submissão (máx 2, 1 ativa por juiz, TTL 5 min com +5, ou desistir), veem log + fonte + veredicto computado (a tela não recarrega enquanto se avalia) e escolhem um veredicto de uma lista configurável (final-verdicts.json, {label,verdict}; default = as 6: 1-YES…6-Contact staff). O voto é permanente e libera o juiz na hora (ele já pode pegar outra submissão). Dois no mesmo → vai ao aluno; diferentes → conflito, que só o juiz-chefe resolve (avisado pelo alerta global de conflito em qualquer página). A liberação enfileira setverdict, consumido pelo daemon e finalizado pelo escritor único (update_history + results/<id>.json), então o veredicto manual entra no timeline de auditoria como qualquer outro. TUDO é auditado (clar-*, news-edit, final-/ auto-verdicts-set, review-claim/extend/giveup/vote/agree/conflict/resolve, verdict-held/released). Mudou o daemon → reinicie-o (mantendo INTAKE_MODE/JUDGE_BACKEND).

  • /contest/staff/Impressão (.staff): o usuário .staff opera o balcão de impressão de uma sede. Ao logar é redirecionado para cá (não acessa a home do contest). Não submete (sem home de contest nem clarifications); vê o placar como usuário normal (congela no freeze) e a fila de tarefas recebidas, filtrada pela sua lista de regex (sedes distribuídas; lista vazia = vê tudo; o admin configura na aba Impressão). Fluxo: pegar (claim, evita impressão dupla entre abas) → imprimir o PDF gerado pelo servidor (pr_build_pdf em lib/print.sh: capa+documento normalizado em A4 via paps/magick/ libreoffice+pdfunite, build-once com cache; código sai com linhas numeradas) → entregue. A folha de rosto (raster, letras garrafais via caption: auto-ajustável que sempre cabe) traz o nome do time/participante (+ universidade) e o login, o nº sequencial (conferência), o nº de páginas do documento (exceto a capa) e um campo assinatura + hora. Há modo automático: a aba reserva, imprime (iframe + window.print()) e marca processada ao detectar a impressão (onafterprint); para impressão sem o diálogo do SO, rode o navegador em kiosk. Toda operação é auditada (print-request/-claim/-served/-processed/-delivered/-download, staff-filters). O admin habilita/desabilita por conf PRINT (toggle allow_print).
    • Balões 🎈: na mesma fila do .staff, o sistema gera automaticamente uma tarefa de balão quando um time resolve um problema (veredicto Accepted) — 1 por (time, problema) na 1ª solução. Geração preguiçosa ao carregar a fila (pr_reconcile_balloons varre o history do store por-usuárioemit_history_stream sobre users/<login>/history —, dedup por id determinístico, gateado pelo mtime de var/.score-dirty, sob flock) — sem mexer no daemon; como lê o veredicto final do history, no modo manual o balão só nasce depois que os .judge decidem o Accepted. Só o .staff que enxerga aquele time (mesmo escopo regex) recebe. A folha do balão (1 página, pr_build_balloon, sem .src) traz time + universidade
      • login, o problema (letra), a cor do balão desenhada + o nome por extenso (PT + inglês padrão ICPC, ex.: "rosa (pink)", "azul-claro (light blue)"; cor por balloons.json/ default ICPC A–O, com tabela hex→nome + cor mais próxima no custom), o nº da tarefa (seq) e assinatura + hora. Reusa o fluxo pegar→imprimir→entregar e é auditado (balloon-task/-claim/-processed/-served/-delivered). Balão não aparece p/ o aluno.
    • Chefe de sede (.cstaff): papel de supervisão da sede. Usa a mesma página da fila em modo somente leitura (acompanha o escopo dele; sem pegar/imprimir/entregar — a API corta print-action/print-pdf/print-file com 403) e é o único papel além do admin que vê as 🏷️ Etiquetas de credenciais (/contest/badges, senha sempre presente — o .staff perdeu o acesso e o antigo toggle staff_password foi extinto). Vê o placar congelado como usuário normal (admin libera o completo via SCORE_FULL_USERS) e, pós-fim p/ todas as sedes, abre a cerimônia de revelação da sede (navbutton 🏆). O escopo (mesmo staff-filters.json, entradas region:<nome>/regex) governa fila, etiquetas e cerimônia — configure-o sempre (vazio = vê tudo, inclusive todas as senhas).

Auditoria: ações administrativas são logadas em contests/<c>/var/admin-audit.log (e treino/var/admin-audit.log no treino) — o contest fica auto-contido.

Juiz & daemons

Submissão assíncrona (spool + inotify), judged.sh, e julgamento pull: o daemon enfileira por prioridade (judge-gw/sched-lib.sh) e os juízes (moj-agent@) puxam o job no heartbeat, baixam o pacote sob demanda, calibram e reportam veredicto/TL por HTTP — sem master, sem push de entrada. Backends mock/local do judge.sh ficam só p/ dev/legado. Detalhes em FLOW.md e judge-gw/PULL.md.

Testes

Suítes de smoke em server/test/smoke-*.sh (cada uma sobe o router.sh com CONTESTSDIR/ SESSIONDIR de fixture e exercita os handlers de ponta a ponta). Rode todas:

cd server/test && for t in smoke*.sh; do bash "$t"; done

Compilar esta documentação em HTML

bash docs/build-html.sh     # gera docs/html/*.html + index (usa pandoc)