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 apenasCo-Authored-By:— nunca uma linhaClaude-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.shrefaz o HTML.
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
- Roteador único
router.sh: sanitiza os segmentos (sem traversal), mapeia/a/b/c→handlers/a/b/c.she fazsourcedo handler (que usa$REQUEST_METHOD). - Envelope JSON consistente:
{success:true, …}ou{success:false,error:{message,code}}, sempre com o status HTTP correto (fail/ok_jsonemlib/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 emrun/sessions/(modo 700), gravadas comprintf %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 injetaCONTEST_HOST; orouter.shsó 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 comprintf %q.
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.
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/TOC — Fila &
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}.
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.
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.b →
org#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.
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).
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úblico — score/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 (.teamdo 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 denome— 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 comusers_fromfica 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 MESMOsettings-editordo 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/queuecom 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 (oreview/listsó 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 oCONTEST_JUDGESdo 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>.adminexistente é reutilizado).
- sessões com alerta de UA/IP diferente, deslogar por UA, log de acessos + backups dos
usuários);
Tarefas do staff (
/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, viareview/{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.
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.staffopera 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_pdfemlib/print.sh: capa+documento normalizado em A4 viapaps/magick/libreoffice+pdfunite, build-once com cache; código sai com linhas numeradas) → entregue. A folha de rosto (raster, letras garrafais viacaption: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 confPRINT(toggleallow_print).- Balões 🎈: na mesma fila do
.staff, o sistema gera automaticamente uma tarefa de balão quando um time resolve um problema (veredictoAccepted) — 1 por (time, problema) na 1ª solução. Geração preguiçosa ao carregar a fila (pr_reconcile_balloonsvarre o history do store por-usuário —emit_history_streamsobreusers/<login>/history—, dedup por id determinístico, gateado pelo mtime devar/.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.judgedecidem oAccepted. Só o.staffque 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.
- 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
- 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 cortaprint-action/print-pdf/print-filecom 403) e é o único papel além do admin que vê as 🏷️ Etiquetas de credenciais (/contest/badges, senha sempre presente — o.staffperdeu o acesso e o antigo togglestaff_passwordfoi extinto). Vê o placar congelado como usuário normal (admin libera o completo viaSCORE_FULL_USERS) e, pós-fim p/ todas as sedes, abre a cerimônia de revelação da sede (navbutton 🏆). O escopo (mesmostaff-filters.json, entradasregion:<nome>/regex) governa fila, etiquetas e cerimônia — configure-o sempre (vazio = vê tudo, inclusive todas as senhas).
- Balões 🎈: na mesma fila do
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.
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.
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"; donebash docs/build-html.sh # gera docs/html/*.html + index (usa pandoc)