diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 551c77f..1aaa93e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: (cd test/e2e/site && python3 -m http.server 8765 >/dev/null 2>&1 &) sleep 1 mkdir -p e2e-out - for t in 01-basics 03-rules-and-fast-results 04-find-zoom-history 05-research-session 09-playback-limit 12-pip-bookmarks-adblock 13-ask-ai 15-agents-mcp 16-research-agent 17-talk 18-redteam 19-browser-basics 20-first-run-and-help 21-rules-editor 22-ui-refresh 23-beta-survey 24-mosaic-pick 25-video-loop 26-gestures-tour 27-privacy-visible 28-ask-across-tabs 29-blocker-onpage 30-tab-focus-sleep 31-passwords 32-passwords-more 33-sidebar-chrome 34-command-bar 35-frames-tabs 36-trail-tabs 37-threads 38-trail-share 39-trail-open 40-share-link 41-survey-newtab 42-trail-canvas 43-canvas-tools 44-share-open 45-mosaic-gallery 48-canvas-media 49-trail-rename-delete 50-provenance 51-trail-recall 52-media-moments; do + for t in 01-basics 03-rules-and-fast-results 04-find-zoom-history 05-research-session 09-playback-limit 12-pip-bookmarks-adblock 13-ask-ai 15-agents-mcp 16-research-agent 17-talk 18-redteam 19-browser-basics 20-first-run-and-help 21-rules-editor 22-ui-refresh 23-beta-survey 24-mosaic-pick 25-video-loop 26-gestures-tour 27-privacy-visible 28-ask-across-tabs 29-blocker-onpage 30-tab-focus-sleep 31-passwords 32-passwords-more 33-sidebar-chrome 34-command-bar 35-frames-tabs 36-trail-tabs 37-threads 38-trail-share 39-trail-open 40-share-link 41-survey-newtab 42-trail-canvas 43-canvas-tools 44-share-open 45-mosaic-gallery 48-canvas-media 49-trail-rename-delete 50-provenance 51-trail-recall 52-media-moments 53-saved-copies; do echo "::group::$t" xvfb-run -a node test/e2e/$t.js "$PWD/e2e-out" 2>&1 | tee e2e-out/$t.log status=${PIPESTATUS[0]} @@ -71,7 +71,7 @@ jobs: (cd test/e2e/site && python -m http.server 8765 --bind 127.0.0.1 >/dev/null 2>&1 &) sleep 2 mkdir -p e2e-out - for t in 01-basics 05-research-session 19-browser-basics 20-first-run-and-help 22-ui-refresh 23-beta-survey 24-mosaic-pick 25-video-loop 26-gestures-tour 27-privacy-visible 28-ask-across-tabs 29-blocker-onpage 30-tab-focus-sleep 31-passwords 32-passwords-more 33-sidebar-chrome 34-command-bar 35-frames-tabs 36-trail-tabs 37-threads 38-trail-share 39-trail-open 40-share-link 41-survey-newtab 42-trail-canvas 43-canvas-tools 44-share-open 45-mosaic-gallery 48-canvas-media 49-trail-rename-delete 50-provenance 51-trail-recall 52-media-moments; do + for t in 01-basics 05-research-session 19-browser-basics 20-first-run-and-help 22-ui-refresh 23-beta-survey 24-mosaic-pick 25-video-loop 26-gestures-tour 27-privacy-visible 28-ask-across-tabs 29-blocker-onpage 30-tab-focus-sleep 31-passwords 32-passwords-more 33-sidebar-chrome 34-command-bar 35-frames-tabs 36-trail-tabs 37-threads 38-trail-share 39-trail-open 40-share-link 41-survey-newtab 42-trail-canvas 43-canvas-tools 44-share-open 45-mosaic-gallery 48-canvas-media 49-trail-rename-delete 50-provenance 51-trail-recall 52-media-moments 53-saved-copies; do echo "::group::$t" node test/e2e/$t.js "$PWD/e2e-out" 2>&1 | tee e2e-out/$t.log status=${PIPESTATUS[0]} diff --git a/core/trail-file.js b/core/trail-file.js index 2d9bab1..942e3c4 100644 --- a/core/trail-file.js +++ b/core/trail-file.js @@ -516,6 +516,12 @@ function merge(base, incoming, answers = {}) { const FORMAT_V2 = 2; const SIZE_V2 = { page: [260, 200], video: [260, 220], capture: [260, 210], report: [260, 240], question: [260, 170], note: [260, 150], voice: [260, 170], search: [260, 140], media: [300, 240], moment: [280, 190] }; +// Saved copies (B87): "7 Nov 2026 21:40". +const copyTime = (t) => { + const d = new Date(t); + return `${d.getDate()} ${MONTH_NAMES[d.getMonth()]} ${d.getFullYear()} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; +}; +const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; // Media moments (B88): "41:12" and back, in seconds. const clockOf = (t) => { t = Math.max(0, Math.round(Number(t) || 0)); @@ -695,7 +701,9 @@ function timelineLine(sess, byId) { // options: { by, at (ms or ISO), note, startHere (card id), timeline (true), // openTabs: [card ids] (none by default) } -function toTrailMd(snap, { by = "", at = Date.now(), note, startHere, timeline = true, openTabs = [] } = {}) { +// copies (B87): { folder, include: [sha256] } — saved copies sent beside the +// file, in that folder. Copies not sent are still listed, as kept by the sender. +function toTrailMd(snap, { by = "", at = Date.now(), note, startHere, timeline = true, openTabs = [], copies = null } = {}) { const t = snap.trail || {}; const cards = (snap.cards || []).filter(sendable).map(forFile); const byId = new Map(cards.map((c) => [c.id, c])); @@ -708,7 +716,10 @@ function toTrailMd(snap, { by = "", at = Date.now(), note, startHere, timeline = L.push("---", `# ${oneLine(t.name) || "Trail"}`, ""); if (oneLine(words)) L.push(oneLine(words), ""); if (start) L.push(`**Start here:** [${mdText(cardTitle(start))}](#${start.id})`, ""); - const block = (c) => L.push(cardHeading(c), layoutComment(c), ...cardBody(c), ""); + const sent = new Set(copies?.include || []); + const copyLines = (c) => + (c.copies || []).map((x) => `Copy: ${x.atText || copyTime(x.at)} · SHA-256 ${x.sha256}${copies?.folder && sent.has(x.sha256) ? ` · [saved copy](${encodeURI(`${copies.folder}/${x.sha256}.mhtml`)})` : ""}`); + const block = (c) => L.push(cardHeading(c), layoutComment(c), ...cardBody(c), ...(c.kind === "page" ? copyLines(c) : []), ""); const placed = new Set(); for (const g of groups) { L.push(`## ${oneLine(g.label) || "New group"}`, ``, ""); @@ -889,7 +900,14 @@ function readCard(raw) { case "page": { const kw = b.find((l) => /^Keywords:/.test(l)); const reached = b.find((l) => /^Reached: /.test(l)); - const ex = b.filter((l) => l !== kw && l !== reached); + // Saved copies (B87): "Copy: