Base de conhecimento pessoal auto-hospedada, focada em organização, conexões e recuperação rápida de informações.
| 📁 Hierarquia ilimitada | Documentos dentro de documentos, arrastar e soltar para reorganizar |
| ✏️ Editor rico | TipTap v3 — negrito, itálico, títulos, listas, código, citações, imagens inline |
| 📐 Barra de ferramentas completa | Tabelas, imagem por URL, alinhamento de parágrafo, destaque de texto com cor personalizável |
| 🖼️ Redimensionamento de imagens | Passe o mouse sobre uma imagem e arraste a alça (canto inferior direito) para redimensioná-la; largura persiste no documento |
| ⬇️⬆️ Importar / Exportar Markdown | Botão ⬇ .md converte o documento para Markdown e baixa o arquivo; ⬆ .md faz o caminho inverso — escolha um .md e o conteúdo é inserido na posição do cursor |
| 🧜 Diagramas Mermaid colapsáveis | Blocos de código com fences ```mermaid renderizam o diagrama e escondem o código-fonte por padrão; clique no diagrama ou no chip </> para alternar a exibição do código |
| 🎯 Modo foco | Botão de tela cheia abre o editor em uma janela separada, sem distrações; estado de conteúdo sincronizado ao fechar |
| 🔗 Notas relacionadas | Relacione documentos pelo painel "Notas relacionadas"; as relações são simétricas — ambos os documentos exibem a mesma lista de relacionados, sem distinção de direção |
| 🔗 Links inline no editor | Digite [ no editor para abrir autocomplete de documentos internos; selecione para inserir um chip de link clicável diretamente no texto |
| 🔒 Títulos únicos | Títulos funcionam como identificadores de documentos — duplicatas são bloqueadas com diálogo informativo e botão para navegar ao documento existente |
| 📍 Breadcrumb de hierarquia | Ancestrais clicáveis exibidos abaixo do título no editor — navegue para qualquer nível da hierarquia com um clique |
| 📡 Captura externa | Envie links de outros apps via PWA share target; Open Graph extraído automaticamente |
| 🔍 Busca híbrida (RRF) | Campo de busca da árvore lateral funde ranking léxico (FTS5 + LIKE) e semântico (embeddings Gemini) por Reciprocal Rank Fusion; sem GEMINI_API_KEY degrada automaticamente para busca léxica, sem erro |
| 💬 Chat com os documentos | Ícone 💬 na topbar (rota #/chat) abre uma conversa fundamentada apenas no conteúdo do seu PKD: recupera os documentos pela mesma busca híbrida, envia os corpos completos ao Gemini e responde em streaming. Lista os documentos consultados servida pelo backend (não pelo modelo, portanto não alucinável), e quando nada relevante é encontrado diz isso em vez de inventar. Botão para salvar a conversa como documento. Requer GEMINI_API_KEY; sem ela o ícone fica desabilitado com tooltip |
| 📅 Calendário | Navegue pelos documentos pela data de criação |
| 🗓️ Calendário de Memórias | Navegue pelas Memórias pela Data da Memória; segue o filtro Ativos/Arquivados/Todos |
| 🕰️ Histórico de versões | Snapshot automático a cada save com dedup SHA-256 (saves idênticos não geram versão). Visualize, compare e restaure qualquer versão anterior via botão ⏱ na barra do documento. Retenção configurável (padrão 50 versões/documento) |
| ⭐ Favoritar da barra | Botão ⭐ na barra de ações do documento — alterna favorito sem precisar ir à sidebar |
| 🗓️ Memória Cronológica | Registre eventos no tempo (almoço, entrega, consulta) como Memórias, em árvore própria Ano → Mês → Dia no menu lateral. ID público estável MEM-…, API para agentes (Hermes). Ver Memória Cronológica (MC) |
| 📝 Nota | Texto curto e rápido (endereço, contato, lista) no bloco "Notas" da barra lateral, fora da árvore normal, sem pai nem filhos. Conversível em Documento (arrastar para a árvore) ou Memória (arrastar para a MC). Ver Nota |
| 🔎 Filtro unificado | Campo de busca na sidebar filtra a árvore em tempo real por título; pesquisa e navegação integradas em um único controle |
| 🎨 Ícones Boxicons | Cada documento tem um ícone Boxicons selecionável; pasta ganha ícone de pasta automaticamente ao receber filhos; folha volta ao ícone padrão ao ficar sem filhos |
| 🗂️ Estado persistido | Nós expandidos e colapsados na árvore são lembrados entre sessões |
Cada documento possui uma área de associações no rodapé, com três colunas:
| Coluna | Funcionalidade |
|---|---|
| 📄 Notas relacionadas | Relacione documentos com busca autocomplete; relações simétricas — ambos os documentos exibem a mesma lista |
| 📎 Arquivos | Imagens mostram thumbnail; PDFs, áudios e outros tipos exibem ícone. Clicar abre modal de visualização (lightbox, embed PDF, player de áudio/vídeo) |
| 🔗 Links externos | URLs com título opcional; verificação de validade no painel de administração |
Quando um documento possui filhos diretos na hierarquia, eles são exibidos como cards clicáveis logo abaixo do conteúdo principal, antes da área de associações. Cada card mostra o ícone, o título e um preview do texto (até 160 caracteres). Clicar em um card navega diretamente para o sub-documento.
| 🏷️ Hashtags com autocomplete | Marque documentos com tags; ao digitar, sugeridas as tags já existentes no sistema |
| 🎨 Cores por tag | Cada tag possui cor configurável; chips coloridos em toda a interface (sidebar e editor) |
| 🗂️ Arquivamento | Arquive documentos pelo ícone na barra; arquivar pai cascateia para todos os filhos recursivamente. Documentos arquivados são auto-trancados; desarquivar pai libera todos os filhos |
| 🕸️ Graph View | Grafo D3.js force-directed: nós de documentos, tags e hierarquia pai/filho. Toggle Semântico exibe arestas de similaridade (Gemini, roxo tracejado) e agrupa docs em comunidades Louvain — nós coloridos por comunidade, layout em clusters, filtro por comunidade na legenda. Toggles independentes; filtro de texto integrado |
| 🔗 Links públicos | Links de compartilhamento revogáveis, somente leitura |
| 📊 Dashboard | Aba padrão da Administração: cards com totais de documentos, arquivos, links e tags ativos; cards de uso de disco (banco de dados, WAL, arquivos, total) |
| 💾 Backup / Restore do banco | Download do SQLite + restore com confirmação |
| 📦 Backup / Restauração de anexos | Backend local: ZIP síncrono. Backend S3: backup assíncrono que monta o ZIP no próprio bucket (sem usar disco da instância) e oferece URL pré-assinada (TTL 15 min). Restauração cross-backend: ZIP gerado em S3 pode ser restaurado em backend local e vice-versa. Manifesto interno usa SHA256 (dedup natural + verificação de integridade). Job tracking com progresso e contadores (gravados / mantidos / órfãos / hash inválido). |
| 🗑️ Lixeira | Documentos excluídos ficam em lixeira; restauráveis individualmente ou em lote |
| 🏷️ Tags | Renomear, editar cor, excluir ou mesclar tags globalmente; botão para remover tags órfãs |
| 📎 Arquivos | Grade com todos os anexos do sistema (thumbnail para imagens); botão "Verificar Órfãos" lista arquivos sem documento associado; seção "Imagens externas" lista documentos com imagens hospedadas fora do PKD e permite importá-las individualmente com um clique |
| 🔗 Links externos | Tabela de gerenciamento de todos os links externos; testa validade (HTTP HEAD) e permite excluir inválidos em lote |
| ☁️ Armazenamento | Migração de anexos entre armazenamento local e Amazon S3; teste de conexão, migração SHA256-verificada com progresso em tempo real; reconciliação S3↔DB e LOCAL↔DB (corrige storage_location sem mover arquivos); limpeza da origem com verificação de cópia no destino antes de apagar; backup/restauração assíncrona com streaming multipart e URL pré-assinada |
| 🧹 Limpeza | Executa VACUUM no banco de dados para recuperar espaço em disco |
| ⚙️ Configurações | Retenção de versões configurável (padrão 50/documento); painel de embeddings semânticos: status da chave Gemini, contagem de vetores, seletor de modelo Gemini (3 opções; persiste no DB, sobrepõe env var, dispara re-embed automático ao trocar) |
| 📧 2FA por e-mail (login vinculado ao dispositivo) | Opcional — habilitado com SES_USERNAME, SES_PASSWORD, EMAIL_SENDER e EMAIL_2FA. Dispositivos não reconhecidos recebem um código de 6 dígitos por e-mail após a senha mestra; uma vez validado, o dispositivo é lembrado permanentemente e não pede código de novo |
| 🛡️ Proteção de documentos | Ícone de escudo no editor criptografa o corpo do documento em repouso (AES-256-GCM). Título, ícone e tags continuam visíveis na árvore; o conteúdo some da busca e dos embeddings semânticos enquanto protegido. Abrir exige um código por e-mail; a sessão do servidor mantém o documento decifrado até o logout ou reinício do servidor |
| 🌙 Tema claro / escuro | Alternância persistida no localStorage |
| 📱 Mobile master-detail | Em telas ≤ 640px: tela-lista (topbar 2 linhas, busca larga, árvore inteira) e tela-detalhe (botão ← para voltar). Sem drawer hambúrguer. Desktop (≥ 641px) inalterado |
| 📲 PWA | Instalável como app; share target no celular; offline somente leitura |
| ↩️ Histórico de navegação | Botões ← / → na barra superior (desktop) e atalhos Alt+← / Alt+→ navegam pelo histórico de documentos visitados na sessão |
| 🔄 Sessão persistente | Login lembrado por até 30 dias de inatividade; último documento aberto restaurado automaticamente ao retornar à ferramenta |
O Hermes cria os três tipos de conteúdo com Bearer PKD_IMPORT_TOKEN, sem sessão de login. Uma Skill única cobre os três: prompt em docs/promptHermes.md.
| Tipo | Rotas com Bearer |
|---|---|
| Documento | POST /api/documents — {title, content (HTML), tags?, attachments? (base64, [ADR-003](docs/adr/003-import-de-anexos-do-notas.md)), parent_id?, idempotency_key?} → 201 (200 se a idempotency_key já existe). Sem parent_id, o Documento fica na raiz. Sem tag automática. Só criação |
| Nota | POST, GET, PATCH /api/notes (ver Nota) |
| Memória | POST, GET, PATCH /api/memories (ver Memória Cronológica) |
Sem PKD_IMPORT_TOKEN, essas rotas aceitam só a sessão de login. O antigo POST /api/import (app Notas) foi removido.
Memórias registram eventos no tempo (um almoço, uma entrega, um evento de saúde). Cada Memória é um documento com busca, embeddings, Chat, tags, anexos e links, mas vive somente na árvore da MC do menu lateral (Ano → Mês → Dia), posicionada pela Data da Memória — nunca na árvore normal, sem pai nem filhos. Termos em docs/adr/glossary.md; ID público em ADR-007.
- Data da Memória: só o ano é obrigatório; mês, dia e hora exata ou Período (madrugada, manhã, almoço, tarde, lanche, jantar, noite) entram quando conhecidos
- Árvore da MC: bloco próprio no menu lateral, entre a árvore de documentos e "+ Novo documento", recolhível. Anos, meses e dias do mais recente para o mais antigo; dentro do dia: sem hora → início do dia → Período mais longo → hora exata → criação
- Criar pela interface: "+ Nova Memória" abre um diálogo com Ano e Mês de hoje e o Dia vazio. No editor, o controle Data da Memória substitui a Data Associada e o ID tem botão Copiar. Ícone padrão
bx-calendar-event - Sem hierarquia: Memória não tem pai nem filhos (bloqueado no backend e na interface); associe por link. Memórias entram na busca, no Chat e no Graph View
- ID de Memória:
MEM-AAAA[-MM[-DD[T<HH>[MM]]]]-SUFIXO6, ex.MEM-2026-09-23T12-7QF3K9. Congelado na criação: corrigir a data não muda o ID - API para agentes (Bearer
PKD_IMPORT_TOKEN, ou sessão de login):POST /api/memories—{title, content (HTML), tags?, attachments?, date:{year, month?, day?, hour?, minute?, period?}, idempotency_key?}→ 201. A mesmaidempotency_keydevolve a Memória existente (200), sem duplicarGET /api/memories/{MEM-id}ePATCH /api/memories/{MEM-id}(title,content,tags,date—datesubstitui a data inteira)- Sem
DELETEe sem busca: apagar é ação do usuário na interface
- Prompt para a Skill do Hermes:
docs/promptHermes.md
Notas guardam um texto curto e de formatação simples (um endereço, um contato, uma lista). Cada Nota é um documento com busca, embeddings, Chat, Graph View, tags, anexos e links, mas vive somente no bloco "Notas" da barra lateral — nunca na árvore normal, sem pai nem filhos. Termos em docs/adr/glossary.md.
- Bloco Notas: lista plana na barra lateral, favoritas primeiro, depois mais recentes; recolhido por padrão. Respeita os filtros de tag e favoritos da barra lateral
- Criar pela interface: "+ Nova Nota" pede só o título e abre o editor completo (mesmo TipTap do Documento)
- Sem hierarquia: Nota não tem pai nem filhos (bloqueado no backend e na interface). Ícone padrão
bx-sticky-note - Conversão só de ida: arraste uma Nota para a árvore normal para virar Documento na posição solta (sem diálogo); arraste para o bloco MC para virar Memória (diálogo pede a Data da Memória, ano obrigatório). Corpo, tags, anexos e favorito são preservados; não existe conversão de volta
- API para agentes (Bearer
PKD_IMPORT_TOKEN, ou sessão de login):POST /api/notes—{title, content (HTML)?, tags?, attachments?, favorite?, idempotency_key?, created_at?, updated_at?}→ 201. A mesmaidempotency_keydevolve a Nota existente (200), sem duplicar.created_at/updated_atsó são honrados com BearerGET /api/notes/{id}ePATCH /api/notes/{id}(title,content,tags)GET /api/notes(só sessão) — lista do bloco Notas, com os mesmos filtros de tag/favorito da árvorePOST /api/notes/{id}/convert(só sessão) —{to:"document", parent_id?, before_id?}ou{to:"memory", date:{year, ...}}
- Migração do app Notas (2026-09-26): as 74 notas ativas (não arquivadas, fora da lixeira) viraram Notas por
POST /api/notes, com anexos, tags, favoritas (as notas fixadas) e as datas originais. Título = primeira linha da nota; linhas só de hashtags viraram tags. O script foi descartável e não está no repositório; detalhes emdocs/proximosPassos.md
mkdir -p /caminho/pkd/db /caminho/pkd/attachments
docker run -d \
--name pkd \
--restart unless-stopped \
-p 8080:8080 \
-v /caminho/pkd/db:/data/db \
-v /caminho/pkd/attachments:/data/attachments \
-e PKD_PASSWORD='SUBSTITUA_POR_UMA_SENHA_FORTE' \
-e PKD_DB_PATH=/data/db/pkd.sqlite \
-e PKD_ATTACHMENTS_PATH=/data/attachments \
ghcr.io/edalcin/pkd:latestAcesse http://localhost:8080 e digite a senha mestra.
Guia completo em português (sem terminal): UNRAID.md
services:
pkd:
image: ghcr.io/edalcin/pkd:latest
container_name: pkd
restart: unless-stopped
ports:
- "8080:8080"
environment:
PKD_PASSWORD: ${PKD_PASSWORD:?PKD_PASSWORD is required}
PKD_DB_PATH: /data/db/pkd.sqlite
PKD_ATTACHMENTS_PATH: /data/attachments
# Opcional: ativa as APIs para agentes (/api/documents, /api/memories, /api/notes)
PKD_IMPORT_TOKEN: ${PKD_IMPORT_TOKEN}
volumes:
- ./data/db:/data/db
- ./data/attachments:/data/attachments
healthcheck:
test: ["/pkd", "-healthcheck"]
interval: 30s
timeout: 3s
retries: 3# .env — nunca versione este arquivo
PKD_PASSWORD=senha-forte-aqui
PKD_IMPORT_TOKEN=token-secreto-compartilhado-com-agentes # opcional| Variável | Obrigatória | Padrão | Descrição |
|---|---|---|---|
PKD_PASSWORD |
sim | — | Senha mestra (nunca armazenada, comparada apenas em memória) |
PKD_DB_PATH |
sim | — | Caminho do arquivo SQLite dentro do container |
PKD_ATTACHMENTS_PATH |
sim | — | Caminho do diretório de anexos dentro do container |
PKD_LISTEN_ADDR |
não | :8080 |
Endereço de escuta HTTP |
PKD_SESSION_IDLE_MINUTES |
não | 43200 |
Minutos de inatividade até expirar a sessão (padrão ≈ 30 dias) |
PKD_MAX_IMAGE_MB |
não | 10 |
Tamanho máximo de imagem inline (MB) |
PKD_MAX_ATTACHMENT_MB |
não | 100 |
Tamanho máximo de arquivo anexado (MB) |
PKD_TRUST_PROXY_HEADERS |
não | 0 |
Defina como 1 apenas atrás de proxy reverso confiável |
PKD_BASE_URL |
não | (host da request) | URL pública base para links de compartilhamento (ex: https://pkd.exemplo.com/) |
PKD_IMPORT_TOKEN |
não | (desativado) | Token secreto (Bearer) para as APIs de agentes: POST /api/documents, Memórias (/api/memories) e Notas (/api/notes). Se vazio, essas rotas aceitam só sessão de login. Gere com openssl rand -hex 32 |
PKD_S3_BUCKET |
não | (vazio) | Nome do bucket S3; junto com PKD_S3_REGION habilita S3 como backend disponível |
PKD_S3_REGION |
não | (vazio) | Região AWS do bucket (ex: us-east-1) |
PKD_S3_PREFIX |
não | (vazio) | Prefixo de caminho dentro do bucket (ex: pkd/attachments/) |
PKD_S3_ACCESS_KEY_ID |
não | (credenciais da instância) | Access Key ID; se vazio, usa credenciais padrão da instância/IAM role |
PKD_S3_SECRET_ACCESS_KEY |
não | (credenciais da instância) | Secret Access Key correspondente |
GEMINI_API_KEY |
não | (desativado) | Chave de API Google Gemini, usada por três recursos: embeddings semânticos, Chat com os documentos e sugestão de nome de comunidade no Graph View. Sem ela o toggle Semântico no Graph View fica indisponível, o worker de embedding não roda, o ícone 💬 do Chat fica desabilitado (POST /api/chat → 503), e a busca da árvore lateral opera só com o léxico (RRF degrada automaticamente, nunca erro) |
PKD_EMBED_SWEEP_MINUTES |
não | 15 |
Cadência do worker de embedding em background (minutos). O worker também dispara imediatamente ao criar/editar documentos |
SES_USERNAME |
não | (desativado) | Usuário SMTP do Amazon SES. Junto com SES_PASSWORD, EMAIL_SENDER e EMAIL_2FA, habilita o 2FA por e-mail no login (vinculado ao dispositivo) e a criptografia de documentos protegidos |
SES_PASSWORD |
não | (desativado) | Senha SMTP do Amazon SES |
EMAIL_SENDER |
não | (desativado) | Endereço From: do e-mail de código (precisa ser uma identidade verificada no SES) |
EMAIL_2FA |
não | (desativado) | Endereço que recebe todo código de 2FA (login e desbloqueio de documento) |
SES_HOST |
não | email-smtp.us-east-1.amazonaws.com |
Host SMTP do Amazon SES |
SES_PORT |
não | 587 |
Porta SMTP (STARTTLS) |
⚠️ A chave de criptografia dos documentos protegidos é derivada dePKD_PASSWORD. Trocar a senha mestra torna os documentos já protegidos indecifráveis — use Desproteger em cada um antes de rotacionar a senha.
O campo de busca da árvore lateral (topbar) sempre executa busca híbrida: o termo digitado é buscado tanto pelo motor léxico (FTS5 + LIKE, cobrindo título, corpo, tags e títulos de links externos) quanto pelo motor semântico (similaridade de cosseno sobre embeddings Gemini), e os dois rankings são fundidos por Reciprocal Rank Fusion (RRF, k=60) numa única lista plana ordenada por relevância.
- Sem
GEMINI_API_KEY, ou antes do primeiro embed de um documento, a perna semântica fica vazia e o RRF degrada automaticamente para a ordem léxica — a busca nunca falha e nunca retorna erro. - O score de similaridade não é exibido na interface: a fusão RRF consome apenas a ordem dos hits. Para observar a grandeza dos pisos de similaridade, use a densidade de arestas do Graph View.
- A busca abrange automaticamente ativos e arquivados enquanto o campo estiver preenchido; a view anterior (Ativos/Arquivados/Todos) é restaurada ao limpar a busca.
- Não há mais seletor "Léxica | Semântica" — a fusão é sempre automática.
O ícone 💬 na topbar abre a rota #/chat, uma conversa fundamentada apenas
no conteúdo do PKD. O caminho de uma pergunta:
- Recuperação — a pergunta (concatenada com as últimas mensagens do usuário, para que "e sobre a fase escura?" ainda encontre o assunto do turno anterior) entra na mesma busca híbrida descrita acima.
- Piso de relevância — se o melhor candidato semântico não atingir
chatRelevanceFloor(~0,50), o servidor responde "não encontrei nada relevante" sem chamar o modelo. Isso existe porque a busca híbrida nunca retorna vazio: sem o piso, uma pergunta fora do acervo produziria uma resposta de conhecimento geral sob uma lista de fontes que não a sustenta. - Contexto — até 8 documentos, com o corpo completo (não trechos), sob
um teto de tokens. Documento que não cabe é omitido, nunca truncado.
Documentos protegidos por senha e na lixeira ficam fora. Cada documento leva
uma linha
Data:com a Data da Memória ou a data associada (com hora ou Período da Memória, quando existem), para o Chat responder "quando?" e "a que horas?". - Resposta — streaming SSE via
POST /api/chat, com prompt de sistema estrito ("responda apenas com base nos documentos"). Fechar a aba cancela a geração. - Atribuição — a lista de documentos consultados vem do servidor, não do modelo, e por isso não pode ser alucinada. Cada item é um link que abre o documento em nova aba.
A conversa é efêmera: trocar de rota ou recarregar a página a descarta. O botão "Salvar como documento" transforma a conversa num documento PKD normal — indexado, versionado e linkável como qualquer outro.
O Modelo de Chat é escolhido em Administração → Preferências → Chat com
os documentos (chave chat.model). Não confundir com o modelo de
embedding, que é fixo: trocar o de Chat afeta apenas a próxima resposta,
enquanto trocar o de embedding exigiria reprocessar todo o acervo.
Decisões completas em ADR-006.
Use o campo "Buscar nota para relacionar…" na coluna Notas relacionadas no rodapé do documento para criar relações entre documentos. As relações são simétricas — ambos os documentos exibem a mesma lista de relacionados. Para remover um vínculo, clique no ✕ ao lado do documento em qualquer um dos lados.
Digite no campo + tag no cabeçalho do documento. Ao digitar, um dropdown sugere as tags já existentes no sistema — selecione ou continue digitando para criar uma nova. Confirme com Enter ou ,.
Para personalizar a cor de uma tag, acesse Administração → Tags, clique no seletor de cor ao lado da tag desejada e confirme. A cor propaga imediatamente para os chips de tag na sidebar e no editor.
Cada documento na árvore lateral possui um ícone Boxicons selecionável. Clique no ícone ao lado do título para abrir o seletor — há busca dinâmica por nome. Documentos com filhos ganham automaticamente um ícone de pasta; ao perder todos os filhos, voltam ao ícone padrão.
Clique no botão de modo foco (⛶) na barra de ferramentas. O editor abre em uma janela separada sem sidebar, tags ou cabeçalho — apenas o conteúdo. Ao fechar a janela, o conteúdo editado é sincronizado de volta ao documento principal.
Crie documentos filhos normalmente pela sidebar (clique direito → "Novo sub-documento"). Ao abrir o documento pai, os filhos diretos aparecem automaticamente como cards logo abaixo do conteúdo, com preview do texto. Clicar em um card navega para o sub-documento.
Arraste um arquivo ou clique em "+ Anexar arquivo". Imagens exibem thumbnail diretamente; clicar em qualquer anexo abre um modal:
- Imagem → lightbox com zoom nativo do browser
- PDF → visualizador embutido
- Áudio → player HTML5
- Vídeo → player HTML5
- Outros → tela de download
Insira uma imagem no editor (upload, URL ou colar). Passe o mouse sobre ela — uma alça azul aparece no canto inferior direito. Clique e arraste horizontalmente para ajustar a largura. Ao soltar, a nova largura é salva no documento automaticamente.
Clique no botão ⏱ na barra de ações do documento. O dialog de histórico exibe todas as versões salvas com data/hora e preview do conteúdo.
- Restaurar — clique em "Restaurar esta versão" para reverter o documento ao snapshot selecionado. A restauração reutiliza o fluxo normal de save (incluindo bloqueio otimista e sincronia de links).
- Excluir versão — passe o mouse sobre uma versão para revelar o ícone de lixeira; confirme para excluí-la individualmente.
- Configurar retenção — em Administração → Configurações, ajuste o campo "Máx. versões por documento" (padrão 50, aceita 1–10000).
Saves idênticos não geram versão nova (dedup por SHA-256 do conteúdo).
Clique no ícone de caixa (📦) na barra superior do editor para arquivar ou desarquivar um documento. Ao arquivar:
- O documento e todos os seus filhos são arquivados recursivamente e trancados automaticamente
- Documentos arquivados somem da árvore principal e aparecem na view Arquivados
- Um documento arquivado pode ser editado se destrancado individualmente
Ao desarquivar o documento pai, todos os filhos são desarquivados em cascata e ficam novamente disponíveis na árvore principal.
Ambos os recursos usam o mesmo canal de e-mail (Amazon SES) e ficam desativados até as quatro variáveis SES_USERNAME, SES_PASSWORD, EMAIL_SENDER e EMAIL_2FA estarem definidas (ver Variáveis de ambiente). Sem elas, o login continua de etapa única, como antes.
Login com 2FA (vinculado ao dispositivo)
- Digite a senha mestra normalmente.
- Se o navegador não for reconhecido, um código de 6 dígitos chega por e-mail em
EMAIL_2FA— digite-o na tela seguinte. - Uma vez validado, esse navegador é lembrado permanentemente (cookie de dispositivo) e não pede código de novo, mesmo após logout/login.
- Para revogar todos os dispositivos (ex: perda de acesso a um navegador confiável), acesse Administração → Configurações → Esquecer dispositivos confiáveis. O próximo login em qualquer navegador volta a pedir código.
Proteção de documento (criptografia em repouso)
- Abra o documento e clique no ícone de escudo (🛡) na barra de título.
- O corpo é criptografado (AES-256-GCM, chave derivada de
PKD_PASSWORD) e permanece visível apenas na sessão atual — você não perde o que estava editando. O ícone muda para escudo preenchido confirmando a proteção. - Ao reabrir o documento numa sessão nova (outro navegador, ou após logout/login), o editor mostra um painel de desbloqueio no lugar do conteúdo: clique em "Enviar código por e-mail", digite o código de 6 dígitos recebido e o conteúdo é decifrado para aquela sessão.
- Documentos protegidos saem da busca por conteúdo e dos embeddings semânticos enquanto criptografados — o título continua pesquisável e visível na árvore, já que não é secreto.
- Clique no escudo novamente para desproteger (exige que o documento já esteja desbloqueado na sessão atual).
⚠️ A chave de criptografia é derivada dePKD_PASSWORD— trocar a senha mestra torna documentos protegidos indecifráveis. Desproteja-os antes de rotacionar a senha.
Configure as variáveis PKD_S3_BUCKET e PKD_S3_REGION para habilitar o backend S3. Depois acesse Administração → Storage:
- Testar conexão — verifica credenciais e acesso ao bucket
- Migrar para backend ativo — copia todos os anexos do backend de origem para o ativo com verificação SHA256 por arquivo; exibe barra de progresso em tempo real e relatório final; idempotente (pode ser repetido após falha)
- Ativar S3 — troca o backend ativo; novos uploads vão para o S3
- Reconciliar S3 ↔ DB — corrige
storage_locationno banco quando ele diverge do que existe no bucket (ex: após restore do DB); não move arquivos - Reconciliar LOCAL ↔ DB — mesmo que acima, para o backend local; sempre visível mesmo sem S3
- Limpar origem — remove arquivos do backend anterior; verifica que existe cópia confirmada no backend ativo antes de cada exclusão; arquivos sem cópia são ignorados e reportados
Todas as operações (Migrar, Reconciliar, Limpar) são assíncronas com job tracking: barra de progresso durante execução e relatório detalhado ao concluir. Somente uma operação por backend por vez (segunda tentativa retorna 409).
Fluxo completo Local → S3:
Testar conexão → Migrar → Ativar S3 → Limpar origem (verificar ignorados = 0)
Quando o backend ativo é S3, o PKD oferece backup/restauração assíncrona sem materializar o ZIP no disco da instância de aplicação:
- Backup assíncrono —
Administração → Storage → Backup assíncrono (S3). Pipelinearchive/zip → io.Pipe → manager.Uploadergrava o ZIP diretamente no bucket em<prefix>/_backup-tmp/<job-id>.zip. Retorna URL pré-assinada (TTL 15 min) para download direto S3 → navegador. Progresso e contadores via polling. - Restauração assíncrona —
Administração → Storage → Restauração assíncronaaceita ZIP gerado pela aplicação. Restauração cross-backend (ZIP de prod S3 restaurado em dev local e vice-versa) e in-place (mesmo backend, recupera arquivos perdidos). 3 modos de conflito:sobrescrever(padrão),manter existente,abortar. - Manifesto interno (
manifest.jsoncomo última entrada do ZIP) — mapeia SHA256 →stored_filenames. Dedup natural quando múltiplas linhas deattachmentscompartilham o mesmo conteúdo; fan-out automático no restore. - Integridade — cada entrada restaurada tem hash verificado antes de gravar. Hash mismatch é relatado por arquivo sem abortar a operação (FR-008). Entradas órfãs (SHA256 sem linha em
attachments) são ignoradas e listadas no resultado. - Limpeza automática — no startup, sweep não-bloqueante remove objetos em
_backup-tmp/com idade > 24h (recuperação de crashes). - Concorrência — uma operação ativa por backend por vez; segunda requisição retorna 409.
IAM mínimo: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket, s3:DeleteObjects, s3:AbortMultipartUpload.
Endpoints legados (/api/admin/storage/backup-attachments síncrono local) continuam funcionando.
Detalhes operacionais em docs/operations.md.
Ao colar conteúdo de outra página (blog, artigo, e-mail), imagens com URL externa ficam referenciadas no documento mas não são armazenadas no PKD — podem quebrar se o site original sair do ar.
No editor — quando o documento aberto contém imagens externas, um botão 🌐⬇ aparece na barra de ferramentas. O botão fica desabilitado durante a importação (evita duplo-clique) e desaparece imediatamente ao concluir quando não restam mais imagens externas. Um feedback inline exibe quantas foram importadas e quantas falharam (visível por 5 segundos).
Em lote via Administração → Arquivos — a seção "Imagens externas" lista todos os documentos com imagens externas, com o número de imagens por documento. Clique em "🌐⬇ Importar" ao lado de cada documento para importar individualmente. Se o documento importado estiver aberto no editor, o conteúdo é atualizado automaticamente sem necessidade de recarregar a página.
As imagens importadas passam a aparecer na grade de "Arquivos anexados" e ficam disponíveis mesmo se o site externo for removido.
- Exportar — clique em ⬇ .md na barra de ferramentas. O browser baixa imediatamente o arquivo
<título>.mdcom o conteúdo do documento convertido para Markdown (headings#, código em triple-backtick, links[Documento]preservados). - Importar — clique em ⬆ .md, escolha um arquivo
.md/.markdown/.txtno seletor do sistema. O conteúdo é convertido e inserido na posição atual do cursor (ou no fim do documento, se o editor não estiver focado). Desabilitado em documentos trancados. Suporta headings, listas (incluindo checklist quando todos os itens da lista usam- [ ]/- [x]), tabelas e formatação inline.
Um bloco de código com fence ```mermaid renderiza o diagrama automaticamente. O código-fonte fica oculto por padrão — apenas o diagrama é exibido, com um chip </> no canto superior direito do bloco.
- Clique no diagrama ou no chip para alternar a exibição do código-fonte.
- Posicionar o cursor dentro do código (para editá-lo) revela a fonte automaticamente; ela volta a ocultar-se ao mover o cursor para fora do bloco.
- Funciona em documentos trancados — o chip continua alternando a visualização mesmo sem poder editar.
- Blocos de código de outras linguagens (
```js,```python, etc.) não são afetados — permanecem sempre com o código visível, sem renderização.
Digite [ em qualquer posição do editor. Um dropdown de autocomplete aparece buscando documentos pelo título em tempo real (FTS5). Continue digitando para filtrar os resultados; use ↑/↓ para navegar, Enter ou clique para confirmar.
Ao selecionar, o texto digitado é substituído por um chip de link clicável com o título do documento. Clicar no chip navega para o documento vinculado. Os links são bidirecionais — ambos os documentos exibem a relação no painel "Notas relacionadas" e no Graph View (aresta de tipo link).
No export Markdown (botão ⬇ .md), chips de link são convertidos para [Título do Documento].
Acesse pelo ícone 🕸️ na barra superior. O grafo mostra:
- Nós de documento → círculos coloridos pela tag primária
- Nós de tag → círculos com borda tracejada rosa
- Arestas de hierarquia → linhas tracejadas
--accententre pai e filho - Arestas de link → relações manuais entre documentos (azul)
- Arestas de tag → relação documento ↔ tag (rosa)
- Arestas semânticas → similaridade de conteúdo via Gemini embeddings (roxo tracejado); visíveis quando
GEMINI_API_KEYestá configurada
O campo de filtro da barra superior também atua no grafo — ao digitar, apenas os nós que correspondem ao filtro são exibidos, mantendo as arestas de seus vizinhos diretos para preservar o contexto.
Toggles de visibilidade (independentes, sem reiniciar simulação):
| Toggle | O que controla |
|---|---|
| Hierarquia | Exibe/oculta arestas e nós de relação pai/filho |
| Links entre docs | Exibe/oculta arestas e nós de relação manual entre documentos |
| Relações com tags | Exibe/oculta arestas e nós de tag |
| Semântico | Exibe arestas de similaridade semântica (requer GEMINI_API_KEY); ao ativar, detecta comunidades via Louvain, colorindo nós por cluster e posicionando-os em layout agrupado; legenda filtra comunidades individualmente ou em bloco |
| Todos os docs | Força exibição de todos os documentos, mesmo sem conexões |
Clicar em um nó de tag filtra o grafo por aquela tag. Clicar em um nó de documento navega para o documento.
Embeddings semânticos são gerados proativamente em background: no startup do servidor, ao criar/editar documentos e a cada PKD_EMBED_SWEEP_MINUTES minutos. Os vetores ficam armazenados no próprio SQLite — o grafo semântico carrega instantaneamente sem nova chamada à API no momento do clique no toggle.
Comunidades semânticas — ao ativar o toggle Semântico o PKD detecta comunidades automaticamente:
- Algoritmo Louvain local-moving (JS puro, sem dependências) sobre o grafo de similaridade visível
- Nós coloridos por comunidade via ângulo dourado HSL — suporta qualquer número de clusters sem paleta fixa
- Singletons (nós sem aresta semântica com peso suficiente) exibidos em cinza
- Layout inicial em posições circulares por cluster, forças
forceX/forceYmantendo agrupamento - Legenda interativa: checkbox por comunidade + checkbox "Todas"; visibilidade via D3
displaysem reiniciar a simulação física - Barra de status exibe
N nós · M arestas · K comunidades
O PKD não gerencia TLS — encerre no proxy reverso. Exemplo com Caddy:
pkd.exemplo.lan {
reverse_proxy localhost:8080
}Ao usar proxy reverso, adicione PKD_TRUST_PROXY_HEADERS=1 e configure PKD_BASE_URL com a URL pública para que os links de compartilhamento gerem URLs corretas.
Requer Go 1.25+ e Node.js 22+.
git clone https://github.com/edalcin/pkd.git
cd pkd
# 1. Build do frontend Svelte
cd frontend && npm install && npm run build && cd ..
# 2. Rodar localmente
PKD_PASSWORD=devpassword \
PKD_DB_PATH=/tmp/pkd.sqlite \
PKD_ATTACHMENTS_PATH=/tmp/pkd-att \
go run ./cmd/pkdnpm run build:editor (dentro de frontend/) gera uma build separada de editor.html em frontend/dist-editor/ (base: './', sem o service worker/manifest da PWA), com as mesmas extensões TipTap da PWA (src/lib/editor/extensions.js). O pkdMobile vendoriza esse bundle via scripts/update-editor-bundle; ver ADR pkdMobile 0002.
Todo push para o branch main dispara o workflow do GitHub Actions que:
- Executa
go test ./... - Builda a imagem Docker (linux/amd64)
- Publica
ghcr.io/edalcin/pkd:edge(UNRAID/dev — atualiza automaticamente) eghcr.io/edalcin/pkd:sha-<hash>(imutável)
Promoção para produção — execute manualmente o workflow promote-to-prod (via workflow_dispatch no GitHub Actions) ou crie uma tag semântica:
git tag -a v1.2.3 -m "descrição" && git push origin v1.2.3O workflow re-etiqueta a imagem :edge como :stable e :v1.2.3 — sem rebuild.
Limpeza automática — workflow semanal remove tags sha-* com mais de 30 dias do GHCR.
Nenhuma credencial é armazenada no repositório — o workflow usa o GITHUB_TOKEN automático do GitHub Actions.
graph TD
User(["👤 Usuário"]) -->|"HTTPS / Browser"| App
Mobile(["📱 Mobile OS"]) -->|"PWA Share Target"| App
Hermes(["🤖 Hermes"]) -->|"Bearer token\n/api/documents, /api/memories, /api/notes"| App
subgraph Container ["🐳 Docker Container"]
App["⚙️ Go HTTP Server\n(chi router · handlers · middleware)"]
SPA["🌐 Svelte 5 SPA\n(TipTap v3 · D3.js · embutido no binário)"]
end
App --- SPA
App -->|"SQL / modernc-sqlite"| DB[("🗄️ SQLite\ndocuments · document_links\ndocument_urls · attachments\ntags · shares · FTS5")]
App -->|"storage.Backend"| Vol[("📂 Local Volume\n(padrão)")]
App -.->|"storage.Backend (opcional)"| S3[("☁️ Amazon S3\n(PKD_S3_BUCKET)")]
| Tabela | Descrição |
|---|---|
documents |
Documentos com hierarquia via parent_id, soft-delete, versionamento otimista |
documents (Memória) |
Memória = documento com memory_id não nulo. Colunas memory_id (ID público, UNIQUE), memory_hour, memory_minute, memory_period, memory_key (idempotência, UNIQUE); dia em assoc_year/month/day |
documents (Nota) |
Nota = documento com is_note = 1. Coluna note_key (idempotência, UNIQUE); sem pai nem filhos, fora da árvore normal |
document_links |
Arestas simétricas entre documentos; o "outro doc" é derivado via CASE WHEN independente de qual lado é source_id. Flag manual distingue links do painel de notas relacionadas |
document_urls |
URLs externas com título opcional associadas a documentos |
attachments |
Metadados de arquivos; binários em volume externo com path sharding |
tags + document_tags |
Tags normalizadas com campo color; join N:N com documentos |
documents_fts |
Tabela virtual FTS5 (contentless) para busca full-text |
share_links |
Links públicos com hash de token e campo revoked_at |
document_embeddings |
Cache de vetores semânticos: embedding BLOB float32 LE (3072 dims em ambos os modelos suportados), content_hash SHA-256 sobre nome do modelo + texto formatado, para invalidação. Gerenciado pelo worker proativo; limpeza automática de docs trashed/archived a cada sweep |
settings |
Configurações persistidas em chave-valor (chat.model, retenção de versões, backend de armazenamento). O modelo de embedding não vive aqui: é constante compilada |
document_versions |
Snapshots de conteúdo com dedup SHA-256 e retenção configurável |
| Documento | Idioma | Descrição |
|---|---|---|
| UNRAID.md | 🇧🇷 PT-BR | Instalação no UNRAID via GUI |
| docs/c4/context.md | 🇺🇸 EN | C4 Level 1 — Contexto do sistema |
| docs/c4/container.md | 🇺🇸 EN | C4 Level 2 — Containers |
| docs/c4/component.md | 🇺🇸 EN | C4 Level 3 — Componentes Go |
| docs/c4/code.md | 🇺🇸 EN | C4 Level 4 — Structs e fluxos de código |
| docs/security.md | 🇺🇸 EN | Referência de segurança |
| docs/operations.md | 🇧🇷 PT-BR | Guia de operações, atualização, promoção dev → prod e backup |
| docs/proximosPassos.md | 🇧🇷 PT-BR | Estado atual, próximos passos e decisões de cada feature |
| docs/adr/glossary.md | 🇧🇷 PT-BR | Glossário (Memória, Nota, app Notas, busca, embeddings…) |
| docs/adr/ | 🇧🇷 PT-BR | Decisões de arquitetura (ADRs) |
Nota
- Terceiro tipo de conteúdo: Nota, documento sem pai nem filhos, fora da árvore normal, com bloco próprio "Notas" na barra lateral (favoritas primeiro, depois mais recentes; filtros de tag/favorito)
- API
POST /api/notes(idempotente poridempotency_key,created_at/updated_athonrados só via Bearer),GET/PATCH /api/notes/{id},GET /api/notes; BearerPKD_IMPORT_TOKENou sessão - Conversão só de ida:
POST /api/notes/{id}/convertpara Documento (posição na árvore) ou Memória (Data da Memória, IDMEM-…); arrastar a Nota para a árvore ou para o bloco MC dispara a conversão na interface - "+ Nova Nota" pede só o título e abre o editor completo; ícone padrão
bx-sticky-note - Migração aditiva (
is_note,note_keyemdocuments, sem backfill) - As 74 notas ativas do app Notas foram migradas (homologação e produção); o app Notas foi desligado
Correções
/healthzprendia a única conexão do banco (QueryRow().Err()semScan); depois da primeira checagem o servidor inteiro parava de responder- O bloco Notas perdia o filtro de tag/favoritos ao recarregar depois de renomear, arquivar ou mandar para a lixeira
Memória Cronológica (MC)
- Nova árvore Ano → Mês → Dia no menu lateral para Memórias: documentos posicionados pela Data da Memória (só o ano é obrigatório; hora exata ou Período opcionais)
- ID público estável
MEM-AAAA[-MM[-DD[T<HH>[MM]]]]-SUFIXO6; corrigir a data não muda o ID (ADR-007) - API
POST /api/memories(idempotente poridempotency_key),GET/PATCH /api/memories/{MEM-id}; BearerPKD_IMPORT_TOKENou sessão - "+ Nova Memória" na interface; Data da Memória e ID copiável no editor
- Migração aditiva (colunas e índices novos em
documents, sem backfill) - Prompt para a Skill do Hermes em
docs/promptHermes.md
Busca híbrida por fusão RRF — fim do seletor de modo
GET /api/tree?q=…passa a rodar sempre os dois recuperadores (léxico via FTS5+LIKE, semântico via embeddings Gemini) e funde os resultados por Reciprocal Rank Fusion (k=60); semGEMINI_API_KEYa perna semântica fica vazia e a fusão degrada exatamente para a ordem léxica — sem503, sem seletor de modo- Removido o
<select>"Léxica | Semântica" da topbar e os storessearchMode/semanticAvailable— busca sempre única e automática - Piso semântico reduzido de 0.45 para 0.30 e top-k de 50 para 100 (o RRF já pondera por posição de rank, não por corte rígido de similaridade)
- Ver ADR-002 para a decisão completa
Sessão persistente e restauração do último documento
- Timeout de inatividade da sessão aumentado de 60 min para 30 dias (43200 min) — usuário permanece logado entre sessões do browser sem precisar re-autenticar. Variável
PKD_SESSION_IDLE_MINUTESainda permite sobrescrever o valor em produção - Último documento visitado salvo em
localStoragea cada navegação; restaurado automaticamente ao abrir a ferramenta quando a URL não possui hash específico - Home (
/) não é salvo como "último estado" — navegação intencional à raiz nunca polui o restore
Títulos de documentos únicos
- Títulos funcionam como identificadores nas relações entre documentos — duplicatas passam a ser bloqueadas no nível do banco (índice
UNIQUEparcial) e da API (HTTP 409) - Ao tentar salvar um título já existente, diálogo informa o conflito e oferece botão para navegar ao documento existente
- Documentos existentes com títulos duplicados detectados e corrigidos na migração
Navegação por histórico de documentos
- Botões ← e → na barra superior (desktop) navegam pelo histórico de documentos visitados na sessão atual
- Atalhos de teclado:
Alt+←(voltar) eAlt+→(avançar) — funcionam em qualquer view - Histórico em memória: preservado durante a sessão, não persiste entre recarregamentos de página
Filtro na Graph View
- Campo de filtro da barra superior passa a atuar também na visualização em grafo
- Nós que não correspondem ao texto filtrado são ocultados; arestas de vizinhos diretos dos nós visíveis são mantidas para preservar contexto de conexões
Links inline no editor ([Título])
- Digite
[no editor para ativar autocomplete de documentos internos com busca FTS5 em tempo real - Selecionar um resultado substitui o texto digitado por um chip de link clicável (
docLinkinline node) - Clicar no chip navega para o documento vinculado (
#/doc/{id}) - Links bidirecionais: sincronizados automaticamente na tabela
document_linksa cada save (parser HTML existente viadata-doc-link) - Export Markdown: chips renderizam como
[Título do Documento]
Importar imagens externas como attachments
- Ao colar conteúdo de outros sites ("corta-e-cola"), imagens hospedadas externamente (
<img src="https://...">) podem ser convertidas em attachments internos - No editor: botão
🌐⬇aparece na barra de ferramentas quando o documento contém imagens externas. Desabilitado durante a operação (evita duplo-clique); desaparece imediatamente após importação completa; exibe resultado inline ("N importada(s) / N falhou") por 5 s - Na Administração → Arquivos: nova seção "Imagens externas" lista todos os documentos afetados com contagem de imagens por documento. Botão "Importar" por linha; se o documento estiver aberto no editor, o conteúdo é sincronizado automaticamente sem recarregar a página
- Endpoints:
POST /api/documents/{id}/attachments/from-url(editor),GET /api/admin/external-images,POST /api/admin/documents/{id}/import-external-images - Segurança: fetch server-side protegido contra SSRF com bloqueio de IPs privados, loopback e metadata (169.254.x) via
net.Dialer.Controlpós-DNS
Gerenciador de arquivos órfãos em Administração → Arquivos
- Botão "🔍 Verificar Órfãos" ao lado de "↻ Atualizar" na aba Arquivos carrega a lista de arquivos no disco sem registro no banco (órfãos de uploads com falha ou exclusões incompletas)
- Cada arquivo órfão exibe nome, tamanho, botão "⬇ Baixar" (download individual) e "🗑 Eliminar" (exclusão com confirmação)
- Endpoints:
GET /api/admin/attachments/orphansretorna lista completa com tamanhos;GET /api/admin/attachments/orphans/download?key=…faz download;DELETE /api/admin/attachments/orphans/item?key=…elimina - Segurança: download e delete verificam que a chave é de fato um órfão antes de agir
- Aba Limpeza: passa a fazer apenas
VACUUM(gerenciamento de órfãos movido para Arquivos)
Histórico de versões de documentos
- Snapshot automático do conteúdo (título + corpo + ícone) a cada save, com dedup SHA-256 — saves idênticos não geram versão
- Retenção padrão: 50 versões por documento; configurável em Administração → Configurações → Versões
- Botão
⏱na barra de ações do documento abre dialog de histórico: lista versões com data/hora, preview do conteúdo, restauração com confirmação - Excluir versão individual: ícone de lixeira por entrada (aparece no hover), com confirmação
- Endpoints:
GET /api/documents/:id/versions,GET /api/documents/:id/versions/:vid,POST /api/documents/:id/versions/:vid/restore,DELETE /api/documents/:id/versions/:vid - Nova tabela:
document_versions(CASCADE ao excluir o documento, indexada por doc+id DESC) - Admin → Configurações: campo numérico para
versions.max_per_doc(validado 1–10000)
Reconciliação S3 ↔ DB (fix em Administração → Storage)
- Novo botão "Reconciliar S3 ↔ DB": varre o bucket S3, identifica arquivos com
storage_locationincorreto no banco (comum após restore de backup do banco sem os arquivos S3) e corrige sem mover arquivos - Migração agora expõe
total_foundalém decopied— quandototal_found=0a UI sugere usar o reconcile antes de migrar - Endpoint:
POST /api/admin/storage/reconcile
Mobile master-detail
- Interface mobile (≤ 640 px) redesenhada: tela-lista com topbar de 2 linhas (ícones de ação na primeira linha, campo de busca largo na segunda) e árvore de documentos ocupando a tela inteira
- Documentos, grafo, calendário e admin abrem em tela-detalhe com botão ← na topbar para voltar à lista
- Drawer hambúrguer removido (padrão master-detail torna-o redundante)
- Desktop (≥ 641 px) inalterado
Favoritar da barra de ações
- Botão ⭐ adicionado à barra de ações do documento (entre "Copiar link" e "Trancar")
- Alterna favorito diretamente no documento aberto; sincroniza automaticamente com a árvore lateral
Backup e restauração de anexos com backend S3 (feature 005)
- Backup assíncrono S3 — novo endpoint
POST /api/admin/storage/backup-startgera ZIP no próprio bucket via streaming multipart (io.Pipe+manager.Uploader), sem usar disco da instância. URL pré-assinada para download direto (TTL 15 min, re-gerável). Job tracking com polling de progresso. - Restauração assíncrona cross-backend —
POST /api/admin/storage/restore-startaceita o ZIP gerado pela aplicação. ZIP de produção (S3) pode ser restaurado em desenvolvimento (local) e vice-versa. Backend S3 usa Range GETs (io.ReaderAt) para evitar disco; backend local usa temp file. - Manifesto interno com SHA256 como chave lógica — dedup natural no ZIP, fan-out automático no restore quando múltiplas linhas de
attachmentscompartilham conteúdo. - Integridade verificada por entrada — hash mismatch reportado sem abortar restauração; entradas órfãs ignoradas e listadas.
- 3 modos de conflito no restore:
overwrite(padrão),keep,abort. - Sweep automático no startup remove ZIPs temporários órfãos com idade > 24h em
_backup-tmp/. - Single-in-flight por backend com
BackupJobManager(mutex + LRU 50,ErrJobInFlight→ 409). - Backfill SHA256 automático para linhas históricas sem hash, executado durante o backup via
io.TeeReader. - IAM: novas ações necessárias
s3:DeleteObjects(sweep) es3:AbortMultipartUpload.
Tests: 20 unit tests novos (manifest, jobs, writer, reader, fan-out, on_conflict modes, hash mismatch, in-place, per-entry failure isolation). Documentação completa em docs/operations.md.
Editor
- Breadcrumb de hierarquia abaixo do título: ancestrais clicáveis via CTE recursiva no backend; navegue para qualquer nível da hierarquia com um clique
Armazenamento S3
- Backend S3 (AWS SDK v2, SSE-S3, Intelligent-Tiering) com interface
storage.Backende injeção de dependência - Migração Local → S3 com verificação SHA256 por arquivo; processo idempotente e seguro para re-execução
- Painel de administração: teste de conexão, migração, ativação de backend e limpeza da origem
- Novas variáveis:
PKD_S3_BUCKET,PKD_S3_REGION,PKD_S3_PREFIX,PKD_S3_ACCESS_KEY_ID,PKD_S3_SECRET_ACCESS_KEY
Backup de anexos
- Download ZIP de todos os anexos do backend local (
GET /api/admin/storage/backup-attachments) - Restauro via ZIP (
POST /api/admin/storage/restore-attachments) — migração entre instâncias sem rsync/SCP
Graph View
- Hierarquia pai/filho visualizada como arestas tracejadas com cor de destaque (
--accent) - Toggles independentes: Hierarquia, Links entre docs, Relações com tags — oculta/exibe nós e arestas sem reiniciar a simulação de força
- Filtro de nós por tipo de relação: ao ativar um toggle, apenas documentos com aquele tipo de relação aparecem (a menos que "Todos os docs" esteja ativo)
- Cores distintas por tipo de aresta: azul (links), rosa (tags), accent tracejado (hierarquia)
Arquivamento
- Arquivar/desarquivar via ícone na barra do editor (sempre visível, como o cadeado)
- Arquivar pai cascateia recursivamente para todos os filhos; auto-tranca ao arquivar
- Desarquivar pai cascateia para todos os filhos recursivamente
- Documento arquivado pode ser editado normalmente se destrancado individualmente
- Auto-save desativado para documentos trancados
Links entre documentos
- Relações passam a ser simétricas e sem direção: ambos os documentos exibem a mesma lista única de "Relacionados", sem distinção entre "Notas relacionadas" e "Referenciado por"
Graph View
edge_typeseparado emlinketagno backend, preparando suporte a toggles de visibilidade
Admin
- Tabela de gerenciamento de links externos na aba Links (responsiva ao espaço disponível)
Integração com Notas
- Endpoint
POST /api/importautenticado via Bearer token (PKD_IMPORT_TOKEN); cria documento com conteúdo HTML, tags da nota de origem e tagnotas; desativado quando a variável não está configurada - CSRF middleware atualizado para ignorar requisições com
Authorization: Bearer— chamadas server-to-server não são vulneráveis a CSRF e não precisam de cookie
Árvore lateral
- Filtro unificado: campo de busca integrado à árvore, substituindo os dois controles anteriores (busca e filtro separados)
- Ícones Boxicons para cada documento: seletor dinâmico com busca por nome; ícone de pasta atribuído automaticamente a documentos com filhos
- Estado expandido/colapsado da árvore persistido no
localStorageentre sessões
Editor
- Modo foco abre o editor em uma janela separada (popup) sem a interface principal; conteúdo sincronizado de volta ao fechar
CI/CD
- Workflow GitHub Actions adicionado: build e push automático de
ghcr.io/edalcin/pkd:latesta cada push nomain
Primeira versão estável e completa do PKD.
Editor
- Redimensionamento de imagens inline por alça de arrastar
- Exportação de documentos como Markdown (
.md) com download direto no browser - Correção de race condition que causava documento vazio ao navegar rapidamente entre documentos
- Prevenção de conflito de versão espúrio causado por saves concorrentes no auto-save
Organização
- Reordenação manual de documentos por drag-and-drop na árvore lateral
- Ordenação A-Z e cronológica na árvore
- Filtro por documentos favoritos (⭐)
- Marcar/desmarcar documentos como favoritos
Compartilhamento
- Redesign da view pública de documentos compartilhados
PKD_BASE_URLpara geração correta de links atrás de proxy reverso
Segurança
- Sanitização de URLs relativas no HTML (correção de regressão)
MIT © 2026 Eduardo Dalcin