docs: arquitetura, SSO, tema, operacao e o que sabemos do LearnHouse
Documenta como o cursos.overia.com.br e construido (LearnHouse 1.3.6 community no NAS4, login pelo Keycloak via oauth2-proxy e ponte, tema overia injetado pelo nginx, aulas por proxy /aulas), as armadilhas ja encontradas, o runbook de publicacao, rollback, backup e atualizacao, e as decisoes e pendencias. learnhouse-upstream.md resume o repositorio e a documentacao oficiais e marca onde a doc diverge do codigo da imagem (cookies LH_*, politica de login por organizacao). deploy/publicar-tema.sh publica deploy/theme e o nginx.prod.conf com nginx -t e volta automatica; recebe host, porta, usuario e senha por variaveis de ambiente. O repositorio e publico: nenhum IP, porta, usuario, conta ou segredo nos arquivos. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
48
deploy/publicar-tema.sh
Executable file
48
deploy/publicar-tema.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
# Publica deploy/theme e deploy/nginx.prod.conf no NAS4 e recarrega o nginx do LearnHouse.
|
||||
# Se "nginx -t" falhar, restaura sozinho o conf anterior. NÃO recria a pasta theme no servidor
|
||||
# (apagar e recriar quebra o bind mount do nginx: tudo vira 404); copia o conteúdo para dentro dela.
|
||||
#
|
||||
# Variáveis obrigatórias (este repositório é público: nunca grave os valores aqui):
|
||||
# NAS4_HOST, NAS4_PORT, NAS4_USER acesso SSH ao NAS4 (pela VPN)
|
||||
# NAS4_SUDO_PW senha do sudo, sem aspas simples (o Docker do Synology exige sudo)
|
||||
#
|
||||
# Depois de rodar: suba o ?v=N do CSS em deploy/nginx.prod.conf ANTES quando o CSS mudou (o Cloudflare guarda por horas).
|
||||
set -eu
|
||||
|
||||
for v in NAS4_HOST NAS4_PORT NAS4_USER NAS4_SUDO_PW; do
|
||||
eval "[ -n \"\${$v:-}\" ]" || { echo "falta a variável $v" >&2; exit 2; }
|
||||
done
|
||||
|
||||
R=$(cd "$(dirname "$0")/.." && pwd)
|
||||
STACK=/volume1/docker_1/xen-learnhouse
|
||||
NGINX=learnhouse-nginx-4bbd69be
|
||||
|
||||
ssh_() { ssh -o BatchMode=yes -o LogLevel=ERROR -p "$NAS4_PORT" "$NAS4_USER@$NAS4_HOST" "$@"; }
|
||||
|
||||
tar -C "$R" -cf - deploy/theme deploy/nginx.prod.conf \
|
||||
| ssh_ 'rm -rf ~/stage-tema && mkdir -p ~/stage-tema && tar -C ~/stage-tema -xf -'
|
||||
|
||||
ssh_ "P='$NAS4_SUDO_PW' STACK='$STACK' NGINX='$NGINX' sh -s" <<'REMOTE'
|
||||
S() { echo "$P" | sudo -S -p '' "$@"; }
|
||||
D=/usr/local/bin/docker
|
||||
N=$HOME/stage-tema/deploy
|
||||
B=$HOME/backups/learnhouse-$(date +%Y%m%d)
|
||||
PREV=$B/nginx.prod.conf.$(date +%H%M%S)
|
||||
mkdir -p "$B"
|
||||
|
||||
S cp -p "$STACK/extra/nginx.prod.conf" "$PREV"
|
||||
S mkdir -p "$STACK/theme"
|
||||
S cp -r "$N/theme/." "$STACK/theme/"
|
||||
S chmod -R a+rX "$STACK/theme"
|
||||
S sh -c "cat $N/nginx.prod.conf > $STACK/extra/nginx.prod.conf"
|
||||
|
||||
if S $D exec "$NGINX" nginx -t >/dev/null 2>&1; then
|
||||
S $D exec "$NGINX" nginx -s reload >/dev/null 2>&1
|
||||
echo "nginx recarregado (conf anterior guardada em $PREV)"
|
||||
else
|
||||
S sh -c "cat $PREV > $STACK/extra/nginx.prod.conf"
|
||||
echo "nginx -t FALHOU: conf anterior restaurada" >&2
|
||||
exit 1
|
||||
fi
|
||||
REMOTE
|
||||
31
docs/README.md
Normal file
31
docs/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Documentação — cursos.overia.com.br
|
||||
|
||||
Estado documentado em **2026-10-04**. Este repositório é **público** no Gitea: não coloque aqui IPs, portas de SSH, usuários,
|
||||
senhas, tokens nem segredos de qualquer tipo. Os segredos vivem só nos servidores (`oauth2-proxy.env`, `.env` do stack) e no
|
||||
gerenciador de senhas.
|
||||
|
||||
| Documento | Para que serve |
|
||||
|---|---|
|
||||
| [arquitetura.md](arquitetura.md) | Como o site é construído: componentes, fluxos e onde cada coisa mora |
|
||||
| [sso.md](sso.md) | Login pelo `login.overia.com.br` (Keycloak): desenho, segurança e armadilhas já encontradas |
|
||||
| [tema.md](tema.md) | O visual do overia (claro, escuro e botão de tema) aplicado por cima do LearnHouse |
|
||||
| [operacao.md](operacao.md) | Como publicar mudanças, voltar atrás, fazer backup e diagnosticar problemas |
|
||||
| [learnhouse-upstream.md](learnhouse-upstream.md) | O que sabemos do LearnHouse (repositório e documentação oficiais) e o que isso muda para nós |
|
||||
| [decisoes-e-pendencias.md](decisoes-e-pendencias.md) | Decisões tomadas (com o porquê), riscos aceitos e o que ainda está aberto |
|
||||
| [superpowers/](superpowers/) | Spec e plano originais do pipeline de publicação de cursos (2026-09-29) |
|
||||
|
||||
## Em uma frase
|
||||
|
||||
O `cursos.overia.com.br` é uma instalação **LearnHouse 1.3.6 (edição community)** hospedada no NAS4, com login pelo Keycloak
|
||||
(oauth2-proxy + uma ponte própria), visual do `overia.com.br` injetado pelo nginx e conteúdo publicado por um pipeline neste
|
||||
repositório. A imagem do LearnHouse **não foi alterada**.
|
||||
|
||||
## Onde está o código deste repositório
|
||||
|
||||
| Pasta | O que é |
|
||||
|---|---|
|
||||
| `courses/` | Conteúdo dos cursos (Markdown) e `course.json` de cada um |
|
||||
| `scripts/` | Pipeline que publica os cursos pela API do LearnHouse |
|
||||
| `sso_bridge/` | Ponte que cria a sessão do LearnHouse a partir do login do Keycloak |
|
||||
| `deploy/` | Arquivos que vão para o servidor: `nginx.prod.conf`, `docker-compose.override.yml`, `theme/`, `publicar-tema.sh` |
|
||||
| `tests/` | Testes do pipeline e da ponte (`uv run pytest`) |
|
||||
76
docs/arquitetura.md
Normal file
76
docs/arquitetura.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Arquitetura
|
||||
|
||||
## Visão geral
|
||||
|
||||
```
|
||||
navegador
|
||||
│ HTTPS
|
||||
▼
|
||||
Cloudflare ──(Cloudflare Tunnel "tunnel-nas4")──► nginx do stack LearnHouse (porta 8106 no NAS4)
|
||||
│ conf: deploy/nginx.prod.conf
|
||||
┌───────────────┬───────────────┬─────────────┼──────────────────┬───────────────────┐
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
/oauth2/* /login /signup /aulas/*.md /overia-theme/* /dash /user tudo o mais
|
||||
oauth2-proxy /logout proxy para CSS, JS, fontes (exigem a conta learnhouse-app
|
||||
(v7.7.1) /sso/login o Gitea (pasta ./theme) overia) (web + API + collab)
|
||||
│ sso-bridge │
|
||||
▼ │ Postgres 16 + Redis
|
||||
Keycloak └─ roda com o código do próprio LearnHouse
|
||||
(NAS3, realm overia)
|
||||
```
|
||||
|
||||
## Componentes
|
||||
|
||||
| Componente | Onde | Função |
|
||||
|---|---|---|
|
||||
| **LearnHouse** `ghcr.io/learnhouse/app:1.3.6` | NAS4, stack `xen-learnhouse` (`/volume1/docker_1/xen-learnhouse`) | Frontend Next.js, API FastAPI e servidor de colaboração numa imagem só. **Sem alterações** |
|
||||
| `learnhouse-db` (Postgres 16 + pgvector), `learnhouse-redis` | mesmo stack | Dados e cache. A organização é `overia` (`org_id = 1`) |
|
||||
| `learnhouse-nginx` | mesmo stack | Proxy de borda: roteia, injeta o tema e protege `/login`, `/signup`, `/dash`, `/user` |
|
||||
| `learnhouse-oauth2-proxy` | mesmo stack (`docker-compose.override.yml`) | Faz o login OIDC no Keycloak (client `cursos`, PKCE S256) |
|
||||
| `learnhouse-sso-bridge` | mesmo stack (`docker-compose.override.yml`) | Cria a sessão do LearnHouse depois do login (ver [sso.md](sso.md)) |
|
||||
| `tunnel-nas4` (cloudflared) | NAS4 | Publica `cursos.overia.com.br` apontando para o nginx. Configuração remota, por token |
|
||||
| **Keycloak** | NAS3, `login.overia.com.br`, realm `overia` | Provedor de identidade único dos produtos overia (Google como broker) |
|
||||
| **Gitea** `ai-bot/cursos` | `git.xenserver.com.br` | Guarda este repositório; as aulas Markdown são lidas dele |
|
||||
|
||||
## Fluxos
|
||||
|
||||
### Visitante
|
||||
`/`, `/courses`, `/course/<id>`, `/library`, `/podcasts`, `/communities`, `/playgrounds` e `/api/` são públicos. O nginx só injeta o
|
||||
tema (CSS e script) e, para quem tem sessão, o botão **Sair**.
|
||||
|
||||
### Login
|
||||
`/login` → nginx redireciona para `/oauth2/start?rd=/sso/login` → Keycloak (`login.overia.com.br`) → `/oauth2/callback` →
|
||||
`/sso/login` (ponte) → home logada. `/signup` faz o mesmo, mas abre direto o cadastro do Keycloak. Detalhes e segurança em [sso.md](sso.md).
|
||||
|
||||
### Logout
|
||||
Botão **Sair** (verde, canto inferior direito) e o **Sign Out** do menu do avatar levam a `/logout` → ponte limpa os cookies do
|
||||
LearnHouse → `/oauth2/sign_out` → logout do Keycloak (com tela de confirmação) → home.
|
||||
|
||||
### Aulas (Markdown)
|
||||
Cada aula é uma atividade `SUBTYPE_DYNAMIC_MARKDOWN` cujo conteúdo é `{"markdown_url": "https://cursos.overia.com.br/aulas/<caminho>.md"}`.
|
||||
O navegador busca no próprio domínio e o nginx repassa para `git.xenserver.com.br/ai-bot/cursos/raw/branch/main/courses/<caminho>.md`.
|
||||
O proxy existe porque o Gitea não envia `Access-Control-Allow-Origin` e o navegador bloquearia a leitura direta.
|
||||
|
||||
### Publicação de um curso
|
||||
`uv run python -m scripts.publish <slug> [--dry-run|--publish]` lê `courses/<slug>/`, cria curso, capítulos e atividades pela API do
|
||||
LearnHouse e monta a URL de cada aula com `GITEA_RAW_BASE`, que **deve** ser `https://cursos.overia.com.br/aulas` (ver [operacao.md](operacao.md)).
|
||||
|
||||
## O que existe fora da imagem do LearnHouse
|
||||
|
||||
Toda a customização está nestes pontos, o que facilita atualizar o LearnHouse (e é onde olhar se algo quebrar depois):
|
||||
|
||||
1. **`extra/nginx.prod.conf`** no stack. É o arquivo que o instalador do LearnHouse gera; **nós o substituímos** por `deploy/nginx.prod.conf`.
|
||||
2. **`docker-compose.override.yml`** no stack: acrescenta `oauth2-proxy` e `sso-bridge` e monta `./theme` no nginx.
|
||||
3. **`theme/`** no stack (cópia de `deploy/theme/`) e **`sso_bridge/`** (cópia de `sso_bridge/`).
|
||||
4. **`oauth2-proxy.env`** no stack, com o segredo do client e o segredo do cookie. **Fora do repositório.**
|
||||
5. **Banco** (alterações feitas à mão; backups em `~/backups/learnhouse-20261003/` no NAS4):
|
||||
- `organizationconfig`: `admin_toggles.security.allowed_auth_methods = ["password","magic_login","sso"]` (sem `google`) e `customization.general.color = #2f7a34`;
|
||||
- `organization.logo_image`: marca transparente do overia;
|
||||
- `activity.content.markdown_url` das 8 aulas Markdown: passaram de `git.xenserver.com.br/...` para `cursos.overia.com.br/aulas/...`.
|
||||
6. **Redis**: depois de mexer na organização direto no banco, apagar `org_cache:slug:overia` e `org_cache:config:1`, senão a API serve o valor antigo.
|
||||
|
||||
## Sistemas relacionados (outros repositórios)
|
||||
|
||||
- **`overia-portal`** (`overia.com.br`): lista o Cursos como quinto produto (commits `862a1a6` e `2dd23c1`). O container em produção recebeu o build por
|
||||
cópia direta; ver pendências em [decisoes-e-pendencias.md](decisoes-e-pendencias.md).
|
||||
- **`keycloak-overia`** (`login.overia.com.br`): tema de login/conta com o app "cursos" na lista e o `Makefile` de operação.
|
||||
55
docs/decisoes-e-pendencias.md
Normal file
55
docs/decisoes-e-pendencias.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Decisões, riscos aceitos e pendências
|
||||
|
||||
Estado em **2026-10-04**.
|
||||
|
||||
## Decisões
|
||||
|
||||
| Data | Decisão | Por quê |
|
||||
|---|---|---|
|
||||
| 2026-10-03 | **SSO por fora** (oauth2-proxy + ponte), não o OIDC nativo do LearnHouse | O OIDC nativo é Enterprise (licença à parte). Os outros produtos overia já usam o Keycloak |
|
||||
| 2026-10-03 | **Vitrine pública**: `/`, `/courses`, `/course/*`, `/library`, `/podcasts`, `/communities`, `/playgrounds`, `/api/` e `/content/` abertos; `/login`, `/signup`, `/dash`, `/user` exigem a conta overia | Decisão do mantenedor. Proteger o site inteiro foi tentado e **revertido** ("já estava certo"); não propor de novo |
|
||||
| 2026-10-04 | `/login` e `/signup` **sempre** passam pelo Keycloak, mesmo com sessão antiga do oauth2-proxy | Antes, quem já tinha sessão via o login do LearnHouse e achava que o SSO não estava configurado |
|
||||
| 2026-10-04 | `/signup` abre **direto o cadastro do Keycloak** (endpoint `/registrations`) | `prompt=create` é ignorado pelo Keycloak 26.0 |
|
||||
| 2026-10-04 | Destino padrão depois do login é `/` (não `/dash`) | Conta nova é "User" (aluno) e não tem acesso ao painel; `/dash` causaria laço |
|
||||
| 2026-10-03 | O **login com Google do LearnHouse foi escondido** (`google` fora de `allowed_auth_methods`) | Não há `LEARNHOUSE_GOOGLE_CLIENT_ID/SECRET` no `.env`: o botão não funcionava. O Google continua disponível pelo Keycloak |
|
||||
| 2026-10-04 | **Tema claro e escuro** do overia, seguindo o sistema, mais **botão manual** (auto/claro/escuro) | O navegador do mantenedor é escuro e o `overia.com.br` aparece escuro para ele |
|
||||
| 2026-10-04 | **Aulas Markdown servidas em `/aulas`** (proxy do nginx) | O Gitea não envia CORS; toda aula dava "Failed to fetch". O `GITEA_RAW_BASE` do pipeline passa a ser `https://cursos.overia.com.br/aulas` |
|
||||
| 2026-10-04 | **Cursos como quinto produto** do `overia.com.br`, gratuito e **sem selo de licença** na lista | Decisão do mantenedor (o texto de apresentação foi rascunho do Claude, aprovado) |
|
||||
| 2026-10-04 | No Anexo dos Termos, o Cursos consta como **AGPL-3.0, operado sem alterações** (instância, não fork) | Licença confirmada no README do LearnHouse; a frase "opera um fork independente" seria falsa |
|
||||
| 2026-10-04 | `overia.com.br` atualizado por **cópia direta do build para o container** | Para ganhar tempo durante os ajustes; consolidar depois (pendência 1) |
|
||||
| 2026-10-04 | Documentação **sem IPs, portas, usuários ou segredos** | O repositório `ai-bot/cursos` é público no Gitea |
|
||||
|
||||
## Riscos aceitos
|
||||
|
||||
- **Pré-sequestro de conta**: o cadastro por API do LearnHouse não verifica e-mail ([sso.md](sso.md)).
|
||||
- **MFA do LearnHouse não se aplica** a quem entra pelo SSO.
|
||||
- **Tela de confirmação** do logout do Keycloak (falta `id_token_hint`).
|
||||
- O script do **Sign Out** depende do texto do menu do LearnHouse; o botão verde **Sair** é a rede de segurança.
|
||||
- O **tema** depende das classes do Tailwind do LearnHouse e do formato do HTML; atualizar o LearnHouse pode exigir ajustes.
|
||||
- Anexo dos Termos e demais páginas legais do `overia.com.br` **sem revisão de advogado**.
|
||||
|
||||
## Pendências (por prioridade)
|
||||
|
||||
1. **Consolidar o `overia.com.br`.** O container `xen-overia` tem o Cursos só porque o `.next` foi copiado para dentro dele; a imagem `xen-overia:latest` no NAS3 ainda é a antiga. **Se o container for recriado, o Cursos some do site.**
|
||||
Construir a imagem a partir do repo `overia-portal`, enviá-la (73 MB; um envio único por SSH já caiu no meio: mandar em pedaços de ~12 MB conferindo `sha256`), `docker load` e `docker compose -p xen-overia up -d`.
|
||||
Pontos de volta já criados: `/app/.next.old` no container, `xen-overia:pre-cursos-20261004` e um backup do `.next` anterior no NAS3.
|
||||
2. **Backup do LearnHouse.** Não há rotina. Criar `learnhouse backup` diário (banco + `.env`), cópia do volume de conteúdo (logos, favicons, fundo de login, uploads) e cópia fora do NAS.
|
||||
3. **Descobrir se `npx learnhouse update` regrava `extra/nginx.prod.conf`** e o `docker-compose.yml`. Até saber, tratar toda atualização com as cópias e a checklist de [operacao.md](operacao.md).
|
||||
4. **Testar o cadastro novo até o fim**: criar uma conta de verdade pelo `/signup` (de preferência descartável) e conferir que a ponte provisionou o usuário no LearnHouse. Só o caminho de usuário já existente foi exercitado.
|
||||
5. **Verificar o `/dash` como administrador e a tela em largura de celular** nos dois temas.
|
||||
6. **Decidir quem é administrador do LearnHouse** além de quem já é (as contas criadas pela ponte, inclusive a de teste, entram como "User"). Promover é feito por um Admin da organização.
|
||||
7. **Teste automatizado** do caminho feliz do SSO (hoje só há testes das funções puras e verificação manual).
|
||||
8. **Publicar os commits** (Gitea). `cursos-overia` e `overia-portal` têm remoto no Gitea e dependem de autorização; **`keycloak-overia` não pode ir para o `origin`** (é o repositório oficial do Keycloak). Lembrar que `ai-bot/cursos` é público.
|
||||
9. **Produzir os demais cursos** (a spec prevê ~29; existe o `python`, com 2 capítulos e 4 aulas) e **revisão jurídica** dos Termos.
|
||||
|
||||
## Commits relevantes (`cursos-overia`)
|
||||
|
||||
| Commit | Assunto |
|
||||
|---|---|
|
||||
| `0e0e33d` | Ponte SSO (`sso_bridge/`) e testes |
|
||||
| `5e79bf4` | nginx e compose do oauth2-proxy e da ponte |
|
||||
| `5df9e41` | Tema overia (claro) |
|
||||
| `68a128b` | Tema escuro e rótulo "cursos" |
|
||||
| `8a08364` | "cursos" do mesmo tamanho de "overia" |
|
||||
| `48d038d` | Aulas pelo proxy `/aulas` (sem CORS) |
|
||||
| `63687de` | Botão de tema e escuro das aulas |
|
||||
96
docs/learnhouse-upstream.md
Normal file
96
docs/learnhouse-upstream.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# LearnHouse: o que sabemos do projeto
|
||||
|
||||
Fontes: repositório `github.com/learnhouse/learnhouse` e documentação `docs.learnhouse.app`, consultados em 2026-10-04, **parafraseados** (não é cópia).
|
||||
Onde a documentação e o que vimos no código da imagem 1.3.6 divergem, está marcado como **(código)**: foi verificado lendo o próprio container, e é isso que vale para nós.
|
||||
|
||||
## O que é
|
||||
|
||||
Plataforma de aprendizagem (LMS) de código aberto: cursos com editor em blocos, tarefas e envios, comunidades e discussões, podcasts, análises de engajamento, execução de código
|
||||
com correção automática, quadros colaborativos em tempo real, ferramentas de IA, certificados, SEO e marca/tema personalizáveis.
|
||||
|
||||
## Stack
|
||||
|
||||
| Camada | Tecnologia |
|
||||
|---|---|
|
||||
| Frontend | Next.js (App Router), React, TypeScript, TailwindCSS, Radix UI, editor Tiptap |
|
||||
| Backend | FastAPI (Python), SQLModel, migrações Alembic |
|
||||
| Dados | PostgreSQL; Redis (cache/sessão) |
|
||||
| Tempo real | Yjs + servidor Hocuspocus (WebSocket) |
|
||||
| Opcionais | S3 para arquivos, Stripe (pagamentos), IA (Gemini, OpenAI, Anthropic, Ollama...) |
|
||||
|
||||
Repositório: `apps/web`, `apps/api`, `apps/collab`, `apps/cli`, `docker/`, `docs/`. A imagem `ghcr.io/learnhouse/app` junta web, API e colaboração, com um nginx interno entre eles.
|
||||
|
||||
## Edições e licença
|
||||
|
||||
- **Código aberto: AGPL-3.0.** É o que rodamos (edição community, 1.3.6).
|
||||
- **Enterprise: licença comercial à parte.** Segundo o README cobre pagamentos, SSO/OAuth e multi-organização; instalações enterprise usam `LEARNHOUSE_LICENSE_KEY`.
|
||||
- A página de releases consultada lista a **1.3.6 como a mais recente** (a data veio resumida; não confio no ano). Ou seja, estamos na última versão.
|
||||
- Multi-organização por subdomínio (`LEARNHOUSE_TENANCY=multi`) exige Enterprise; nós usamos `single`.
|
||||
|
||||
### O que isso muda para nós
|
||||
|
||||
- **SSO nativo (Custom OIDC e SAML via WorkOS) é Enterprise.** Na doc ele se configura na interface da organização (Issuer URL, Client ID, Client Secret, escopos, padrão `openid email profile`) e o vínculo de usuário é por e-mail. É por isso que construímos o SSO por fora ([sso.md](sso.md)).
|
||||
- A edição community tem senha e Google. O Google exige `LEARNHOUSE_GOOGLE_CLIENT_ID` e `LEARNHOUSE_GOOGLE_CLIENT_SECRET` (a doc cita também `LEARNHOUSE_GOOGLE_OAUTH_CLIENT_ID`); sem eles o botão não funciona, e por isso o escondemos.
|
||||
- **Planos e recursos:** a doc diz que tokens de API (prefixo `lh_`) e webhooks são do plano Pro. Na nossa instância a organização está no plano `free` e a API aparece como disponível **(código/instância)**; não testamos webhooks.
|
||||
|
||||
## Autenticação e sessão
|
||||
|
||||
- Métodos documentados: e-mail e senha, Google, SAML via WorkOS e Custom OIDC (os dois últimos Enterprise). A doc não cita o link por e-mail, mas ele existe no código e está ativo aqui.
|
||||
- Sessão por JWT HS256: **access token de 8 h e refresh de 30 dias**. Login com tentativas limitadas por IP e bloqueio temporário da conta. Em SaaS é exigido e-mail verificado; em instalação própria não.
|
||||
- Cadastro da organização: aberto ou só por convite.
|
||||
- **(código)** Os cookies reais na 1.3.6 são `LH_access` e `LH_refresh` (HttpOnly) e **`LH_session=1`** (legível por JavaScript), este último decisivo: o frontend e o middleware do Next o usam para saber se há alguém logado.
|
||||
A doc ainda fala em `access_token_cookie`, o que está desatualizado.
|
||||
- **(código)** Existe política **por organização** de métodos de login (`admin_toggles.security.allowed_auth_methods`); a sessão carrega o método (`amr`) e é rejeitada se o método não estiver na lista.
|
||||
A doc diz que não há restrição por método; o código tem. Foi introduzida na 1.3.3 (sessões isoladas, políticas por organização, tolerância no refresh).
|
||||
- A 1.3.3 também trouxe autenticação em dois fatores e passkeys.
|
||||
|
||||
## API
|
||||
|
||||
- Prefixo `/api/v1/`. Autenticação por `Authorization: Bearer <token>` (JWT do login, ou token de API `lh_...`) ou pelo cookie.
|
||||
- Criação e edição aceitam, em geral, **multipart/form-data**, porque muitos endpoints também recebem arquivos. O `LearnHouseClient` deste repositório usa form em algumas chamadas e JSON em outras.
|
||||
- Referência completa em `/reference` (e Swagger local em `/docs` em modo desenvolvimento). A visão geral da doc remete a ela.
|
||||
- Webhooks (Pro): eventos de progresso, usuários, ciclo do curso, conteúdo, colaboração, comunidade, grupos e configuração; entrega por POST JSON assinado com HMAC-SHA256 (segredo `whsec_...`).
|
||||
|
||||
## Conteúdo
|
||||
|
||||
Estrutura **Curso → Capítulos → Atividades**, com coleções e trilhas. Tipos de atividade (identificadores exatos):
|
||||
|
||||
| Tipo | Subtipo | Conteúdo |
|
||||
|---|---|---|
|
||||
| `TYPE_DYNAMIC` | `SUBTYPE_DYNAMIC_PAGE` | documento TipTap/ProseMirror (`{"type":"doc","content":[...]}`) |
|
||||
| `TYPE_DYNAMIC` | **`SUBTYPE_DYNAMIC_MARKDOWN`** | **Markdown remoto**: `{"markdown_url": "..."}`. É o que o nosso pipeline usa |
|
||||
| `TYPE_DYNAMIC` | `SUBTYPE_DYNAMIC_EMBED` | `{"embed_url": "..."}` (iframe) |
|
||||
| `TYPE_VIDEO` | `SUBTYPE_VIDEO_HOSTED` / `SUBTYPE_VIDEO_YOUTUBE` | vídeo enviado, ou YouTube/Vimeo |
|
||||
| `TYPE_DOCUMENT` | `SUBTYPE_DOCUMENT_PDF` | PDF (PDF.js) |
|
||||
| `TYPE_ASSIGNMENT` | `SUBTYPE_ASSIGNMENT_ANY` | três passos: atividade, registro da tarefa, ao menos uma *task* (arquivo, quiz, formulário) |
|
||||
| `TYPE_CUSTOM` | `SUBTYPE_CUSTOM` | JSON livre, para renderizador próprio |
|
||||
| `TYPE_SCORM` | `SUBTYPE_SCORM_12` / `SUBTYPE_SCORM_2004` | pacote SCORM |
|
||||
|
||||
O Markdown remoto é buscado **pelo navegador** (não pelo servidor), por isso a origem precisa permitir CORS, ou o arquivo precisa ser servido no mesmo domínio (nosso `/aulas`).
|
||||
|
||||
## Papéis
|
||||
|
||||
Quatro papéis globais: **Admin** (controle total), **Maintainer** (opera cursos e usuários, sem configuração da plataforma), **Instructor** (cria os próprios cursos e acompanha alunos)
|
||||
e **User** (aluno: navega, faz atividades, vê o próprio progresso). Dá para criar papéis personalizados e ajustar cada permissão. O `/dash` é o painel de administração: o papel "User" vê "sem permissão".
|
||||
A conta criada pela ponte entra como **User**; promover alguém é feito pelo Admin da organização.
|
||||
|
||||
## Operação (oficial)
|
||||
|
||||
| Comando (`npx learnhouse ...`) | Para que serve |
|
||||
|---|---|
|
||||
| `setup` | Assistente que gera `docker-compose.yml`, `.env`, `learnhouse.config.json` e a configuração do proxy (nginx ou Caddy) |
|
||||
| `start`, `stop`, `status`, `logs` | Ciclo de vida e diagnóstico |
|
||||
| `update [--version X] [--migrate/--no-migrate]` | Atualiza: `pg_dump` em `./backups/`, troca a tag da imagem, baixa e reinicia, health check, migrações Alembic |
|
||||
| `backup`, `restore` | **Só banco e `.env`.** Conteúdo enviado fica de fora |
|
||||
| `env`, `config` | Ver e alterar variáveis |
|
||||
| `doctor`, `health`, `shell`, `deployments` | Diagnóstico, shell no container, escala/memória |
|
||||
|
||||
Variáveis que mais importam para nós: `LEARNHOUSE_DOMAIN`, `LEARNHOUSE_SSL`, `LEARNHOUSE_TENANCY`, `LEARNHOUSE_COOKIE_DOMAIN` (ponto inicial só se subdomínios precisarem compartilhar login; o `.env` daqui tem
|
||||
`.overia.com.br`, ignorado em `single`), `LEARNHOUSE_ALLOWED_ORIGINS`, `LEARNHOUSE_AUTH_JWT_SECRET_KEY` (mínimo 32 caracteres), `LEARNHOUSE_SQL_CONNECTION_STRING`, `LEARNHOUSE_REDIS_CONNECTION_STRING`,
|
||||
`LEARNHOUSE_CONTENT_DELIVERY_TYPE` (`filesystem` ou `s3api`) e as de IA, e-mail e Sentry.
|
||||
|
||||
## O que a documentação não cobre (e resolvemos por fora)
|
||||
|
||||
- CSS ou script próprios por organização, e modo escuro: não documentados → injetados pelo nginx ([tema.md](tema.md)).
|
||||
- Se o `update` preserva um `nginx.prod.conf` alterado à mão: não documentado → ver cuidados em [operacao.md](operacao.md).
|
||||
- Hospedagem do Markdown remoto e CORS: não documentados → proxy `/aulas`.
|
||||
103
docs/operacao.md
Normal file
103
docs/operacao.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Operação
|
||||
|
||||
Acesso: SSH ao NAS4 (e ao NAS3, para o Keycloak) pela VPN Headscale/Tailscale. Host, porta e usuário estão no `CLAUDE.md` global do mantenedor,
|
||||
não aqui. O Docker do Synology exige `sudo`, e o binário está em `/usr/local/bin/docker` (fora do `PATH` de sessões não interativas).
|
||||
|
||||
Nomes que aparecem nos comandos: stack `/volume1/docker_1/xen-learnhouse`, projeto do compose **`xen-learnhouse`** (o nome ativo, não o `name:` do arquivo; usar outro
|
||||
faz o compose tentar criar rede nova e falhar), containers `learnhouse-{app,db,redis,nginx,oauth2-proxy,sso-bridge}-4bbd69be`.
|
||||
|
||||
## Publicar mudança no nginx ou no tema
|
||||
|
||||
```sh
|
||||
# 1. se o CSS mudou, suba o ?v=N em deploy/nginx.prod.conf (Cloudflare guarda por horas); fontes: ?v=N dentro do CSS
|
||||
# 2. publique (copia deploy/theme e deploy/nginx.prod.conf, faz nginx -t e recarrega; volta sozinho se falhar)
|
||||
NAS4_HOST=... NAS4_PORT=... NAS4_USER=... NAS4_SUDO_PW='...' sh deploy/publicar-tema.sh
|
||||
```
|
||||
|
||||
O script guarda o conf anterior em `~/backups/learnhouse-AAAAMMDD/nginx.prod.conf.HHMMSS` no NAS4. Quando uma mudança **cria** um volume novo (como o `./theme`),
|
||||
é preciso recriar o nginx uma vez: `docker compose -p xen-learnhouse up -d --no-deps --force-recreate nginx`.
|
||||
|
||||
## Mudar o compose, o oauth2-proxy ou a ponte
|
||||
|
||||
```sh
|
||||
cd /volume1/docker_1/xen-learnhouse
|
||||
sudo docker compose -p xen-learnhouse up -d --no-deps oauth2-proxy sso-bridge # variáveis de ambiente ou imagem
|
||||
sudo docker restart learnhouse-sso-bridge-4bbd69be # mudou só o código de sso_bridge/ (não há reload automático)
|
||||
```
|
||||
|
||||
Copie `sso_bridge/*.py` para `<stack>/sso_bridge/` e `deploy/docker-compose.override.yml` para `<stack>/docker-compose.override.yml` antes. O `oauth2-proxy.env`
|
||||
(`OAUTH2_PROXY_CLIENT_SECRET` e `OAUTH2_PROXY_COOKIE_SECRET`, permissão 600) existe só no servidor.
|
||||
|
||||
## Voltar atrás
|
||||
|
||||
| O que | Como |
|
||||
|---|---|
|
||||
| nginx | restaurar `~/backups/learnhouse-AAAAMMDD/nginx.prod.conf.*` em `<stack>/extra/nginx.prod.conf` e `docker exec learnhouse-nginx-4bbd69be nginx -s reload` |
|
||||
| compose | restaurar a cópia do `docker-compose.override.yml` e recriar o serviço |
|
||||
| configuração da organização / logo / aulas | os backups das linhas estão em `~/backups/learnhouse-20261003/` (`organizationconfig.before-*.json`, `activity-markdown.before-aulas.json`); depois apagar o cache do Redis (abaixo) |
|
||||
| desligar o SSO inteiro | restaurar o nginx original gerado pelo instalador (`~/backups/learnhouse-20261003/nginx.prod.conf.orig`) e recarregar; o `/login` volta a ser o do próprio LearnHouse (senha). Pode deixar `sso` em `allowed_auth_methods`, não atrapalha |
|
||||
|
||||
## Alterar a organização direto no banco
|
||||
|
||||
Faça backup da linha (`copy (select row_to_json(c) from organizationconfig c where org_id=1) to stdout`), altere em transação e **apague o cache**:
|
||||
|
||||
```sh
|
||||
docker exec learnhouse-redis-4bbd69be redis-cli del org_cache:slug:overia org_cache:config:1
|
||||
```
|
||||
|
||||
Sem isso a API continua devolvendo o valor antigo.
|
||||
|
||||
## Backups
|
||||
|
||||
- **Hoje não há rotina automática de backup do LearnHouse.** O que existe são os backups manuais de `~/backups/learnhouse-20261003/` (versões do nginx e linhas alteradas).
|
||||
- O `learnhouse backup` (CLI oficial) guarda **só o banco (`pg_dump`) e o `.env`**. O conteúdo enviado (logos, favicons, imagens, vídeos) fica no volume Docker do conteúdo e **precisa de backup à parte**
|
||||
(`rsync`, snapshot de volume ou tar). Os logos e o fundo de login do overia estão nesse volume.
|
||||
- Recomendado: `learnhouse backup` diário (cron) + cópia do volume de conteúdo + cópia fora do NAS. Ver [decisoes-e-pendencias.md](decisoes-e-pendencias.md).
|
||||
|
||||
## Atualizar o LearnHouse
|
||||
|
||||
`npx learnhouse update` faz `pg_dump`, troca a tag da imagem no `docker-compose.yml`, baixa as imagens, reinicia e oferece rodar as migrações (Alembic). **Antes**, com a nossa customização:
|
||||
|
||||
1. Leia as notas da release (autenticação, cookies e política por organização já mudaram na 1.3.3).
|
||||
2. Guarde cópias de `extra/nginx.prod.conf`, `docker-compose.override.yml`, `theme/`, `sso_bridge/`, `oauth2-proxy.env`. O instalador **gera** o `docker-compose.yml`, o `.env` e a configuração do proxy; **não sabemos se o update
|
||||
regrava o `extra/nginx.prod.conf`**. Se regravar, reaplique `deploy/nginx.prod.conf` com o script.
|
||||
3. Depois de atualizar, confira: `/login` ainda cai no Keycloak, o login volta com a sessão (a ponte usa `mint_session_tokens`, `set_auth_cookies` e `create_user` do código do LearnHouse; se algum nome mudar, a ponte quebra),
|
||||
o botão **Sair**, o Sign Out, o tema e uma aula Markdown.
|
||||
4. Rollback: restaurar o `pg_dump` pré-update e voltar a tag da imagem.
|
||||
|
||||
## Publicar curso novo
|
||||
|
||||
```sh
|
||||
uv run python -m scripts.publish <slug> --dry-run # mostra a árvore e as URLs, sem chamar a API
|
||||
uv run python -m scripts.publish <slug> --publish # cria e publica
|
||||
```
|
||||
|
||||
`.env` na raiz (não versionado): `LEARNHOUSE_API_TOKEN`, `LEARNHOUSE_BASE_URL`, `LEARNHOUSE_ORG_ID`, `LEARNHOUSE_ORG_SLUG` e
|
||||
**`GITEA_RAW_BASE=https://cursos.overia.com.br/aulas`**. Se usar a URL raw do Gitea direto, as aulas mostram "Failed to fetch" (sem CORS). O arquivo da aula precisa estar
|
||||
**no branch `main` do Gitea** para o proxy encontrá-lo. O token pode ser um token de API da organização (em *Settings* da organização) ou o JWT de `/api/v1/auth/login`.
|
||||
Hoje só o curso `python` está neste repositório.
|
||||
|
||||
## Diagnóstico rápido
|
||||
|
||||
| Sintoma | Causa provável | O que fazer |
|
||||
|---|---|---|
|
||||
| Site inteiro fora | container `learnhouse-nginx` parado, ou túnel `tunnel-nas4` | `docker ps --filter name=learnhouse`; reiniciar o nginx; checar o túnel |
|
||||
| Aula mostra "Failed to fetch" | atividade ainda aponta para o Gitea, ou o arquivo não existe no `main` | conferir `content->>'markdown_url'` no banco; `curl https://cursos.overia.com.br/aulas/<caminho>.md` |
|
||||
| Tema/CSS/fontes sumiram ou 404 | pasta `theme` recriada (bind mount vazio), ou 404 guardado no Cloudflare | recriar o nginx; subir o `?v=N` |
|
||||
| Logo ou cor da organização não mudam | cache do Redis | apagar as duas chaves `org_cache:` |
|
||||
| Login volta para a home sem entrar | `Code mismatch`/PKCE: algum link do Next disparando `/oauth2/start` | `docker logs overia-keycloak \| grep CODE_TO_TOKEN`; ver [sso.md](sso.md) |
|
||||
| Login em laço no `/dash` | falta o cookie `LH_session` | a ponte deve gravá-lo; reiniciar a ponte depois de mudar o código |
|
||||
| `502` no callback | buffer do nginx pequeno para o `Set-Cookie` | manter `proxy_buffer_size 64k` nas rotas `/oauth2/` |
|
||||
| "Entrar" logo depois do Sign Out entra direto | o script do Sign Out não foi injetado (usuário sem `LH_access`) ou o texto do menu mudou | usar o botão verde **Sair**; revisar o texto em `nginx.prod.conf` |
|
||||
| `kcadm` recusa a senha | o admin do `master` **não** se chama `admin` (esse usuário não existe; o login falha com `user_not_found`); o username real e a senha estão no `Makefile`/`.env` do repo `keycloak-overia` (NAS3), não aqui | `make kcadm-login` no repo `keycloak-overia` |
|
||||
|
||||
## Verificação depois de qualquer mudança
|
||||
|
||||
```sh
|
||||
B=https://cursos.overia.com.br
|
||||
for p in / /courses /api/v1/health /login /signup /dash "/aulas/python/01%20-%20Introdu%C3%A7%C3%A3o/01%20-%20O%20que%20%C3%A9%20Python.md" "/overia-theme/overia.css?v=N"; do
|
||||
printf "%-60s " "$p"; curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" "$B$p"; done
|
||||
# esperado: 200 nas públicas e na aula; 302 para /oauth2/start em /login, /signup e /dash
|
||||
```
|
||||
|
||||
Depois, no navegador: login pelo Google, home logada com o nome no cabeçalho, uma aula com código, **Sign Out** (confirmação do Keycloak → home) e "Entrar" pedindo autenticação.
|
||||
60
docs/sso.md
Normal file
60
docs/sso.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Login pelo Keycloak (SSO)
|
||||
|
||||
## Por que é assim
|
||||
|
||||
O LearnHouse tem SSO por OIDC próprio (Custom OIDC, via WorkOS para SAML), mas ele é um recurso **Enterprise** (exige
|
||||
`LEARNHOUSE_LICENSE_KEY` e a edição enterprise). Na edição community só existem senha, link por e-mail e Google. Para que o
|
||||
`cursos.overia.com.br` use a conta do `login.overia.com.br`, como Overleaf, Parsifal e OverOffice, foi montada uma camada **fora** do
|
||||
LearnHouse: oauth2-proxy + uma ponte que cria a sessão dele. Ver [learnhouse-upstream.md](learnhouse-upstream.md).
|
||||
|
||||
## Peças
|
||||
|
||||
| Peça | Configuração relevante |
|
||||
|---|---|
|
||||
| **Client `cursos`** (Keycloak, realm `overia`) | Confidential, só *standard flow*, PKCE `S256`, sem *direct access grants*. Redirect: `https://cursos.overia.com.br/oauth2/callback`. Logout: `https://cursos.overia.com.br/`. Foi criado importando o JSON (padrão do client `mentoria`) |
|
||||
| **Mapper de audiência** `audience-cursos` | No escopo dedicado do client: coloca `cursos` no `aud` do access token. **Obrigatório**: sem ele o token vem com `aud: account` e o oauth2-proxy recusa |
|
||||
| **oauth2-proxy v7.7.1** | `keycloak-oidc`, issuer `https://login.overia.com.br/realms/overia`, `CODE_CHALLENGE_METHOD=S256`, `SET_XAUTHREQUEST=true`, cookie `_overia_cursos` (Secure, SameSite Lax, 7 dias). `WHITELIST_DOMAINS` inclui `login.overia.com.br` (para o `rd` do logout) |
|
||||
| **Ponte** (`sso_bridge/`) | App FastAPI que roda na **mesma imagem** do LearnHouse (usa o código e o segredo dele, sem copiá-lo). Rotas `/sso/login`, `/sso/logout`, `/sso/health` |
|
||||
| **nginx** | `auth_request` em `/dash` e `/user`; `/login`, `/signup`, `/logout`, `/sso/login` e o tratamento de erro do callback. Ver `deploy/nginx.prod.conf` |
|
||||
|
||||
## Como a ponte decide
|
||||
|
||||
`/sso/login` **não confia em nada que venha do navegador**:
|
||||
|
||||
1. Pega o cookie do oauth2-proxy e pergunta a ele (`/oauth2/auth`). Só aceita se responder 202 com o e-mail verificado (`X-Auth-Request-Email`). Cabeçalho de e-mail forjado, cookie falso ou sem cookie dão sempre redirecionamento de volta ao Keycloak, nunca sessão.
|
||||
2. Procura o usuário do LearnHouse pelo e-mail (sem diferenciar maiúsculas). Se não existe, cria na organização `overia` (`create_user`, `signup_provider="sso"`).
|
||||
3. Emite a sessão com `mint_session_tokens(email, "sso", org_id)` e grava **três** cookies: `LH_access` (8 h), `LH_refresh` (30 dias), ambos HttpOnly, e `LH_session=1` (30 dias, legível por JavaScript).
|
||||
4. Redireciona para um destino **restrito a caminhos internos** (`safe_redirect`): padrão `/`; recusa `//host`, `http(s)://`, `\`, quebras de linha e `/login`, `/signup`, `/sso`, `/oauth2`.
|
||||
|
||||
`/sso/logout` apaga `LH_access`, `LH_refresh` e `LH_session` (inclusive versões antigas com `Domain=`) e manda para o `sign_out` do oauth2-proxy, que termina no logout do Keycloak.
|
||||
|
||||
## Armadilhas já encontradas (e como foram resolvidas)
|
||||
|
||||
| Sintoma | Causa | Correção |
|
||||
|---|---|---|
|
||||
| `500` no `/oauth2/callback`, laço de redirecionamentos e depois `502` no Keycloak | `aud: account` no token; o oauth2-proxy rejeita | Mapper `audience-cursos` |
|
||||
| `502` logo depois do login | `Set-Cookie` da sessão (com os tokens) maior que o buffer padrão de 4 KB do nginx | `proxy_buffer_size 64k` nas rotas `/oauth2/` |
|
||||
| Login falha com `pkce_verification_failed: Code mismatch` / "Code not valid" | O Next.js **pré-carrega** `/login?_rsc` e `/signup?_rsc` em toda página, e cada um rodava `/oauth2/start`, sobrescrevendo o cookie CSRF/PKCE do login em andamento | `if ($arg__rsc) { return 204; }` em `/login` e `/signup`. Qualquer rota nova que dispare `/oauth2/start` e seja link do Next precisa do mesmo guarda |
|
||||
| Depois do login o `/dash` volta para o `/login` em laço | O frontend decide "logado" pelo cookie legível `LH_session` (criado pela rota `/api/auth/token-exchange` do Next no login normal). Sem ele, o middleware manda para o `/login`. A API aceitava a sessão o tempo todo | A ponte grava `LH_session` |
|
||||
| Sessão `sso` derrubada pelo LearnHouse | A política por organização rejeita sessão cujo método (`amr`) não esteja em `allowed_auth_methods` | Manter `sso` na lista (hoje `["password","magic_login","sso"]`) |
|
||||
| "Sentimos muito... Falha ao sair" no Keycloak e sessão do Keycloak ainda viva | O menu do LearnHouse dispara a saída no `pointerup` e o `click` do mesmo gesto chamava `/logout` uma segunda vez; o segundo pedido invalida o código do primeiro | O script do tema redireciona **uma vez só** (`var d=0; if(d)return;`) |
|
||||
| "Entrar" depois do "Sign Out" loga direto | O Sign Out nativo só limpa a sessão do LearnHouse; a do Keycloak segue viva | O script do tema desvia o Sign Out para `/logout` |
|
||||
| `500` ao reabrir a URL de callback (F5, "voltar") | O código do Keycloak vale uma vez | O nginx troca esse erro por redirecionamento para a home |
|
||||
|
||||
Para diagnosticar login: `docker logs overia-keycloak | grep CODE_TO_TOKEN` (no NAS3) mostra o motivo exato; o log do oauth2-proxy só diz
|
||||
"Code not valid" e some quando o container é recriado.
|
||||
|
||||
## Propriedades de segurança e riscos aceitos
|
||||
|
||||
- A ponte só é alcançável pelo nginx (`/sso/login`, `/logout`); a porta dela não é publicada.
|
||||
- O e-mail só vale se o Keycloak o marcou como verificado (o oauth2-proxy rejeita e-mail não verificado por padrão).
|
||||
- **Pré-sequestro de conta**: o cadastro direto pela API do LearnHouse não verifica e-mail. Alguém poderia criar lá, antes da pessoa entrar pelo SSO, uma conta com o e-mail dela e ficar com acesso a ela. Fecharia com cadastro só por convite (a ponte precisaria de ajuste) ou desligando o login por senha (perde-se a saída de emergência).
|
||||
- **MFA do LearnHouse não vale** para quem entra pela ponte: quem autentica é o Keycloak.
|
||||
- O **logout** do Keycloak mostra uma tela de confirmação ("Você realmente deseja sair?") porque não enviamos `id_token_hint`.
|
||||
- O script que captura o **Sign Out** identifica o botão pelo texto do menu; uma atualização do LearnHouse que mude o texto pode quebrá-lo (o botão verde **Sair** não depende disso).
|
||||
|
||||
## Testes
|
||||
|
||||
`tests/test_sso_bridge_helpers.py` cobre `safe_redirect`, `normalize_email`, `identity_from_proxy_response`, `username_from_email` e
|
||||
`keycloak_logout_url`. O caminho feliz (login real → sessão → logout) foi verificado à mão no Chrome em 2026-10-04; não há teste automatizado dele.
|
||||
Cenários de ataque verificados contra a ponte: sem cookie, cookie falso, cabeçalho de e-mail forjado, `rd` externo e `rd` com quebra de linha.
|
||||
51
docs/tema.md
Normal file
51
docs/tema.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Tema overia sobre o LearnHouse
|
||||
|
||||
O LearnHouse não oferece, na organização, CSS ou script próprios nem modo escuro. O visual do `overia.com.br` é aplicado **por fora**:
|
||||
o nginx injeta uma folha de estilo e um script em todas as páginas HTML.
|
||||
|
||||
## Arquivos (`deploy/theme/`)
|
||||
|
||||
| Arquivo | Função |
|
||||
|---|---|
|
||||
| `overia.css` | Tokens, tema claro (padrão), tema escuro e estilos do cabeçalho, marca, botões, cartões e do conteúdo das aulas |
|
||||
| `theme-toggle.js` | Botão de tema (automático → claro → escuro) |
|
||||
| `fonts/*.woff2` | Red Hat Display, Red Hat Text e JetBrains Mono (variáveis, subconjuntos `latin` e `latin-ext`), copiadas de `overia.com.br` |
|
||||
|
||||
No servidor a pasta fica em `<stack>/theme` e é montada no nginx como `/usr/share/nginx/overia-theme` (somente leitura), servida em `/overia-theme/`.
|
||||
|
||||
## Como é injetado
|
||||
|
||||
No `nginx.prod.conf`, nas duas locations que repassam ao LearnHouse (`/` e `/dash|/user`):
|
||||
|
||||
```nginx
|
||||
proxy_set_header Accept-Encoding ""; # o sub_filter precisa de resposta sem compressão
|
||||
sub_filter '</head>' '<link rel="stylesheet" href="/overia-theme/overia.css?v=N"><script src="/overia-theme/theme-toggle.js?v=1"></script></head>';
|
||||
sub_filter '</body>' '$ov_logout_btn</body>'; # botão "Sair" e script do Sign Out, só para quem tem LH_access
|
||||
```
|
||||
|
||||
O `$ov_logout_btn` vem de um `map $http_cookie` (o `$cookie_...` do nginx não lê nome em maiúsculas, como `LH_access`).
|
||||
|
||||
## Temas
|
||||
|
||||
- **Claro** é o padrão. **Escuro** vale quando o sistema pede escuro (sem escolha manual) **ou** quando o botão escolhe "escuro". As 26 regras são as mesmas
|
||||
nos dois casos: `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) ... }` e `:root[data-theme="dark"] ...`.
|
||||
- O LearnHouse tem **tokens do shadcn em HSL** (`--background`, `--primary`, `--border`...), que o tema sobrescreve, e **classes fixas do Tailwind** (`bg-white`, `text-gray-900`,
|
||||
`bg-neutral-900`, `text-white/70`...), que o tema remapeia uma a uma no escuro. O cabeçalho original é verde por cor inline da organização, por isso as regras usam `!important`.
|
||||
- O conteúdo das aulas usa `.markdown-body` (github-markdown-css), com fundo branco fixo; o escuro tem regras próprias para texto, links, código, citações e tabelas.
|
||||
- A cor da organização no banco é `#2f7a34` (o verde do overia).
|
||||
- **Botão de tema** (canto inferior esquerdo; o **Sair** fica no direito): guarda a escolha em `localStorage` (`ov-theme`) e aplica `data-theme` em `<html>` antes da pintura, para não piscar.
|
||||
- **Marca** do cabeçalho: o símbolo e, empilhados ao lado, "cursos" (verde) e "overia", com a mesma fonte, tamanho e espaçamento (grade no link do logo). O logo da organização é o símbolo transparente.
|
||||
|
||||
## Armadilhas
|
||||
|
||||
1. **O Cloudflare guarda respostas, inclusive 404, por horas** (`cf-cache-status: HIT`, `max-age` de 4 h). Por isso o CSS e as fontes levam `?v=N`: **suba o `N` em toda mudança**
|
||||
(CSS no `nginx.prod.conf`; fontes no próprio CSS) e nunca sirva um caminho antes de ele existir.
|
||||
2. **Não recrie a pasta `theme` no servidor** (`rm -rf` + `cp -r`): o bind mount do nginx fica apontando para a pasta antiga/vazia e tudo vira 404. Copie o **conteúdo** para dentro da
|
||||
pasta existente; se já quebrou, `docker compose -p xen-learnhouse up -d --no-deps --force-recreate nginx`. O `deploy/publicar-tema.sh` já faz do jeito certo.
|
||||
3. O Chrome devolve algumas cores em `lab()`/`oklab()`; uma auditoria de contraste que leia isso como RGB dá falsos positivos. Confira por imagem.
|
||||
4. Print em headless pode perder imagens carregadas tardiamente (o logo): confirme no navegador de verdade.
|
||||
5. Para capturar claro/escuro em headless: `--blink-settings=preferredColorScheme=1` (claro) e `=0` (escuro).
|
||||
|
||||
## O que não foi verificado
|
||||
|
||||
O painel `/dash` como administrador (só vimos visitante e aluno) e a tela em largura de celular. O CSS é global, então pode haver detalhe a ajustar.
|
||||
Reference in New Issue
Block a user