sagadeck 1.11.3 → 1.11.5
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 +46 -7
- package/package.json +1 -1
- package/src/build.js +2 -1
- package/src/export/browser.js +17 -5
- package/src/export/recent-html.js +24 -0
- package/src/library.js +62 -8
- package/src/studio/exporting.js +3 -2
- package/src/studio/library-routes.js +2 -2
- 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,46 @@ começar. Trabalho em curso vai para o GitHub (commit + push) a cada etapa, nunc
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## 1.11.5: parada do servidor, biblioteca no OneDrive — 23/10/2026 (branch `claude/parada-e-fotos`)
|
|
12
|
+
|
|
13
|
+
- **Parada**: o Playwright, ao abrir o Chrome, instala um tratador de SIGTERM que só fecha o navegador; com ele, o
|
|
14
|
+
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:
|
|
15
|
+
"Failed with result 'timeout'"). `src/studio/shutdown.js`: SIGTERM/SIGHUP param de aceitar conexões, dão 8 s para
|
|
16
|
+
as que estão em uso, cortam o resto, fecham o Chrome das miniaturas e saem. Teste: `test/studio-shutdown.test.js`
|
|
17
|
+
(o prazo; e o Studio de verdade, depois de abrir o Chrome, sai em segundos com SIGTERM; só no Linux, porque no
|
|
18
|
+
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.
|
|
19
|
+
- **Biblioteca no OneDrive**: a listagem lia e interpretava o YAML de todas as apresentações a cada abertura (com
|
|
20
|
+
"arquivos só na nuvem", isso baixa cada uma). Agora guarda título, slides, tema e avisos pela data, tamanho e inode
|
|
21
|
+
do `.yaml` e do relatório de avisos, em memória e em `lista.json` no cache. Teste: `library.test.js` (nenhum
|
|
22
|
+
`.yaml` relido sem mudança, nem depois de reiniciar; só o que mudou; apagado some; cache estragado não quebra).
|
|
23
|
+
- **Cache fora da nuvem**: `libraryCacheDir` põe capas, vitrine e lista em `%LOCALAPPDATA%\sagadeck\cache\<biblioteca>`
|
|
24
|
+
quando a biblioteca está dentro do OneDrive (variáveis `OneDrive`, `OneDriveCommercial`, `OneDriveConsumer`); o
|
|
25
|
+
`.cache` antigo de dentro dela sai (só `capas`, `vitrine` e `lista.json`). Fora do OneDrive e no servidor, nada muda.
|
|
26
|
+
- **Fotos da transformação, medido e deixado como está**: cada foto custa ~0,4 s e o lote tem no máximo 5 slides;
|
|
27
|
+
abrir o deck uma vez por lote pouparia ~10 s numa transformação de 40 slides, que leva minutos de IA.
|
|
28
|
+
|
|
29
|
+
## 1.11.4: CI mais rápida, implantação por roteiro, diário em dia — 23/10/2026 (branch `claude/melhorias-infra`)
|
|
30
|
+
|
|
31
|
+
- **Antivírus do Windows segurava a abertura do deck** (exportação, fotos, PDF, PPTX e conferência): o Node lia o
|
|
32
|
+
HTML recém-gravado só para ver se tinha mapa, e no Windows essa primeira leitura de um HTML grande fica parada 15 a
|
|
33
|
+
20 s (o Chrome abre na hora). Agora quem grava usa `writeDeckHTML` (`src/export/recent-html.js`: build, exportação
|
|
34
|
+
do Studio) e a abertura usa o texto da memória; sem ele, quem lê é o Chrome, e o Node só relê se houver mapa (o
|
|
35
|
+
mapa abre por endereço local, servido da memória). `lengths.test.js` caiu de 84 s para 18 s nesta máquina; cada
|
|
36
|
+
exportação no Windows de quem usa deixa de esperar. Teste: `test/open-deck.test.js` (conta as leituras do arquivo
|
|
37
|
+
pelo Node; falha com o código antigo).
|
|
38
|
+
- **CI em 3 partes por sistema**, ao mesmo tempo: `scripts/test-parte.mjs` reparte os arquivos pelo peso (os lentos
|
|
39
|
+
numa tabela lá dentro; dividir pela ordem dos arquivos dava 22 min × 13 min). Os checks exigidos na main continuam
|
|
40
|
+
`test (ubuntu-latest, 24.x)` e `test (windows-latest, 24.x)`: o job `test` de cada sistema confere pela API do run
|
|
41
|
+
que as 3 partes dele passaram. Antes: 26 min no Ubuntu e 37 no Windows por PR. Teste: `test/test-parte.test.js`
|
|
42
|
+
(todo arquivo em exatamente uma parte, partes equilibradas, pesos só de arquivos que existem).
|
|
43
|
+
- **Implantação no Oracle por roteiro**: `node scripts/implantar-oracle.mjs` (dev primeiro, estável só se o dev subiu
|
|
44
|
+
na versão certa; git como root; npm ci só quando o lock muda além do número da versão; confere `/api/instance`).
|
|
45
|
+
Hoje à mão o git sem sudo falhou calado e o npm ci rodou sem precisar. Teste: `test/implantar-oracle.test.js` roda o
|
|
46
|
+
roteiro de verdade com git real e sudo/systemctl/curl/npm de mentira (no Linux; conferido no próprio Oracle).
|
|
47
|
+
- `SAGADECK_ADMINS=305cc6c7416383c8` (a Narumi) em `/etc/sagadeck.env` no Oracle: vale para os dois serviços.
|
|
48
|
+
- Diário: as pendências "Falta: PR…" do Painel Conferência (#204), do Pedido da criação (#203), da Conferência das
|
|
49
|
+
contas (#201, 1.11.0) e da Frente E (Frente F) viraram "Concluído".
|
|
50
|
+
|
|
11
51
|
## 1.11.3 — 23/10/2026 (branch `claude/versao-1.11.3`)
|
|
12
52
|
|
|
13
53
|
Junta o envio de cópia para outra pessoa (#217), o Corrigir tudo (#218) e os testes ao vivo a cada 3 versões (#216).
|
|
@@ -216,7 +256,7 @@ Testes: `ai.test.js` (itens, ignorar, recordMath, Conferir de novo com e sem IA)
|
|
|
216
256
|
usado como a pessoa usaria: selo, Conferir de novo, Ignorar, Corrigir com IA numa conta e num slide da IA,
|
|
217
257
|
conferindo deck salvo, relatório e o que foi ao modelo; zero erros JS). Ícones Lucide novos: list-checks,
|
|
218
258
|
calculator, hash, image-off. Suíte completa: `npm test`, 1.116 testes, 1.087 aprovados, 29 condicionais pulados, zero falhas.
|
|
219
|
-
|
|
259
|
+
Concluído: entrou no PR #204 (depois do #201) e saiu na 1.11.0.
|
|
220
260
|
|
|
221
261
|
## Pedido da criação gravado — 22/10/2026 (branch `claude/pedido-da-criacao-0d34bc`, sobre `claude/conferir-contas`)
|
|
222
262
|
|
|
@@ -234,7 +274,7 @@ pasta inteira; o pacote `.sagadeck` agora leva `.sagadeck/geracao.json`.
|
|
|
234
274
|
Testes: `storage.test.js` (biblioteca com anexo; segredos e `anteriores`), `library-ui.test.js` (pedido guiado
|
|
235
275
|
digitado; só com anexo grava o texto padrão marcado, duplicar e o pacote levam o arquivo, zero erros JS),
|
|
236
276
|
`studio-ai.test.js` (Deck com IA do editor e criação pelo chat com docx). Conferido que falham sem a gravação.
|
|
237
|
-
|
|
277
|
+
Concluído: entrou no PR #203 (depois do #201) e saiu na 1.11.0.
|
|
238
278
|
|
|
239
279
|
## Modelo para conferir — 22/10/2026 (branch `claude/modelo-por-etapa-d74f41`, parte de `claude/conferir-contas`)
|
|
240
280
|
|
|
@@ -292,9 +332,8 @@ erros que ninguém tinha visto (velocidades da tabela do slide 26). A correção
|
|
|
292
332
|
corrigir o material" da pessoa vale sobre a regra da autoria.
|
|
293
333
|
Suíte completa: `npm test`, 1.108 testes, 1.079 aprovados, 29 condicionais pulados, zero falhas. Studio local na
|
|
294
334
|
3517 rodando `c65772e` a partir desta worktree, para validação dela.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
ela autorizou caprichar), corrija os avisos da criação e melhore a aula. A correção é da IA, não à mão.
|
|
335
|
+
Concluído: entrou no PR #201 e saiu na 1.11.0 (Oracle e npm); a IA do sagadeck corrigiu o deck da conta da Mary pelo
|
|
336
|
+
chat, e a versão revisada foi para a pasta dela como outra apresentação.
|
|
298
337
|
|
|
299
338
|
## UX para programação e APIs com cURL — 12/10/2026 (branch `codex/ux-dev-slides`, versão 1.10.0)
|
|
300
339
|
|
|
@@ -2242,8 +2281,8 @@ raciocínio, a mesma conferência sai em 1–2 s e acha o que importa (título r
|
|
|
2242
2281
|
"a partir de um arquivo" só com o anexo), `studio-themes.test.js` (prévia sem gravar, clique aplica),
|
|
2243
2282
|
`studio.test.js` (Baixar tudo: zip com pptx com notas, pdf e roteiro), `storage.test.js` (gerado na biblioteca).
|
|
2244
2283
|
|
|
2245
|
-
|
|
2246
|
-
|
|
2284
|
+
Concluído depois, na Frente F (28/09/2026, logo acima): modo simples e fontes embutidas, sem o Google. O modo
|
|
2285
|
+
simples saiu mais tarde (a faixa mostra tudo).
|
|
2247
2286
|
|
|
2248
2287
|
## Frente D (preguiçoso): trocar o tema muda o arranjo dos slides de conteúdo — 28/09/2026
|
|
2249
2288
|
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -19,6 +19,7 @@ import { esc, notesHTML, plain, md, INLINE_MATH } from "./markup.js";
|
|
|
19
19
|
import { normalizeSpec } from "./fiscal/normalize.js";
|
|
20
20
|
import { readRecordings } from "./api-client.js";
|
|
21
21
|
import { explorationStates } from './exploration.js';
|
|
22
|
+
import { writeDeckHTML } from "./export/recent-html.js";
|
|
22
23
|
import { calcModel } from './lessons.js';
|
|
23
24
|
import { loadMapConfig, mapaFile, tilesForDeck } from "./map-config.js";
|
|
24
25
|
|
|
@@ -257,7 +258,7 @@ export function buildFile(file, outFile) {
|
|
|
257
258
|
const spec = loadSpec(file);
|
|
258
259
|
const r = buildHTML(spec);
|
|
259
260
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
260
|
-
|
|
261
|
+
writeDeckHTML(outFile, r.html); // a conferência e a exportação abrem em seguida sem reler (export/recent-html.js)
|
|
261
262
|
return { ...r, spec, outFile };
|
|
262
263
|
}
|
|
263
264
|
|
package/src/export/browser.js
CHANGED
|
@@ -4,6 +4,8 @@ import http from "node:http";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { chromium } from "playwright-core";
|
|
7
|
+
import { recentHTML } from "./recent-html.js";
|
|
8
|
+
export { writeDeckHTML } from "./recent-html.js";
|
|
7
9
|
|
|
8
10
|
const CANDIDATES = [
|
|
9
11
|
process.env.SAGADECK_BROWSER,
|
|
@@ -24,10 +26,12 @@ export function findBrowser() {
|
|
|
24
26
|
|
|
25
27
|
// Deck com mapa abre por um endereço local (http://127.0.0.1), não como arquivo: o servidor de tiles do OpenStreetMap
|
|
26
28
|
// pede que o navegador diga de onde vem o pedido, e página aberta do disco não diz. Só lê a pasta do deck.
|
|
27
|
-
|
|
29
|
+
// main: { "deck.html": texto } serve o deck da memória (sem o Node reler o arquivo; veja writeDeckHTML)
|
|
30
|
+
async function serveDir(dir, main = {}) {
|
|
28
31
|
const server = http.createServer((req, res) => {
|
|
29
32
|
const rel = decodeURIComponent(new URL(req.url, "http://x").pathname).replace(/^\/+/, "");
|
|
30
33
|
const file = path.resolve(dir, rel);
|
|
34
|
+
if (req.method === "GET" && Object.hasOwn(main, rel)) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); return res.end(main[rel]); }
|
|
31
35
|
if (req.method !== "GET" || !file.startsWith(path.resolve(dir)) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { res.writeHead(404); return res.end(); }
|
|
32
36
|
res.writeHead(200, { "Content-Type": file.endsWith(".html") ? "text/html; charset=utf-8" : "application/octet-stream" });
|
|
33
37
|
fs.createReadStream(file).pipe(res);
|
|
@@ -41,15 +45,23 @@ export const settleSlide = (page) => page.evaluate(() => window.SagaMap?.settled
|
|
|
41
45
|
|
|
42
46
|
export async function openDeck(htmlFile, { scale = 1 } = {}) {
|
|
43
47
|
const browser = await chromium.launch({ executablePath: findBrowser() });
|
|
44
|
-
const hasMap = fs.readFileSync(htmlFile, "utf8").includes('class="map-box');
|
|
45
|
-
const server = hasMap ? await serveDir(path.dirname(htmlFile)) : null;
|
|
46
|
-
if (server) { const close = browser.close.bind(browser); browser.close = async () => { await close(); server.close(); }; }
|
|
47
48
|
try {
|
|
48
49
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: scale });
|
|
49
50
|
const errors = [];
|
|
50
51
|
page.on("pageerror", (e) => errors.push(e.message));
|
|
51
52
|
page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
|
|
52
|
-
|
|
53
|
+
const fileURL = pathToFileURL(htmlFile).href + "?export=1";
|
|
54
|
+
// tem mapa? Pelo texto na memória; sem ele, quem lê o arquivo é o Chrome, e o Node só relê se houver mapa
|
|
55
|
+
let html = recentHTML(htmlFile), hasMap;
|
|
56
|
+
if (html != null) hasMap = html.includes('class="map-box');
|
|
57
|
+
else { await page.goto(fileURL); hasMap = await page.evaluate(() => !!document.querySelector(".map-box")); }
|
|
58
|
+
if (hasMap) {
|
|
59
|
+
const name = path.basename(htmlFile);
|
|
60
|
+
const server = await serveDir(path.dirname(htmlFile), { [name]: html ?? fs.readFileSync(htmlFile) });
|
|
61
|
+
const close = browser.close.bind(browser); browser.close = async () => { await close(); server.close(); };
|
|
62
|
+
errors.length = 0;
|
|
63
|
+
await page.goto(`http://127.0.0.1:${server.address().port}/${encodeURIComponent(name)}?export=1`);
|
|
64
|
+
} else if (html != null) await page.goto(fileURL);
|
|
53
65
|
await page.waitForFunction(() => window.sagadeck && window.sagadeck.cur >= 0 && document.fonts.status === "loaded");
|
|
54
66
|
// a janela com o tamanho do slide (deck.aspect: 4:3 → 1920 × 1440): foto, PDF e PPTX sem faixa nem corte
|
|
55
67
|
const size = await page.evaluate(() => window.sagadeck.size || { w: 1920, h: 1080 });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// HTML que este processo acabou de gravar (build, exportação, conferência). No Windows, o antivírus segura por 15 a
|
|
2
|
+
// 20 s a primeira leitura pelo Node de um HTML grande recém-gravado (o Chrome abre na hora): a abertura no Chrome
|
|
3
|
+
// (browser.js: openDeck) usa o texto da memória em vez de reler o arquivo. Vale só enquanto o arquivo no disco é o
|
|
4
|
+
// mesmo (tamanho e data); guarda só os últimos (o Studio fica aberto o dia todo). Módulo à parte, sem o Playwright,
|
|
5
|
+
// para o build gravar por aqui sem carregar o navegador.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const RECENT = new Map(); // caminho -> { html, size, mtimeMs }
|
|
10
|
+
|
|
11
|
+
export function writeDeckHTML(file, html) {
|
|
12
|
+
fs.writeFileSync(file, html);
|
|
13
|
+
const key = path.resolve(file), st = fs.statSync(file);
|
|
14
|
+
RECENT.delete(key);
|
|
15
|
+
RECENT.set(key, { html, size: st.size, mtimeMs: st.mtimeMs });
|
|
16
|
+
while (RECENT.size > 4) RECENT.delete(RECENT.keys().next().value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function recentHTML(file) {
|
|
20
|
+
const r = RECENT.get(path.resolve(file));
|
|
21
|
+
if (!r) return null;
|
|
22
|
+
const st = fs.statSync(file, { throwIfNoEntry: false });
|
|
23
|
+
return st && st.size === r.size && st.mtimeMs === r.mtimeMs ? r.html : null;
|
|
24
|
+
}
|
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/studio/exporting.js
CHANGED
|
@@ -9,6 +9,7 @@ import os from "node:os";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import JSZip from "jszip";
|
|
11
11
|
import { buildHTML } from "../build.js";
|
|
12
|
+
import { writeDeckHTML } from "../export/recent-html.js";
|
|
12
13
|
import { THEMES } from "../themes.js";
|
|
13
14
|
import { loadPreferences } from "../preferences.js";
|
|
14
15
|
import { packDeck, EXTENSION, MIME } from "../package.js";
|
|
@@ -46,7 +47,7 @@ export async function generateExport(kind, spec, name, { notes = true, audience,
|
|
|
46
47
|
if (["pptx","pdf","tudo"].includes(kind)) spec=expandApiCollections(spec);
|
|
47
48
|
const r = buildHTML(spec);
|
|
48
49
|
const htmlFile = path.join(tmp, "deck.html");
|
|
49
|
-
|
|
50
|
+
writeDeckHTML(htmlFile, r.html);
|
|
50
51
|
const errors = [];
|
|
51
52
|
const make = async (what) => {
|
|
52
53
|
const progress = event => onProgress({ ...event, format: what });
|
|
@@ -60,7 +61,7 @@ export async function generateExport(kind, spec, name, { notes = true, audience,
|
|
|
60
61
|
// Preferências › Exportação: tema escuro com par claro (manual-noite → manual) sai claro no PDF, bom para imprimir
|
|
61
62
|
const light = loadPreferences().exportacao.pdfClaro === true ? lightVariant(spec) : null;
|
|
62
63
|
let file = htmlFile;
|
|
63
|
-
if (light) { file = path.join(tmp, "deck-claro.html");
|
|
64
|
+
if (light) { file = path.join(tmp, "deck-claro.html"); writeDeckHTML(file, buildHTML(light).html); }
|
|
64
65
|
await pdf(file, out, { onProgress: progress });
|
|
65
66
|
} else if (what === "estudo" || what === "estudo-html") {
|
|
66
67
|
const { shots } = await import("../export/shots.js");
|
|
@@ -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);
|
|
@@ -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
|
+
}
|