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} |
/auth/status?contest=<c> |
GET | Bearer | {logged_in,login,name,contest,is_admin,is_judge,is_staff,is_cstaff,is_chief}
(.cjudge = juiz-chefe →
is_judge:true,is_chief:true; .cstaff = chefe
de sede → is_cstaff:true, sem herdar
is_staff) |
/auth/logout |
POST | Bearer | {logged_out:true} |
| 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}).
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). 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}]
(collections = .moj-meta.json do pacote, um
problema pode estar em várias). 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/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} — máx. 2/ano,
cascata nos arquivos de controle incluindo 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); POST aceita também
favorite_editor, profile_public |
/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/problem-stats?id=<p> |
— | estatísticas do problema (métricas, veredictos, por-linguagem c/
solvers distintos, editores, avatares públicos) — 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/access-log?day=YYYY-MM-DD |
GET | log de acessos (filtra por dia) |
/treino/admin/queue |
GET | pendentes por lista + calibração
{total_pending,spool_queued,calib_pending,calib_inflight,calib_targeted,lists:[{contest,name,pending}]}
(calib_pending = fila de calibração
kind=calibrate, separada de index;
calib_targeted = recalibrações direcionadas por host) |
/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 |
/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 |
/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,updated_at}]}.
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,calibrated_checksum,hosts,updated_at,calibrated_at,calibrated,stale,needs_recalibration}.
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,hosts:[{host,tl,missing,at,log,reports}]}.
missing_langs = linguagens good sem TL em
nenhum host (solução good falhou em TODAS as máquinas);
hosts[].missing = faltantes naquele juiz. Sols
.py2/.py3 legadas contam como
py |
/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/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> |
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}
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) |
/problems/preview |
POST
{enunciado_md, enunciado_format?, examples?, title?} |
pré-visualização HTML (= o renderizador único
render-statement.sh, idêntico ao servido) — injeta o
título (h1) e os exemplos (cada um com
explanation opcional) → {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/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 tudo (commit+push) — máquinas sem git /
offline |
/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 |
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). |/problems/create| POST{repo,prob,enunciado_md?,author?,tags?,examples?,good_sol?,title?,collections?,languages?,...}| cria problema novo; commit+push;{id,sha}.languages= ids permitidos de submissão ([]/ausente = todas) | |/problems/edit| POST{id, ...campos}| edita (só campos presentes); commit+push autorado. Aceitalanguages(ids de submissão permitidos no.moj-meta.json; ausente = não toca,[]= limpa/todas). Aceita tambémscripts_files(SUBSTITUIscripts/inteiro quando presente — paths validados, sem.., confinado ascripts/,execvira +x,symlinkrecriado se o alvo resolvido fica dentro descripts/; campo ausente = não toca) escore_text(gravatests/scoreverbatim;""remove). Mexer emscripts/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 enviascripts_filesno save | |/problems/script-templates| GET | templates de corretor especial (lidos demojtools/script-templates/<key>/— criar template = criar uma pasta lá):{templates:[{key,name,description,conf_hints,files:[{path,content_b64,exec} \| {path,symlink}]}]}—filesno MESMO shape doscripts_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:confirmtem de repetir EXATAMENTE oid. Dono/colaborador ou admin | |/problems/set-public| POST{id, public:bool}| público on => valida + calibra (index_problem_bgno servidor; só entra no treino se o portão passar) e gravapublicno.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 trazcalibration:"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) | |/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"; síncrono estourava o timeout do nginx). RETOMADA:nameinexistente +toexistente = 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 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|
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) |
/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 |
/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 |
|---|---|---|---|
/submit?contest=<c> |
POST | Bearer | body {problem_id,filename,code_b64,source?}
(source=web|file) →
{submission_id,status:"queued"} (não bloqueia). 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. |
/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). 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) |
/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) |
/contest/problems?contest=<c> |
Bearer | {problems:[{short_name,full_name,problem_id,statement_html_b64,statement_pdf_b64,time_limits,languages}]}
(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. |
/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/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) |
/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) | 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> |
— (secreto exige sessão) | PNG da foto do time (lado máx 1000; o placar mostra 📷 clicável quando existe). 404 sem foto |
/contest/team-logo?contest=<c>&user=<l> |
— (secreto exige sessão) | 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 |
.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}],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 (não pode remover a si mesmo) |
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. |/contest/score?contest=<c>| (Bearer opcional) | TXT (1ª linha = modo) — verSCOREBOARD.md. Cache preguiçoso: (re)geraplacar.txt(público, com freeze) eplacar-full.txt(completo, sem freeze) sehistory/confmudou. Privilegiados (.admin/.judge/.cjudge+ allowlistSCORE_FULL_USERS— vale p/ liberar um.cstaff) com token recebem o completo; demais, o público.&view=publicforça a visão pública/congelada mesmo p/ privilegiado — é 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 = settingsfreeze: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.cstaffquando o contest terminou para todas as sedes (contest_over_for_all: fim do conf + o maiorenddetime-overrides.json— sede prorrogada segura a revelação). Em contest SUPER SECRETO (confSECRET=1) o placar deixa de ser público: sem sessão daquele contest → 401secret_login_required(idemballoons/regions/teams-meta). |
| Rota | Método | Papel | Ação |
|---|---|---|---|
/contest/allsubmissions?contest=<c> |
GET | admin/chief | TXT 9 campos |
/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,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} |
/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 | judge | {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) com
histerese/cooldown e drena o outbox:
{items:[{id,text,chats:[<chat_id>…]}]}. O bot só
entrega (+ grupo). Estado em run/alerts/; sem cron (o poll
do bot é o relógio) |
As rotas
admin/*eops/*(excetoops/alerts, que usa bot-token) são consumidas pelo painel admin e pelo moj-cli. O mojinho-bot hoje é transporte fino: usa sótreino/signup/*,treino/recover-passwordeops/alerts(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}}
(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 |
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). Pode-se criar vazio
(allow_empty) e configurar depois. | 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
e remoção. 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[]}, submissions:{total,pending,pending_list[],max_wait_s,response:{avg_s,max_s,p50_s,p95_s},timeline[]}}
(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,
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; editar em prova recomputa o placar. 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 2 juízes (exceto o que a matriz auto-verdicts
libera) |
/contest/admin/problems?contest=<c> |
GET/POST | admin | GET inclui languages e judges por
problema; {action:add|remove|reorder|rename} (reescreve
PROBS), {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), por-linguagem, veredictos, linha do tempo. Tempo =
sub_epoch - CONTEST_START (não EPOCH). Só usuários
normais (descarta .admin/.judge/.staff/.mon).
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/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/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) |
/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). Aba "✅ Pré-prova" do admin |
/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}]}]} |
/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). 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) |
/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.