MOJ — API v1 (referência) — MOJ docs

MOJ — API v1 (referência)

Base: /api/v1. Roteador único: server/api/v1/router.shhandlers/<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.

Auth

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}

Index (home)

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)

Treino

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)

Treino — cadastro & vínculo Telegram (overlay do treino)

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)

Treino — painel admin (.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)

Gestão de problemas (Bearer)

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 em owners_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 em lib/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 é delemembro 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)

Autoria (escrita keyless — git escondido, commit autorado pelo login via 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/edit cobrem o pacote inteiro: title (vem do campo, não de % Título no texto — o render injeta o h1), enunciado_md, conf_text (TL/ulimits/STOPWHEN/…, ver saad-problems/README.org), examples (sample; cada um aceita explanation opcional → docs/sample-notes.json, mostrada após o exemplo), tests (ocultos), sols por categoria {good,wrong,slow,pass,upcoming} (cada [{filename,code}]), score (grupos de pontuação; cada grupo tem {name,weight,glob} e o glob pode ser uma lista ", "-separada de padrões, ex.: g2_*, g3_*) e editorial_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. 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). 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) | | /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: 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 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: .admin ou 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).

Orgs (modelo MOJ-nativo)

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ícita409 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 overlay contests/treino/var/authored.json (mesclado ao índice). Público só se a org permitir (public_allowed) — camada anti-vazamento de prova.

Submissão (assíncrona)

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 .mondurante 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_kindtests|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)

Contest

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.jsonCONTEST_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"} (.stafflocked:"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; .staff403 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/teamsou 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 do contest (logado como .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 no passwd compartilhado (ex.: treino), mantendo o .admin próprio. | /contest/score?contest=<c> | (Bearer opcional) | TXT (1ª linha = modo) — ver SCOREBOARD.md. 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=public forç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 = 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). |

Admin / Judge / Ops (Bearer + papel)

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/* e ops/* (exceto ops/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-password e ops/alerts (todos bot-token mojb_…), + /index/status (público).

Status do sistema (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

Criação de contest (treino)

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 é reutilizadoadmin_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 de news-*, 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.

Ambiente de contest (subdomínio + admin do contest)

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 --slurpfilenunca --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_FROM409 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_anon no conf → modo anônimo (agregado/quartis, sem nomes); home abre contests pelo subdomínio. Auditado em contests/<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.