MOJ — Visão geral (comece por aqui) — MOJ docs

MOJ — Visão geral (comece por aqui)

O MOJ (Melhor/Meta Online Judge, moj.naquadah.com.br) é um juiz online escrito em bash. Este repositório é a v2 API-first: nginx + backend bash (fcgiwrap) + frontend estático modular, lendo o mesmo contests/<id>/ de sempre (sem migração de dados).

Fluxo de submissão/julgamento e como os daemons conversam: ver FLOW.md. Contrato de rotas: API.md (+ web/api/openapi.json). Formato do pacote de problema (orgs, coleções, metadados): PACOTE.md. Placar: SCOREBOARD.md. Deploy: DEPLOY.md. Plano original: PLAN.md.

Convenção de commit: mensagens em português, no presente, prefixadas pelo componente (ex.: problemas: …). O rodapé leva apenas Co-Authored-By:nunca uma linha Claude-Session: (é ruído no histórico). Vale também p/ assistentes de IA.

Documentação junto com o código (doc atrasada = bug): ao mudar comportamento ou contrato, atualize a doc no mesmo commit — rotas/campos em API.md e em ../web/api/openapi.json (mantenha os dois em sincronia); arquitetura/fluxo aqui e em FLOW.md. bash docs/build-html.sh refaz o HTML.

Estrutura

moj/
  server/          backend bash sob nginx + fcgiwrap
    api/v1/
      router.sh    front-controller único: PATH_INFO -> handlers/<segmentos>.sh
      lib/         common.sh (resposta/JSON/validação/audit), params.sh, auth.sh, profile.sh, contest-create.sh
      handlers/    auth/ index/ treino/ contest/ submission/ admin/ ops/  (1 arquivo por rota)
    daemons/       judged.sh (consumidor do spool, inotify)
    judge-gw/      sched-lib.sh (escalonador pull: registro+fila+claim) + judge.sh (mock/local, dev) + PULL.md
    score/         build.sh (recalcula placar), stats-gen.sh (gera o cache de estatísticas), jplag-run.sh
    etc/           common.conf, nginx/, systemd/
  web/             frontend vanilla (ES modules, sem build), servido estático
    shared/        api.js auth.js ui.js editor.js charts(/lib) flags.js sonic.js contest-host/guard/shell.js contest-config/
    index/ contests/ status/ treino/ contest/  (home, arquivo de encerrados, status público, treino, contest)
  judge/           agente pull (moj-agent@pos/gpu/cm/hu, puxa job no heartbeat) + mojtools (sandbox bubblewrap)
  mojinho-bot/     bot do Telegram (vira cliente da API)
  contests/<id>/   DADOS (conf, users/<login>/ (account.json+history+metrics+submissions), var/placar.txt, …) — fonte da verdade
  run/             estado de runtime (sessions/, spool/, results/, registry/, sockets)  [não versionado]
  docs/            esta documentação

Camada de API

Frontend

Vanilla ES modules, sem build, servido estático. shared/ concentra o cliente de API (fetch + Bearer + envelope), auth/token (localStorage), ui.js (el(), avatares, i18n pt/en), o editor CodeMirror 6 (bundle vendorizado em shared/vendor/codemirror/ — sem CDN, contest roda em LAN isolada; fallback textarea), os gráficos SVG build-free (/lib/charts.js), e os assets offline: bandeiras locais (shared/flags/, 271 países + 27 estados do BR) e GIFs do Sonic (shared/assets/sonic/). Editores de configuração de contest reaproveitáveis em shared/contest-config/ (cores, países/escolas, regiões, básico, settings/toggles (settings-editor.js), seletor de linguagens (lang-picker.js) e o painel de busca+sorteio do banco por coleção/tag/dificuldade (bank-panel.js)) — os mesmos na criação e no admin do contest.

O que existe (funcional)

Home & treino livre

