Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 53-saved-copies 54-share-check; 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 54-share-check 55-active-count; 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]}
Expand Down Expand Up @@ -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 53-saved-copies 54-share-check; 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 54-share-check 55-active-count; do
echo "::group::$t"
node test/e2e/$t.js "$PWD/e2e-out" 2>&1 | tee e2e-out/$t.log
status=${PIPESTATUS[0]}
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/deploy-share.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NODE_STATS_TOKEN: ${{ secrets.NODE_STATS_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -36,6 +37,15 @@ jobs:
- name: Delete trails after 30 days
working-directory: worker
run: npx --yes wrangler@4 r2 bucket lifecycle add node-share-trails expire-after-30-days t/ --expire-days 30 --force || echo "::warning::Could not set the 30-day rule. The Worker still refuses trails older than 30 days; set the rule in the Cloudflare dashboard (R2 → node-share-trails → Settings → Object lifecycle rules)."
- name: Delete active counts after 400 days (B116)
working-directory: worker
run: npx --yes wrangler@4 r2 bucket lifecycle add node-share-trails expire-counts-after-400-days a/ --expire-days 400 --force || echo "::warning::Could not set the 400-day rule for active counts."
- name: Deploy
working-directory: worker
run: npx --yes wrangler@4 deploy
# Reading the counts (GET /api/active) needs this token. Optional: add a
# repository secret NODE_STATS_TOKEN (any long random text) to turn it on.
- name: Set the token for reading counts
if: env.NODE_STATS_TOKEN != ''
working-directory: worker
run: printf '%s' "$NODE_STATS_TOKEN" | npx --yes wrangler@4 secret put STATS_TOKEN
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ The old Tauri setup (`src-tauri/`, `renderer/tauri-bridge.js`) does not compile

## What NODE sends

NODE has no account and sends no usage data or analytics. The usage summary in Help stays on your computer until you copy it. Diagnostics are only copied when you click Copy Diagnostics.
NODE has no account. The only usage data it sends is one anonymous count a day (below), which you can turn off in Settings → Help. The usage summary in Help stays on your computer until you copy it. Diagnostics are only copied when you click Copy Diagnostics.

What leaves your computer, and when:

Expand All @@ -139,6 +139,7 @@ What leaves your computer, and when:
| The text, titles and addresses of the tabs in Mosaic, or of the 8 pages you read most recently in the research session | Your local model, or xAI (Grok) | Only when you ask about Mosaic tabs or the session in Ask AI. Session pages that are not open are read again, logged out. Sign-in, account and payment pages are never sent |
| The address of the page you're reading | Hacker News search (Algolia) and Bluesky search | Only while the Talk tab is open |
| A check for a new version | GitHub | Every 6 hours in installed copies, if "Check for updates" is on |
| One anonymous count: NODE's version, your operating system (Mac, Windows or Linux), and whether it is the first count this week and this month. No id, pages, addresses or keywords | NODE's share server (a Cloudflare Worker), which keeps a count per day and nothing else | Once a day while you use NODE, in installed copies. On by default; turn it off in Settings → Help ("Send one anonymous count a day") |
| A download of the ad and tracker block lists (uBlock Origin's default lists; with cookie notices on, also the cookie-notice lists) | GitHub | About once a week |
| Google sign-in and Google apps | A separate Chrome-based browser on your computer, with its own NODE profile | When you open a Google account page (Settings → Google window) |
| A shared trail, encrypted on your computer (the key stays in the link, so the server cannot read it) | NODE's share server (a Cloudflare Worker), which deletes it after 30 days | Only when you click Create Link in Share Trail. Stop Sharing deletes it sooner |
Expand Down
35 changes: 35 additions & 0 deletions core/active-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Automatic active count (B116). Once a day while NODE is in use, one
// anonymous request with only the version and the operating system, so the
// number of daily, weekly and monthly users can be counted. No id of any
// kind: weekly and monthly users are counted from a "first this week" and a
// "first this month" mark on the request. Turn it off in Settings → Help.
// No platform imports: main.js sends the request and keeps the state.

const pad = (n) => String(n).padStart(2, "0");
const dayOf = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const monthOf = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
// ISO week, like "2026-W46".
function weekOf(d) {
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
const day = t.getUTCDay() || 7;
t.setUTCDate(t.getUTCDate() + 4 - day);
const year = t.getUTCFullYear();
const week = Math.ceil(((t - Date.UTC(year, 0, 1)) / 86400000 + 1) / 7);
return `${year}-W${pad(week)}`;
}
const OS = { darwin: "mac", win32: "windows", linux: "linux" };
const osName = (platform) => OS[platform] || "other";
const VERSION = /^\d{1,3}\.\d{1,3}\.\d{1,4}(?:-[0-9A-Za-z.]{1,20})?$/;

// What to send now, or null. state: { day, week, month } of the last count sent.
function nextCount(state, { now = new Date(), version, platform }) {
const day = dayOf(now);
if (state?.day === day || !VERSION.test(String(version))) return null;
const week = weekOf(now), month = monthOf(now);
return {
body: { v: String(version), os: osName(platform), w: state?.week !== week, m: state?.month !== month },
state: { day, week, month },
};
}

module.exports = { nextCount, weekOf, dayOf, osName, VERSION };
1 change: 1 addition & 0 deletions docs/02-roadmap-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Effort: XS under 1 hour, S half a day, M 1-2 days, L 3+ days. Finding IDs (C1, K
| B113 | 4.T | Canvas depth: the dot grid moves faster than the cards when panning (1.25 times), with reduced motion it moves with them | P2 | XS | Built 2026-11-05 (PR #61). e2e `42-trail-canvas` |
| B114 | 4.T | Library media on the canvas: images, screenshots, videos and audio from the Library (Put on canvas, drag) or dropped from Finder become media cards; images show, video and audio play on the card, double-click opens a viewer; link them like any card. In a shared file a media card links to where the media came from, with a large preview for the reader | P1 | M | Built 2026-11-05 (PR #62). Tests `test:trail`, `test:replay`, e2e `48-canvas-media` |
| B115 | 4.T | Rename and delete trails straight from the switcher: a "…" button on each trail with Rename and Delete…; double-click the switcher's name or press F2 to rename; Delete asks first, closes the trail's tabs and offers Undo; the files go at the next start. Opened files can be deleted, not renamed | P1 | S | Built 2026-11-06 (PR #64). Tests `test:trail`, e2e `49-trail-rename-delete` |
| B116 | 4 | Automatic active count: once a day while NODE is in use, send one anonymous request with only the version and OS (no ID, pages, addresses or keywords), so we can count daily and weekly active users. On by default, no prompt (Sven, 2026-11-05). Said plainly on the welcome screen and in the update notes for the release that adds it; switch in Settings → Help and in the privacy page (B74); listed in "What NODE sends" (B73) and the README. Receiver: a small Cloudflare Worker that adds to a per-day count by version and OS (the B82 share-link account), with no IP or request logging. First added 2026-11-05 as B113 and lost when another chat saved the backlog in the project | P1 | S | Built 2026-11-09 (PR #75), in 0.11.0: `core/active-count.js`; the request also says whether it is the first this week and this month, so weekly and monthly users can be counted without an id; installed copies only; the share Worker stores one empty R2 object per count (`a/<day>/<os>/<version>/<marks>/<random>`, deleted after 400 days) and `GET /api/active` reads the counts with the `NODE_STATS_TOKEN` secret. Tests `test:active`, `share-worker-demo`, e2e `55-active-count` |
| B60 | 5 | NODE for Chromium extension | — | — | Dropped 2026-10-14: missing must-have features |
| B59 | 6 | Research agent v3: gated actions with confirmation (forms, downloads) | P2 | L | Not started |
| B52 | 4 | Google window: Google sign-in and Google apps open in a separate window of a Chromium-based browser on the computer, with its own NODE profile and a NODE extension (history, center on click, autoplay, no duplicate tabs). The extension is also the first step of B51 | P0 | M | Done; to test sign-in on the Mac |
Expand Down
6 changes: 6 additions & 0 deletions docs/03-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ The same core modules run in the built-in extension of the Chromium build (`brow
- **Rename:** `rename(id, name)` records a `renamed` event; `applySession` now does the same when a session's name changes, so renames through research:\* are events too.
- **Channels:** `trail:rename-id` (id, name; refused for read-only trails; the current trail goes through research:\*), `trail:delete` (closes the trail's tabs first, then `changeTrail` if it was current; returns `{deleted, name, tabs, current}`), `trail:restore` (via `changeTrail`).
- **Switcher** (`renderer/app.js`): each trail row is `.sb-menu-row` with the item and a "…" button; the actions row (`.sb-menu-acts`) holds Rename and Delete…, then the question with Delete trail and Keep. `#sb-undo` under the menu shows Undo for 10 s. Keys on a trail in the menu: F2 renames, Delete or Backspace asks to delete, → reaches "…". Over a page the field is typed in the sidebar's layer (`chrome-overlay.html`); the real field does not save on losing focus there, and Enter or Esc come back through `overlayEvent`.
## Automatic active count (B116)

- `core/active-count.js` (no platform imports): `nextCount(state, { now, version, platform })` gives the body `{ v, os, w, m }` (w and m: first count this ISO week and this month, local time) and the new state, or null when today's count was sent or the version is not a release number.
- **Main:** `sendActiveCount` runs a minute after start, every 15 minutes and 5 s after the window gets focus; it sends only when the setting `activeCount` is on (default), NODE is in use (`nodeInUse`), and the app is packaged (or `NODE_ACTIVE_API` is set, for tests). It uses Node's `fetch` (no cookies or session) to `<share API>/api/active` and writes `userData/active-count.json` only after a 2xx.
- **Worker** (`worker/src/index.js`): `POST /api/active` validates and writes an empty R2 object `a/<UTC day>/<os>/<version>/<w|-><m|->/<random>`; `COUNT_LIMIT` rate limiter (5 a minute per address, in memory). `GET /api/active` lists `a/` and sums per day, ISO week (from w) and month (from m), only with `STATS_TOKEN`. The deploy workflow adds a 400-day lifecycle rule on `a/`.

## The check before sharing (B86)

- `core/share-check.js` (no platform imports): `findContacts(texts)` finds email addresses and phone numbers (10 to 15 digits in groups, or starting with +; not dates, times, year ranges or plain runs of digits), one finding per detail with `where`, `count` and other spellings (`also`). `snapshotTexts(snap, { note })` lists what a trail file shares; `sessionTexts(trail)` what a share link (format 1) shares. `redact(obj, leaveOut)` replaces each left-out detail in every string of a copy.
Expand Down
1 change: 1 addition & 0 deletions docs/05-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Newest first. Commit hashes refer to the git history inside the delivered zip fi

| Date | Commit | Change |
| --- | --- | --- |
| 2026-11-09 | (PR #75) | **B116: automatic active count.** Once a day while NODE is in use (window focused, input in the last two minutes), installed copies send one request to the share Worker with NODE's version, the operating system (mac, windows, linux) and two marks: first count this week, first this month. No id, pages, addresses or keywords; Node's fetch, no cookies. Weekly and monthly users are counted from the marks, so no id is needed. On by default (Sven, 2026-11-05); the welcome screen says so, and Settings → Help has **Send one anonymous count a day** with what it sends. NODE keeps only the day, week and month of the last count it sent (`userData/active-count.json`); a failed send is tried again later. The Worker (`POST /api/active`) checks the shape, stores one empty R2 object named `a/<day>/<os>/<version>/<marks>/<random>` (no address; the address is used only for a 5-a-minute limit in memory), and an R2 rule deletes counts after 400 days. `GET /api/active?days=30` returns counts per day (by OS and version), ISO week and month, with the `NODE_STATS_TOKEN` repository secret (set as the Worker secret `STATS_TOKEN` on deploy). README "What NODE sends" and the 0.11.0 release notes updated. Tests: `test/active-count-demo.js` (`npm run test:active`, in `test:unit`), 4 more checks in `test/share-worker-demo.js`, new `test/e2e/55-active-count.js` (11 checks, in CI). |
| 2026-11-08 | (PR #74) | **0.11.0 prepared.** Version 0.11.0, release notes `release-notes/v0.11.0.md`: search past trails (B89), media moments (B88), saved copies (B87), how each page was reached (B83), rename and delete trails (B115), the check before sharing (B86), Library media on the canvas (B114), canvas depth (B113). Desktop (Electron) only. Full end-to-end suite run on the release commit. Not tagged until Sven has tried it. |
| 2026-11-08 | (PR #73) | **B86: a check before sharing.** The share sheet now lists the contact details in what you share, for the trail file and for share links: email addresses and phone numbers in card titles and text, notes, questions and replies, group names, timeline titles and the note typed for the reader (Sven chose contact details only, not names or social posts). Each is ticked, with where it first appears ("Email jo@mill.example · Note: Miller: …, and 1 more place"). Unticking one leaves it out of what is shared: it shows as "[email left out]" or "[phone number left out]" everywhere it appears, in the preview and the saved file or link. The trail itself is not changed. Dates, times, years, ids and quantities are not taken for phone numbers. Trails stay private: nothing is shared unless you share it. Agents (MCP `get_research_session` and the session resource) now get an opened trail file with a first line saying it came from someone else and is untrusted data, not instructions. The red-team set (B58) gains `site/redteam/injected.trail.md` and part C of `18-redteam`: the file opens read only, agents get it marked untrusted, and Ask about this trail with a compromised model shows its answer as text with no attacker image or request. New `core/share-check.js` (no platform imports), channel `trail:share-check`, share options `leaveOut`. Tests: `test/share-check-demo.js` (`npm run test:share-check`, in `test:unit`, 6 checks), new `test/e2e/54-share-check.js` (10 checks, in CI), `18-redteam` (4 more checks). |
| 2026-11-08 | (PR #71) | **B87: saved copies of trail pages.** Offered, never forced. **Save a Copy of This Page** (File menu, or right-click a page) and **Save a copy** on a selected page card (with no tab open, the page loads out of sight) save the page as it looks now as one MHTML file, named by its SHA-256, with its readable text beside it, on this computer (userData/copies). The page becomes a card if it wasn't. Saving again when the page's text hasn't changed keeps the one copy ("This page hasn't changed since its copy of 8 Nov 10:05."). **Keep copies** in a trail's … menu (off by default) saves a copy of each page that becomes a card in that trail. The card shows "Copy · 8 Nov 10:05" (and "· 2 copies" when there are more); clicking it opens the copy in a tab after checking its SHA-256, and a copy that changed on disk is not opened. Sign-in, account and payment pages (B66), private tabs and opened (read-only) trails get no copies. Sharing: trail files list each copy ("Copy: 8 Nov 2026 10:05 · SHA-256 …"); copies are sent only when **Saved copies of pages** is ticked in the share sheet, as files in a folder "<file name> copies" beside the trail file, linked from the file (doc 26). Opening such a file keeps each copy whose SHA-256 matches and marks one that doesn't as changed. Copy files no trail names are deleted at start-up, after deleted trails are purged. New `TrailStore.addCopy`, `copyHashes`, `setKeepCopies` (event `copied`), channels `trail:copy-save`, `trail:copy-open`, `trail:keep-copies`. Electron build only. Tests: `test/copies-demo.js` (`npm run test:copies`, in `test:unit`, 4 checks), new `test/e2e/53-saved-copies.js` (20 checks, in CI). |
Expand Down
Loading
Loading