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/ ~30 libs: common.sh (resposta/JSON/validação/audit), auth.sh, users.sh, problems.sh,
orgs.sh, langs.sh (whitelist+CHÃO), tl-store.sh (TL+TLOVERRIDE), verdict.sh,
review.sh (veredicto manual), print.sh (fila/balões/escopo de sede),
cohorts.sh, registration.sh, contest-{create,docs,rounds,gate,offline}.sh,
webcast.sh, team-{photo,music}.sh, ua-gate.sh, telegram.sh, alerts.sh, …
handlers/ auth/ index/ treino/ contest/ problems/ orgs/ judge/ submission/ admin/ ops/
+ submit.sh (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), updatescore-*.sh (um por modo), stats-gen.sh,
report-gen.sh + relatorio-gen.sh (relatório offline), webcast-gen.sh (pacote do
telão), treino-list-gen.sh, problem-panorama-gen.sh, 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) — repo próprio
mojtools/ sandbox bubblewrap + calibração + enunciados — repo IRMÃO (não vive sob judge/)
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
router.sh: sanitiza os
segmentos (sem traversal), mapeia /a/b/c →
handlers/a/b/c.sh e faz source do handler (que
usa $REQUEST_METHOD).{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).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 / .animeitor. .cstaff =
chefe de sede (etiquetas de credenciais, fila do staff
em leitura, fotos/músicas da sede e a cerimônia dela — não
herda .staff); .animeitor =
mesa do telão (placar SEMPRE descongelado,
fotos/músicas de todos os times, as chaves de webcast e as estatísticas
— não submete, não vê enunciado, e não é coberto por
is_judge: os handlers testam
is_judge || is_animeitor explicitamente). Cada papel tem
tutorial próprio com screenshots em
/contest/ajuda/<papel>.html (pt/en), alcançável pelo
botão 📖 Como funciona este papel na tela dele; os manuais de
usuário são MANUAL-{STAFF,JUIZ,ANIMEITOR}.md. Login
SEM sufixo é o COMPETIDOR, e ele também tem o seu —
/contest/ajuda/competidor.html, pelo botão 📖 Como
funciona a prova da página da prova (só p/ quem não tem papel:
organização e juiz já têm o deles), com o manual longo em
MANUAL-CONTEST.md. Índice dos seis:
/contest/ajuda/. As screenshots saem de
server/bin/shots-ajuda.sh (contest fictício + router de
verdade).<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.contests/<c>/conf é sourced — por isso a
criação de contest escreve tudo com printf %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 — idioma pelo LOCALE do contest
› ?lang= na URL › seletor do header ›
navegador; o ?lang= é o que faz um link mandado por e-mail
abrir na versão certa), 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.
moj-problems/A árvore de pacotes (23 GB, 1402 problemas) é
território da gestão de problemas. Só chegam nela: as rotas de
autoria (handlers/problems/**,
handlers/orgs/**), as de conversa com os
juízes (handlers/judge/** — o juiz baixa o pacote
e devolve TL/veredicto para run/), as de ops de
admin (handlers/ops/**) e os utilitários de
server/bin/.
Contest e treino ficam do lado de fora: respondem do
que o servidor já materializou — o índice de donos
(contests/treino/var/problem-owners.json), o json servível
(var/jsons{,-private}/<id>.json, que já traz
enunciado renderizado, autor completo, tags, linguagens e o TL com
TLOVERRIDE aplicado), o run/tl/, e os
enunciados do próprio contest
(contests/<c>/enunciados/). Rota de prova não abre
pacote por três motivos: latência (é I/O de gigabytes
no caminho mais polado do dia), contenção (autores
commitam durante a prova) e acoplamento ao formato
interno, que é da gestão e muda por conta dela.
O que acontece quando a fronteira é atravessada tem nome e número: o
/contest/problems chamava tl-checksum.sh por
problema, que lê o conteúdo de tests/ —
112,8 MB hasheados a cada regeração do cache, e 2,0 s de carga a frio,
só para exibir tempo-limite. A regra e o inventário dos resíduos estão
em cdmoj/CLAUDE.md; o inventário é executável:
bash server/test/sem-pacote.sh.
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.
Três mecanismos de autoria em volta da calibração/TL (2026-08-19):
calibreitor grava um vetor estruturado
(pkg/.calib-sols.json) que o juiz sobe no
/judge/calib-report e o GET /problems/calib
serve por host:
[{file,lang,category,verdict,tests:[{name,code,time,tl}]}]
— o mesmo formato do vetor tests de uma
submissão. O editor o renderiza no cartão de cada juiz, e a CLI o
entrega cru (moj calib --json) para ferramentas
externas.TLOVERRIDE[<lang>|default] no conf do
PACOTE: o autor decide o TL na marra. O efetivo
(override[lang] // override[default] // calibrado) vence
no julgamento (o juiz aplica depois dos
TLMOD) e em toda exibição (treino,
contest, folha de TL, /problems/tl, que mostra os três
valores). O servidor lê o conf por grep — conf de autor
é código, nunca é sourced fora da jaula.POST /problems/test-run,
moj testrun): o autor roda uma solução avulsa no
juiz real — mesma jaula, mesmo TL — sob o contest sentinela
_testrun; o ingest_result do daemon desvia o
resultado para run/testrun/ e nunca toca
history/metrics/placar. Gate de edição no problema, teto de fonte, rate
por login e TTL de 7 dias.A aba 🕘 Histórico do editor é o git de cada
problema (/problems/history, download?sha=,
restore — que grava um commit NOVO, nunca reescreve
história).
Para prova em rede isolada, o moj-comp (CLI do
competidor) assina pacotes sem rede (RSA-OAEP+AES, com
um beacon assinado que carimba o horário) e o organizador os
injeta depois por POST /contest/offline-submit (máx 50 por
vez). O servidor valida envelope, login, contest, monotonicidade do
horário reivindicado, dedup por sha256 e a whitelist de
linguagem — e contabiliza no horário
reivindicado, não no da chegada. Ver
lib/contest-offline.sh,
handlers/contest/{offline-submit,beacon}.sh.
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).
Contest que usa as contas do treino (USERS_FROM=treino)
pode exigir inscrição prévia:
contests/<c>/registrations.json — existir =
ligado (mesma doutrina do cohorts.json). Motor em
lib/registration.sh; a pessoa se inscreve pela página
/contests/inscricao/?c=<id> do site
principal (o token é por ORIGEM: o subdomínio do contest não
enxerga a sessão do treino), individual ou em time de até 3
contas existentes (convite + aceite do convidado). A
janela vem do conf
(REG_OPEN/REG_CLOSE e
REG_LATE_MINUTES p/ a entrada atrasada, que cai numa coorte
unranked — a extra registration do Codeforces) e
ancora na PROVA OFICIAL, não na rodada corrente
(reg_official_window): o AQUECIMENTO pode ficar dias no ar,
mas por DEFAULT só inscrito entra em QUALQUER rodada,
aquecimento incluso (decisão 2026-08-04) —
REG_WARMUP_OPEN=y no conf é o opt-in que
reabre a porta no warmup a qualquer conta da fonte
(reg_gate_active). Só nesse caso a
promoção faz sentido varrer:
reg_sweep_unregistered derruba a sessão de quem não se
inscreveu (sessão não expira sozinha) e apaga o diretório vazio dele, p/
o placar da prova não nascer com linhas zeradas de quem só passeou no
esquenta. A porta é a API:
handlers/auth/login.sh recusa quem não está no roster (403
not_registered) e passou a valer
LOGIN_ENABLED/LOGIN_START_TIME, que antes só
existiam no desenho da tela.
O time é uma conta local do contest
(users/time-<slug>/, senha desativada) e o membro
entra com a própria credencial do treino: o login faz o
alias (sessão = time, ACTOR = a pessoa),
então placar, balões, impressão e clarifications continuam vendo uma
linha só, sem nenhuma mudança neles. Cada mudança no roster
materializa o store (overlay account.json
sem senha p/ o inscrito — a credencial segue sendo a da fonte) e semeia
as coortes individual/times, que têm
placar próprio (ranking:true em
cohorts.json ⇒ var/placar-view-<id>.txt,
seletor no /contest/score/). Admin: painel Pessoas
› Inscrições + item no checklist pré-prova.
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.
server/bin/user-merge.sh <c> <de> <para> [--apply]
funde um dir de usuário órfão (sem
account.json) na conta viva — history ordenado por epoch,
arquivos de submissão, metrics_recompute,
.score-dirty e o login do var/editor-log; o
resíduo vai p/ var/merged/ (nada é apagado) e os logs de
auditoria (access.log, activity-*.log) ficam
como estão. Órfão desses só nascia pelo furo da sessão que sobrevivia ao
rename (ver API.md, "Invariante da sessão"); a ferramenta segue valendo
p/ qualquer resíduo. 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. O mesmo outbox
carrega DM dirigida a UMA pessoa
(alert_dm: o produtor resolve o chat, o item vai com
group:false p/ não vazar no grupo e loud:true
p/ notificar) — é assim que o convite de time avisa o
convidado na hora e cutuca quem não respondeu na véspera
(lib/invite-notify.sh; o poll do bot continua sendo o
relógio, agora com stamp próprio). E carrega também a mensagem
só para o grupo (alert_group:
chats:[] + group:true) — é por ela que sai o
relatório de quartil (lib/relatorio.sh +
POST /ops/relatorio, comando /relatorio do
bot, gate .admin pelo telegram_id): top-10 de
contests por submissões no semestre + treino + comparações com o ano
anterior, automático ao vencer cada quartil.
/treino/criar/) — wizard multi-etapaPermissã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).
<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ú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 tem fonte única
em lib/langs.sh (effective_problem_langs), com
quatro níveis (mais-específico-vence): problem-langs.json
do contest › LANGUAGES do conf › languages do
PACOTE › PLATFORM_LANGS (as 17 de
mojtools/lang/). É FORÇADA no
/submit (400 lang_not_allowed) —
lista vazia = o CHÃO da plataforma, nunca "qualquer extensão"; o
dropdown (shared/languages.js) é só conveniência. 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/ — o painel do
organizador. O admin.js é só o SHELL (~130
linhas): 4 grupos (🏁 Central · 🧩 Prova · 👥 Pessoas ·
🎛️ Operação) × painéis, hash #grupo/painel,
ALIAS de TODOS os hashes antigos das 13 abas planas
(#settings→central/regras,
#log→pessoas/sessoes,
#backups→operacao/auditoria, …) e a guarda de
admin. Cada painel é um módulo
web/contest/admin/<nome>-tab.js com o MESMO contrato
{panel, load} — construído uma vez e só
escondido (mantém filtros e timers), e reusável fora do painel (o
chief.js monta Documentos e Rodadas assim). Helpers comuns
(CSV, download autenticado, formatação, field/chk) em
shared/admin-ui.js. A 🏁
Central é a porta de entrada: Falta para começar (o
preflight como lista ACIONÁVEL — cada item com botão que
abre o painel exato, mapa id→painel no front),
Gerar (cartões dos artefatos com o estado atual), Ao
vivo (resumo do dashboard, o único bloco com
auto-refresh — re-renderiza só ele p/ não apagar o que está sendo
digitado) e Regras da prova (janela/freeze inline). Os painéis:
👥 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, para o telão); 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); Central ›
Regras (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, com
os campos redistribuídos em 5
<details> por assunto
(settings-tab.js realoca os nós VIVOS do editor por índice:
mexeu na ordem do editor, ajuste o mapa GROUPS lá);
desmarcar o secreto exige digitar o id; + a prorrogação
por sede); 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"); Balões
(balloons-tab.js — a cor de cada letra, que o staff imprime
na folha do balão; o default cobre A–O) e Sedes &
escolas (sites-tab.js — as SEDES (nome + regex),
que alimentam filtro do placar/escopo do staff/etiquetas/gate por sede,
e países/escolas com preview de matches + import/export JSON +
template dos sem match; era a antiga aba "Aparência", onde sede
ficava perdida junto de cor de balão); Contas +
Sessões
(users-tab.js/sessions-tab.js:
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)
lib/contest-rounds.sh +
handlers/contest/admin/rounds.sh — aquecimento
(dress rehearsal) e prova oficial no MESMO contest: mesma URL,
mesmo login, config intacta. rounds.json é o PLANO (janela
+ lista de problemas por rodada) e a rodada ATIVA continua sendo
o conf — nada no caminho quente (placar, gates,
daemon) fica "consciente de rodada". Promover =
arquivar + zerar + reapontar, sob flock, com checklist que
RECUSA enquanto houver job em voo / veredicto pendente / review aberto
(um job que atravessasse a troca usaria o CONTEST_START
novo e o ingest_result recriaria a submissão do aquecimento
no history da prova). O arquivo rounds/<slug>/ guarda
tudo p/ auditoria — inclusive o site estático do
report-gen, que segue navegável em
/contest/round (publicável p/ os times) — e o
.seq da impressão, os balões e a prorrogação por sede
voltam ao zero. Config (contas, senhas, sedes,
staff-filters.json, cores, TL, templates de documento)
sobrevive); Coortes
(lib/cohorts.sh +
handlers/contest/admin/cohorts.sh — times oficiais ×
CONVIDADOS (extra-oficiais/"CCL"): coorte privada não
entra no placar público nem no /contest/teams, os regulares
não sabem que ela existe, os convidados veem todos, e
release libera tudo. build.sh gera um
placar por VISÃO (var/placar-view-<id>.txt)
porque a ESTRELA de first-to-solve é um mínimo global sobre
sc_users — cortar só no TXT pronto daria estrela de
problema que, para aquela visão, ninguém resolveu primeiro. Convidado
entra intercalado sem consumir posição oficial (coluna
guest). Ver docs/SCOREBOARD.md);
Máquinas
(handlers/contest/admin/machines.sh — time × IP × UA
agregado do var/access.log pela janela da rodada: é no
aquecimento que os times ligam as máquinas, então é ali que se descobre
de onde cada um vem, quem divide IP e, na prova, quem trocou de
máquina; alimenta a sede do time e o gate de UA pelos endpoints
que já existem); Documentos
(web/contest/admin/docs-tab.js — os três documentos
impressos da prova em PDF+HTML × pt/en: info
sheet (versões de compilador do run/registry,
memória/pilha do conf, TL calibrado, linguagens),
caderno (capa + enunciados; usa o PDF
próprio do problema quando existe e junta com
pdfunite, capa regerada no fim com o total real de páginas)
folha de time limits e EDITORIAL (a
solução de cada problema, lida do docs/solucao.md do
pacote; publicável só depois do FIM da prova; times só veem
caderno/times publicados A PARTIR do início — a sede vê antes, para
imprimir); motor em lib/contest-docs.sh, PDF por
soffice — o único engine da imagem. Capa em 3
modos: PDF enviado › markdown editado (marcadores
{{…}}) › gerada. Publicar escreve o
resources.json (seção "Prova" do contest) e libera o
download p/ a sede (.cstaff, página
/contest/docs/), opcionalmente criando a notícia
com o PDF anexo; antes de publicar o caderno só existe p/
admin/.cjudge — /contest/doc responde
404. A MESMA aba é montada na página do juiz-chefe);
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).
.judge/.mon veem a lista
anônima (a API zera usuário/time — corte no servidor,
não na UI)./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..judge, juiz-chefe .cjudge & veredicto
manualPapé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). N votos unânimes → 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 — e
o próprio lib/print.sh é entrada do cache,
então um deploy que mude o desenho do papel refaz o pedido antigo
sozinho) → entregue. Código-fonte (o
caso mais comum da sala) passa por _pr_text2pdf:
iconv -c p/ UTF-8 → nl -ba → paps
(PostScript, nunca --format=pdf: a imagem
tem paps 0.6.8) → ps2pdf — monoespaçado, indentação
preservada, linhas numeradas e linha comprida QUEBRA em
vez de ser cortada. Toda página de código se
identifica: no alto, o --header do paps (data +
nome do arquivo + página); no rodapé, um selo A4
transparente (magick xc:none) carimbado em todas
as páginas por qpdf --overlay --repeat=1 com login
do time + arquivo + nº da tarefa — a folha que se solta da capa
continua identificada. (O login não cabe no cabeçalho do paps: a fonte
dele é fixa e um título de mais de ~14 caracteres sobrepõe a data.) Selo
ou conversão que falhem degradam — sai o PDF sem
rodapé, nunca deixa de imprimir. 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).
.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ário — emit_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
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. Balão × FREEZE
(default: RETÉM): acerto com
sub_epoch >= FREEZE_TIME não vira
tarefa — o balão atravessando a sala conta à plateia exatamente
o que o placar congelado esconde (o vazamento é físico,
não de rota: o competidor nunca recebe balão por API). A supressão é
gravada em print-requests/.balloon-frozen (JSONL, auditado
balloon-frozen) e a lápide é permanente —
sem ela, o "Encerrar evento" (que zera o FREEZE_TIME)
reativaria o gate e TODOS os suprimidos nasceriam de uma vez, bem na
geração do relatório final. A chave é o sub_epoch
da submissão, nunca o instante do veredicto (mesma semântica do
placar: em modo manual, um AC enviado antes do freeze e julgado depois
seria retido por engano). O opt-in do clássico ICPC é
BALLOONS_DURING_FREEZE=1 no conf (toggle
balloons_during_freeze), e ligar libera
retroativamente os retidos. Impressão não é
afetada pelo freeze..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). Baixa também os 📄 Documentos
publicados da prova (/contest/docs/) para imprimir
na sede./contest/docs/ — Documentos
da prova (só-leitura): a mesma aba do admin em modo leitura,
montada para .staff/.cstaff (e qualquer login
do contest) — lista o que foi publicado e baixa/abre o
PDF ou o HTML para imprimir na sede. Quem gera e publica é o admin ou o
juiz-chefe; o gate é da API (/contest/doc sem publicação =
404).
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 (particionável em K workers por hash(login) —
JUDGED_SHARDS, ver FLOW.md §2), 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)