Base: /api/v1. Roteador único:
server/api/v1/router.sh →
handlers/<rota>.sh. Auth:
Authorization: Bearer <token>. Respostas JSON com
envelope {success:true, …} ou
{success:false, error:{message,code}} + status HTTP
correto. Histórico e placar são TXT cru. Horários em
EPOCH. IDs validados contra path-traversal.
| Rota | Método | Auth | I/O |
|---|---|---|---|
/auth/login?contest=<c> |
POST | — | body {username,password} →
{token,logged_in,username,name,contest,server_utc}.
Contest (≠ treino) inclui o kit da submissão
OFFLINE do moj-comp:
offline_pubkey_pem (pública RSA-4096 do contest, gerada
lazy em contests/<c>/secrets/) e beacon
(carimbo de tempo assinado — ver /contest/beacon).
server_utc permite à CLI medir o desvio do relógio
local. |
/auth/status?contest=<c> |
GET | Bearer | {logged_in,login,name,contest,is_admin,is_judge,is_staff,is_cstaff,is_chief,has_photo}
(.cjudge = juiz-chefe →
is_judge:true,is_chief:true; .cstaff = chefe
de sede → is_cstaff:true, sem herdar
is_staff). has_photo existe
p/ o avatarEl NÃO pedir a foto de quem não tem: o avatar do
cabeçalho aparece em toda página e cada 404 desses é um fork de bash
(5.712 no dia 24/08/2026, 54% de todos os 404). Mesmo campo em
/index/open_training (top_users[] e
recent_solved[].user) e em
/treino/problem-stats |
/auth/logout |
POST | Bearer | {logged_out:true} — apaga o arquivo de sessão
mesmo se ela já não vale (senão o zumbi ficava p/
sempre no store) |
A porta do contest (/auth/login,
forçada pela API — o countdown do front é só conveniência):
LOGIN_ENABLED=n → 403
login_disabled; antes de
LOGIN_START_TIME → 403
login_not_open; com INSCRIÇÃO ligada
(contests/<c>/registrations.json existe) quem não
está no roster leva 403 not_registered
(janela ainda aberta),
registration_not_open ou
registration_closed — aquecimento
INCLUSO (default): só inscrito entra em qualquer rodada.
REG_WARMUP_OPEN=y no conf restaura a porta aberta durante
rodada warmup (opt-in); nesse caso a promoção da oficial
derruba a sessão de quem não se inscreveu
(reg_sweep_unregistered) e apaga o diretório vazio. Conta
de PAPEL (.admin/.judge/.cjudge/.staff/.cstaff/.mon) nunca
é barrada. Alias de TIME: se o login é membro de um
time inscrito, a credencial é a DELE mas a sessão é do
time — a resposta traz actor (quem
digitou) e is_team:true, e /auth/status,
/contest/userinfo, o access.log (5ª coluna) e
o var/actor-log guardam o ator. Ver
lib/registration.sh.
Invariante da sessão: o token só continua valendo
enquanto a CONTA existir (users/<login>/account.json
do contest da sessão ou, com USERS_FROM, o da fonte
compartilhada). Conta renomeada, removida ou contest apagado ⇒
401 auth_required na primeira requisição,
e o cliente cai no login. Sessão do MOJ não expira por
tempo: sem essa checagem uma sessão aberta antes de uma troca de handle
seguia autenticada com o login VELHO e o /submit (que faz
mkdir -p no dir do usuário) recriava o diretório do
nome antigo — resíduo sem account.json que ainda
aparecia como "solver" nas estatísticas. Ver lib/auth.sh
(_session_account_alive) e
server/bin/user-merge.sh (conserto do resíduo).
| Rota | I/O |
|---|---|
/index/news |
{news:[{id,title,date,summary,url}]} |
/index/contests?page=N |
{open:[…],upcoming:[…],closed:{items:[…],page,per_page,total}}
(cada item
{id,title,start_time,end_time,problems_count,url,scoreboard_url}
— problems_count é 0 em contest
upcoming: contest por vir não revela a quantidade
de problemas, mesma regra do placar pré-início +
registration:{opens_at,closes_at,late_until,url}
quando o contest tem INSCRIÇÃO ligada — o cartão do front decide
"aberta/atrasada/encerrada" pelo relógio do cliente, sem duplicar a
regra do lib/registration.sh). Encerrados paginados
(20/pág); ?all=1 devolve todos (usado pela
página de arquivo /contests/). Contest SUPER
SECRETO (conf SECRET=1) não
aparece em nenhuma das três listas (nem no
/index/status, que também omite o nome na fila por
lista). |
/index/open_training |
{top_users:[…],recent_solved:[…],most_solved_week:[…],most_solved_prev_week:[{problem_id,problem_title,solved_count,url}],most_used_editor_prev_week:{top:{editor,count}|null,total,ranking:[{editor,count}]}}
(prev_week=resolvedores distintos por problema;
editor=mais usado nas aceitas da semana passada,
web ou editor declarado; perfil PRIVADO não
entra em top_users/recent_solved — o
filtro pula p/ o próximo). Cache var/open-training.json
por EVENTO (.score-dirty OU
.treino-list-dirty mais novos = regenera; despublicado some
da home; piso 5 min sob rajada; flock) |
| Rota | Auth | I/O |
|---|---|---|
/treino/problems |
— | array
[{id,title,tags,collections,solved_count,attempted_count,public_at?}]
(collections = .moj-meta.json do pacote, um
problema pode estar em várias; public_at = epoch da 1ª
publicação, vindo do índice de donos — AUSENTE quando desconhecido;
alimenta a ordenação "Novidades" do treino). Contagens do STORE
NOVO: agregação de users/*/metrics.json
(.solved/.attempted, 1 usuário = 1 por
problema), sobreposta à base legada var/json-count/ quando
existir. Cache var/problems.json invalidado POR
EVENTO (gerador server/score/treino-list-gen.sh):
composição da lista = stamp var/.treino-list-dirty
(foreground sob flock); contagens = var/.score-dirty + piso
de 10 min (refresh em BACKGROUND, serve o stale); TTL de 60 min só rede
de segurança |
/treino/trending |
— | top-10 problemas por submissões (todas) nos últimos 7
dias (janela móvel), p/ o estado inicial do Treino Livre:
{success,window_days:7,generated_at,problems:[{id,title,count,url}]}
(ordenado por count desc). Anônimo;
problema privado NÃO entra (_private: json só em
jsons-private/). Cache var/trending.json por
EVENTO (.score-dirty/.treino-list-dirty) com
piso LONGO de 6h (varre o history de ~927 contas; a
janela é semanal) + flock |
/treino/problem?id=<id> |
— | {id,title,author,statement_html_b64,time_limits,tags,collections,languages}
(author = arquivo author do pacote, verbatim;
vários autores juntados por , ; vazio se ausente;
collections = coleções do .moj-meta.json;
languages = ids de linguagem de submissão permitidos deste
problema, [] = todas as PADRÃO — o front filtra o dropdown
por essa lista; linguagens EXÓTICAS/opt-in
(pddl/grepe/sas/l/lpp/downward)
só aparecem quando o problema as DECLARA aqui). Registra
problem-view no log de atividade
(load_session soft: Bearer presente = login, sem =
anon; a rota segue pública) |
/treino/admin/activity-log |
GET .admin |
feed COMPLETO do treino (6 fontes no instante
exato): login (access.log) · submit (history)
· verdict (results finalized_at) ·
read (activity-YYYY-MM.log:
problem-view/log-view/source-download)
· admin (admin-audit sem ruído de máquina) ·
calib (tl-report/calib-report, EXCLUÍDO por default —
?kinds=calib inclui). Filtros
since/until (epoch), kinds (csv),
user, action, limit (≤5000).
format=csv = download do range INTEIRO
filtrado (Content-Disposition; cabeçalho
epoch,datahora,tipo,quem,acao,detalhes,ip) p/ análise
externa. Aba 📜 Atividade do admin do treino |
/treino/solvetry?user=<u> |
opc | {solved:[ids],attempted:[ids]} |
/treino/history?id=<id> |
Bearer | TXT 7 campos
tempo:user:probid:lang:verdito:epoch:subid. Veredicto
SEMPRE canônico (lib/verdict.sh;
pendentes/strings desconhecidas intactos) — o detalhe
(testes/pontos/grupos) vem do /submission/summary |
/treino/history-full?user=<u> |
opc | TXT 7 campos (todo o histórico). Veredicto canônico (idem acima) — visitante do perfil público vê só o rótulo, sem resumo (summary é só do dono) |
/treino/profile |
Bearer | GET: perfil + cota de username +
telegram:{linked,username,linked_at}
(vínculo do próprio login; sem o telegram_id) · POST
{name?,university?} |
/treino/profile/password |
Bearer | POST {old_password,new_password} |
/treino/profile/username |
Bearer | POST {new_username} →
{updated,new_username,username_changes_used,username_changes_remaining,sessions_updated}.
máx. 2/ano, cascata nos arquivos de controle
incluindo as SESSÕES
(rename_contest_sessions: TODAS as sessões daquele login —
outra aba, outro dispositivo, token do moj-cli e as de
contests que herdam os usuários via USERS_FROM — passam a
valer com o nome novo; sessions_updated = quantas; ninguém
é deslogado) e as ORGs (orgs_rename_login:
o login troca em members/admins de todas; o NOME da org — inclusive a
implícita antiga, que vira comum — não muda: é o prefixo dos ids). O
owner carimbado nos problemas mantém o login histórico
(como autor de commit) — o ACESSO vem da org, que segue o rename.
Sufixo de papel é PRESERVADO:
sufixo(novo)==sufixo(atual) — .admin troca p/
outro.admin (400 uname_role_suffix se tentar
derrubar o sufixo; uname_reserved se usuário comum tentar
assumir um) |
/treino/profile?user=<u> |
opc | GET visão pública (respeita privacidade):
{login,name,university,favorite_editor,has_photo,is_public,created_at}
(created_at=epoch de criação da conta, p/ o "membro desde"
do perfil; a visão do dono também o traz — que inclui ainda
managed:{minor,by,birthdate,note,expires_at}|null p/ conta
GERIDA); POST aceita também favorite_editor,
profile_public (400
managed_minor se conta gerida de menor tentar
tornar público). Conta gerida de MENOR é sempre privada
(profile_is_public corta perfil/foto/history/listas da
home); link-start do Telegram → 403
managed_minor; login com
.managed.expires_at vencido → 403
account_expired |
/treino/contest-registration?contest=<c> |
Bearer | INSCRIÇÃO do próprio login num contest que usa as
contas do treino (USERS_FROM=treino): individual ou em TIME
de até REG_TEAM_MAX (3) contas EXISTENTES. Fica AQUI (e não
no contest) porque o token é por ORIGEM — <id>.moj…
não enxerga a sessão do treino. GET →
{enabled, contest, contest_name, start_time, end_time, window:{state:soon|open|late|closed,opens_at,closes_at,late_until,official_start,official_round}, round_kind, gate_active, team_max, teams_allowed, me:{kind:none|individual|team,team?,cohort?,univ?,ai?,flag?}, team:{login,name,captain,members[],invited[]}|null, invites:[{login,name,captain,members}], totals}.
POST {contest,action}:
register {univ?,ai?,flag?} (a
meta pode vir junto; flag inválida = 400 SEM inscrever) ·
individual-meta
{univ?,ai?,flag?} (inscrito INDIVIDUAL declara/edita
universidade/IA/bandeira — paridade com o team-meta; vai p/
a entry do roster e materializa no .team do overlay, então
placar/🤖/bandeira funcionam igual ao time) ·
team-create
{name,univ?,ai?,flag?} ·
team-invite {login} (o
mojinho manda DM ao convidado na hora, com o link
/contests/inscricao/?c=<c> de aceitar/recusar —
lib/invite-notify.sh; best-effort: sem Telegram vinculado o
convite vale igual) ·
team-accept/team-decline
{team} · team-rename
{name} · team-meta
{univ?,ai?,flag?} (capitão: a universidade vai em
.team.univ_short — o renderer do placar exibe "[SIGLA]
Nome"; flag = país ISO-2 ou estado br-xx →
.team.flag, a bandeira do placar, 400
flag_invalid se não casar; ai = `yes |
/treino/profile/photo?user=<u> |
opc/Bearer | GET serve png 100×100 · POST {image_b64}
(redimensiona) |
/treino/editors |
— | ranking dos editores favoritos declarados
{editors:[{editor,count}],total} |
/treino/achievements |
— | registro de CONQUISTAS do perfil
{custom,version,achievements:[{id,icon,pt,en,kind,params,enabled}]}
— serve var/achievements.json (gerido pela aba 🏅 do admin)
quando válido, senão o default embarcado
(lib/achievements-default.json); avaliação é no CLIENTE.
Kinds e formato: PERFIL.md |
/treino/problem-stats?id=<p> |
— | estatísticas do problema (métricas, veredictos, por-linguagem c/
solvers distintos, editores, avatares públicos) + séries
temporais (fuso America/Sao_Paulo):
daily{YYYY-MM-DD:n} (heatmap),
monthly[{m,subs,ac}],
dow_hour[{dow 0=dom,hour,n}],
first_ac_epochs[] (curva de resolvedores),
tries[{bucket,n}]+tries_median (subs até o 1º
AC), time_to_solve[{bucket,n}]+t2s_median (1ª
sub→AC),
facts{first_sub_epoch,last_sub_epoch,peak_day,first_solver{epoch,login?,name?}}
(login/nome do 1º solver SÓ se perfil público),
difficulty_percentile{harder_than_pct,cohort,success_rate}
(taxa de sucesso POR USUÁRIO vs. o acervo público do
var/problems.json; ranking com suavização de Laplace +
midrank — coorte pequena 100% não esmaga a ponta fácil; elegível = ≥5
tentantes, null se coorte <10), runtimes[{lang,t}]
(estilo Kattis: t = teste mais LENTO de cada submissão ACEITA, dos
results/<subid>.json — só era newmoj) — cache
por EVENTO (.score-dirty mais novo =
regenera; sem submissão nova vale p/ sempre; piso 2 min sob rajada;
flock) |
Cadastro web-first verificado pelo Telegram (1
Telegram = 1 conta; anti-duplicata). Os endpoints
verify/telegram/recover-password
são autenticados pelo token do bot
(Authorization: Bearer mojb_…, require_bot,
segredo em run/secrets/bot.token) — o bot
não loga como .admin.
| Rota | Auth | I/O |
|---|---|---|
/treino/signup/start |
público (POST) | {login?,fullname,university?} →
{nonce, deep_link, expires_at}. Valida o login (bloqueia
sufixo de papel) e cria um nonce (TTL 15 min). Não cria
conta. |
/treino/signup/status?nonce= |
público (GET) | {status: pending|created|already_linked|linked|expired, login?}
— nunca devolve a senha |
/treino/signup/verify |
bot (POST) | {nonce,telegram_id,telegram_username?,first_name?,last_name?}
→ consome o nonce (uso único), anti-duplicata, cria+vincula
(created) ou vincula conta logada (linked);
devolve {status,login,password?} (senha só p/ DM) |
/treino/signup/telegram |
bot (POST) | bot-first (/participar): {telegram_id,…} →
cria+vincula ancorado no telegram_id (idempotente) ou
already_linked |
/treino/recover-password |
bot (POST) | {telegram_id} → resolve o login pelo vínculo, gera nova
senha → {status:ok|not_linked,login?,password?} |
/treino/telegram/link-start |
Bearer | conta logada gera nonce purpose:link p/ vincular o
próprio Telegram (ex.: .admin receber alertas) →
{nonce,deep_link,expires_at}. UI: seção 📨
Telegram do perfil |
/treino/telegram/unlink |
Bearer | POST {} — desvincula o Telegram do PRÓPRIO login (404
not_linked sem vínculo). Cota anti
conta-descartável: usuário comum desvincula no máx
TELEGRAM_CHANGE_LIMIT (1)/ano (403
telegram_limit com a data da próxima; histórico em
account.json telegram_changes); .admin
é livre. Trocar de Telegram exige desvincular ⇒ a cota cobre a
troca. A cota sai no GET /treino/profile
(telegram.changes_used/limit/remaining/next_available;
limit:null = livre) |
.admin, Bearer)Acesso registra IP
(X-Forwarded-For/REMOTE_ADDR) e
User-Agent na sessão e em
var/access.log.
| Rota | Método | Ação |
|---|---|---|
/treino/admin/sessions |
GET | sessões ativas
{count,sessions:[{login,name,ip,user_agent,login_at}]} |
/treino/admin/managed-users |
GET | contas GERIDAS (menores, sem Telegram — CONTAS-GERIDAS.md):
{users:[{login,fullname,by,note,birthdate,minor,expires_at,disabled,created_at}]} |
/treino/admin/managed-create |
POST | cria contas geridas
{users:[{fullname,birthdate,login?,note?,expires_at?}]}
(1..500; login vazio = slug do nome com dedup; sufixo de papel recusado)
→
{created:[{login,password,fullname,birthdate}],skipped:[{…,reason}]}
— senhas só nesta resposta; audit
managed-create |
/treino/admin/managed-reset |
POST | {login} (só gerida) → senha nova (user_genpass)
devolvida UMA vez + derruba sessões; audit
managed-reset |
/treino/admin/managed-update |
POST | {login, note?, birthdate?, expires_at?|null, disabled?}
— edita .managed; disabled:true =
senha-sentinela !…+derruba sessões;
disabled:false = reabilita com senha nova devolvida; audit
managed-update |
/treino/admin/managed-remove |
POST | {login} (só gerida) → mv p/
.removed-users/<login>-<epoch>; audit
managed-remove |
/treino/admin/achievements |
POST | salva o registro de conquistas do perfil:
{achievements:[…]} (valida ids únicos
[a-z0-9-], kind conhecido, params por kind; grava atômico
var/achievements.json; audit
achievements-save) ou {restore_default:true}
(remove o registro; volta ao default). Erro de validação = 400
achievements_invalid com a mensagem. Aba 🏅
Conquistas do admin do treino; doc: PERFIL.md |
/treino/admin/access-log?day=YYYY-MM-DD |
GET | log de acessos (filtra por dia) |
/treino/admin/queue |
GET/POST | pendentes por lista + calibração
{total_pending,spool_queued,calib_pending,calib_inflight,calib_targeted,lists:[{contest,name,pending}], routing, pending_details}
(routing = roteamento do ESCRITOR com shards:
{shards,workers:[{shard,alive_age_s,in_submit,in_results,in_other}],orphans,queue_depth,assigned,delivered_5m}
— alive_age_s:-1 = worker do shard nunca bateu;
orphans = arquivos em s<j> com
j>=K, mismatch de JUDGED_SHARDS entre API e
daemon) (calib_pending = fila de calibração
kind=calibrate, separada de index;
calib_targeted = recalibrações direcionadas por host).
&details=1 →
pending_details:[{contest,login,problem,lang,id,since,age_s,state,has_source}]
— CADA submissão pendente, com estado no pipeline (`no-spool |
/treino/admin/judges |
GET | máquinas de juiz (modelo pull)
{online,busy,machines:[{host,online,busy,status,langs,cage_root,cache,tl,current,current_jobs,queued_calibrate,slots,partition,topology,config,report}]}
— current_jobs = TODOS os jobs em execução (multi-slot; UM
por slot ocupado, com since = epoch do claim;
current = o 1º, compat — a UI da fila itera
current_jobs); status = auto-relato do agente
novo (ok|draining|disabled, null
= agente antigo) e busy-sem-job vira
[{kind:"draining"|"disabled"|"unknown_busy"}];
slots:{free,total}; partition = vigente no
agente; config = a config DESEJADA (judges-config) ou null;
cache = pacotes em disco do juiz (não
RAM); report.gpu = GPU de compute
comprovada ({vendor:nvidia|amd,names}) ou
null |
/ops/judge-config |
GET ?host= / POST
{host, partition?:off|numa|cpus:<X>, reserve?, disabled?} |
(admin) config fina POR JUIZ (multi-slot):
particionamento da máquina em slots com pinning, cpus reservadas e
desabilitar (drena). Vive em
contests/treino/var/judges-config.json; o heartbeat entrega
ao agente quando muda (cfg_hash) e o agente aplica após
DRENAR os jobs em andamento. CLI: moj judges config |
/ops/judge-reset |
POST {host, action?:kill|restart} |
(admin) RECUPERAÇÃO sem SSH: kill
(default) manda o agente SIGKILL-ar o grupo de processos de cada slot
(job inteiro), reportar judge-error/calib-fail (nada espera TTL) e
reconciliar a config; restart = kill + o agente se
re-executa (register boot:true re-enfileira o que estava
atribuído — fila não se perde). Entregue no próximo heartbeat MESMO com
o juiz ocupado/desabilitado. CLI:
moj judges reset/restart |
/ops/calib-cancel |
POST {id, inprogress?:false} |
(admin) cancela calibrações do problema na fila:
remove pendentes + direcionadas não entregues →
{removed_pending,removed_targeted,removed_inprogress,inflight};
as EM EXECUÇÃO só com inprogress:true (senão só contadas em
inflight — prefira judge-reset). CLI:
moj judges cancel |
/ops/judge-results |
GET ?host=&limit= |
(admin) relatório de correções por juiz: últimas N
correções (run/results/, com host/verdict/duração) +
agregado
by_host:{total,accepted,judge_errors,avg_duration,last_at}.
CLI: moj judges results |
/ops/judge-cache |
POST {host, action?:clearcache} |
(admin) limpa o cache local de pacotes de um juiz:
enfileira um comando POR-HOST que o agente pega no próximo heartbeat
(quando estiver livre), apaga o $JUDGE_CACHE e se
re-registra com inventário vazio. Não bloqueia — devolve
{action,host,cmdid,status:"queued"} e o efeito aparece no
/judge/list. Use quando um juiz ficou com pacote
velho/corrompido em cache |
/treino/admin/stats |
GET | {users,active_sessions,problems:{total,public,private},by_author:[{author,owner,total,public,private}],problems_public_by_day:[{day,count}],logins_per_day,submissions_per_day}
— contagens da plataforma (privados contados, não
listados); problems_public_by_day alimenta o mapa
de calor de entrada de públicos (data aproximada; ver
public_at) |
/treino/admin/response-stats |
GET | tempo de resposta + volume (cacheado):
{coverage, overall, per_day, by_dow_hour, subs_per_day:[{day,count}], subs_by_dow_hour:[{dow,hour,n}]}.
Tempo só de submissões com finalized_at;
volume conta TODAS as linhas do history. EPOCH/UTC |
/treino/admin/calib-activity |
GET | volume de calibrações no tempo (cacheado; do log
run/updates/log):
{calib_per_day:[{day,count}],calib_by_dow_hour:[{dow,hour,n}],total}.
run/ pode rotacionar → histórico parcial |
/treino/admin/logout-user |
POST | {login} ou {logins:[…]} → remove as
sessões (um ou vários) |
/treino/admin/lock-user |
POST | {login} ou {logins:[…]} →
trava (troca a senha por aleatória) + desloga |
/treino/admin/logout-ip |
POST | {ip} → encerra todas as sessões daquele IP
(IPv4/IPv6) |
O FORMATO do pacote (arquivos,
.moj-meta.json,.moj-id), o que são ORGs e COLEÇÕES e o ciclo validar → calibrar → publicar estão em PACOTE.md (fonte única). Aqui ficam só as rotas. Roteiro de montar um pacote:mojtools/README.md.
Backend = repo git LOCAL por problema
(MOJ_PROBLEMS_DIR/<org>/<prob>, o servidor
commita direto via problem_commit; sem serviço externo),
mas o autor só usa o login do MOJ (sem chave/git).
Listagens leem o índice de donos
contests/treino/var/problem-owners.json (gerado por
mojtools/gen-problem-owners.sh; regen em background, TTL
PROBLEM_OWNERS_TTL_MIN). O índice é a fonte
única: todo problema tem owner (login). Problema
sem dono (legado não-migrado) é ignorado no índice;
/mine = owner==login (sem casamento difuso).
Não há mais "legado".
Controle de acesso — garantido na API, NUNCA só na interface. A fronteira é a ORG: ver o source/pacote/soluções/calibração e editar/operar é p/ MEMBRO da org (
require_problem_edit=org_is_member) — sem atalho de.admin. Ver o detalhe/statement (get/validation) é membro da org ou se o problema é público (require_problem_view). Membro da org VÊ TODOS os problemas dela, inclusive privados, em toda listagem/painel (decisão 2026-07-16); problema PRIVADO não é nem LISTADO p/ quem não é membro da org nem colaborador por-problema (as listagens pré-filtram emowners_emit), inclusive p/.admin— provas em elaboração não podem vazar. Não-autorizado recebe 404 (não revela a existência). Helpers centrais emlib/problems.sh;moj-cli/curl batem na mesma API e não burlam.
| Rota | Método | I/O |
|---|---|---|
/problems/mine |
GET | {problems:[{id,title,author,owner,collections,public,html,claimed}]}
— claimed=true se owner==login, senão
"provável" (nome casa) |
/problems/shared |
GET | problemas compartilhados com o login: tudo que ele pode editar e não é dele — membro da org OU colaborador por-problema (não dono) |
/problems/public |
GET | problemas públicos (no treino livre) — visão de gestão (dono/autor) |
/problems/collection?name=<c> |
GET | problemas da coleção (curso/diretório, ex.:
obi-problems) |
/problems/collections |
GET | {collections:[{name,count,public,owner,mine,can_manage}]}
— coleções = TAGS curadas (do registro), com contagem
visível. Coleção (agrupamento, m:n) ≠ ORG (acesso, 1:1
— ver /orgs/*) |
/problems/collection |
GET ?name |
problemas de uma coleção (filtra pela tag
collections) |
/problems/get?id=<id> |
GET | detalhe: índice + validation (relatório do portão) +
statement_html_b64/tags +
time_limits (EFETIVO) /
time_limits_calibrated / tl_override.
⚠ O TL vem do pacote (tl_store_served,
override aplicado), não do json servível: o json público só existe
depois de publicar — em problema privado (o estado de
quem está calibrando) o campo sumia e o editor caía num fallback que
mostra o máximo CRU entre juízes — e
edit/upload não reindexam, então mesmo público
o número podia estar velho. O checksum vem materializado do índice,
então não há hash de pacote por request. O índice inclui
languages (whitelist de submissão do
.moj-meta.json; [] = todas as padrão) — a
gestão exibe no detalhe (badges + atalho p/ o widget do editor) |
/problems/validation?id=<id> |
GET | último relatório de validação
{checks:[{name,ok,detail}],html_built,render_warnings,ok} |
/problems/status |
GET | painel dos problemas do login
(dono+colaborador+membro da org; privado de org alheia
não aparece — owners_visible):
{total,counts:{validated,…,needs_recalibration,good_sol_no_tl,public_unvalidated,needs_review,errors},calibrating_ids,attention_ids,problems:[{id,title,owner,author,public,validated,calibrated,being_calibrated,stale,needs_recalibration,good_sol_no_tl,good_sol_missing_langs,public_unvalidated,error,needs_review,review_reasons,time_limits,time_limits_calibrated,tl_override,updated_at}]}.
time_limits é o EFETIVO (com o
TLOVERRIDE do conf aplicado — o override vem carimbado no
índice de donos por gen-problem-owners.sh, então o Painel
não abre pacote nenhum); time_limits_calibrated é o cru dos
juízes e tl_override é o declarado ({} sem override).
good_sol_no_tl = tem solução good sem TL (linguagem
suportada que falhou em TODOS os juízes); needs_review =
precisa revisão (erro / good sem TL / público não validado ou não
calibrado). stale/needs_recalibration do
checksum do índice (≤30 min); sem hash de pacote por
request; TL/validação vêm dos sumários
por-evento run/{tl,validation}-summary.json
(upsert pelos escritores; sem varrer run/tl por request) |
/problems/tl?id=<id> |
GET | time limits ao vivo (recomputa o checksum agora) +
stale/needs_recalibration exatos:
{problem,checksum,time_limits,time_limits_calibrated,tl_override,calibrated_checksum,hosts,updated_at,calibrated_at,calibrated,being_calibrated,stale,needs_recalibration}.
time_limits = o EFETIVO (o que o aluno vê
e o juiz honra): com TLOVERRIDE no conf do pacote,
override[lang] // override[default] // calibrado[lang];
time_limits_calibrated = o cru dos juízes;
tl_override = o declarado no conf ({} sem
override). being_calibrated = há
calibração pendente/em execução p/ este problema AGORA
(mesma varredura do painel) — distingue "TL vazio porque acabou de
enfileirar (validate/calibrate)" de "calibrou e não obteve TL". Quando
needs_recalibration, explica o PORQUÊ:
reason (checksum velho→novo),
changes = commits desde a calibração que
tocaram os caminhos que afetam o TL (conf/tests-input/sols-good/scripts
— o que o tl-checksum cobre; [{sha,at,author,subject}],
≤20) e changed_files (≤30). Acesso: membro da org
ou público (require_problem_view;
404 senão). Versão não-admin do
/ops/problemtl. Python é UMA linguagem:
py (pypy3) — chaves
py3/py2 legadas são fundidas em
py nos time_limits servidos (o cru de
hosts pode ainda trazê-las até recalibrar) |
/problems/recalibrate-stale |
POST {} | {ids:[...]} |
recalibra em LOTE tudo que "precisa recalibrar" no
painel do login (calibrado + checksum divergente — mesma conta do
/problems/status); ids restringe (intersectado
com o conjunto AUTORIZADO — a fronteira é owners_visible,
nunca o input). Cada item via cal_request (idempotente +
serializado por-problema no claim — lote é seguro). Resposta
{count, queued:[{id,reqid}]}. Web: botão "⚙ Recalibrar
todos (N)" no Painel; CLI: moj calibrate --all-stale |
/problems/calib?id=<id> |
GET | calibração por juiz (membro da org):
{id,checksum,good_langs,missing_langs,tl_override,time_limits,time_limits_calibrated,hosts:[{host,tl,missing,at,log,reports,sols}]}.
⚠ hosts[].tl e sols[].tests[].tl são a
MEDIÇÃO da calibração, nunca o override — o calibreitor roda
com MOJ_CALIBRATING=1 justamente para medir de verdade;
time_limits (efetivo) existe para o cartão poder dizer
o julgamento usa outro número. missing_langs =
linguagens good sem TL em nenhum host (solução good
falhou em TODAS as máquinas); hosts[].missing = faltantes
naquele juiz. sols = a calibração POR
EXTENSO daquele juiz, estruturada p/ ferramentas externas:
[{file,lang,category:good|pass|slow|wrong,verdict,tests:[{name,code,time,tl}]}]
— o MESMO formato do vetor tests de uma submissão normal
(nome do teste, código curto AC/WA/TLE/…, tempo em s, TL usado);
[] = juiz ainda não reportou o vetor (mojtools/agente
antigos — o log texto continua). tl_override =
o TLOVERRIDE do conf do pacote (ver PACOTE.md),
{} sem override. Sols .py2/.py3
legadas contam como py |
/problems/calib-report?id=<id>&host=<host>&name=<name> |
GET | o report.html rico (o do
build-and-test) de UMA solução, como saiu da calibração
NAQUELE juiz
(run/calib/<id>/r/<host>/<name>.html). Os
nomes válidos vêm de hosts[].reports do
/problems/calib. Devolve HTML, não JSON.
Acesso: require_problem_edit — dono/colaborador, sem atalho
de .admin (é código de solução). CLI:
moj calib-report |
/problems/my-stats |
GET | análise dos problemas do login (dono+colaborador)
agregada em TODA a plataforma (treino + turmas; cache precomputado).
{totals:{owned,with_activity,attempts,accepts,solvers},overall_verdicts:[{verdict,count}],overall_languages:[{lang,submissions,accepted}],most_popular:{id,title,attempts},problems:[{id,title,attempts,accepts,wrong,acceptance_rate,distinct_users,solvers,contests_count,verdicts,languages,first,last}]}.
Só os problemas do login; sem logins, sem nomes de
contests (só contests_count) — não vaza prova
privada |
/problems/judges |
GET | o parque de juízes para a calibração DIRECIONADA do
editor:
{judges:[{host,cpu,arch,langs,cage_root,last_seen,online}]},
ordenado por online › cpu › host (online = heartbeat nos
últimos 30 s). O editor agrupa por cpu para oferecer "1 por
processador". Só exige login (é inventário de máquina, não conteúdo de
problema). CLI: moj calibrate --judges |
/problems/validate |
POST {id} |
portão de qualidade, NÃO publicação: valida (portão
estático: HTML compila + seções
## Entrada/## Saída + exemplos pareados) +
gera o índice + pede calibração a um juiz (que roda as
good e reporta o TL). NÃO mexe no
public — problema privado continua privado
(publicar é /problems/set-public, que checa a trava da
ORG). Relatório: /problems/validation. Só membro da
org. |
/problems/publish |
POST {id} |
DEPRECADO — alias de
/problems/validate (o nome fazia parecer que validar
publicava) |
/problems/request-calibration |
POST {id, hosts?:[...]} |
enfileira calibração (juiz roda
calibreitor.sh, gera tl.<host>).
IDEMPOTENTE: se já existe calibração pendente/em
execução p/ o id, devolve o reqid existente com
status:"already_queued" (nunca duplica job — lição do
incidente 2026-07-15); direcionada (hosts) dedupa por host
os comandos ainda não entregues (hosts[].status) |
problem_commit)| Rota | Método | I/O |
|---|---|---|
/problems/repos |
GET | diretórios/orgs de que o login é membro
{repos:[{repo,owner,collaborators,collections,mine}]} |
/problems/repo-create |
POST {repo, collections?} |
cria o diretório (org no namespace do login; provisiona a org implícita lazy) |
/problems/source?id=<id>[&tests=meta|full] |
GET | source editável
{editable,title,enunciado_md,enunciado_format,author,tags,conf_text,public,collections,languages,examples,tests,sols{good,slow,wrong,pass,upcoming},score,score_text,editorial_md,scripts,scripts_files,docs_files}
SÓ MEMBRO da org (require_problem_edit);
não-autorizado recebe 404 (sem read-only, sem atalho de
.admin). Cada examples[i] traz
explanation (opcional); editorial_md =
resolução só p/ setter; scripts = caminhos relativos de
scripts/ (árvore do editor web);
scripts_files = ROUND-TRIP da correção
especial — [{path,content_b64,exec} | {path,symlink}]
(base64 suporta binário; symlink cobre os drivers interativos
scripts/<lang> -> c);
score_text = tests/score cru
(round-trip byte-fiel do moj push/clone);
languages = ids de linguagem de submissão
permitidos (.moj-meta.json, [] = todas);
docs_files = ROUND-TRIP das IMAGENS de
docs/ — [{name,content_b64}] (figuras do
enunciado/notas; nomes simples com extensão de imagem).
examples[i].explanation vem de
docs/notes/<sample>.md (formato de autoria) ou do
legado sample-notes.json. tests=meta
(DEFAULT): os testes OCULTOS saem sem conteúdo —
{name,size_in,size_out,omitted:true} — e a resposta traz
tests_omitted:true; o conteúdo de um teste vem por
/problems/test. Motivo: problema com testes grandes (OBI:
inputs de 12 MB) gerava corpo de centenas de MB (52 s medidos) e o
editor web ficava todo esse tempo com o formulário VAZIO, idêntico ao de
"problema novo". tests=full devolve o
conteúdo (é o que o moj clone usa — round-trip). Ao salvar,
teste com {name, keep:true} preserva o
conteúdo que está no servidor (o editor manda isso para os testes que
não baixou) |
/problems/preview |
POST
{enunciado_md, enunciado_format?, examples?, title?, id?, images?} |
pré-visualização HTML (= o renderizador único
render-statement.sh, idêntico ao servido) — injeta o
título (h1) e os exemplos (cada
explanation renderizada em markdown com embed).
Imagens-arquivo aparecem: com id (exige
require_problem_edit) as imagens de docs/ do
pacote são semeadas no render; images:[{name,content_b64}]
(≤16, nomes de imagem saneados) cobre figura ainda não enviada →
{html_b64} |
/problems/download?id=<id>[&sha=<sha>] |
GET | baixa o pacote .tar.gz (inclui
soluções → membro da org); com sha, a versão
daquele commit (git archive, worktree intocado);
stream binário |
/problems/test?id=<id>&name=<teste> |
GET | conteúdo de UM teste {name,input,output} — o par do
tests=meta; mesmo gate do source (só membro da org; 404 p/
os demais) |
/problems/test-run |
POST {id, filename, code_b64} |
roda UMA solução avulsa NO JUIZ (autoria): job real
na fila (banda lista-privada, contest sentinela
_testrun), mesma jaula e mesmo TL da submissão de aluno,
sem tocar history/placar de ninguém →
{run:<32hex>, status:"queued"}. Gate: membro
da org (require_problem_edit, 404 — rodar contra
os testes ocultos revela o problema). Teto SUBMIT_MAX_KB
(413); linguagens aceitas = PLATAFORMA ∪ languages
do pacote (a whitelist de SUBMISSÃO do problema não vale aqui —
autor testa o que quiser que rode); rate: máx 3 runs queued
por login (429 testrun_busy); auditado
(test-run). Registro em run/testrun/ com TTL
de 7 dias (GC preguiçoso) |
/problems/test-run?run=<32hex> |
GET | polling do test-run:
{run,problem_id,filename,lang,status:queued|done,requested_at}
e, quando done,
+{verdict,verdict_canon,score,correct,total_tests,duration_s,tl_used,tests:[{name,code,time,tl}],finished_at,report:bool}
— o vetor tests é o MESMO da submissão normal. Gate pelo
problema DO REGISTRO (membro da org; 404) |
/problems/test-run-report?run=<32hex> |
GET | o report.html do test-run (HTML; 404 enquanto
julga/expirado). Mesmo gate do registro |
/problems/history?id=<id>[&limit=N][&sha=<sha>] |
GET | histórico git do problema (membro da org — expõe
soluções/testes). Sem sha:
{id,commits:[{sha,at,author,subject,files,insertions,deletions}]}
(limit≤200, default 50). Com sha: o
git show -p →
{sha,at,author,subject,truncated,diff_b64} (diff limitado a
400 KB) |
/problems/restore |
POST {id, sha, confirm} |
restaura o problema ao estado do commit
sha como um COMMIT NOVO (história nunca é
reescrita; confirm repete o sha). O
.moj-meta.json (público/coleções/owner) é
PRESERVADO — meta antigo não republicaria prova
privada. Sem revalidação/recalibração automática (igual ao edit). Membro
da org |
/problems/upload |
POST {id|repo,prob, tar_b64} |
sobe um pacote
(.tar/.tar.gz/.tar.bz2/.tar.zst/.zip)
e substitui o conteúdo (commit). Do
.moj-meta.json do tar lê os campos de CONTEÚDO —
display_title, collections,
languages (ausente/[] ⇒ preserva); os de
ACESSO (public/public_at/owner)
nunca vêm do tar. Tar sem o arquivo tags ⇒
preserva as do servidor (curadoria); com (mesmo vazio) ⇒ substitui |
/problems/export?id=<id> |
GET | baixa o problema como pacote ICPC/Kattis (2025-09)
.tar.gz (problem.yaml+statement+data+submissions); inclui
soluções → exige escrita/admin
(mojtools/kattis/export.sh) |
/problems/import |
POST {repo, prob?, tar_b64} |
importa um pacote ICPC/Kattis
(mojtools/kattis/import.sh) → cria um problema MOJ julgável
(checker custom via bridge); exige permissão de criação. Round-trip sem
perda via .kattis.json |
/problems/create |
POST
{repo,prob,enunciado_md?,author?,tags?,examples?,good_sol?,title?,collections?,languages?,...} |
cria problema novo; commit+push; {id,sha}.
prob = slug minúsculo
^[a-z0-9][a-z0-9._-]{1,80}$ (400
prob_invalid); collections tem de EXISTIR no
registro curado (400 coll_unknown — MESMA trava do
edit/set-collections; a homônima da org é isenta).
languages = ids permitidos de submissão
([]/ausente = todas) |
/problems/edit |
POST {id, ...campos} |
edita (só campos presentes); commit+push autorado. Aceita
languages (ids de submissão permitidos no
.moj-meta.json; ausente = não toca, [] =
limpa/todas). Aceita também scripts_files
(SUBSTITUI scripts/ inteiro quando presente — paths
validados, sem .., confinado a scripts/,
exec vira +x, symlink recriado se o alvo
resolvido fica dentro de scripts/; campo ausente = não
toca) e score_text (grava
tests/score verbatim; "" remove). Aceita
docs_files (SUBSTITUI as imagens de
docs/ quando presente — nomes saneados, só extensão de
imagem, cap ~3MB; ausente = não toca).
examples[].explanation grava
docs/notes/<sampleN>.md (1 markdown por exemplo — e
REMOVE o legado sample-notes.json). Mexer em
scripts/ muda o tl-checksum ⇒ recalibração. O
editor web gere a correção especial na sub-aba "⚙
correção" (Soluções & Correção — lista + templates) e envia
scripts_files no save |
/problems/script-templates |
GET | templates de corretor especial (lidos de
mojtools/script-templates/<key>/ — criar template =
criar uma pasta lá):
{templates:[{key,name,description,conf_hints,files:[{path,content_b64,exec} | {path,symlink}]}]}
— files no MESMO shape do scripts_files
(aplicar = preencher a seção da UI e salvar). Symlink externo do
template (drivers canônicos do mojtools) vem RESOLVIDO como conteúdo;
symlink interno (cpp -> c) vem como symlink. Iniciais:
checker-testlib, interativo,
interativo-rank, compare-float,
ban-funcoes-c |
/problems/delete |
POST {id, confirm} |
REMOVE o problema (git rm da subpasta + push) e do
treino. Destrutivo: confirm tem de repetir
EXATAMENTE o id. Dono/colaborador ou admin |
/problems/set-public |
POST {id, public:bool} |
público on => valida + calibra
(index_problem_bg no servidor; só entra no treino se o
portão passar) e grava public no
.moj-meta.json; off => sai do treino na
hora. A calibração só entra na fila se o pacote MUDOU
desde a última calibrada (tl-checksum atual ≠ checksum do store servido)
— resposta traz calibration:"queued"|"up_to_date"; publicar
em massa sem mudança não enfileira recalibração redundante |
/problems/set-collections |
POST {id, collections:[...]} |
define as coleções (tags) do problema no
.moj-meta.json; valida contra o registro
(curada: a coleção tem de existir) |
/problems/move |
POST {id, to_org} |
move um problema de rascunho p/ outra org (muda o
id <org>#<prob>); bloqueia se
público/em uso (senão órfãoria o histórico); exige ser membro
das DUAS orgs |
/problems/repo-collaborators |
GET ?repo / POST {repo,add?,remove?} |
compartilha o diretório (membro da org; só o dono
gerencia). Cada login em add precisa existir no
treino e poder criar problemas
(cc_can_create) — senão 422 login_invalid/404
user_notfound/403 cannot_create, recusa
ATÔMICA; remove não valida |
/problems/collection-create |
POST {name} |
cria uma coleção (TAG) no registro curado. Nome é TEXTO LIVRE (pode ter espaços/acentos — é só rótulo). Exige permissão de criação; criador = dono. (NÃO é org: acesso é por org) |
/problems/collection-rename |
POST {name, to} |
renomeia a coleção: registro NA HORA + re-tag dos N problemas em
BACKGROUND (retag:"background", devolve
retag_job p/ acompanhar; síncrono
estourava o timeout do nginx). RETOMADA:
name inexistente + to existente = bulk
anterior morreu ⇒ repete só o retag (resumed:true). Só dono
ou .admin |
/problems/collection-delete |
POST {name} |
exclui a coleção: untag dos N problemas em
BACKGROUND (devolve
retag_job) e o registro só sai NO FIM
(untag:"background"; morreu no meio ⇒ a coleção ainda
existe, repetir o delete RETOMA). Só dono ou .admin |
/problems/collection-retag-status?[job=<id>][&name=<coleção>] |
GET | situação dos jobs de retag (rename/delete):
{jobs:[{id,from,to,by,started_at,total?,done,failed,finished_at?}]}
mais novos primeiro (últimos ~50); sem finished_at =
rodando (done/total = progresso, total é
estimativa). job= filtra pelo id devolvido em
retag_job; name= por from/to |
source/create/editcobrem o pacote inteiro:title(vem do campo, não de% Títulono texto — o render injeta o h1),enunciado_md,conf_text(TL/ulimits/STOPWHEN/…, versaad-problems/README.org),examples(sample; cada um aceitaexplanationopcional →docs/sample-notes.json, mostrada após o exemplo),tests(ocultos),solspor categoria{good,wrong,slow,pass,upcoming}(cada[{filename,code}]),score(grupos de pontuação; cada grupo tem{name,weight,glob}e oglobpode ser uma lista", "-separada de padrões, ex.:g2_*, g3_*) eeditorial_md(resolução em markdown →docs/solucao.md, só p/ setter, não vai ao aluno).
Quem pode criar (problemas/pastas/coleções) = mesma regra de criar contest (
cc_can_create:.adminou allowlist ou ≥ N resolvidos, menos a denylist) — gerida em/treino/admin/contest-perms.create/repo-create/collection-create/upload-novo exigem isso; editar/compartilhar problema existente continua por colaborador (org_is_member).
Conceito completo (ORG = acesso, COLEÇÃO = agrupamento, e por que são ortogonais): PACOTE.md.
Storage = repo git local por problema
(MOJ_PROBLEMS_DIR/<org>/<prob>), e o acesso é
por ORG (o <org> do id
<org>#<prob>): quem é membro
escreve em qualquer problema da org; a org tem uma trava de
público (public_allowed, privada por PADRÃO →
problemas nunca ficam públicos: anti-vazamento de prova), e só
admin da org a muda. Cada usuário tem uma org
implícita <login> (sempre privada).
Registro: contests/treino/var/orgs.json
(lib/orgs.sh).
| Rota | Método | Descrição |
|---|---|---|
/orgs/list |
GET | orgs de que o login é membro (inclui a implícita,
criada aqui):
{orgs:[{name,title,members,admins,public_allowed,implicit,count,public,mine,can_manage}]}.
Não lista org alheia |
/orgs/get |
GET ?name |
detalhe de 1 org; só membro/admin ou .admin global,
senão 404 (não vaza existência) |
/orgs/create |
POST
{name,members?,admins?,title?,public_allowed?} |
cria org; o criador vira membro+admin (exige
cc_can_create, a regra de criar contest). Cada login de
members/admins precisa existir no
treino e poder criar problemas — 422/404/403
senão (recusa ATÔMICA: a org nem nasce) |
/orgs/members |
GET ?name / POST
{name,add?,remove?,admins_add?,admins_remove?} |
só admin da org (ou .admin) gerencia; criador blindado;
org implícita não tem gestão. add/admins_add
validam cada login (existe no treino + cc_can_create; 422
login_invalid/404 user_notfound/403
cannot_create, atômico);
remove/admins_remove não validam (lixo já
gravado precisa poder sair) |
/orgs/set-public-allowed |
POST {name,public_allowed:bool} |
liga/desliga a trava (só admin da org; implícita ⇒
409). Desligar DESPUBLICA em cascata
os problemas públicos da org (tira do treino) — resposta traz
unpublished |
/orgs/delete |
POST {name} |
remove uma org VAZIA (sem problemas — conferido em
disco); só admin da org (ou .admin); org
implícita ⇒ 409
implicit_org; org com problema ⇒ 409
org_not_empty |
O CLI moj (web/moj,
servido em GET /moj; fonte em moj-cli/) usa
essas rotas para autoria sem git/sem chave:
moj new/clone/push/publish/share/org/mv. Storage
MOJ-nativo: o servidor commita no repo git LOCAL de cada problema
(MOJ_PROBLEMS_DIR/<org>/<prob>).
Permissão de escrita = membro da ORG do problema (
org_is_member; sem atalho de.admin). Visibilidade imediata via overlaycontests/treino/var/authored.json(mesclado ao índice). Público só se a org permitir (public_allowed) — camada anti-vazamento de prova.
| Rota | Método | Auth | I/O |
|---|---|---|---|
/contest/beacon?contest=<c> |
GET | Bearer | → {beacon,server_utc}. Beacon de tempo
p/ a submissão offline (moj-comp):
payload_b64.sig_b64, payload {v,c,l,t,n}
assinado RSA-PSS com a chave do contest. A CLI re-ancora a cada comando
com rede; o beacon embutido no pacote offline prova que ele nasceu
depois de .t (piso do carimbo). Ver
lib/contest-offline.sh e FLOW.md §offline. |
/contest/offline-submit?contest=<c> |
POST | Bearer | body {packets:["<pkt-json>",…]} (máx 50).
Rota emergencial do moj-comp: pacotes
cifrados (RSA-OAEP+AES-256-CBC com sha do conteúdo no envelope) criados
SEM rede. Valida por pacote: decripta; v/login/contest
conferem; beacon assinado do mesmo login/contest;
beacon.t ≤ claimed_utc ≤ now+30s; claimed na janela DO
aluno (start…fim efetivo, extend conta); claimed monotônico vs último
aceito; dedup por sha256; extensão na whitelist de linguagens do
problema (mesma regra do /submit; fora dela =
pacote rejected na chegada). Aceito ⇒ spool com
time=claimed (contabiliza no horário
reivindicado — placar/penalidade usam sub_epoch) +
var/offline-log + audit (offline-submit, com
gaps beacon→claimed→chegada p/ o organizador adjudicar). →
{results:[{sha,status:accepted|rejected|duplicate,…}],accepted,rejected} |
/submit?contest=<c> |
POST | Bearer | body {problem_id,filename,code_b64,source?}
(source=web|file) →
{submission_id,status:"queued"} (não bloqueia). O
filename é NORMALIZADO pelo servidor
(safe_src_filename): o cliente manda o que quiser, o juiz
recebe um nome sadio — sai o caminho, saem espaços, sai o
(N) que o navegador gruda em download repetido
(l(1).cpp → l.cpp) e saem os metacaracteres de
shell/make; acento é preservado (em Java o arquivo tem de casar a classe
pública). Sem isso o mesmo código dava AC como
l.cpp e Compilation Error como
l(1).cpp — o nome chega cru ao recipe do make, que
o entrega ao /bin/sh (relato de time, 2026-08-24). Vale
igual no /contest/offline-submit e no
/problems/test-run. Teto de fonte
SUBMIT_MAX_KB (1024) → 413 source_too_large;
whitelist com CHÃO: lista de linguagens vazia = as da
PLATAFORMA (PLATFORM_LANGS, as 17 de
mojtools/lang/), nunca "qualquer extensão"
(.exe → 400 lang_not_allowed); e o submit é
fail-closed: o spool é validado ANTES do OK (falha →
500 spool_write_failed, sem linha pendente no history) — as
três correções do incidente 2026-08-19. Registra o editor em
var/editor-log p/ o card "editor da semana". Gate
por fase+papel (forçado pela API):
.admin/.judge submetem sempre;
.staff nunca
(403 submit_forbidden); usuário normal e
.mon só durante a janela
(403 contest_not_started antes do início,
403 contest_ended após o fim) — o .mon submete
mas fica fora do placar. Whitelist de
linguagens FORÇADA (400 lang_not_allowed): a
extensão do filename (canonicalizada: py3→py, cc/cxx→cpp…)
tem de estar na lista efetiva do problema — override do contest
(problem-langs.json) → LANGUAGES do conf →
languages do pacote → todas (fonte única
lib/langs.sh, a MESMA da listagem
/contest/problems). |
/submission/source?contest=<c>&id=<subid> |
GET | Bearer | código-fonte (texto) |
/submission/log?contest=<c>&id=<subid> |
GET | Bearer | log do julgamento (report.html; expõe
input+diff de TODOS os testes). Juiz/admin sempre; dono
conforme o SHOWLOG efetivo
(showlog_effective em lib/verdict.sh):
SHOWLOG explícito no conf manda; ausente = OCULTO
em modo icpc (anti-vazamento de prova) e visível
nos demais modos |
/submission/summary?contest=<c>&ids=<csv> |
GET | Bearer | resumo ESTRUTURADO em lote (p/ a linha de detalhe sob o veredicto
canônico), de results/<id>.json:
{ "<id>":{verdict,verdict_canon,score,score_max,score_kind,correct,total,groups,heur_score?,heur_adjusted?} }.
REDIGIDO pelo modo do contest
(lib/verdict.sh): full (treino/lista) = tudo;
score (obi/heurístico/outro) = canônico +
score/groups/heur sem
correct/total; none
(icpc/ausente) = só o canônico (anti-leak: nem o dono
recebe score) — nos níveis redigidos verdict = canônico.
Juiz/admin: sempre full com verdict cru.
Mesmo gate do log (dono/admin/juiz; respeita o
SHOWLOG efetivo — explícito manda, ausente = oculto em
modo icpc); ids de terceiros são omitidos
(não 403). score_kind ∈ tests|points;
groups = [{earned,max},…] na ordem do
tests/score (earned null = grupo
não executado). Até 1000 ids; results antigos:
verdict_canon derivado da string e groups da
cauda legada Pontos | … | (com max:null) |
| Rota | Auth | I/O |
|---|---|---|
/contest/basic?contest=<c> |
— | {contest_id,contest_name,start_time,end_time,login_start_time,locale,login_enabled,freeze_time,score_anon,languages[],secret}
(languages = whitelist do conf LANGUAGES=;
[] = todas; locale =
pt/en explícito impõe o
idioma da interface do contest, "" = não setado ⇒ o front
cai no seletor/idioma do browser; round =
{slug,name,kind,warmup} da rodada ATIVA ou
null — é o que faz o front avisar em faixa fixa que aquilo
é AQUECIMENTO; cohort =
{id,name,unranked,public,view,released,views[]} da coorte
do login (só com sessão; null sem coortes) — o front avisa
o convidado e mostra o seletor Oficial × Geral;
score_views = [{id,name}] das
coortes PÚBLICAS com placar próprio (ranking:true — ex.:
times × individual), lista pública que vira o seletor do placar).
Continua público mesmo em contest secreto — a tela de
login/countdown precisa do nome p/ quem tem o link. Com Bearer de sessão
deste contest (opcional), end_time é o
fim EFETIVO do login (prorrogação por sede/grupo via
time-overrides.json — o countdown mostra o certo) Inclui
balloon_style
(icon|fill, default icon) = como
o placar pinta a célula resolvida — icon: fundo neutro +
ponto da cor (a cor deixa de ser o único sinal de "resolvido"; o balão
BRANCO da paleta padrão dava 1,00:1 contra o fundo e sumia) ·
fill: cor do balão no fundo + contorno derivado. Vale p/
placar, cerimônia e relatório; ver SCOREBOARD.md. Inclui
penalty_minutes (regra de pontuação, não
segredo — a cerimônia de revelação precisa dela p/ recalcular penalidade
e ORDEM; antes só existia no /contest/admin/settings,
admin-only, e o telão caía no default 20). Resposta em CACHE por
VARIANTE (`var/basic-cache.<anon |
/contest/userinfo?contest=<c> |
Bearer | {login,name, …team/país/univ/show_log opcionais} |
/contest/navbuttons?contest=<c> |
Bearer | botões por papel
(.admin/.judge/.staff/.cstaff
— o .cstaff ganha 🏷️ Etiquetas e, quando o
contest terminou p/ todas as sedes,
🏆 Revelação; o .staff não
tem mais Etiquetas; .staff/.cstaff ganham
📄 Documentos) Resposta em CACHE por PAPEL
(`var/nav-cache.<animeitor |
/contest/problems?contest=<c> |
Bearer | {problems:[{short_name,full_name,problem_id,has_statement_html,has_statement_pdf,time_limits,languages,author?}]}
(author = crédito de quem escreveu, do
arquivo author do pacote; só sai depois do fim para
todas as sedes — ou p/ admin/juiz-chefe/juiz —, porque durante
a prova o nome do autor é pista) (problem_id = forma
canônica coleção#problema, igual ao treino — é o que o juiz
usa p/ achar o pacote; time_limits =
{lang:seg} do store, {} se o conf ocultar via
SHOWTL=0 — com pool de juízes definido
(override do problema em problem-judges.json →
CONTEST_JUDGES do conf) o MAX é só entre os hosts
do pool efetivo; languages = ids permitidos do
problema: override por problema (problem-langs.json) →
whitelist do contest (LANGUAGES) → default do
próprio pacote (.moj-meta.json
languages, servido no índice do treino) → []
(=todas) — o último elo faz um problema "só-pddl" restringir sozinho sem
o contest configurar nada; com restrição, o front mostra um chip
de TL por linguagem permitida — o TL medido dela ou o
default — e omite o chip "padrão"; sem restrição, chips
medidos + "padrão"). Gate de visibilidade (forçado pela
API): .admin/.judge veem sempre;
.staff nunca; usuário normal só
após o início — antes disso retorna
{problems:[], locked:"not_started"} (.staff →
locked:"staff"), e o front mostra a tela de contagem
regressiva. Resposta em CACHE
(`var/problems-cache.<author |
/contest/statement?contest=<c>&problem=<letra|problem_id>&format=html|pdf |
Bearer | UM enunciado, cru (text/html ou
application/pdf; format default
html). Gate IDÊNTICO ao da lista
(can_see_problems):
.staff/.cstaff nunca, competidor só depois do
início, admin/juiz sempre — e a recusa é 404, não 403
(pedir o enunciado direto não pode confirmar que o problema existe antes
de a prova abrir). A chave do arquivo sai sempre do PROBS
do conf, nunca do parâmetro (problem=../x = 404). Responde
ETag (mtime+tamanho) +
Cache-Control: private, max-age=60 e honra
If-None-Match com 304 — recarregar a
página não repuxa MB, e enunciado corrigido no meio da prova invalida
sozinho. |
/contest/news · /contest/resources |
Bearer | seções opcionais (vazias = ocultar). Notícia pode ter anexo
{file:{name,size}} |
/contest/news-file?contest=<c>&id=<news_id> |
GET | Bearer |
/contest/backup?contest=<c> |
GET/POST | Bearer |
/contest/backup-file?contest=<c>&id=<id>[&login=<l>] |
GET | Bearer |
/contest/print?contest=<c> |
GET/POST | Bearer |
/contest/print-file?contest=<c>&id=<id> |
GET | Bearer |
/contest/staff/queue?contest=<c> |
GET | Bearer (.staff/.cstaff/admin) |
/contest/staff/print-action?contest=<c> |
POST | Bearer (.staff/admin; .cstaff
não — 403) |
/contest/staff/print-pdf?contest=<c>&id=<id> |
GET | Bearer (.staff/admin; .cstaff
não — 403) |
/contest/badges?contest=<c>[&staff=<l>&include_disabled=1] |
GET | Bearer (.cstaff/admin; .staff →
403 cstaff_required) |
/contest/doc?contest=<c>[&type=<info-sheet|contest|times>&lang=<pt|en|es>&fmt=<pdf\ **Tipos**: info-sheet|contest|times|editorial. **Gate de FASE** (além do de publicação): p/ quem NÃO é organização (admin/chief/judge/staff/cstaff/mon), contest/times publicados só aparecem/baixam **a partir do início** (contest_phase
!=
before) e editorial só **depois do fim p/ TODAS as sedes** (contest_over_for_all— prorrogação segura);info-sheet`
= publicado é visível. A LISTAGEM filtra igual (o time nem vê a
linha). |
html>]` | GET |
/contest/rounds?contest=<c> |
GET | Bearer |
/contest/round?contest=<c>&round=<slug>[&file=index.html] |
GET | Bearer |
/contest/updates?contest=<c>&news_since=&clar_since= |
Bearer | resumo leve p/ polling de notificações:
{news:{last,count,unread}, clar:{last,count,unread}} (clar
= respondidas visíveis ao usuário; unread =
date/answered_at > since) |
/contest/history?contest=<c> |
Bearer | TXT (submissões do usuário). O veredicto (campo 5) sai
SEMPRE canônico
(Accepted/Wrong Answer/… —
lib/verdict.sh), em todos os modos: a
string de display com score/grupos fica no disco e o detalhe por modo
vem do /submission/summary (redigido). Pendentes e strings
desconhecidas passam intactos; o sufixo
(Ignored) é preservado. O history em disco não muda |
/contest/balloons?contest=<c> |
Bearer | mapa letra/short→cor (default ICPC A–O) Resposta em
CACHE (var/balloons-cache.json, sem variante — o
mapa é o mesmo p/ todos). Sem teto de idade: as entradas cobrem 100% do
corpo (balloons.json + a paleta padrão, que é código — o
próprio handler entra como entrada, então um deploy invalida). |
/contest/regions?contest=<c> |
Bearer | regiões p/ filtro do placar (o filtro casa por nome
— igualdade com a sede .team.region do time via
/contest/teams — ou pelo
regex no login) |
/contest/teams-meta?contest=<c> |
— | regras regex→{country,school,school_full} {rules:[…]} —
placar resolve bandeira/escola e filtra por país/escola (bandeiras
locais em /shared/flags/). Fallback: só
preenche o que o por-usuário (/contest/teams) não
trouxe |
/contest/teams?contest=<c> |
— (secreto exige sessão) | ⚠️ com coortes, só os logins das coortes que o
chamador pode ver (é o endpoint PÚBLICO que mais vazaria um convidado).
diretório de TIMES por-usuário p/ o placar mesclar:
{teams:{<login>:{univ_short?,univ_full?,flag?,region?,has_logo,has_photo}}} (o NOME vem do TXT do placar — fullname)
— do .team do account.json + presença de
logo.png/photo.png; só logins
não-privilegiados com algo a dizer. Precedência no placar: isto
> teams-meta (regex) > vazio |
/contest/team-photo?contest=<c>&user=<l>[&thumb=1] |
— | foto do time (thumb=1 = miniatura de
320px, ~7 KB, com cache longo — é o que a galeria do
.animeitor usa). ⚠ Time sem foto NÃO dá mais
404: devolve a foto padrão do contest (200)
com o cabeçalho X-MOJ-Photo: placeholder — é o que faz o
Animeitor achar imagem para todo time. Quem precisa saber quem MANDOU
foto usa o has_photo das listagens (lado máx 1000) — o
placar não mostra isso (2026-08-24: "deixar simples");
quem cobra quem não mandou é a galeria do telão e o painel Pessoas ›
Times. Serve image/webp (formato de hoje) ou
image/png (acervo antigo — ver
lib/team-photo.sh). 404 só quando nem a padrão existe.
Pública, inclusive em contest SUPER SECRETO
(2026-08-24): a foto e a música existem para ir ao TELÃO, e o telão é um
sistema EXTERNO (o Animeitor) que busca sem sessão — o gate protegia
pouco e atrapalhava muito (nem o <img> do próprio MOJ
funcionava: tag de mídia não manda Authorization). O
SECRET continua trancando o que é dado de prova:
score, teams, teams-meta,
balloons e regions. |
/contest/team-music?contest=<c>&user=<l> |
— | música do time (audio/mpeg +
Content-Length): a faixa que o telão toca quando ele
resolve. Mesma doutrina da foto — time sem música NÃO dá
404: devolve a música padrão com
X-MOJ-Music: placeholder; quem precisa saber quem MANDOU
usa o has_music das listagens. Guardada como veio (mp3
validado por MIME, sem conversão — não há ffmpeg na
imagem). Sem Range: o player toca progressivo.
Pública, inclusive em contest SUPER SECRETO
(2026-08-24): a foto e a música existem para ir ao TELÃO, e o telão é um
sistema EXTERNO (o Animeitor) que busca sem sessão — o gate protegia
pouco e atrapalhava muito (nem o <img> do próprio MOJ
funcionava: tag de mídia não manda Authorization). O
SECRET continua trancando o que é dado de prova:
score, teams, teams-meta,
balloons e regions. |
/contest/placeholder?contest=<c>[&kind=photo|music][&thumb=1] |
— | o padrão do contest — o que a API responde no lugar
do asset de quem não mandou o seu: kind=photo (default) = a
foto, kind=music = a música. Escolhido pelo
.animeitor; sem escolha, o de fábrica
(server/etc/team-placeholder.webp / .mp3).
Pública, inclusive em contest SUPER SECRETO
(2026-08-24): a foto e a música existem para ir ao TELÃO, e o telão é um
sistema EXTERNO (o Animeitor) que busca sem sessão — o gate protegia
pouco e atrapalhava muito (nem o <img> do próprio MOJ
funcionava: tag de mídia não manda Authorization). O
SECRET continua trancando o que é dado de prova:
score, teams, teams-meta,
balloons e regions. |
/contest/team-logo?contest=<c>&user=<l> |
— | PNG do brasão do time (máx 128; célula do time no
placar — vence o logo por regra do teams-meta). 404 sem brasão.
Pública, inclusive em contest SUPER SECRETO
(2026-08-24): a foto e a música existem para ir ao TELÃO, e o telão é um
sistema EXTERNO (o Animeitor) que busca sem sessão — o gate protegia
pouco e atrapalhava muito (nem o <img> do próprio MOJ
funcionava: tag de mídia não manda Authorization). O
SECRET continua trancando o que é dado de prova:
score, teams, teams-meta,
balloons e regions. |
/contest/webcast?contest=<c>&key=<K> |
— (só a chave) | ZIP do placar no protocolo do Animeitor (o mesmo do
webcast.php do BOCA:
contest/runs/time/version/icpc,
campos separados por 0x1C). Rota SEM
SESSÃO, de propósito: é o sistema Animeitor buscando em loop. A
chave (criada pelo .animeitor) declara a visão de
coorte servida; chave inválida/revogada → 404
(e linha em var/webcast-denied.log). O pacote vai SEMPRE
descongelado — quem anima a virada é o Animeitor, que sabe a hora do
freeze pelo lastmilescore. Cache com piso de 10 s.
Formato inteiro em docs/WEBCAST.md |
/contest/score?contest=<c> |
(Bearer opcional) | TXT (1ª linha = modo, que pode trazer flags:
icpc s = célula resolvida em SEGUNDOS desde o início,
exibida pelos clientes como floor(seg/60); sem a flag =
minutos, legado) — ver SCOREBOARD.md.
Pré-início (regra: o placar nunca revela a quantidade
de problemas antes de a competição começar): antes do
CONTEST_START, quem não é is_judge recebe a
vitrine (var/placar-prestart.txt — os
times da visão pública com bandeira/sigla/nome e zero colunas de
problema; build.sh <c> --prestart). Cache
preguiçoso: (re)gera placar.txt (público, com
freeze) e placar-full.txt (completo, sem
freeze) se history/conf mudou.
Privilegiados
(.admin/.judge/.cjudge +
allowlist SCORE_FULL_USERS — vale p/ liberar um
.cstaff) com token recebem o completo; demais, o público.
&view=<visão> escolhe a visão de
coorte: public força a
pública/congelada mesmo p/ privilegiado,
oficial = só as coortes públicas, geral = tudo
(só vale p/ quem já pode ver tudo) e o id de uma coorte pública
com ranking (ex.: individual,
times) devolve o placar paralelo dela — é
público, não exige sessão, e a página
/contest/score/?c=<c>&view=<id> abre direto
nele. view=public — é o que a cerimônia de
revelação (/contest/score/reveal.html, estilo ICPC
resolver, nativa) usa p/ computar o delta frozen→full e revelar de baixo
p/ cima; o botão "Descongelar tudo" da cerimônia = settings
freeze:0 (só admin). &scope=mine (honrado
só p/ .cstaff) recorta o TXT servido
(frozen e full) aos usuários que o chefe de sede
enxerga (staff-filters) — é a cerimônia por
sede; fora da allowlist, o full só sai p/ o
.cstaff quando o contest terminou para todas as
sedes (contest_over_for_all: fim do conf + o
maior end de
time-overrides.json — sede prorrogada segura a revelação).
Em contest SUPER SECRETO (conf SECRET=1) o
placar deixa de ser público: sem sessão daquele contest
→ 401 secret_login_required (idem
balloons/regions/teams-meta). |
.animeitor, o admin do contest — e a
SEDE, recortada)A sede entra recortada pelo
staff-filters.json(o mesmo da fila/etiquetas/cerimônia): a listagem vem só com os times dela (scoped:true). O.cstaffusaphotos,photo,music,photos-zipe o GET deplaceholder— escrever em time de fora dá 403staff_scope. O.staffé somente leitura: sóphotose o GET deplaceholder(photo/music/photos-zip→ 403). Nenhum dos dois troca o padrão (POST 403) nem vê as chaves do webcast (403). | Rota | Método | I/O | |---|---|---| |/contest/animeitor/photos?contest=<c>| GET | galeria:{teams:[{login,name,univ,cohort,region,flag,has_photo,format,bytes,mtime,has_music,music_bytes,music_mtime}], total, with_photo, with_music, scoped, placeholder:{custom,mtime,music_custom,music_mtime}}(conta de papel fora). UMA varredura (find -printf+find\|xargs jq) para foto e música — com 1000 times são 0,1 s; umjqpor conta levava 5,3 s. Para a sede (.cstaff/.staff) a lista vem recortada nela (scoped:true; +0,05 s da 2ª varredura dostaff_visible_logins) | |/contest/animeitor/photo?contest=<c>| POST |{login\|filename, file_b64}sobe/troca a foto (convertida p/ webp, máx ~8 MB) ·{action:"delete", login}remove.loginaceita NOME DE ARQUIVO (fulano.jpg→fulano), que é como o envio em lote funciona. Auditado (animeitor-photo); toca.score-dirty..cstaffsó na própria sede (403staff_scope). ⚠ diferente doadmin/team-assets, não recusa contest comUSERS_FROM(a foto é asset local) | |/contest/animeitor/music?contest=<c>| POST |{login\|filename, file_b64}sobe/troca a música do time ·{action:"delete", login}remove. MP3 validado pelo MIME (file --mime-type=audio/mpeg; extensão não basta) → 400music_bad; máx 15 MB (413file_large). Corpo lido em ARQUIVO (read_body_file)..cstaffsó na própria sede (403staff_scope).loginaceita NOME DE ARQUIVO (fulano.mp3→fulano), que é como o envio em lote funciona. Auditado (animeitor-music) | |/contest/animeitor/photos-zip?contest=<c>| GET | ZIP do telão:fotos/<login>.webppara todos os times (quem não mandou foto leva a padrão) +musicas/<login>.mp3só de quem mandou +placeholder.webpeplaceholder.mp3na raiz +teams.csv(login,nome,universidade,coorte,bandeira,foto,padrao,musica,musica_padrao—padrao/musica_padraotrue= está com o padrão). A música padrão não é copiada por time: 5 MB × 1000 times viraria um pacote de gigabytes. Para o.cstaffo pacote sai recortado na sede dele | |/contest/animeitor/placeholder?contest=<c>| GET/POST | o padrão do contest: GET →{custom,bytes,mtime, music:{custom,bytes,mtime}}(o topo é a FOTO — contrato antigo); POST{file_b64}troca a foto (webp 1000px + miniatura),{kind:"music", file_b64}troca a música (mp3, máx 15 MB); POST{action:"reset"[,kind]}volta à de fábrica.kindfora dephoto\|music→ 422kind_invalid. Auditado (animeitor-placeholder). A sede (.cstaff/.staff) faz só o GET — o padrão é do contest inteiro (POST → 403) | |/contest/animeitor/webcast?contest=<c>| GET |{keys:[{id,key,view,label,created_by,created_at,revoked_at,fetches,last_at,last_ip}], views:[{id,name}], url_path, contest}— a chave aparece em claro (é o que se copia p/ o Animeitor) | |/contest/animeitor/webcast?contest=<c>| POST |{action:"create", view, label?}→{key, view}·{action:"revoke", id}. Visão inexistente → 422view_invalid. Auditado (webcast-key) |
.admin daquele contest)| Rota | Método | I/O |
|---|---|---|
/contest/admin/config?contest=<c> |
GET | {name,mode,start,end,letters[],colors,regions,teams_meta,basic:{locale,login_start,login_enabled,freeze}} |
/contest/admin/config?contest=<c> |
POST | {colors?,regions?,teams_meta?,basic?} → grava
balloons.json/regions.json/teams-meta.json
+ vars basic no conf (vazio = reseta) |
/contest/admin/users?contest=<c> |
GET | {users:[{login,fullname,email,admin,disabled,disqualified}],shared}
(sem senha) |
/contest/admin/user-add?contest=<c> |
POST | {login,password?,fullname?,email?, univ_short?,univ_full?,country?,region?}
→ adiciona/reseta, devolve a credencial. fullname é
o nome do time (campo único — usuário de contest É o time); os
campos de TIME mesclam no .team do account.json |
/contest/admin/users-bulk?contest=<c> |
POST | carga em lote
{users:[{login,password?,fullname?,email?, univ_short?,univ_full?,country?,region?}], on_existing?:skip|update}
(default skip; ≤5000; senha vazia = gerada).
fullname é o nome do time (campo único);
os campos de TIME (opcionais) gravam o
.team{univ_short,univ_full,flag,region} — carga
única de credenciais+país+sede+universidade (a UI aceita CSV
com cabeçalho: login,senha,nome,pais,sede,univ,univ_nome,
ordem livre — time/equipe são aliases de
nome). update: senha vazia =
regenerada (semântica de reset em massa);
nome/email só sobrescrevem se vierem na linha (linha
parcial de enriquecimento — ex.: login+sede — não clobbera o nome do
time) e os campos de time mesclam; conta privilegiada
existente (.admin/.judge/.cjudge/.staff/.mon) nunca é
tocada (skip privileged); criar privilegiada nova é
permitido. →
{created:[{login,password,fullname,email}],updated:[…],skipped:[{login,reason:exists|privileged|invalid|duplicate}],counts}.
Auditado users-bulk |
/contest/admin/user-remove?contest=<c> |
POST | {login} → remove (mv p/ .removed-users/,
dados preservados; toca .score-dirty — o placar o esquece
sozinho; não pode remover a si mesmo) |
/contest/classification?contest=<c> |
GET | público (gate de secreto igual ao placar; sessão OPCIONAL) |
/contest/admin/classify?contest=<c> |
POST/GET | admin |
/contest/admin/user-disqualify?contest=<c> |
POST | {login, undo?} → DESCLASSIFICA
(.disqualified=true no account.json): a conta continua
existindo/logando, mas some do placar (sc_users)
e da estatística (stats-gen pula o login por inteiro —
placar e estatística sempre contam a MESMA população).
undo:true reverte. Não mexe em senha/sessões
(desclassificar ≠ desabilitar). Auditado
user-disqualify |
Reusa os editores de
web/shared/contest-config/(os mesmos da criação). Bandeiras locais/offline em/shared/flags/(271 países + 27 estados); GIFs do Sonic em/shared/assets/sonic/.USERS_FROM=<contest>no conf faz o login cair nopasswdcompartilhado (ex.: treino), mantendo o.adminpróprio.
| Rota | Método | Papel | Ação |
|---|---|---|---|
/contest/allsubmissions?contest=<c> |
GET | admin/chief/judge/mon | TXT 9 campos
(tempo:username:problemid:lang:verdict:epoch:subid:fullname:univ).
Admin/chief = completo. .judge puro e .mon =
ANÔNIMO: campos 2 (username), 8 (fullname) e 9 (univ)
vazios, aridade mantida, linhas ordenadas por epoch (o corte é na API —
curl não descobre quem submeteu; anonimato é só desta rota) |
/contest/final-verdicts?contest=<c> |
GET/POST | GET=judge; POST=admin/chief | opções de veredicto manual (configuráveis). GET →
{verdicts:[labels], options:[{label,verdict}]}; POST
{options:[{label,verdict}]} (verdict canônico, sem
:; o "YES" deve começar com Accepted p/ o
placar). Default = as 6 (1-YES…6-Contact staff). Auditado
(final-verdicts-set) |
/contest/auto-verdicts?contest=<c> |
GET/POST | GET=judge; POST=admin/chief | matriz de veredicto automático
{ "<cid>": { "<lang|*>": ["<verdict>"] } }
(problema × linguagem × veredicto). GET →
{matrix,problems,verdicts}; POST {matrix}
(cids validados; lang minúsculo ou *). Auditado
(auto-verdicts-set) |
/contest/review/list?contest=<c> |
GET | judge | fila de revisão manual
{manual, options, items:[{id,login(**admin/chief**; null p/ juiz comum — anonimato),problem_id,lang,computed_verdict,status,conflict,created_at,claimants:[{by,elapsed_s,expires_in_s}],votes_n,my_vote,votes(**admin/chief**; oculto p/ juiz comum — anti-anchoring)}], counts:{not_evaluated,being_evaluated,awaiting_second,conflicts}, my_active, quorum}
— quorum = nº de juízes que validam cada veredicto (conf
REVIEW_JUDGES, 1..5, default 2);
awaiting_second = com voto(s) mas abaixo do quórum |
/contest/review/claim?contest=<c> |
POST | judge | {id,action:claim|extend|giveup} — máx 2 avaliadores,
1 ativa por juiz
(409 already_evaluating/slots_full), TTL 5 min
(extend=+5). Rejeita quem já votou
(already_voted). Auditado
(review-claim/extend/giveup) |
/contest/review/vote?contest=<c> |
POST | judge | {id,label} — registra o voto
(permanente) e libera o juiz (sai dos
avaliadores → pode pegar outra); rejeita voto repetido
(already_voted). 2 iguais → libera ao
aluno (enfileira setverdict,
review-agree); 2 diferentes →
conflict (review-conflict) |
/contest/review/resolve?contest=<c> |
POST | admin/chief | {id,verdict} — o juiz-chefe resolve o conflito; libera
ao aluno. Auditado (review-resolve) |
/contest/review/conflicts?contest=<c> |
GET | admin/chief | sumário dos conflitos
{conflicts:[{id,login,problem_id,lang,sub_epoch,computed_verdict,votes:[{by,label,verdict}]}], n, options}
(lang/sub_epoch p/ abrir log +
código na resolução) — n alimenta o alerta
global de conflito (banner + bip) que segue o chief/admin em
qualquer página (shared/chief-alert.js,
disparado via auth.status) |
/contest/review/stats?contest=<c> |
GET | admin/chief | estatística por .judge (do
admin-audit.log)
{judges:[{judge,votes,avg_response_s,timed,agreements,conflicts}], total:{votes,avg_response_s}}
— nº de veredictos, tempo médio claim→voto,
concordâncias e conflitos; alimenta a aba Situação do
juiz-chefe |
/contest/set-verdict |
POST | admin ou juiz-chefe | {contest,problem_id,verdict,username} — override direto
(modo legado/auto-resposta); agora consumido pelo
daemon (setverdict) e finalizado pelo escritor
único |
/contest/rejudge |
POST | admin/chief | {ids:[…]} — marca cada submissão como pendente e
RE-JULGA (o daemon reconstrói a fonte arquivada + metadados do
history) |
/admin/adduser |
POST | admin | {contest,login,fullname,email?,password?} (gera
senha) |
/admin/passwd |
POST | admin | {contest,login,newpass} |
/admin/contest/extend |
POST | admin | {contest,end_epoch} |
/admin/synctreino |
POST | admin | sincroniza treino |
/admin/rejudge |
POST | admin | {ids:[…]} ou {contest,problem} |
/ops/queue |
GET | admin | tamanho da fila por contest |
/ops/problemtl?problem=<p> |
GET | admin | time limits do problema |
/ops/updateproblemset |
POST | admin | {repo} |
/ops/alerts |
GET | bot | avalia incidentes (juiz offline+fila, fila grande, daemon caído,
bot fora do ar — bot_gone, que só
enfileira a mensagem na VOLTA) com histerese/cooldown e drena o
outbox:
{items:[{id,text,chats:[<chat_id>…],loud,group}]} (no
máx. ALERT_CLAIM_MAX=30 por poll — o Telegram corta acima
de ~30 msg/s; o resto sai no poll seguinte). O bot só entrega (+ grupo,
exceto quando group:false = mensagem
dirigida a UMA pessoa; loud:true = com notificação). Efeito
colateral: toca run/alerts/bot.alive
(heartbeat do bot — vira o campo bot do
/index/status e a linha 🤖 do /status/) e roda
a varredura do convite de time pendente
(inv_sweep_all, stamp próprio a cada
INVITE_SWEEP_THROTTLE=300 s: manda o "último aviso" quando
falta ≤ REG_REMIND_LEAD p/ a inscrição fechar) e o
relatório de quartil (rel_sched_check,
stamp próprio a cada RELATORIO_SWEEP_THROTTLE=3600 s:
quartil do semestre vencido e não enviado ⇒ gera e enfileira o painel
só para o grupo via alert_group — item
{chats:[],group:true}, o único destino é o
ALERT_GROUP_CHAT do bot). Estado em
run/alerts/; sem cron (o poll do bot é o relógio) |
/ops/relatorio |
POST | bot | painel de submissões p/ o grupo dos professores
(comando /relatorio do mojinho). Body
{telegram_id, args:[…]}; o gate é PELO
telegram_id: só conta .admin do treino
com Telegram vinculado (o mesmo conjunto que recebe alertas;
403 admin_required). args:
vazio = relatório do semestre configurado
[inicio, agora] (409
not_configured/not_started) ·
AAAA-MM-DD = override pontual
[data, agora] (400 bad_date) ·
config <ini> <fim> = grava o
semestre em contests/treino/var/relatorio.json (quartis
passam a ser enviados automaticamente pelo sweep acima; os JÁ vencidos
entram pré-marcados — sem spam retroativo; 422
bad_date/bad_range) ·
status = config + quartis + enviados +
próximo. Resposta {html} (Telegram HTML): top-10 de
contests por submissões no período (treino em linha própria,
privilegiados excluídos), usuários ativos, vs mesmo período do ano
anterior e YTD vs anterior. Gerador score/relatorio-gen.sh
(uma passada em todos os users/*/history), cache com TTL
600 s em var/relatorio-cache.json. Base
fria: a geração síncrona tem orçamento de
REL_SYNC_BUDGET=50 s (frio já mediu ~70 s no prod, quente
~3 s); estourou ⇒ termina em background e a resposta
vem {html:"⏳…", pending:true} — repetir o comando em ~1
min serve do cache. O sweep de quartil usa cache PRÓPRIO
(var/relatorio-cache-auto.json, janela de until fixo =
imutável, exact-match sem TTL) e simplesmente envia no sweep
seguinte |
As rotas
admin/*eops/*(excetoops/alertseops/relatorio, que usam bot-token) são consumidas pelo painel admin e pelo moj-cli. O mojinho-bot hoje é transporte fino: usa sótreino/signup/*,treino/recover-password,ops/alertseops/relatorio(todos bot-tokenmojb_…), +/index/status(público).
| Rota | Método | Auth | I/O |
|---|---|---|---|
/index/status |
GET | — | health:
{queue:{total_pending,spool_queued,band_queued,lists[]}, judge:{online,total,busy,healthy,cpus_online,gpus_online}, alert:{no_judges}, daemons:{judged}, bot:{alive,last_poll_age_s}|null}
(cache 20s) — base da página /status/.
gpus_online conta SÓ juízes com GPU de compute
comprovada (registro com vendor nvidia/amd, vindo de
nvidia-smi/rocm-smi; adaptador de
display/lspci não conta). daemons.judged =
processo local (pgrep) ou heartbeat fresco
em run/judged.alive (≤JUDGED_ALIVE_TTL, 120s)
— no deploy podman a API e o daemon estão em containers diferentes e o
pgrep nunca o veria. bot =
saúde do bot de alertas (mojinho): mtime de
run/alerts/bot.alive (tocado a cada poll do bot em
/ops/alerts); alive = último poll ≤180s;
null = instalação sem bot (não é
incidente). Quando o bot fica >5 min sem polar e volta,
alerts_evaluate (bot_gone) enfileira UMA DM
aos .admin com o período fora do ar — o carteiro não avisa
a própria morte, mas avisa a ressurreição |
Permissão: usuários .admin sempre podem; demais por
lista do admin OU threshold de problemas resolvidos no
treino (com denylist). O contest entra no ar
imediatamente. Problemas vêm do banco público
(bank_id), por ID
(source+problem_id, p/ não-públicos) e/ou com
enunciado custom — manualmente ou sorteados por
tag/dificuldade. Usuários: compartilhados do
treino (users_from=treino; login pela conta do
treino, via fallback de verify_password) ou
próprios (users[], senhas geradas se em
branco). O admin do contest é sempre criado (sufixo
.admin garantido). Exige ao menos um
problema — sem isso, 422 no_problems; para
criar vazio e configurar depois mande
allow_empty:true (booleano estrito; na web é o botão "Criar
vazio", na CLI a flag moj contest create --empty).
Acrescentar problemas depois não tem restrição
(/contest/admin/problems, com o contest já no ar). | Rota |
Método | Auth | I/O | |---|---|---|---| |
/treino/contest-create/permission | GET | Bearer |
{can_create,is_admin,reason,solved_count,threshold,in_allow,in_deny,allowed_modes,login,name}
| | /treino/contest-create/problems?q=&limit= | GET |
Bearer+criador | autocomplete dos problemas que o
criador pode usar: públicos + os privados a que tem
acesso (dono, colaborador ou membro da org)
{problems:[{id,title,tags,access:mine\|shared\|public,private}],mine,shared,total}.
Privados primeiro; statement vem de var/jsons-private/.
/create recusa problema privado sem acesso
(problem_denied) e auto-valida (enfileira
index) os privados sem enunciado pronto — o contest mostra
o enunciado assim que o juiz indexa (contest/problems faz
fallback p/ jsons-private e cacheia) | |
/treino/contest-create/tags | GET | Bearer+criador | tags
do banco com contagem {tags:[{tag,count}],total} | |
/treino/contest-create/collections | GET | Bearer+criador |
coleções do banco público com contagem
{collections:[{collection,count}],total} (escopo ≠
/problems/collections, que conta sobre os problemas do
login) | |
/treino/contest-create/draw?tags=&collections=&count=&match=any\|all&difficulty=any\|easy\|medium\|hard\|known&seed=
| GET | Bearer+criador | sorteia problemas por tag,
coleção e dificuldade (filtros em AND), reproduzível
por seed {problems[],candidates,drawn,seed,collections}.
collections = array JSON url-encoded (nome
de coleção é texto livre — pode ter vírgula/espaço); casa
exato; inválido/ausente = sem filtro | |
/treino/contest-create/genpass?n= | GET | Bearer+criador |
N senhas legíveis (palavras-para-senha) {passwords[]} | |
/treino/contest-create/create | POST | Bearer+criador |
{id?,name,mode,priority?,start?,end,languages?,showcode?,allow_empty?, admin:{login?,password?,fullname?}, (users_from? \| users:[{login,password?,fullname?,email?, univ_short?,univ_full?,country?,region?}]), problems:[…], colors?:{A:"RRGGBB",…,enableSonic?}, regions?:[…], teams_meta?:[{regex,country,school?,school_full?}], locale?,login_start?,login_enabled?,freeze?, show_log?,show_editor?,show_tl?,allow_backup?,allow_print?,score_anon?,manual_verdict?,allow_late?,secret?,login_ua_substring?,score_full_users?,penalty_minutes?,penalty_verdicts?}
→
{contest_id,admin_login,admin_reused,admin_password,users[],users_from,url,scoreboard_url}.
Paridade com o settings: os toggles/opções espelham
/contest/admin/settings (grava só o não-default).
languages aceita array de ids canônicos
(normaliza como o settings) ou string legada. priority =
prioridade no escalonador (prova/lista-privada/lista-publica;
super só admin — como mode:outro).
judges[] = pool de juízes do contest (→
CONTEST_JUDGES; entra também no template/export/duplicate).
Por problema: languages[] (vira
problem-langs.json), judges[] (vira
problem-judges.json) e
statement_pdf_b64/statement_pdf_file (além do
HTML). Admin não é sobrescrito: senha digitada é
respeitada; em modo compartilhado, se o <login>.admin
já existe na fonte users_from ele é
reutilizado — admin_reused:true,
admin_password:null | |
/treino/contest-create/template | GET | Bearer+criador |
baixa template JSON completo (documenta todos os campos
do create, incl. toggles/priority/users/visual) | |
/treino/contest-create/import | POST | Bearer+criador |
{tar_b64} (.tar.gz com contest.json +
enunciados/) → cria | |
/treino/contest-create/templates | GET/POST |
Bearer+criador | templates nomeados por criador
(treino/var/contest-templates/<login>.json). GET
lista os meus (?name= → 1, senão 404). POST
{op:save,name,(template{}\|from_contest,include_problems?)}
| {op:delete,name} |
{op:rename,name,new_name}. O spec salvo é
relativizado + whitelist no servidor: datas viram
duration/login_lead/freeze_before_end;
nunca guarda usuários/senhas/id/datas absolutas.
from_contest: só dono do contest ou admin (senão
404). Limites: 20/usuário, spec ≤64KB | |
/treino/contest-create/export?id=&full_statements=0\|1
| GET | Bearer+criador | baixa o spec JSON de um
contest existente (formato do /create — round-trip). Gate:
created-by + (dono ou admin) — senão
404. Nunca exporta
passwd/users/senhas/submissões. Enunciados: default embute só o material
exclusivo do contest (sem json público no banco);
full_statements=1 embute tudo | |
/treino/contest-create/duplicate | POST | Bearer+criador |
{from, id?, name?, start?, end?, admin?, users?\|users_from?}
→ cria contest novo copiando conf+problemas+visual do from
(usuários/submissões nunca; enunciado custom copiado
por arquivo). Datas: start=agora,
end=start+duração original;
login_start/freeze relativos preservados;
name default "Cópia de …". Gate do from = o do
export (404) | | /treino/contest-create/mine | GET |
Bearer+criador | contests criados por mim
(owner==login + created-by)
{contests:[{id,name,mode,created_at,start,end,problems_count}],total}
(admin usa /treino/admin/contests p/ a lista completa) | |
/treino/admin/contest-perms | GET/POST | admin | lê/define
{threshold,allow[],deny[]} | |
/treino/admin/contests | GET | admin | contests criados
pela interface | | /treino/admin/contest-remove | POST |
admin | {contest} → move p/ lixeira (só os criados pela
interface) |
Ações auditadas (em
treino/var/admin-audit.log):contest-create,contest-template,contest-export,contest-perms,contest-remove— além denews-*,logout-*,lock-user.
O CLI moj-contest
(web/moj-contest, servido em GET /moj-contest;
fonte em moj-cli/; moj contest … delega a ele)
cobre estas rotas e as de /contest/admin/*: criação (spec/
template), templates nomeados, export/duplicate, settings, problemas
(com sorteio por coleção), usuários, sessões,
auditoria, remoção e os documentos da prova
(docs ls|gen|get|publish|unpublish|cover|set|text —
ls/get valem p/ QUALQUER conta do contest,
então a sede (.cstaff) baixa o publicado
pelo terminal, útil em rede isolada). Sessões: criação/reuso = token do
treino (moj login); administração = token
daquele contest
(moj-contest login <cid>, conta *.admin
do contest) — o corte de acesso é sempre o do servidor.
Acessado por <id>.moj.<base> (subdomínio): o
nginx injeta CONTEST_HOST; a API só serve aquele contest
(auth/contest/submit/submission)
e o frontend redireciona o resto para /contest/. Login com
gate opcional por substring de User-Agent
(LOGIN_UA_SUBSTRING, só não-privilegiados). Papéis:
.admin/.judge/.cjudge
(juiz-chefe, herda juiz)/.staff/.mon.
| Rota | Método | Papel | I/O |
|---|---|---|---|
/contest/admin/sessions?contest=<c> |
GET | admin | sessões ativas + alerta de UA/IP diferentes |
/contest/admin/access-log?contest=<c>&day= |
GET | admin | log de acessos (epoch/login/ip/UA) + alertas |
/contest/admin/audit-log?contest=<c>&since=&action=&user=&limit= |
GET | admin | feed unificado (trace no instante exato de cada
evento) {events:[{time,who,kind,action,details}],count}. 4
fontes: admin (var/admin-audit.log),
login (var/access.log), submit (1
por submissão, no sub_epoch do
users/<login>/history), verdict (1 por
correção, no finalized_at do
users/<login>/results/<subid>.json — traz o
juiz; who = o aluno). Cada submissão gera 2
entradas: a submissão (quando o aluno enviou) e o veredicto
(quando o juiz respondeu); pendente = só a submissão. As 4 fontes viram
NDJSON num temporário e saem numa passada de jq --slurpfile
— nunca --argjson (o array do admin-audit
sozinho passa dos 128 KiB de MAX_ARG_STRLEN) |
/contest/admin/dashboard?contest=<c> |
GET | admin | situação ao vivo:
{judges:{online,busy,total,queue_depth,assigned,pool[],list[]}, routing, submissions:{total,pending,pending_list[],max_wait_s,response:{avg_s,max_s,p50_s,p95_s},timeline[]}}
(routing = shards do escritor, mesmo shape do
/treino/admin/queue) (janela = últimas N submissões;
pool = hostnames de CONTEST_JUDGES,
[] = sem pool — o front marca ⭐ os hosts do pool e alerta
pool offline) |
/contest/admin/settings?contest=<c> |
GET/POST | admin | tempos, login on/off, abertura, freeze, locale,
tz (fuso IANA da prova →
CONTEST_TZ; vazio/null volta ao MOJ_TZ da
instalação, 422 tz_invalid se não existir no zoneinfo —
governa TODA hora que o SERVIDOR escreve p/ gente sobre o contest: DM do
mojinho, preflight, caderno, relatório; a web sempre mostrou no relógio
do browser), toggles
show_code/show_log/show_editor/show_tl/allow_late/score_anon/allow_backup/allow_print/manual_verdict/secret,
login_ua_substring, languages[] (whitelist do
contest), judges[] (pool de juízes do
contest: hostnames do registro, vazio = qualquer juiz online; vira
CONTEST_JUDGES no conf — o job leva
allowed_hosts e o escalonador é ESTRITO:
pool offline segura a fila; o TL de /contest/problems passa
a ser só do pool), score_full_users[] (logins que veem o
placar completo além de
.admin/.judge/.cjudge).
Penalidade ICPC: penalty_minutes (int,
default 20) e penalty_verdicts (array de códigos
wa/tle/mle/rte/ce, default sem ce) — quais
verdicts contam penalidade e o peso por tentativa; Judge Error/pendentes
nunca contam; mudar freeze/penalidade dispara
rebuild FORÇADO (score_kick_rebuild — imune à
corrida de mtime com build em voo; ver
/contest/admin/finish). O GET devolve também
mode (read-only, modo do placar).
show_log é o valor EFETIVO: em modo
icpc com SHOWLOG ausente do conf o default é
false (o report expõe os testes — anti-vazamento); no
POST, show_log:true grava SHOWLOG=1
explícito (religar fica registrado) e
false grava SHOWLOG=0. secret =
SUPER SECRETO (fora das listagens públicas;
placar/visual exigem login no contest; a UI exige digitar o id p/
desmarcar). manual_verdict (opt-in, default OFF) liga o
veredicto manual: o daemon SEGURA o veredicto computado
p/ revisão de juízes humanos (exceto o que a matriz
auto-verdicts libera).
review_judges (int 1..5, default 2 =
ausente do conf; vira REVIEW_JUDGES) = QUANTOS juízes
validam cada veredicto — N votos unânimes liberam; divergência vira
conflito p/ o chief; 1 = revisão simples. Desligar
manual_verdict VARRE a fila de revisão: o que
ninguém contestou (sem voto e sem conflito) é liberado com o veredicto
COMPUTADO — senão as sobras ficavam presas p/ sempre (o
juiz comum não consegue mais votar e o competidor fica em Not
Answered Yet); item com voto ou em CONFLITO não é
atropelado e fica p/ o juiz-chefe. A resposta traz
review_released/review_pending; auditado
review-manual-off.
balloons_during_freeze (bool, default
false = retém) = entregar balão com o placar CONGELADO.
Default protege o freeze: AC feito no congelamento não vira tarefa de
entrega e não é entregue depois (ver /contest/staff/queue).
LIGAR libera retroativamente o que ficou retido (apaga
as lápides + o stamp; o próximo carregamento da fila materializa tudo —
id determinístico, não duplica) e a resposta traz
balloons_released; auditado
balloon-freeze-release. O GET traz também
balloons_frozen = quantos estão suprimidos
agora. balloon_style
(icon|fill, default icon =
SCORE_BALLOON_STYLE ausente do conf; 422
balloon_style_invalid) = como a célula "resolveu" é pintada
no placar/cerimônia/relatório (ver SCOREBOARD.md) |
/contest/admin/seed?contest=<c> |
POST | admin do contest, e só com DEMO=1 no
conf (senão 403
demo_required) |
povoa um contest de DEMONSTRAÇÃO com times e
submissões SINTÉTICAS — existe para quem desenvolve o
Animeitor (ou qualquer cliente de placar) ter um placar
de verdade para trabalhar sem uma prova acontecendo. Body (tudo
opcional):
{teams:20, submissions:200, seed:1, freeze_minute, window_minutes, password:"demo1234", verdicts:{accepted,wrong,tle,rte,ce,pending}}.
Cria os times que faltarem (time-01…N, com
.team sigla/bandeira/sede) e escreve as submissões pelos
mesmos escritores do veredicto real
(user_history_append + metrics_recompute +
score/build.sh) — o resultado é indistinguível para placar,
estatística, webcast e balões. Determinístico pelo
seed (mesmo seed ⇒ mesmo placar; a janela é
arredondada a minuto cheio e window_minutes a fixa).
freeze_minute grava o FREEZE_TIME (é o que faz
placar.txt diferir de placar-full.txt). O
probid gravado é o canônico
(PROBS[i+4]) — qualquer outra grafia deixaria a célula em
branco no placar em silêncio. Limites: teams 1..500,
submissions 0..20000. É ADITIVO: chamar de
novo soma ao que já existe (times que já existem não são recriados) —
para começar do zero, apague o contest e crie outro. Resposta:
{teams, teams_created, submissions, seed, freeze_time, window_minutes, password, board_lines, runs_after_freeze, hint, by_verdict{}}
— runs_after_freeze:0 com freeze pedido vem com
hint: o congelamento caiu na borda da janela
semeada, o placar congelado sai igual ao completo e não há revelação
para testar. Auditado (seed). ⚠ a marca DEMO=1
só é gravada na CRIAÇÃO do contest (demo:true no spec do
/treino/contest-create/create) — não há toggle que a ligue
depois |
/contest/admin/problems?contest=<c> |
GET/POST | admin | GET inclui languages e judges por
problema; {action:add|remove|reorder|rename} (reescreve
PROBS) — rename
{letter, name?, new_letter?} também troca o
IDENTIFICADOR (^[A-Za-z0-9]{1,3}$; em uso = 422
letter_taken; a cor no balloons.json migra
junto) e reorder só re-letra pela posição quando as
letras atuais são a sequência automática A,B,C,… —
identificador customizado (W1…) sobrevive à reordenação,
{action:langs,letter,languages[]} (whitelist por problema
em problem-langs.json),
{action:judges,letter,judges[]} (pool de juízes por
problema em problem-judges.json; vazio = herda o
pool do contest) ou
{action:statement,letter, html_b64?|pdf_b64?|remove_html?|remove_pdf?|refresh?}
(enunciado por problema em
enunciados/<skey>.{html,pdf}; refresh
re-indexa do banco). add de problema
PRIVADO: só se o dono do contest (arquivo
owner) for dono/colaborador do problema (mesmo guard da
criação); senão 404 (não vaza a existência). Contest
sem owner (legado): só público |
/contest/admin/bank?contest=<c>&q=&limit=&collection= |
GET | admin | busca p/ adicionar problemas: banco público + os PRIVADOS a
que o dono do contest tem acesso (dono/colaborador no índice —
o mesmo sujeito do gate de add; a busca lista exatamente o
que pode entrar). Privados primeiro.
{problems:[{id,title,tags,collections,access:mine|shared|public,private,has_statement}],total,mine,shared}.
Contest sem owner (legado) → só públicos.
?meta=1 →
{tags:[{tag,count}],collections:[{collection,count}]}
(agregado do banco público — sorteio é público) |
/contest/admin/draw?contest=<c>&tags=&collections=&count=&match=&difficulty=&seed= |
GET | admin | sorteio no banco público (mesmo contrato do draw do wizard:
coleção/tag/dificuldade em AND, collections = array JSON
url-encoded, reproduzível por seed) |
/contest/statistics?contest=<c> |
GET | admin/judge/mon | totais, por-problema (first_minute
relativo ao início + first_seconds p/
desempate + first_solver_name = nome do
time de quem resolveu primeiro; estatística nunca mostra só o login, e o
nome é resolvido no CACHE porque os dois consumidores — painel e
relatório offline — não consultam contas), por-linguagem, veredictos,
linha do tempo. Tempo = sub_epoch - CONTEST_START (não
EPOCH). Só usuários normais (descarta
.admin/.judge/.staff/.mon). Recortes
prontos (2026-08-30): by_region:{<sede>: …}
e by_country:{<flag>: …} — cada valor tem o MESMO
shape do agregado global
(totals/problems/languages/verdicts/timeline/dists,
first_solver* recalculado DENTRO do recorte), computados na
mesma passada do gerador; sede = .team.region e
também cada NÓ da árvore de regions.json (país ›
região/supersede › sede — o nó agrega por REGEX de login, como o
regionMatch do placar, com dedup quando nome == sede; regex
inválida é descartada), então o seletor de Sede da estatística oferece a
MESMA árvore do placar; país = o PREFIXO do
.team.flag minúsculo (br-pr → br:
time brasileiro declara bandeira de ESTADO e "estatísticas do Brasil"
tem de juntá-los — o filtro "Bandeira" do placar casa pela mesma
hierarquia); conta sem o dado fica fora do recorte correspondente. A UI
(/contest/statistics/) expõe dois selects mutuamente
exclusivos. População (2026-08-31, relato da LATAM):
totals traz enrolled (INSCRITOS
não-privilegiados, a mesma população do placar), users
(quem submeteu) e absent (a diferença) — no global e em
cada recorte; o bucket 0 do problems_solved_dist INCLUI os
ausentes (a distribuição casa com o placar). Nó do
regions.json com view:true
(supersede/femininos — recorte que SOBREPÕE as sedes) sai com
view:true na fatia e a UI avisa "não some com as sedes" (⚠
fatia é chaveada por NOME: dê nomes próprios aos recortes).
Estatísticas 2.0 (2026-09-01): problems[]
ganha
avg_ac_min/tries_per_ac/dirt
(métrica do resolver ICPC: % de subs erradas entre quem
resolveu)/ac_langs; cada recorte tem dirt; o
GLOBAL ganha ac_events
([[login,prob,minuto,tentativas]…], 1º AC de cada
time×problema, convidados inclusos — base ÚNICA das seções
corrida/comparação/desempenho, que a UI filtra pelo recorte corrente),
teams_idx (login→{n:nome,c:país,r:sede} de
todo time com AC), penalty_minutes e
unranked_regex (a regex das coortes convidadas, p/ o
cliente aplicar o MESMO corte do ranking), top_teams (10,
oficiais) e performance (média/mediana/quartis/p90 de
resolvidos; média/mediana/quartis de penalidade ICPC;
first_ac_median) — a UI recomputa desempenho/top 15
client-side por recorte e só mostra o quadro com ≥30 times com AC. Cache
em var/statistics.cache.json
(server/score/stats-gen.sh), invalidado por
history/conf. |
/contest/clarifications?contest=<c> |
GET | Bearer | role-aware (admin/judge/mon = todas; demais = próprias + públicas,
sem answered_by). O asker
(.login) NUNCA é exposto — nem aos juízes
(tratamento isonômico; recuperável só pelo admin via auditoria
clarification-ask). Privilegiado recebe
answer_claim (reserva) e is_chief |
/contest/clarification-ask?contest=<c> |
POST | Bearer | {problem?,question} |
/contest/clarification-claim?contest=<c> |
POST | admin/judge/mon | {id,action:claim|release} — reserva p/
responder (dois juízes não pegam a mesma; TTL 5 min, expira na
leitura). Auditado
(clar-claim/clar-release) |
/contest/clarification-answer?contest=<c> |
POST | admin/judge/mon | {id,answer,public?} — sob flock + reserva;
já respondida só o juiz-chefe/admin edita
(409 already_answered); abertas exigem a reserva
(409 clar_claimed). Auditado (edited=) |
/contest/clarification-broadcast?contest=<c> |
POST | admin/judge/mon | aviso oficial
{problem?,question,answer} — Q+A público já respondido,
autor oculto (login:"",
broadcast:true; UI mostra "Organização"). Auditado |
/contest/admin/cohorts?contest=<c> |
GET/POST | Bearer (GET admin ou .cjudge; POST
só admin) |
coortes de placar (times oficiais ×
CONVIDADOS/extra-oficiais; motor em lib/cohorts.sh, formato
em docs/SCOREBOARD.md). GET →
{cohorts:[{id,name,regex,public,unranked,default,sees}], results_released, views, counts:{id:n}, by_regex_only:[login]}.
POST {action}:
add/set
{id,name?,regex?,public?,unranked?,sees?,default?} (regex
tem de COMPILAR; máx 8 coortes; a marca default é única e
sempre existe) · rm {id}
(recusa a default e coorte com time: 409
is_default/cohort_in_use) ·
assign {login,cohort} (grava
.team.cohort; "" devolve o time à regra) ·
materialize (carimba o campo em quem hoje
casa só por regex) · release
{on} = liberar os resultados (todos passam
a ver todos). Tudo auditado (cohorts-*) e toca
var/.score-dirty |
/contest/admin/rounds?contest=<c> |
GET/POST | Bearer (GET admin ou .cjudge; POST
só admin) |
rodadas: GET →
{active, rounds[], next, promote_ready:{ok, blockers:[{code,detail}]}}
(a rodada ATIVA é espelhada do conf a cada leitura — editar
em ⚙️ Configurações/📚 Problemas nunca diverge). POST
{action}: add
{slug,name?,kind?,start,end,freeze?} (rodada planejada;
kind ∈ warmup|official|extra; freeze tem de
cair na janela) · set
{slug,new_slug?,…} (renomeia e edita; a ativa vai direto p/
o conf — pelo OBJETO editado, senão o espelho conf→json
anularia a edição) · problems
{slug,problems[]} (mesma guarda de problema privado do
wizard) · remove {slug} (só
pending — arquivada é auditoria) ·
publish {slug,on} ·
promote {to?,force?}.
Promover = arquivar a rodada ativa + zerar o store +
aplicar a janela/PROBS da próxima; recusa com 409
not_ready + blockers
(round_running, jobs_in_flight,
pending_verdicts, review_pending,
judged_down, no_next_round,
shared_users), force:true ignora todos menos
no_next_round/shared_users. Tudo auditado
(round-add/set/problems/remove/publish/promote[-forced]) |
/contest/admin/round-archive?contest=<c>&round=<slug> |
GET | Bearer (admin) | tar.gz do arquivo CRU da rodada
(rounds/<slug>/: history, código-fonte, mojlog,
results, review, clarifications, backups, logs copiados,
conf.snapshot, meta.json). Auditado
(round-archive-download) |
/contest/admin/ua-gate?contest=<c>[&login=<l>] |
GET/POST | Bearer (GET admin ou .cjudge; POST
só admin, action:"check" também p/
chefe) |
gate de navegador POR SEDE
(lib/ua-gate.sh,
contests/<c>/ua-gate.json): a imagem de prova de cada
sede manda um UA com um pedaço do próprio login do time
(teambrspso001 → brspso). GET →
{gate:{mode,from_login,by_region,by_regex,exempt,fallback}, legacy, regions, check}
— com ?login= faz o dry-run (o esperado
daquele time, sem ele logar). POST {action:"set", …} valida
que todo regex COMPILA
(422 regex_invalid), ≤200 chars, ≤100 sedes, ≤50 regras,
≤200 isentos; {action:"check", login} = o dry-run. Ordem de
resolução: exempt › conta de papel › by_regex
› by_region (sede do time) › from_login
(captura \1) ›
fallback/LOGIN_UA_SUBSTRING legado.
mode:"off" desliga sem apagar a config. Auditado
(ua-gate-set) |
/contest/admin/machines?contest=<c>[&round=<slug>] |
GET | Bearer (admin ou .cjudge) |
mapa de máquinas da rodada — time × IP × UA,
agregado do var/access.log recortado pela janela da rodada
(nada novo é capturado):
{window, by_login:[{login,name,region,ips,uas,pairs:[{ip,ua,n,first,last}],multi_ip,changed}], by_ip:[{ip,logins,shared}], uas[], ua_suggestion, totals}.
changed = IP/UA diferente do machines.json da
última rodada arquivada (o time trocou de máquina depois do
aquecimento); ua_suggestion = maior substring comum dos UAs
vistos; ua_expected/ua_match por
time = o que a imagem da sede dele deveria mandar × o que veio
(é como se conserta a sala no aquecimento), e
totals.ua_mismatch conta quem está fora. Só
leitura: preencher sede =
POST /contest/admin/teams {set:…}, armar gate =
POST /contest/admin/settings {login_ua_substring}. Auditado
(machines-view) |
/contest/admin/docs?contest=<c> |
GET/POST | Bearer (admin OU .cjudge —
is_admin_or_chief) |
gestão dos documentos da prova (aba 📄 Documentos;
motor em lib/contest-docs.sh, PDF via soffice,
único engine da imagem). GET →
{docs:[{type,lang,…,published,uploaded,uploaded_bytes,uploaded_at}], config:{…}, langs:["pt","en","es"], templates:{info_sheet:{<lang>},cover:{<lang>}, …chaves planas antigas}, cover_uploaded:{<lang>:bool}, problems:[{letter,name,has_pdf,has_html}]}.
POST {action}: config
{caderno_version?,cover_note?,errata?,info_sheet_<lang>?,cover_<lang>?}
(texto vazio = volta ao default embarcado de server/etc/);
cover {lang,pdf_b64} \ Tipo
novo editorial (a solução de cada
problema, do docs/solucao.md do PACOTE via
pkg_path — o campo que nunca vai ao aluno; nota
introdutória editorial_note no config):
publish de editorial exige
contest_over_for_all (403
contest_running); publish com
news:true de contest/times
antes do início é recusado (409
news_before_start — a notícia anexa o PDF e vaza fora do
gate de fase). |
/contest/admin/news?contest=<c> |
POST | admin/judge/mon (edit só
admin/chief) |
{action:add|remove|edit,…} notícias do contest;
add aceita anexo {filename,file_b64} (em
news-files/<id>/); edit {id,title,text}
(notícia já enviada). Auditado (news-add/remove/edit) |
/contest/admin/backups?contest=<c>[&user=&q=] |
GET | admin | lista TODOS os backups (filtra por login/nome)
{backups:[{login,id,name,size,time}],users:[{login,count,bytes}]} |
/contest/nutella?contest=<c> |
GET/POST | GET: admin/chefe/.cstaff/.staff · POST
config/collect/push-roster: admin · POST command: admin ou c/staff
escopado |
Integração NUTELLABOOT (máquinas mlinux das sedes)
— ver NUTELLABOOT.md. GET →
{configured, url, status, can_admin, scoped, data};
data = o panorama coletado
(var/nutella.cache.json, version:2:
contest{start,end}, `link{mode:ua |
/contest/admin/backup-zip?contest=<c>&login=<l> |
GET | admin | baixa um zip com todos os backups do usuário (nomes originais, prefixados por data) |
/contest/admin/report?contest=<c> |
GET | admin | baixa o relatório estático da prova:
tar.gz com um site navegável offline
(file:// ou qualquer web server): index.html
(placar ABERTO, sem freeze + info + links dos
enunciados), score-frozen.html (só com freeze),
runs.html (todas as submissões:
min/hora/time/login/prob/ling/veredicto canônico),
statements/<letra>.{html,pdf},
clarifications.html (todas, asker e
respondente anônimos), statistics.html,
staff-tasks.html (impressões+balões, só metadados) e
infra.html (juízes, espera avg/p50/p95/máx da prova
inteira, por problema/juiz, timeline). Gerado na hora por
server/score/report-gen.sh (roda
build.sh+stats-gen.sh antes; balões
reconciliados). NUNCA sai: código-fonte,
mojlog, tests[]/tl_used dos
results, senhas/e-mails, asker de clarification, .src de
impressão. Concorrência → 429 busy.
Auditado (report-download). Visual e conteúdo
(2026-08): usa a identidade do MOJ (o
web/shared/ui.css é inlinado, topbar com logo em
data: URI), bandeiras viram o mesmo SVG do
placar ao vivo embutido em data: URI (o emoji antigo não
renderizava e código de ESTADO — br-rj — saía como texto),
placar com coluna Penal. + desempate e linha de
convidado iguais aos do placar oficial, lista de problemas com
Autor (arquivo author do pacote),
statistics.html com as mesmas seções do
painel (inlina web/lib/stats-view.js +
charts.js + shared/dom.js, com
<noscript> mantendo as tabelas) e aba 📄
Documentos com os documentos publicados
copiados p/ documentos/. Bilíngue: TODAS
as páginas seguem o LOCALE do contest (tabela
rep_t no gerador, no molde do _doc_t; blocos
awk/jq recebem os rótulos por -v/--arg),
inclusive o formato de data e o <html lang> |
/contest/admin/finish?contest=<c> |
GET/POST | GET: admin ou juiz-chefe · POST: admin | ENCERRAR O EVENTO — o espelho pós-prova do
preflight. GET → checklist
{checks:[{id,level,label,detail}], summary:{ok,warn,fail}, can_finish, can_act, pending_docs:[{type,lang}]}
(ids: fim, freeze, docs = o que o
botão resolve; show_log, show_code,
cohorts, report = informativos, só com atalho
p/ o painel que resolve; can_act = true só p/ o admin — o
juiz-chefe LÊ o checklist mas o POST é do admin, e o front usa o campo
p/ esconder o botão). POST {action:"finish"} → abre
o placar (FREEZE_TIME=0 + rebuild
forçado via score_kick_rebuild: remove
var/.metrics-stamp, toca .score-dirty e
dispara um build destacado que ESPERA o lock — um build em voo que
terminasse depois da escrita do conf carimbava tudo "fresco" com os
metrics pré-mudança e o placar ficava congelado p/ sempre; corrida de
mtime, Maratona 29/08) e publica todo documento já
gerado que ainda não estava publicado (doc_publish
de lib/contest-docs.sh — mesmo caminho do painel de
documentos, incl. o resources.json); devolve
{finished:true, done:[{item,detail}], skipped:[{item,reason}]}.
Idempotente. Só depois do fim para todas as sedes (409
contest_running); concorrência → 429 busy.
Auditado (finish-event) |
/contest/admin/staff-filters?contest=<c> |
GET/POST | admin | escopo dos usuários .staff e
.cstaff (sedes distribuídas): cada entrada é
region:<nome> (igualdade com a sede
.team.region do aluno — sem regex) ou uma
regex no login (clássico). No .staff governa a fila/ações;
no .cstaff governa a fila (leitura), as
etiquetas e a cerimônia da sede —
configure-o sempre. GET →
{staff:[{login,fullname,disabled}], filters:{login:[entradas]}, regions:[{name,regex}]}
(regions p/ semear — a UI insere region:<nome>). POST
{filters:{login:[entradas]}} (chaves =
.staff/.cstaff existentes; vazio = vê tudo) →
grava print-requests/staff-filters.json.
Auditado (staff-filters) |
/contest/admin/teams?contest=<c> |
POST | admin | gerência de times por-usuário (aba 👥 Times).
{set:{<login>:{fullname?,univ_short?,univ_full?,country?,region?}}}
— fullname (o nome do time, campo único)
mescla em .fullname (vazio = ignorado); os demais mesclam
no .team ("" apaga o campo;
ausente não toca); login inexistente → skipped;
{action:"materialize"} = match de 1
clique: aplica teams-meta (regex→país/escola) e regions
(regex→nome de sede) aos campos vazios de cada usuário,
gravando por-usuário (nunca sobrescreve preenchido) →
{materialized,filled}. Contest com USERS_FROM
→ 409 shared_users.
Auditado
(teams-set/teams-materialize) |
/contest/admin/team-assets?contest=<c> |
POST | admin | upload de foto (photo.png, lado máx
1000) e brasão (logo.png, máx 128) do
time:
{kind:photo|logo, filename:"<login>.<ext>", file_b64}
— UM arquivo por POST (a UI envia lotes em sequência); o basename sem
extensão casa o login case-insensitive (não casou →
404). {action:"delete",kind,login} remove. Máx 8MB;
USERS_FROM → 409; converte p/ PNG sem metadados.
Auditado (team-asset) |
/contest/admin/preflight?contest=<c> |
GET | admin/chefe | checklist pré-prova verde/amarelo/vermelho →
{checks:[{id,level:ok|warn|fail,label,detail}],summary}.
Verifica: janela, SHOWLOG efetivo (fail em icpc se
visível — anti-vazamento), show_code, freeze na janela, juízes
online (registry), pool de juízes
(pool: fail se NENHUM host do pool do contest está online —
modelo estrito, fila presa; warn p/ host offline/não registrado,
inclusive dos overrides por problema; pool_problems: fail
se o pool efetivo de algum problema está todo offline),
toolchain das linguagens permitidas (só nos juízes do
pool, quando definido), TL calibrado de cada problema
do conf no pool efetivo (+ cache nos juízes online do
pool), staff de impressão, contas de competidores, spool
travado (daemon), modo/veredicto manual/prorrogações
(informativos, e a prorrogação avisa quando o fim prorrogado passa do
freeze), escopo do staff
(staff_filters: warn quando
.staff/.cstaff sem filtro veem a fila e as
ETIQUETAS COM SENHA de todos), balões
(balloons: warn com >15 problemas e sem
balloons.json — da letra P em diante o balão sai cinza),
coortes (cohorts: quantas privadas; warn
quando os resultados já foram liberados), gate de
navegador (ua_gate: fail se armado e a regra não
casa NINGUÉM, warn com times sem regra — isento declarado não conta),
rodada seguinte (next_round: o slug
pendente + os bloqueadores de rd_promote_blockers) e
documentos (docs: gerados/publicados). O
contador de competidores exclui TODOS os sufixos de papel (inclusive
.cstaff). É a lista da 🏁 Central do
painel de admin |
/contest/admin/registrations?contest=<c> |
GET/POST | Bearer (GET admin ou .cjudge; POST
só admin) |
inscrições do contest (roster + janela; motor em
lib/registration.sh). GET →
{enabled, remind, tz, window:{…,official_start,official_round}, round_kind, gate_active, team_max, teams_allowed, teams:[{login,name,captain,members,invited,invited_meta:[{login,at,dm_at,warned_at,tg}],cohort}], individuals:[{login,cohort,at,univ,flag,ai}], totals:{…,invites,invites_no_tg}}
(invited_meta = desde quando o convite espera, se a pessoa
tem Telegram vinculado (tg) e quando foi
avisada). POST {action}:
enable (cria o roster vazio + semeia as
coortes) · disable (roster vira
.off; ninguém é desmaterializado) ·
window
{open?,close?,late_minutes?,team_max?,teams?,remind?} →
grava
REG_OPEN/REG_CLOSE/REG_LATE_MINUTES/REG_TEAM_MAX/REG_TEAMS/REG_REMIND
no conf (campo ausente não mexe;
null APAGA a chave — é assim que
close:null faz a janela voltar a herdar o início da prova.
Epochs são sempre UTC: quem formata p/ o organizador é
o browser) ·
add/rm
{login} · team-add
{name,members[]} (o 1º membro é o capitão) ·
team-rm {team} ·
team-meta
{team,univ?,ai?,flag?} (a organização ajusta a apresentação
— o painel Pessoas › Times é 409 em contest com USERS_FROM) ·
individual-meta
{login,univ?,ai?,flag?} (idem p/ um inscrito individual) ·
materialize (reescreve os overlays do
store a partir do roster) · invite-remind
{team,login} /
invite-remind-all (o mojinho
cutuca convite pendente por DM, com o link de aceitar/recusar;
devolve remind_result:{sent,skipped[]} junto do estado —
404 no_invite, 409 no_telegram no singular,
409 registration_closed com a janela fechada). Auditado
(reg-*) |
/contest/admin/time-overrides?contest=<c> |
GET/POST | admin | prorrogação de vigência por sede/grupo (ex.: queda
de energia numa sede ⇒ só aqueles times ganham minutos). Regras
[{regex,end,reason?}] testadas contra o
login — a 1ª que casa define o fim EFETIVO daquele
grupo (só estende o CONTEST_END; nunca
encurta; penalidade segue contada do início global). Aplicado na API por
contest_end_effective (lib/contest-gate.sh): vale no
/submit (403 contest_ended usa o fim efetivo)
e no countdown (/contest/basic autenticado). GET →
{rules,contest_end,regions} (regions p/ semear); POST
{rules:[…]} substitui a lista (valida
regex/end; máx 50) → grava
contests/<c>/time-overrides.json.
Auditado (time-overrides). CLI:
moj-contest extend <+min|epoch> --group <regex> |
/contest/admin/jplag-run?contest=<c> |
POST | admin | dispara o jplag (background) |
/contest/admin/jplag-results?contest=<c> |
GET | admin | {status, results:[{problem,lang,pairs:[{a,b,similarity, a_login,a_name,a_univ, b_login,b_name,b_univ}]}]}
— a/b = nome sanitizado do arquivo dado ao
jplag; a_login = login literal, a_name = nome
do time (.team.name // .fullname), a_univ =
sigla (campos aditivos; run antigo não os tem) |
/contest/admin/jplag-match?contest=<c>&run=&i= |
GET | admin | HTML lado-a-lado da comparação |
/contest/userinfo?contest=<c> |
GET | Bearer | + show_editor/show_log/show_code/is_mon/is_chief |
/contest/admin/logout-user?contest=<c> |
POST | admin | {login} → encerra sessões do usuário |
/contest/admin/user-disable?contest=<c> |
POST | admin | {login} → bloqueia (senha !…) + desloga
(reabilita via user-add) — NÃO tira do placar (p/ isso:
user-disqualify ou user-remove). Conta
privilegiada (.admin/.judge/.cjudge/.staff/.cstaff/.mon) →
403 |
/contest/admin/users-set-password?contest=<c> |
POST | admin | {password,include_disabled?} → senha única p/ todos os
não-privilegiados (prova; pula
.admin/.judge/.cjudge/.staff/.cstaff/.mon). Recusa
contest=treino (400 treino_forbidden
— resetaria a plataforma inteira) |
/contest/admin/logout-mismatch?contest=<c> |
POST | admin | desloga sessões cujo UA ≠ LOGIN_UA_SUBSTRING (preserva
contas privilegiadas, incl. .cjudge) |
Tela única
/contest/admin/(hub com sub-abas: Configurações, Problemas, Aparência, Usuários, Log & sessões);/contest/{admin_tasks,log}/redirecionam para ela. Placar:score_anonno conf → modo anônimo (agregado/quartis, sem nomes); home abre contests pelo subdomínio. Auditado emcontests/<c>/var/admin-audit.log:settings,problems-*,clarification-answer,clar-claim,clar-release,clarification-broadcast,news-*,jplag-run,logout-user,user-disable,users-set-password,logout-mismatch.