sagadeck 1.11.4 → 1.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sagadeck.js +2 -0
- package/docs/ANDAMENTO.md +45 -0
- package/docs/REFERENCIA.md +1 -1
- package/package.json +1 -1
- package/src/adaptive-layouts.js +2 -2
- package/src/ai/variety.js +10 -4
- package/src/elements.js +3 -3
- package/src/figures/charts.js +4 -0
- package/src/library.js +62 -8
- package/src/markup.js +6 -0
- package/src/runtime/base.css +2 -0
- package/src/studio/library-routes.js +2 -2
- package/src/studio/public/slide-form.js +1 -1
- package/src/studio/shutdown.js +31 -0
package/bin/sagadeck.js
CHANGED
|
@@ -414,6 +414,8 @@ async function main() {
|
|
|
414
414
|
console.error(`A porta ${port} está ocupada por outro programa. Use --port=OUTRA.`);
|
|
415
415
|
process.exit(1);
|
|
416
416
|
});
|
|
417
|
+
// SIGTERM do systemd ao reiniciar: sai em segundos, sem esperar o SIGKILL (src/studio/shutdown.js)
|
|
418
|
+
(await import("../src/studio/shutdown.js")).installShutdown(server);
|
|
417
419
|
server.listen(port, host, async () => {
|
|
418
420
|
const { studioBanner } = await import("../src/studio/banner.js");
|
|
419
421
|
for (const line of studioBanner({ version: VERSION, url: `http://${shownHost}:${port}/`, library, ai: (await import("../src/ai/llm.js")).llmConfig(), multiuser, usersDir: path.join(library, "usuarios"), isLibrary: !deckFile })) console.log(line);
|
package/docs/ANDAMENTO.md
CHANGED
|
@@ -8,6 +8,51 @@ começar. Trabalho em curso vai para o GitHub (commit + push) a cada etapa, nunc
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## 1.11.6: rodada de uso real — 23/10/2026 (branch `claude/parada-e-fotos`)
|
|
12
|
+
|
|
13
|
+
Quatro pedidos reais no sagadeck-dev (conta de teste `verificacao`, tópico "Rodada de uso real"), pelo caminho da
|
|
14
|
+
biblioteca (Nova › Com IA, `POST /api/library/decks/ai`), com o modelo do servidor (deepseek-v4.1-flash): aula de
|
|
15
|
+
vigas de concreto (20 min, com exemplo numérico), pitch de automação da conciliação (5 min), guia de Git (consulta) e
|
|
16
|
+
palestra sobre burnout (15 min). Os quatro saíram em 4 a 9 min cada, variados (11, 3, 12 e 9 layouts diferentes). A
|
|
17
|
+
revisão automática deixou avisos; cada um foi conferido no YAML e na foto do slide:
|
|
18
|
+
- **Bug: `stats` ignorava `prefix`/`suffix`** (o pitch mostrava "4" por "4%"). Corrigido em `elements.js`, com
|
|
19
|
+
REFERENCIA e formulário do Studio. Teste no `engine.test.js`.
|
|
20
|
+
- **Bug: código quebrava a linha no hífen** (`--no-` / `edit` no dossier do guia). `noHyphenBreak` (markup.js) põe
|
|
21
|
+
cada hífen num `<span class="hy">`, e o CSS um word joiner por `::after` (não vai na cópia); vale no `code()` e no
|
|
22
|
+
código do dossier. Testes: `engine.test.js` e `test/code-hyphen.test.js` (no Chrome, com o slide do guia; falha sem a
|
|
23
|
+
correção).
|
|
24
|
+
- **Bug: barra com `color: hi` usava o marca-texto claro** (palestra, tema editorial: destaque mais fraco que o fundo).
|
|
25
|
+
`charts.js` usa `--hi-ink`, como o texto. Teste no `engine.test.js`.
|
|
26
|
+
- **Bug: variedade cobrava impacto/respiro/tom de material de consulta**, contra a REFERENCIA. `variety.js` só cobra
|
|
27
|
+
isso fora de `purpose: consulta`. Teste no `engine.test.js`.
|
|
28
|
+
- **Alarmes falsos da revisão (não mexidos)**: "barras de 48% e 43% quase iguais" (estão proporcionais) e "nota em
|
|
29
|
+
cinza escuro no preto" (é cinza claro, legível). Ficam como observação: a revisão por imagem ainda erra em proporção
|
|
30
|
+
e contraste; se voltar a aparecer, olhar a resolução das fotos que vão para ela.
|
|
31
|
+
- Conteúdo, para a IA corrigir (Corrigir tudo com IA): coeficiente 0,68 sem a origem (0,85 × 0,8) e diagrama pequeno
|
|
32
|
+
na aula; quarto risco só nas notas do pitch.
|
|
33
|
+
- **Testes com o modelo de verdade (1.11.6 é múltipla de 3)**: 25 de 27 de primeira em 25 min, nenhum quebrado.
|
|
34
|
+
Instáveis (passaram na repetição): "refinamento: conversa e depois 'pode fazer'" (o modelo trocou o slide por outro
|
|
35
|
+
texto em vez de aplicar o combinado; já tinha aparecido antes) e "gerar deck: sai variado" (estourou os 10 min do
|
|
36
|
+
teste esperando o provedor; não é a regra da variedade, que só mudou para `consulta`).
|
|
37
|
+
|
|
38
|
+
## 1.11.5: parada do servidor, biblioteca no OneDrive — 23/10/2026 (branch `claude/parada-e-fotos`)
|
|
39
|
+
|
|
40
|
+
- **Parada**: o Playwright, ao abrir o Chrome, instala um tratador de SIGTERM que só fecha o navegador; com ele, o
|
|
41
|
+
Node não sai mais sozinho. No Oracle, cada reinício ficava 90 s fora do ar até o SIGKILL (o log de toda semana:
|
|
42
|
+
"Failed with result 'timeout'"). `src/studio/shutdown.js`: SIGTERM/SIGHUP param de aceitar conexões, dão 8 s para
|
|
43
|
+
as que estão em uso, cortam o resto, fecham o Chrome das miniaturas e saem. Teste: `test/studio-shutdown.test.js`
|
|
44
|
+
(o prazo; e o Studio de verdade, depois de abrir o Chrome, sai em segundos com SIGTERM; só no Linux, porque no
|
|
45
|
+
Windows o SIGTERM mata na hora). Conferido no Oracle: sem a correção, não saiu em 15 s; com ela, saiu em 2 s.
|
|
46
|
+
- **Biblioteca no OneDrive**: a listagem lia e interpretava o YAML de todas as apresentações a cada abertura (com
|
|
47
|
+
"arquivos só na nuvem", isso baixa cada uma). Agora guarda título, slides, tema e avisos pela data, tamanho e inode
|
|
48
|
+
do `.yaml` e do relatório de avisos, em memória e em `lista.json` no cache. Teste: `library.test.js` (nenhum
|
|
49
|
+
`.yaml` relido sem mudança, nem depois de reiniciar; só o que mudou; apagado some; cache estragado não quebra).
|
|
50
|
+
- **Cache fora da nuvem**: `libraryCacheDir` põe capas, vitrine e lista em `%LOCALAPPDATA%\sagadeck\cache\<biblioteca>`
|
|
51
|
+
quando a biblioteca está dentro do OneDrive (variáveis `OneDrive`, `OneDriveCommercial`, `OneDriveConsumer`); o
|
|
52
|
+
`.cache` antigo de dentro dela sai (só `capas`, `vitrine` e `lista.json`). Fora do OneDrive e no servidor, nada muda.
|
|
53
|
+
- **Fotos da transformação, medido e deixado como está**: cada foto custa ~0,4 s e o lote tem no máximo 5 slides;
|
|
54
|
+
abrir o deck uma vez por lote pouparia ~10 s numa transformação de 40 slides, que leva minutos de IA.
|
|
55
|
+
|
|
11
56
|
## 1.11.4: CI mais rápida, implantação por roteiro, diário em dia — 23/10/2026 (branch `claude/melhorias-infra`)
|
|
12
57
|
|
|
13
58
|
- **Antivírus do Windows segurava a abertura do deck** (exportação, fotos, PDF, PPTX e conferência): o Node lia o
|
package/docs/REFERENCIA.md
CHANGED
|
@@ -93,7 +93,7 @@ Tabela em markdown no meio de um texto (`problem`, `body`, `text`, passo do `sol
|
|
|
93
93
|
| `hub` | `kicker, title, question, options: [{icon, title, text, meta, goto}], cols, build` | mapa de caminhos: cada opção leva (`goto`) à parte da apresentação daquele caminho; ver [Navegação por caminhos](#navegação-por-caminhos-hub-goto-back-next) |
|
|
94
94
|
| `onepage` | `kicker, title, subtitle, journey: [texto ou {icon, title, text, goto}], journeyTitle, problem: {title, text, numbers: [{value, label}], items}, solution: {…}, dashboard: {numbers: [{value, label, trend, title}], figures: [gráfico ou ufmap, com title]}` | tudo numa página (jornada, problema, solução, painel); ver [One-page](#one-page-onepage) |
|
|
95
95
|
| `status` | `kicker, title, health: ok\|risco\|atrasado, healthLabel, progress (0–100), highlight, done, doing, blocked, risks, upcoming: [texto ou {text, owner, due}], shots: [{image, caption}]` | status semanal; ver [Status semanal](#status-semanal-status) |
|
|
96
|
-
| `stats` | `title, stats: [{value, label, text, icon, trend, trendUp, color}], cols, build` | indicadores (KPIs) em cartões, com tendência (`trend: "+12%"`) |
|
|
96
|
+
| `stats` | `title, stats: [{value, prefix, suffix, label, text, icon, trend, trendUp, color}], cols, build` | indicadores (KPIs) em cartões, com tendência (`trend: "+12%"`) |
|
|
97
97
|
| `timeline` | `title, events: [{when, title, text, tag, more}], highlight, after, build` | linha do tempo; `more` põe um botão + no marco que abre um balão (ver abaixo) |
|
|
98
98
|
| `chart` | `title, chart: {…}, side (texto ou elemento), chartHeight` | gráfico + comentário |
|
|
99
99
|
| `table` | `title, head, rows, style, color, highlight, total, side, caption, source` | tabela de verdade nas cores do tema |
|
package/package.json
CHANGED
package/src/adaptive-layouts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {md,esc} from './markup.js';
|
|
1
|
+
import {md,esc,noHyphenBreak} from './markup.js';
|
|
2
2
|
import {iconSVG} from './figures/icons.js';
|
|
3
3
|
export function adaptiveGrid(items=[],variant='mosaic') {
|
|
4
4
|
const n=items.length;
|
|
@@ -14,5 +14,5 @@ export function adaptiveHTML(s,variant) {
|
|
|
14
14
|
<div class="adaptive-index f-label">${item.icon?iconSVG(item.icon,{size:38}):String(i+1).padStart(2,'0')}</div>
|
|
15
15
|
${item.value!=null?`<div class="adaptive-value t f-display">${md(String(item.value))}</div>`:''}
|
|
16
16
|
<h3 class="t f-heading">${md(item.title||'')}</h3>${item.text?`<div class="adaptive-text t f-body">${md(item.text)}</div>`:''}
|
|
17
|
-
${item.code?`<pre class="adaptive-code f-mono">${esc(item.code)}</pre>`:''}${item.foot?`<div class="adaptive-foot t f-label">${md(item.foot)}</div>`:''}</article>`).join('')}</div>`;
|
|
17
|
+
${item.code?`<pre class="adaptive-code f-mono">${noHyphenBreak(esc(item.code))}</pre>`:''}${item.foot?`<div class="adaptive-foot t f-label">${md(item.foot)}</div>`:''}</article>`).join('')}</div>`;
|
|
18
18
|
}
|
package/src/ai/variety.js
CHANGED
|
@@ -25,10 +25,16 @@ export function varietyReport(spec) {
|
|
|
25
25
|
if (n >= 6) {
|
|
26
26
|
const want = Math.min(8, Math.ceil(n * 0.5));
|
|
27
27
|
if (distinct < want) problems.push(`só ${distinct} layouts diferentes em ${n} slides de conteúdo (bom: ${want} ou mais)`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
28
|
+
// material de consulta (purpose: consulta) é para estudar depois: sem "número de impacto" nem slide só de título
|
|
29
|
+
// (REFERENCIA), e lista é o normal. Cobrar respiro e impacto ali era aviso falso, e a revisão empurrava a IA a pôr
|
|
30
|
+
// slides que o propósito pede para não ter (rodada de uso real, guia de Git, 23/10/2026)
|
|
31
|
+
if (spec?.purpose !== "consulta") {
|
|
32
|
+
const listy = body.filter((s) => LISTY.has(s.layout)).length;
|
|
33
|
+
if (listy / n > 0.45) problems.push(`${listy} de ${n} slides são listas/cartões — falta respiro (manchete, número grande, imagem, pergunta)`);
|
|
34
|
+
if (!body.some((s) => IMPACT.has(s.layout))) problems.push("nenhum slide de impacto (manchete, número grande, frase, página inteira, pergunta)");
|
|
35
|
+
// consulta pode ficar toda clara (imprime e lê bem); a regra do tom é de palco
|
|
36
|
+
if (new Set(slides.map((s) => s.tone)).size === 1) problems.push(`todos os slides no mesmo tom (${slides[0].tone}) — alterne com escuro/destaque nos momentos-chave`);
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
const counts = {};
|
|
34
40
|
body.forEach((s) => (counts[s.layout] = (counts[s.layout] || 0) + 1));
|
package/src/elements.js
CHANGED
|
@@ -9,7 +9,7 @@ import katex from "katex";
|
|
|
9
9
|
import { formatNumber, decimalSeparator } from "./locale.js";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { md, esc, plain } from "./markup.js";
|
|
12
|
+
import { md, esc, plain, noHyphenBreak } from "./markup.js";
|
|
13
13
|
import { iconSVG } from "./figures/icons.js";
|
|
14
14
|
import { picto } from "./figures/pictos.js";
|
|
15
15
|
import { diagram } from "./figures/diagrams.js";
|
|
@@ -551,7 +551,7 @@ export function stats(e, ctx) {
|
|
|
551
551
|
${iconHtml}
|
|
552
552
|
${trendHtml}
|
|
553
553
|
</div>
|
|
554
|
-
<div class="st-val t f-display" style="${valColor}">${md(
|
|
554
|
+
<div class="st-val t f-display" style="${valColor}">${md(`${st.prefix ?? ""}${st.value ?? st.stat ?? ""}${st.suffix ?? ""}`)}</div>
|
|
555
555
|
${st.label ? `<div class="st-lab t f-heading">${md(st.label)}</div>` : ""}
|
|
556
556
|
${st.text ? `<div class="st-sub t f-body">${md(st.text)}</div>` : ""}
|
|
557
557
|
</div>`;
|
|
@@ -619,7 +619,7 @@ export function code(e) {
|
|
|
619
619
|
const language = resolveCodeLanguage(e.language, e.filename);
|
|
620
620
|
const lines = language ? highlightCode(source, language) : source.split("\n").map(esc);
|
|
621
621
|
const hl = new Set([].concat(e.highlight || []));
|
|
622
|
-
const body = lines.map((l, i) => `<div class="cl${hl.has(i + 1) ? " hl" : ""}"><span class="cn">${i + 1}</span><span class="cc">${l || " "}</span></div>`).join("");
|
|
622
|
+
const body = lines.map((l, i) => `<div class="cl${hl.has(i + 1) ? " hl" : ""}"><span class="cn">${i + 1}</span><span class="cc">${noHyphenBreak(l) || " "}</span></div>`).join("");
|
|
623
623
|
return `<div${attrs(e, "code f-mono", `font-size:${e.size || 34}px;`)}${language ? ` data-language="${esc(language)}"` : ""}>${body}</div>`;
|
|
624
624
|
}
|
|
625
625
|
|
package/src/figures/charts.js
CHANGED
|
@@ -21,6 +21,10 @@ const cvar = (c, d) => {
|
|
|
21
21
|
if (/^#?[0-9a-f]{6}$/i.test(c)) return `#${String(c).replace("#", "")}`;
|
|
22
22
|
// paleta do tema é namespaced (--c-c1…): nome pelado ("c3") viraria var indefinida = preto
|
|
23
23
|
if (/^c[1-9]\d*$/.test(c) || /^(accent|alert|ink|paper)$/.test(c)) return `var(--c-${c})`;
|
|
24
|
+
// "hi" é a cor do marca-texto, clara nos temas de papel: a barra em destaque saía rosa-claro, quase sumindo, e as
|
|
25
|
+
// de fundo (muted) mais fortes que ela. Como no texto (elements.js: textColorVal), a marca usa a versão com
|
|
26
|
+
// contraste (--hi-ink: o hi se ele aparece no fundo, senão a ênfase). Rodada de uso real, 23/10/2026.
|
|
27
|
+
if (c === "hi") return "var(--hi-ink,var(--hi))";
|
|
24
28
|
return `var(--${c})`;
|
|
25
29
|
};
|
|
26
30
|
const fmt = (v, o) => {
|
package/src/library.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
// rascunho.yaml .yaml solto na raiz = apresentação sem tópico
|
|
11
11
|
// Na raiz, pasta é SEMPRE tópico. "Nova sem tópico" vai para "Sem tópico"; importar sem tópico, para "Importados".
|
|
12
12
|
// .lixeira/ excluídas (1 dia), com .origem.json
|
|
13
|
-
// .cache/ capas geradas
|
|
13
|
+
// .cache/ capas geradas e lista (no OneDrive, fica fora: libraryCacheDir)
|
|
14
14
|
//
|
|
15
15
|
// O id de uma apresentação é o caminho do .yaml dela, relativo à biblioteca, com "/" (ex.: "Palestras/X/X.yaml").
|
|
16
|
+
import crypto from "node:crypto";
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import { renameRetry, copyRetry, removeRetry } from "./fs-retry.js";
|
|
18
19
|
import os from "node:os";
|
|
@@ -69,6 +70,20 @@ export function defaultLibraryRoot(env = process.env, { platform = process.platf
|
|
|
69
70
|
return path.resolve(target);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// Cache do que se refaz sozinho (capas, vitrine, lista da biblioteca). Com a biblioteca no OneDrive, fica na pasta
|
|
74
|
+
// local do Windows (%LOCALAPPDATA%\sagadeck\cache\<biblioteca>), fora da nuvem: não ocupa a cota, não sincroniza a
|
|
75
|
+
// cada capa nova e uma máquina não pega o cache da outra. Fora do OneDrive (e no servidor): <biblioteca>/.cache.
|
|
76
|
+
export function libraryCacheDir(root, { env = process.env, platform = process.platform } = {}) {
|
|
77
|
+
root = path.resolve(root);
|
|
78
|
+
const low = root.toLowerCase();
|
|
79
|
+
const clouds = platform === "win32" ? [env.OneDrive, env.OneDriveCommercial, env.OneDriveConsumer].filter(Boolean).map((p) => path.resolve(p).toLowerCase()) : [];
|
|
80
|
+
if (env.LOCALAPPDATA && clouds.some((c) => low === c || low.startsWith(c + path.sep))) {
|
|
81
|
+
const tag = `${safeName(path.basename(root), "biblioteca")}-${crypto.createHash("sha1").update(low).digest("hex").slice(0, 8)}`;
|
|
82
|
+
return path.join(env.LOCALAPPDATA, "sagadeck", "cache", tag);
|
|
83
|
+
}
|
|
84
|
+
return path.join(root, ".cache");
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
const isLink = (p) => { try { return fs.lstatSync(p).isSymbolicLink(); } catch { return false; } };
|
|
73
88
|
const hasContent = (p) => { try { return fs.readdirSync(p).length > 0; } catch { return false; } };
|
|
74
89
|
|
|
@@ -150,6 +165,13 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
150
165
|
const keepDays = () => trashDays ?? loadPreferences().biblioteca?.lixeiraDias ?? TRASH_DAYS;
|
|
151
166
|
root = path.resolve(root);
|
|
152
167
|
fs.mkdirSync(root, { recursive: true });
|
|
168
|
+
const cacheDir = libraryCacheDir(root);
|
|
169
|
+
// o cache saiu da biblioteca no OneDrive (libraryCacheDir): o que ficou lá dentro é só do sagadeck e se refaz
|
|
170
|
+
if (cacheDir !== path.join(root, ".cache")) {
|
|
171
|
+
const old = path.join(root, ".cache");
|
|
172
|
+
for (const n of ["capas", "vitrine", "lista.json"]) try { fs.rmSync(path.join(old, n), { recursive: true, force: true }); } catch { /* travado pela sincronização: fica para a próxima */ }
|
|
173
|
+
try { fs.rmdirSync(old); } catch { /* não existe ou tem outra coisa */ }
|
|
174
|
+
}
|
|
153
175
|
|
|
154
176
|
// caminho absoluto de um id, sem sair da biblioteca
|
|
155
177
|
function resolveId(id) {
|
|
@@ -172,17 +194,48 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
172
194
|
return path.join(dir, same || files.sort()[0]);
|
|
173
195
|
}
|
|
174
196
|
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
// O que a listagem mostra de cada apresentação (título, slides, tema, avisos), guardado pela data, tamanho e inode
|
|
198
|
+
// do .yaml e do relatório de avisos: só relê o que mudou. Ler a data e o tamanho não abre o arquivo; ler o conteúdo,
|
|
199
|
+
// sim. Com a biblioteca no OneDrive e "arquivos só na nuvem", cada listagem baixava todas as apresentações, e
|
|
200
|
+
// interpretar o YAML de todas a cada abertura da biblioteca crescia com ela. Fica também em .cache/lista.json, para
|
|
201
|
+
// valer depois de reiniciar o Studio.
|
|
202
|
+
const listCacheFile = path.join(cacheDir, "lista.json");
|
|
203
|
+
let listCache = null, listCacheDirty = false;
|
|
204
|
+
const stamp = (f) => { const s = fs.statSync(f, { throwIfNoEntry: false }); return s ? `${s.mtimeMs}:${s.size}:${s.ino}` : "-"; };
|
|
205
|
+
function cachedInfo(file, own) {
|
|
206
|
+
if (!listCache) { try { listCache = JSON.parse(fs.readFileSync(listCacheFile, "utf8")) || {}; } catch { listCache = {}; } }
|
|
207
|
+
const id = idOf(file), key = `${stamp(file)}|${own ? stamp(path.join(path.dirname(file), ".sagadeck", "avaliacao-geracao.json")) : ""}`;
|
|
208
|
+
const hit = listCache[id];
|
|
209
|
+
if (hit?.key === key) return hit.info;
|
|
177
210
|
let spec = {};
|
|
178
211
|
try { spec = YAML.parse(fs.readFileSync(file, "utf8")) || {}; } catch {}
|
|
179
|
-
const own = idOf(file).split("/").length === 3;
|
|
180
212
|
const pending = own ? pendingWarnings(path.dirname(file), spec) : 0;
|
|
213
|
+
const info = { title: String(spec.title || path.basename(file).replace(/\.ya?ml$/i, "")), slides: Array.isArray(spec.slides) ? spec.slides.length : 0, theme: spec.theme || "", ...(pending ? { pendencias: pending } : {}) };
|
|
214
|
+
listCache[id] = { key, info };
|
|
215
|
+
listCacheDirty = true;
|
|
216
|
+
return info;
|
|
217
|
+
}
|
|
218
|
+
function saveListCache(ids) {
|
|
219
|
+
if (!listCache) return;
|
|
220
|
+
for (const id of Object.keys(listCache)) if (!ids.has(id)) { delete listCache[id]; listCacheDirty = true; } // apagada ou movida
|
|
221
|
+
if (!listCacheDirty) return;
|
|
222
|
+
try {
|
|
223
|
+
fs.mkdirSync(path.dirname(listCacheFile), { recursive: true });
|
|
224
|
+
const tmp = `${listCacheFile}.tmp-${process.pid}`;
|
|
225
|
+
fs.writeFileSync(tmp, JSON.stringify(listCache));
|
|
226
|
+
renameRetry(tmp, listCacheFile);
|
|
227
|
+
listCacheDirty = false;
|
|
228
|
+
} catch { /* sem cache em disco: só fica a memória */ }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function deckInfo(file, topic) {
|
|
232
|
+
const st = fs.statSync(file);
|
|
233
|
+
const own = idOf(file).split("/").length === 3;
|
|
234
|
+
const { pendencias, ...info } = cachedInfo(file, own);
|
|
181
235
|
return {
|
|
182
|
-
id: idOf(file), topic: topic || "",
|
|
183
|
-
slides: Array.isArray(spec.slides) ? spec.slides.length : 0, theme: spec.theme || "", edited: st.mtimeMs,
|
|
236
|
+
id: idOf(file), topic: topic || "", ...info, edited: st.mtimeMs,
|
|
184
237
|
folder: own, // true = pasta própria (com imagens etc.); false = .yaml solto
|
|
185
|
-
...(
|
|
238
|
+
...(pendencias ? { pendencias } : {}), // só quando há aviso
|
|
186
239
|
};
|
|
187
240
|
}
|
|
188
241
|
|
|
@@ -212,6 +265,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
212
265
|
topics.push({ id: e.name, name: e.name, color: meta.color || COLORS[topics.length % COLORS.length], count: mine.length, created: meta.created || 0 });
|
|
213
266
|
}
|
|
214
267
|
topics.sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
|
|
268
|
+
saveListCache(new Set(decks.map((d) => d.id)));
|
|
215
269
|
return { root, topics, decks, trash: listTrash(), trashDays: keepDays() };
|
|
216
270
|
}
|
|
217
271
|
|
|
@@ -560,7 +614,7 @@ export function openLibrary(root, { trashDays } = {}) {
|
|
|
560
614
|
return {
|
|
561
615
|
listStyles, saveStyle, loadStyle, applyStyleTo, defaultStyle, setDefaultStyle, withDefaultStyle,
|
|
562
616
|
importOffice,
|
|
563
|
-
root, list, resolveId, idOf,
|
|
617
|
+
root, cacheDir, list, resolveId, idOf,
|
|
564
618
|
createTopic, updateTopic, deleteTopic,
|
|
565
619
|
createDeck, moveDeck, renameDeck, duplicateDeck, sendDeckTo,
|
|
566
620
|
createStagingDeck, publishStagedDeck, discardStaging, recoverStaging,
|
package/src/markup.js
CHANGED
|
@@ -16,6 +16,12 @@ export function esc(s) {
|
|
|
16
16
|
.replace(/"/g, """);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Código que quebra linha (pre-wrap): o navegador quebra depois do hífen, e "git commit --amend --no-edit" saía como
|
|
20
|
+
// "--no-" / "edit" (quem copia do slide digita errado). Cada hífen vira <span class="hy">, e o CSS põe um "word joiner"
|
|
21
|
+
// (U+2060) depois dele por ::after: a linha não quebra ali, e o texto copiado continua o mesmo (o ::after não é copiado).
|
|
22
|
+
// Recebe HTML já escapado ou realçado: só troca o hífen fora das tags.
|
|
23
|
+
export const noHyphenBreak = (html) => String(html).replace(/(<[^>]*>)|-/g, (m, tag) => tag || '<span class="hy">-</span>');
|
|
24
|
+
|
|
19
25
|
// tabela em markdown no meio do texto (uma linha "| a | b |" por linha, com ou sem a linha "|---|---|" depois do
|
|
20
26
|
// cabeçalho): vira tabela de verdade (src/table.js), não texto com barras
|
|
21
27
|
const PIPE_TABLE = /(^|\n)((?:[ \t]*\|[^\n]*\|[ \t]*(?:\n|$)){2,})/g;
|
package/src/runtime/base.css
CHANGED
|
@@ -333,6 +333,8 @@ html[data-motion="none"] .flip-in,.export .flip-in{transition:none}
|
|
|
333
333
|
.cl.hl{background:color-mix(in srgb,var(--c-accent) 26%,transparent);box-shadow:inset 8px 0 0 var(--c-accent)}
|
|
334
334
|
.cn{width:2.2em;color:#777;flex:none}
|
|
335
335
|
.cc{white-space:pre}
|
|
336
|
+
/* hífen do código sem quebra de linha depois dele (markup.js: noHyphenBreak); o ::after não vai na cópia */
|
|
337
|
+
.hy::after{content:"\2060"}
|
|
336
338
|
.code.code-wrap .cl{min-width:0!important}.code.code-wrap .cc{white-space:pre-wrap;overflow-wrap:anywhere;flex:1;min-width:0}.code.code-scroll{overflow:auto;scrollbar-width:thin}
|
|
337
339
|
.tok-keyword{color:#c792ea}
|
|
338
340
|
.tok-string{color:#c3e88d}
|
|
@@ -54,7 +54,7 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, admin, u
|
|
|
54
54
|
if (pathname === "/api/library/gallery-cover" && req.method === "GET") {
|
|
55
55
|
const key = galleryKey(url.searchParams.get("key")), entry = galleryEntry(key);
|
|
56
56
|
const spec = entry.dir ? { ...entry.spec, _dir: entry.dir } : entry.spec;
|
|
57
|
-
const cacheDir = path.join(L.
|
|
57
|
+
const cacheDir = path.join(L.cacheDir, "vitrine");
|
|
58
58
|
const cached = path.join(cacheDir, `${crypto.createHash("sha1").update(`${key}|${JSON.stringify(entry.spec)}`).digest("hex")}.jpg`);
|
|
59
59
|
if (!fs.existsSync(cached)) {
|
|
60
60
|
const [shot] = await slideSnapshots(spec, 0, { mode: "final", width: 480 });
|
|
@@ -68,7 +68,7 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, admin, u
|
|
|
68
68
|
if (pathname === "/api/library/cover" && req.method === "GET") {
|
|
69
69
|
const file = L.resolveId(url.searchParams.get("id"));
|
|
70
70
|
const st = fs.statSync(file);
|
|
71
|
-
const cacheDir = path.join(L.
|
|
71
|
+
const cacheDir = path.join(L.cacheDir, "capas");
|
|
72
72
|
const cached = path.join(cacheDir, `${crypto.createHash("sha1").update(`${L.idOf(file)}|${st.mtimeMs}`).digest("hex")}.jpg`);
|
|
73
73
|
if (!fs.existsSync(cached)) {
|
|
74
74
|
const spec = loadSpec(file);
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
{ addLabel: "Adicionar turno", newItem: () => ({ who: 1, commit: "" }) })];
|
|
71
71
|
const CARD = [f.icon("icon", "Ícone"), f.text("title", "Título"), f.area("text", "Texto"), f.area("flip", "Verso (o cartão vira no clique)"),
|
|
72
72
|
f.more([f.text("number", "Número"), f.text("foot", "Rodapé"), f.text("badge", "Selo"), f.bool("hl", "Destacar"), f.num("step", "Aparece no clique"), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })])];
|
|
73
|
-
const STAT = [f.text("value", "Valor"), f.text("label", "Rótulo"), f.text("trend", "Tendência"), f.icon("icon", "Ícone"),
|
|
73
|
+
const STAT = [f.text("value", "Valor"), f.text("prefix", "Antes do valor", { hint: "Ex.: R$ " }), f.text("suffix", "Depois do valor", { hint: "Ex.: %, h, mil" }), f.text("label", "Rótulo"), f.text("trend", "Tendência"), f.icon("icon", "Ícone"),
|
|
74
74
|
f.more([f.bool("trendUp", "Tendência positiva"), f.color("color", "Cor")])];
|
|
75
75
|
const STEP = [f.text("title", "Título"), f.area("text", "Texto"), f.icon("icon", "Ícone"), f.more([f.text("tag", "Etiqueta"), f.text("goto", "Ao clicar, ir para", { hint: "Id do slide de destino (ou o número dele): navegação por caminhos." })])];
|
|
76
76
|
// bloco do one-page (problema, solução): texto, números grandes e tópicos
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Parada do Studio quando o sistema pede (SIGTERM do systemd ao reiniciar, SIGHUP ao fechar o terminal). Sem isto,
|
|
2
|
+
// o processo não saía: o Playwright, ao abrir o Chrome (miniaturas, fotos, exportação), instala um tratador de SIGTERM
|
|
3
|
+
// que só fecha o navegador, e com um tratador instalado o Node deixa de sair sozinho. No servidor, cada reinício
|
|
4
|
+
// ficava 90 s fora do ar até o systemd matar à força (SIGKILL), no meio do que estivesse rodando.
|
|
5
|
+
// Aqui: para de aceitar conexões, dá `grace` ms para as que estão em uso terminarem (uma resposta da IA ao vivo, uma
|
|
6
|
+
// gravação), corta o que sobrar, fecha o Chrome das miniaturas e sai.
|
|
7
|
+
export const SINAIS_DE_PARADA = ["SIGTERM", "SIGHUP"];
|
|
8
|
+
|
|
9
|
+
export function installShutdown(server, { grace = 8000, log = (m) => console.log(m), exit = (code) => process.exit(code), closeBrowsers } = {}) {
|
|
10
|
+
let stopping = null;
|
|
11
|
+
const stop = (signal) => {
|
|
12
|
+
if (stopping) return stopping;
|
|
13
|
+
stopping = (async () => {
|
|
14
|
+
log(` ${signal}: parando o Studio (até ${Math.round(grace / 1000)} s para o que está em andamento)`);
|
|
15
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
16
|
+
// a conexão que termina a resposta fica aberta à espera de outra (keep-alive) e seguraria o close até o prazo
|
|
17
|
+
server.closeIdleConnections?.();
|
|
18
|
+
const idle = setInterval(() => server.closeIdleConnections?.(), 100);
|
|
19
|
+
let timer;
|
|
20
|
+
const late = new Promise((resolve) => { timer = setTimeout(() => { server.closeAllConnections?.(); resolve(); }, grace); });
|
|
21
|
+
await Promise.race([closed, late]);
|
|
22
|
+
clearTimeout(timer); clearInterval(idle);
|
|
23
|
+
try { await (closeBrowsers ?? (async () => (await import("./snapshot.js")).closeSnapshots()))(); } catch { /* o Chrome já fechou */ }
|
|
24
|
+
exit(0);
|
|
25
|
+
})();
|
|
26
|
+
return stopping;
|
|
27
|
+
};
|
|
28
|
+
const handlers = SINAIS_DE_PARADA.map((s) => [s, () => { stop(s); }]);
|
|
29
|
+
for (const [s, h] of handlers) process.on(s, h);
|
|
30
|
+
return { stop, uninstall: () => { for (const [s, h] of handlers) process.off(s, h); } };
|
|
31
|
+
}
|