Home com notícias, contests (abertos/por vir/encerrados; abre cada um pelo subdomínio), top10 e destaques; página pública /status/ (health: fila por lista, máquinas julgando, daemons). Treino livre: busca de problemas, página do problema com enunciado + editor CodeMirror + upload + histórico com polling (o veredicto exibido é sempre o canônico; cada submissão julgada mostra um resumo abaixo — "Passou em X/Y testes (Z%)", pontos + grupos, ou Score heurístico — via /submission/summary, redigido por modo em contests: lib/verdict.sh), stats por usuário (gráficos, editor favorito, foto, privacidade), stats por problema (cache, linguagens, editores, nuvem de avatares), e painel admin do treino (sessões/logs com UA+IP, busca/regex, bulk logout/lock, notícias, auditoria, máquinas, e — em abas com índice/TOCFila & tempo de resposta (contadores de submissão + calibração, o que cada máquina roda agora (calibração vs submissão), tempo de veredito e mapas de calor de volume de submissões e calibrações) e Estatísticas (usuários, sessões, problemas: total/públicos/privados, quebra por autor, mapa de calor de entrada de públicos e atividade diária). Fontes: /treino/admin/{queue,judges,response-stats,calib-activity,stats}.

Gestão de problemas (MOJ-nativo por org, keyless) & painel de status

Formato do pacote, .moj-meta.json, orgs e coleções: ver PACOTE.md (fonte única). Roteiro de montar um pacote e o que faz cada script: mojtools/README.md.

Autoria/edição em /problemas/ (storage: repo git LOCAL por problema em <org>/<prob>; acesso por ORG — membros escrevem, a trava public_allowed barra vazamento; só o login do MOJ). A aba Painel (GET /problems/status) dá a visão agregada dos problemas de que o login é dono, colaborador ou membro da org: quantos/quais calibrando, validados, calibrados, precisam recalibrar (time-limit desatualizado após mudança no pacote) e com erro, mais a planilha de time-limits. O acesso é cortado na API (owners_visible): problema privado de terceiro não aparece. Staleness vem do checksum do pacote carimbado no índice de donos (barato; ≤30 min de atraso); /problems/tl recomputa na hora p/ 1 problema. A aba Análise (GET /problems/my-stats) dá o panorama de submissões dos seus problemas agregado em toda a plataforma (treino + as turmas): tentativas, acertos, erros mais comuns, linguagens, nº de contests e o mais popular — cache precomputado que reconcilia o namespace do history (problemas-apc#…) com o índice de donos (apc#…) via collections; só agregados (sem logins, sem nomes de contests) — não vaza prova privada.

Store por-usuário, cadastro por Telegram e alertas

Todo contest guarda um diretório por conta (contests/<c>/users/<login>/: account.json + history/metrics.json/submissões/logs/results/photo.png próprios). Não existe passwd: auth (verify_password), placar (sc_users), perfis e listagens leem os account.json direto (USERS_FROM=<src> cai para o users/ do contest-fonte — participantes compartilhados têm dir local sem account.json). Perfil (universidade/editor/privacidade) e metadados de time (.team{name,univ_short,univ_full,flag}) vivem no próprio account.json. Ganhos: trocar de username = mv do diretório e a maioria dos scripts de conta/julgamento só muda o caminho (lib/users.sh, emit_history_stream). Os handlers de usuário do admin do contest (user-add/user-disable/user-remove/users-set-password) escrevem no account.json (fonte da verdade); remover = mv do diretório p/ .removed-users/ (submissões preservadas). Migração de contest pré-reforma (arquivado em contests-legado/): server/bin/store-migrate.sh <c> [--apply] [--from <dir>] — dry-run por padrão; canonicaliza probids (offset/a.borg#prob), leva team/perfil ao account.json, roteia os flat files, gera metrics, move os resíduos p/ .legacy-store/ e só publica em contests/ após verificação (contas, spot-check de senhas, soma do history, metrics, placar). server/bin/store-cleanup.sh <c> limpa resíduos de contest já migrado. Migração vinda do MOJ ANTIGO (backup em contests-backup/, probid já em <repo>#<slug>) é outro caminho — o store-migrate.sh não serve porque aborta se o destino existe e faz mv -T do contest: server/bin/treino-map-gen.sh decide <repo legado>#<slug><org>#<prob> (alias repo/collections, casefold, slug, título e, por último, o TEXTO do enunciado) e emite um TSV auditável; server/bin/treino-migrate.sh {stage|verify|install|audit} consome o TSV (recusa qualquer linha ?) e funde num contest vivo — conta nova entra por mv do diretório, conta que já existe só ganha history (dedup por subid) e submissões, senha do prod prevalece, telegram via tg_link. stage/verify não tocam o destino; audit confere o instalado contra o legado. O treino ganha um overlay de Telegram (lib/telegram.sh): cadastro web-first (/treino/cadastro/) confirmado por deep-link no bot, 1 Telegram = 1 conta (anti-duplicata), recuperação de senha pelo vínculo, e senha entregue só por DM. O mojinho-bot virou transporte fino (bot-token mojb_, sem .admin/GODS) e entrega alertas de incidente que a API decide (lib/alerts.sh + GET /ops/alerts: juiz offline+fila, fila grande, daemon caído, com histerese/cooldown) aos .admin com Telegram vinculado + grupo.

Criação de contest (/treino/criar/) — wizard multi-etapa

Permissão por lista do admin OU threshold de problemas resolvidos. Wizard em 8 passos (shell criar.js + steps/*.js; um objeto draft único — ir-e-voltar não perde nada): 0 Começar (em branco / template salvo / duplicar contest meu / importar .tar.gz / baixar template JSON / salvar template de contest existente), 1 Dados (nome/id/modo/datas), 2 Problemas (painel compartilhado de busca+sorteio por coleção/tag/dificuldade, add por ID, enunciado custom HTML e PDF por problema), 3 Usuários (compartilhados do treino ou próprios, com colagem fluida + senhas legíveis + CSV), 4 Admin (obrigatório), 5 Opções (o MESMO settings-editor da aba Configurações do admin — paridade total, + prioridade de julgamento), 6 Visual (cores/Sonic, países/escolas, regiões), 7 Revisão (resumo + validações + Criar/Criar vazio + salvar como template). Templates nomeados ficam no servidor por criador (/treino/contest-create/templates); duplicar/exportar usam /treino/contest-create/{mine,export,duplicate} (só dono/admin — 404 p/ terceiros).

Ambiente de contest (<id>.moj.<base>)

Login (com gate opcional por substring de User-Agent), página principal (problemas + submissão + editor que o admin pode desligar), placar multi-modo (icpc/obi/treino/ heurístico/outro) com bandeiras locais, filtro por país/escola, modo anônimo (agregado/ quartis), freeze (esconde resultados após o horário; build.sh gera placar.txt público congelado e placar-full.txt completo — .admin/.judge/.cjudge + allowlist SCORE_FULL_USERS veem o completo) com cerimônia de revelação nativa (/contest/score/reveal.html, estilo ICPC resolver: delta frozen→full revelado de baixo p/ cima, passo/auto, botão "descongelar tudo" = settings freeze:0, só admin; o placar aceita &view=public p/ o privilegiado obter a visão congelada; link na aba Situação do admin). O .cstaff conduz a cerimônia POR SEDE: a mesma página com &scope=mine — a API recorta frozen+full aos usuários do escopo dele (staff-filters) e só libera o full quando o contest terminou p/ todas as sedes (contest_over_for_all: fim do conf + o maior end de time-overrides.json; admin pode antecipar via allowlist SCORE_FULL_USERS), tempo de solução relativo ao início (não EPOCH), e nav por papel. Contest 🕵️ SUPER SECRETO (conf SECRET=1, marcável na criação e no admin): fora das listagens públicas (home, arquivo /contests/, /status/) e o placar deixa de ser públicoscore/balloons/regions/ teams-meta exigem sessão daquele contest (401 secret_login_required). A tela de login/ countdown continua funcionando p/ quem tem o link (/contest/basic segue público). Desmarcar exige digitar o id. Usuários comuns têm no menu uma página própria de Backup de arquivos (/contest/backup/) p/ guardar versões de solução (não polui a home); o admin vê/baixa todos na aba Backups (zip por usuário). Quando há usuário .staff no contest, os alunos ganham também a página Impressão (/contest/print/): enviam um arquivo (PDF/imagem/texto/código) e acompanham o status (pendente→processada→entregue) — ver Impressão (.staff) abaixo. Os problemas usam o id canônico coleção#problema (igual ao treino — é o que o juiz usa p/ achar o pacote); o editor é o CodeMirror compartilhado (shared/editor.js, com tela cheia e nova janela) e a seleção de linguagens é a lista inteira do MOJ (shared/languages.js), reduzida à whitelist do conf LANGUAGES= quando definida. O placar é gerado de users/*/metrics.json (mantidos incrementais pelo daemon; score/build.sh + sc_cells — ver SCOREBOARD.md), sem varrer history. O aluno recebe aviso de novidades (notícias + clarifications respondidas, com badge de não lidas — poll de /contest/updates) e vê o tempo-limite por linguagem no detalhe do problema (ocultável pelo admin). Acesso por fase+papel (forçado pela API, não só no front): .admin/.judge veem os problemas e submetem a qualquer momento (antes/durante/depois); o usuário normal só vê os problemas após o início (antes disso, ao logar, recebe uma tela de contagem regressiva) e só submete durante a janela (/contest/problems devolve locked:"not_started" e /submit recusa com 403 fora da janela — contest_not_started/contest_ended); .staff/.cstaff não veem problemas nem submetem; .mon submete só na janela (como o normal) mas fica fora do placar. A janela tem prorrogação por sede/grupo (time-overrides.json, regras regex no login — 1ª que casa estende o fim SÓ daquele grupo, ex.: queda de energia numa sede; contest_end_effective em lib/contest-gate.sh vale no /submit e no countdown do /contest/basic autenticado; editável na aba Configurações do admin e por moj-contest extend --group, auditado). Telas internas:

Juiz .judge, juiz-chefe .cjudge & veredicto manual

Papéis (sufixo no login; ver lib/auth.sh): .judge submete a qualquer hora (fora do placar/estatísticas), responde clarifications e cria avisos; .cjudge (juiz-chefe) herda o juiz (is_judge vale p/ ele) + extras escopados (is_chief): editar notícias/respostas já dadas, ver Situação e Todas Submissões (mesmas ops do admin), resolver conflitos e editar a config de auto-veredicto — não é admin pleno. .cjudge está nas quatro listas de sufixo (auth/score-common/stats-gen/login) p/ ficar fora do placar e isento da janela de login.

Veredicto manual (opt-in por contest, MANUAL_VERDICT): quando ligado, o daemon segura o veredicto computado p/ revisão humana — grava contests/<c>/review/<id>.json e deixa o history provisório (o aluno segue vendo "julgando"); a exceção é a matriz auto-verdicts.json (problema × linguagem × veredicto, editável por admin/chief) que libera combinações automáticas. O casamento da matriz é pelo veredicto canônico (verdict_canon, sem o sufixo de score ,Np que o juiz embute), e erros de juiz (Judge Error/No_Servers) também são segurados — o competidor vê só Not Answered Yet (nenhuma mensagem de erro vaza); o juiz vê o erro no painel e re-julga. Dois .judge pegam a submissão (máx 2, 1 ativa por juiz, TTL 5 min com +5, ou desistir), veem log + fonte + veredicto computado (a tela não recarrega enquanto se avalia) e escolhem um veredicto de uma lista configurável (final-verdicts.json, {label,verdict}; default = as 6: 1-YES…6-Contact staff). O voto é permanente e libera o juiz na hora (ele já pode pegar outra submissão). Dois no mesmo → vai ao aluno; diferentes → conflito, que só o juiz-chefe resolve (avisado pelo alerta global de conflito em qualquer página). A liberação enfileira setverdict, consumido pelo daemon e finalizado pelo escritor único (update_history + results/<id>.json), então o veredicto manual entra no timeline de auditoria como qualquer outro. TUDO é auditado (clar-*, news-edit, final-/ auto-verdicts-set, review-claim/extend/giveup/vote/agree/conflict/resolve, verdict-held/released). Mudou o daemon → reinicie-o (mantendo INTAKE_MODE/JUDGE_BACKEND).

Auditoria: ações administrativas são logadas em contests/<c>/var/admin-audit.log (e treino/var/admin-audit.log no treino) — o contest fica auto-contido.

Juiz & daemons

Submissão assíncrona (spool + inotify), judged.sh, e julgamento pull: o daemon enfileira por prioridade (judge-gw/sched-lib.sh) e os juízes (moj-agent@) puxam o job no heartbeat, baixam o pacote sob demanda, calibram e reportam veredicto/TL por HTTP — sem master, sem push de entrada. Backends mock/local do judge.sh ficam só p/ dev/legado. Detalhes em FLOW.md e judge-gw/PULL.md.

Testes

Suítes de smoke em server/test/smoke-*.sh (cada uma sobe o router.sh com CONTESTSDIR/ SESSIONDIR de fixture e exercita os handlers de ponta a ponta). Rode todas:

cd server/test && for t in smoke*.sh; do bash "$t"; done

Compilar esta documentação em HTML

bash docs/build-html.sh     # gera docs/html/*.html + index (usa pandoc)