Repository navigation
Expand file tree
/
Copy pathSTATUS.html
More file actions
474 lines (449 loc) · 141 KB
/
Copy pathSTATUS.html
File metadata and controls
474 lines (449 loc) · 141 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TidyDisk — Project Status</title>
<link rel="icon" href="data:,">
<script>
/* ===================================================== STATUS DATA — this is the ONLY part Claude edits each session.
Statuses: "done" | "progress" | "planned" | "user"
userActions categories: "testing" | "review" | "external" | "vision"
Keep ids stable — checkbox state is saved against them.
============================================================ */
const STATUS = {
app: "TidyDisk",
version: "1.0.0",
updated: "2026-08-03",
phase: "🚀 v1.0.0 — TidyDisk is LIVE-ready. All three PRs merged to main (landing refresh #22, license gate + analytics #30, rename + honest copy #31); the release gate is lifted; tidydisk.app serves the new site with the €19 Polar checkout wired. The v1.0.0 tag fires the signed-release pipeline (first tidydisk-arm64.dmg). Your last clicks before selling: publish the draft release when the workflow finishes, then run the launch playbook (#27 scan screenshot, #28 assets, #29 the wave).",
summary: "This session (2026-07-16): the menu bar panel is no longer a mini node_modules cleaner. It is a read-only dashboard: an aggregate 'Tracked on disk' hero (every tracked byte counted once, against the sum of your three limits) plus four click-through rows (Projects / Caches / Packages / Docker) that open that tab in the main window. Gone from the panel: the reclaimable list, the Clean stale CTA, the pnpm prune row, and the paywall. NOTE: that removes all three paywall_shown triggers from the panel, so the launcher is now the only paid-conversion surface. Built spec -> plan -> subagent-driven TDD (9 tasks, fresh implementer + independent review each); all arithmetic + gating lives in pure panelAreas.ts / launcherNav.ts so the .tsx stays thin. The hero counts the pnpm store once and is therefore smaller than the four rows added up (rows mirror their tabs so click-through never lies); Docker refreshes on open when stale, but the store re-sizes only on a scan (no du on every popover open), and the panel never computes the package inventory. 315 tests, typecheck/lint/build green. In your court: manual QA (needs a running app + Docker daemon) and whether the launcher needs its own paywall moment now the panel has none. SHIPPED: merged to main via PR #42 (the whole Docker stack + the panel; PR #40 auto-closed) and tagged v1.1.0, which fired the signed-release pipeline to build a DRAFT dmg. Test that draft, then publish it to promote v1.1.0 to latest. Prior session (2026-07-14): finished the Docker feature on feat/docker-project-grouping (spec + plan docs/superpowers/2026-07-14-docker-buildcache-*, subagent-driven TDD, 6 tasks + a final-review fix wave). Two things you asked for. (1) Docker build cache no longer sits in the Docker tab: groupDockerForDisplay drops the build-cache group (items still count in the Docker header gauge, but the list only shows project/repo/orphan resources), and it now appears as a single aggregated 'Docker build cache' row in the CACHES tab next to the pnpm store. Its red Delete routes through the EXISTING requestDockerPrune('buildCache') — same license gate, same confirm, same toast, no new delete path — and the Caches tab was generalized from a hard-coded pnpm row into a list of 'live caches' (keyboard nav + Enter across them; the tab also auto-scans Docker on open when data is missing/stale, mirroring the Docker tab). (2) The deletion buttons now unmistakably read as deletions: the Docker bulk-prune buttons became 'Delete unused images / dangling images / stopped containers / unused volumes' with a trash icon and red danger styling; the build-cache row's Delete is likewise danger-styled (CacheRow gained an opt-in danger + icon variant); the per-item unused-image/volume/container trash stays hover-revealed like the node_modules rows (already danger). SAFETY UNCHANGED: presentation + a delete-button relabel; the typed-name volume confirm gate is byte-identical and build cache is still never individually removable (3-layer block, verified). Whole-branch Opus review: 1 Important fixed — Caches Enter indexed the full cache list while the rows were query-filtered, so Enter could fire a row scrolled off by search; a new tested selectedActionableCache() guard makes Enter a no-op unless the selected row is visible + actionable (confirm-gated the whole time, never a silent delete). Also swept em dashes out of the npm/yarn/bun placeholder copy. 293 tests, typecheck/lint/build green. In your court: manual QA (needs a running app + Docker daemon) and push/PR (rides PR #42 or its own branch, your call). Prior session (2026-07-09): per-tab headline data-viz + Docker auto-scan, BUILT on feat/docker-project-grouping (spec + plan in docs/superpowers/2026-07-09-*, subagent-driven TDD, 8 tasks + a final-review fix). Each launcher tab's header slot and footer line now reflect THAT tab's core metric instead of always node_modules: Projects/Caches/Docker each fill a size-vs-limit Gauge against their OWN configurable GB limit (new cacheThresholdGB=10, dockerThresholdGB=20 in Settings > Scanning > Storage limits, clamped 0.1-1000 like the node_modules threshold); Packages shows a compact SeverityMeter (a stacked critical/high/moderate/low advisory bar + outdated count, full breakdown in the tooltip). A new TabHeadline switches the header slot; a new pure tabSummary() switches the footer line; both null-gate honestly (no gauge when a store/daemon is absent). The Docker tab auto-scans on open when its data is missing or >5 min stale (loop-guarded, decoupled from the disk rescan; manual Cmd-R/refresh kept). Whole-branch Opus review returned one Important finding, fixed: the Packages headline no longer shows a false 'all clear' when advisories were never actually checked (checks off / offline / before enrichment now render nothing) - the same fix removes the first-open flash. 277 tests, typecheck/lint/build green; Projects tab confirmed unregressed and the Docker typed-name confirm safety gate untouched. In your court: manual QA (needs a running app + Docker daemon) and whether this rides the still-pending Docker-grouping PR #42 or gets its own branch/PR. Prior sessions: Two new app features shipped on feat/scan-coverage-and-liveness (PR #39), each brainstormed → spec'd → planned → built via subagent-driven TDD (15 tasks, fresh implementer + independent review each), then a whole-branch review on the strongest tier (READY TO MERGE). (1) External-drive / scan-location coverage — the scanner was hard-wired to ~; now a Settings 'Scan locations' section lists mounted external volumes (detected under /Volumes, boot disk excluded by device id) as toggles plus an 'Add folder…' picker, choices persist in a new scanRoots setting, and resolveScanRoots feeds home + opted-in roots to the scanner each scan (mounted-only, deduped, nesting-dropped) — so node_modules on an external SSD or any folder is finally scannable and cleanable; an unplugged drive's projects just drop off next scan and return on replug, and a delete-time guard refuses trashing a path that vanished. (2) Live/running indicator — a green dot marks any project with a process running from inside it (one lsof cwd sweep per refresh, best-effort port label, soft-fails to no-dots on any error), its delete is disabled and main hard-refuses it re-checked at the instant of delete, so you cannot trash a running app's node_modules; polls every 45s while a window is open. Shared: unified deleteNodeModules → { freed, blocked? }; a batch deleteMany IPC keeps 'Clean N stale' to one liveness check (the final review's one Important finding). 183 tests, typecheck/biome/build green; the real lsof flags smoke-tested on macOS. Awaiting your review/merge + manual pass (needs a physical external drive + a running dev server). Previous session context: SEO + content session (site only, no app code). Audited tidydisk.app's SEO: robots.txt and sitemap.xml were 404, no canonical tags, no structured data, and the apex→www redirect direction contradicts the code's canonical base (fix is one Vercel dashboard click on your side). Built the fixes plus a self-publishing blog on feat/blog-seo (PR #34): markdown articles in site/content/blog/ parsed by a tested lib/blog pipeline; every surface (listing, article pages, sitemap, RSS) revalidates hourly and filters on publish date, so the 10 pre-written keyword-targeted articles release themselves one per week (2026-07-04 → 2026-09-05) with zero deploys. Also: SoftwareApplication + BlogPosting JSON-LD, RSS feed, Blog in nav/footer. 17 unit tests, build green, rendering verified via Playwright. Earlier context: decided to reposition the product as TidyDisk (from 'Clean my node_modules'), broadening the pitch from node_modules to all developer disk waste (caches, builds, later Docker/Xcode), and to monetize via free-scan / paid-clean rather than a free/OSS-only model: the free scan stays the marketing engine (its shocking number is the shareable artifact) and the one-click Clean action becomes a paid, lifetime unlock (€19 founding → €29). Produced two artifacts: TIDYDISK-LAUNCH.html — an interactive, on-brand launch dashboard (live price→sales calculator, pricing ladder, offer stack, competitor table, Show HN / Reddit drafts, and a persistent 2–4 week launch checklist; rendering + interactivity verified via Playwright) — and docs/superpowers/specs/2026-07-01-tidydisk-gtm-and-monetization-design.md, the GTM + monetization spec, whose one implementation slice is a license gate on the destructive actions (new src/main/license/*, offline-verifiable keys, enforced inside the destructive IPC handlers, Merchant-of-Record checkout via Lemon Squeezy/Paddle). Nothing renamed or gated in code yet — awaiting your review of the spec and your go-ahead. Still open from the prior session: publish the v0.2.0 draft release and deploy site/ to Vercel.",
milestones: [
{
title: "Core app",
note: "Everything from the Claude Design handoff",
items: [
{ label: "Menu bar dropdown (tray panel)", detail: "Total size, pixel-cell meter vs GB limit, 4 oldest projects, one-click delete, 'Clean N stale' action", status: "done" },
{ label: "Launcher window", detail: "⌘O Spotlight-style search, sort by last-used/size/name, full keyboard nav (↑↓ ↵ ⌘⌫ ⌘R ⌘, esc)", status: "done" },
{ label: "Real disk scanner", detail: "Walks home dir (skips dot-folders & Library), sizes via du, detects framework, infers last-used from mtimes, caches results", status: "done" },
{ label: "Real vs linked (virtual) storage", detail: "Each node_modules reports REAL freeable bytes (the project's own content) separately from bytes LINKED into the shared pnpm store; deleting frees only the real part. Disk total counts each package once (store + Σ real). Measured structurally via the .pnpm subtree with du — robust to APFS clones, which make per-file link-count detection impossible. New measureNodeModules + Project.uniqueSize; SizeViz/MiniRow show 'X · Y linked' + tooltip; Gauge/DiskSummary surface the linked total. Branch feat/real-vs-virtual-storage", status: "done" },
{ label: "Packages tab — computer-wide dependency inventory", detail: "New third launcher tab (⌘3) listing every directly-installed package across all projects: how many projects use it, size (per unique version, from the store), version(s) in use (a 'unify' badge flags divergence), latest on npm, and a security-advisory pill. Built from already-scanned projects (no re-scan): read direct deps + resolved versions → aggregate by name → du each unique version → enrich via the npm registry (latest dist-tag + a single bulk-advisories POST), 24h-cached, on by default but disableable in Settings (checkUpdates). On-demand compute cached to disk; Enter opens the package on npm. First network code in the app (Electron net, hardened-runtime-safe). New src/main/packages/* (TDD) + PackagesView/PackageRow + usePackages. Branch feat/packages-tab", status: "done" },
{ label: "Smarter project names", detail: "Generic folder names (frontend, backend, web, api, framework…) are qualified with the git-repo root or parent, e.g. 'selfkit / frontend' — package.json name preferred when meaningful", status: "done" },
{ label: "Safe deletion", detail: "shell.trashItem — folders go to Trash, never rm -rf", status: "done" },
{ label: "Background scans + notifications", detail: "6h / daily / weekly / manual schedule, native notification when crossing the GB threshold", status: "done" },
{ label: "Row actions", detail: "Reveal in Finder, open in editor", status: "done" },
{ label: "Runtime tray icon", detail: "Cube glyph rasterized at runtime (glyph-raster.ts + own PNG encoder) — zero image assets", status: "done" },
{ label: "Settings persistence", detail: "Plain JSON in userData; GB limit, schedule, etc.", status: "done" },
{ label: "Launcher list shows ~7 rows with breathing room", detail: "List measures a real row and caps at ~7 rows across densities (4px gap + kept y-padding); long project paths truncate with an ellipsis; the window is clamped to the screen so the footer never clips", status: "done" },
{ label: "Launcher window behaves like Spotlight", detail: "Kept alive (hide, not destroy) so reopening is an instant show() with no renderer reload or gray/white flash; dismisses on blur like esc; footer no longer clipped — auto-height now measures offsetHeight, immune to the open animation's scale() transform. Reopen resets to a clean Projects search", status: "done" },
{ label: "First-run onboarding (guided 2-step)", detail: "On first launch the full window opens a welcome → setup flow: value prop + Trash/scope reassurance, then your alert limit (new pixel stepper) + scan cadence (incl. Off), then the first scan. 'Skip — use defaults' available. Gated on a persisted 'onboarded' flag so it shows once", status: "done" },
{ label: "Pixel-stepper alert threshold + scan Off", detail: "The raw range slider is replaced everywhere by a 10-block pixel stepper (click / drag / ←→, 1 GB steps) matching the usage meter; automatic scanning can now be turned fully Off in both settings surfaces (the panel previously had no off option)", status: "done" },
{ label: "Honest empty & scanning states", detail: "No more 'All clean' before a scan has run; distinct all-clean (fresh vs 'reclaimed X this session'), scanning, and panel 'Finish setup' states, each with a subtle Tailwind-style ping ring (new PingBadge)", status: "done" },
{ label: "Panel & launcher open on the active Space", detail: "setVisibleOnAllWorkspaces (canJoinAllSpaces) so clicking the tray / ⌘O shows on whatever desktop you're on instead of forcing a switch to the Space the app launched on; skipTransformProcessType keeps the dock-hidden process type", status: "done" },
{ label: "Live/running project indicator + delete guard", detail: "BUILT on feat/scan-coverage-and-liveness (PR #39). A green dot marks any project with a process whose working directory is inside it (one lsof -d cwd sweep per refresh, best-effort listening-port label for the tooltip, soft-fails to no dots on any error). Its delete affordance is disabled and main hard-refuses the delete, re-checked at the instant of delete (blocked='live'), so a running app's node_modules can't be trashed out from under it; polls every 45s while a window is open, nothing when idle. New src/main/liveness/* (parser + matcher TDD'd); LiveDot in Row/MiniRow; a batch deleteMany IPC keeps 'Clean N stale' to one liveness check. Blind spots (documented): Docker/containerized apps, nested monorepo projects (over-protects, which is safe). MERGED to main (PR #39, 2026-07-08)", status: "done" }
]
},
{
title: "Ship it as a real Mac app",
note: "From 'works in pnpm dev' to 'lives in /Applications'",
items: [
{ label: "Unsigned .app packaging", detail: "pnpm package via electron-builder works (LSUIElement menu-bar-only app)", status: "done" },
{ label: "Public GitHub repo, clean history, CI, branch/PR flow", detail: "github.com/AhmedFr/clean-my-node-modules — CI runs typecheck, tests, build + unsigned mac packaging on every PR", status: "done" },
{ label: "Code signing + notarization", detail: "Working end-to-end: 'pnpm release' loads creds from .env.signing and builds a signed + notarized DMG/ZIP (scripts/release-mac.sh; --publish never for local builds). You confirmed a successful release on 2026-06-18", status: "done" },
{ label: "Landing page — Next.js + shadcn on Vercel", detail: "Rebuilt site/ from the static HTML into a Next.js 16 + TS app — faithful port of the original design (landing.css kept verbatim; pixel meter, scroll-reveal and hero-tilt ported to React; shadcn configured). Paid tier removed: the old #pricing section is now a free & open-source 'Download' section (two $0 cards) and CTAs point to the latest GitHub release asset. Deploys on Vercel. Branch nextjs-shadcn-landing-free-tier", status: "done" },
{ label: "MIT LICENSE file", detail: "Added after the landing page started advertising MIT without one in the repo", status: "done" },
{ label: "App icon for the packaged .app", detail: "1024px icon generated from the app's cube glyph (scripts/make-icon.mjs reuses the tray glyph-raster + png-encode; matches the in-app AppIcon — white cube on the red gradient squircle). Wired as mac.icon; 'pnpm icon' regenerates it", status: "done" },
{ label: "GitHub release pipeline (tag → signed DMG → draft release)", detail: ".github/workflows/release.yml: pushing a vX.Y.Z tag has a macOS runner build, sign + notarize the arm64 app and upload the DMG/ZIP + latest-mac.yml to a GitHub draft release (build.publish github/draft). Cycle: 'pnpm version patch && git push --follow-tags'. Needs your 5 repo secrets — docs/SIGNING.md", status: "done" },
{ label: "In-app Uninstall button", detail: "Settings now has an Uninstall row: a native macOS confirm dialog, then a clean uninstall — the .app bundle is moved to the Trash (skipped in dev) and the app-data dir (settings + projects cache) is removed, before quitting. New src/main/actions/app-actions.ts via the existing IPC pattern (app:uninstall). Branch feat/uninstall-button", status: "done" },
{ label: "In-app auto-update (check silently, click to download, click to install)", detail: "BUILT on feat/auto-update: UpdaterService wraps electron-updater (autoDownload=false, install-on-quit after a download) behind an injectable interface with a unit-tested state machine; silent background checks run at launch + every 6h, packaged builds only. The panel shows a compact dismissible 'vX.Y.Z available' banner (size + date) that deep-links to Settings → Updates, dismissal is per-version. Settings → Updates tab: current version, manual check, release notes from the GitHub release body, Download → progress → Restart and install. Translocation (app run outside /Applications) is detected up front with a friendly fix-it message instead of a crash. Analytics: update_available (deduped per version), update_download_clicked, update_installed. Release process unchanged — publishing the GitHub release draft remains the go-live switch. Spec: docs/superpowers/specs/2026-07-22-auto-update-design.md. Full code review (2026-07-29) fixed six small issues (671ad80: instant 'downloading' state so double-click can't start a second download, progress broadcast dedupe, repeat deep-link lands on Updates, 4xx not misreported as offline, single-pass entity decoding, explicit repository field). MERGED to main (PR #46, 2026-07-29); 355 tests, typecheck/lint green. Ships with the next tagged release — still needs your manual pass with a signed build (CI can't exercise the real update flow)", status: "done" },
{ label: "Launch at login", detail: "app.setLoginItemSettings + a toggle in Settings", status: "planned" },
{ label: "External drives + configurable scan roots", detail: "BUILT on feat/scan-coverage-and-liveness (PR #39). Settings 'Scan locations' lists mounted external volumes (detected under /Volumes, boot disk excluded by device id) as toggles + an 'Add folder…' picker; choices persist in a new validated scanRoots setting. resolveScanRoots feeds home + opted-in roots to the scanner each scan, keeping only currently-mounted paths (deduped, nesting dropped), so an unplugged drive's projects drop off next scan and return on replug. A delete-time guard refuses trashing a path that vanished since the scan (DeleteResult.blocked='unmounted'). MERGED to main (PR #39, 2026-07-08)", status: "done" }
]
},
{
title: "Beyond node_modules (issue #2)",
note: "Package-manager caches, pnpm first",
items: [
{ label: "M1 — pnpm store size + prune in the panel", detail: "Merged (was PR #3): Caches section, safe 'pnpm store prune' (never deletes the store). Now actually works from a Finder launch — fix/pnpm-path-detection resolves node for the pnpm shebang so detection no longer silently reports the store unavailable", status: "done" },
{ label: "M1b — pnpm store surfaced in the launcher (Caches tab)", detail: "New Projects / Caches segmented tab in the launcher (⌘1/⌘2): pnpm store size + safe Prune (↵), with npm/yarn/bun as disabled 'soon' rows. The launcher home for the rest of issue #2. Branch feat/launcher-caches-tab", status: "done" },
{ label: "M1c — robust pnpm store detection + manual override + rescan hint", detail: "Layered resolution: manual store/binary override > run pnpm (node-prepended) > infer the store dir from disk (PNPM_HOME/~/Library/pnpm/~/.local/share/pnpm, newest v<N>) > none-with-reason. Runtime-bin discovery now finds npm-global/corepack pnpm under nvm/volta/asdf. PnpmStoreInfo gained source/reason/canPrune — sizing survives a broken pnpm; prune stays honest. Settings 'pnpm store' section with native folder/file pickers + a live status line. Stop back-filling uniqueSize=size for pre-split caches (now undefined=unknown); panel + launcher show a one-click 'rescan to compute' hint. Branch feat/pnpm-store-robustness", status: "done" },
{ label: "M1d — Docker cleanup (images / volumes / containers / build cache)", detail: "BUILT on feat/docker-cleanup (spec + plan in docs/superpowers/). A new launcher Docker tab (⌘4) lists Docker's four disk categories with size, created date, and in-use/unused status, sorted like projects. Cleanup via the docker CLI: per-item remove (unused images/volumes/stopped containers) + bulk prune (dangling/unused images, stopped containers, build cache, unused volumes). SAFETY (docker removals are PERMANENT, no Trash): only unused items are removable (in-use are display-only); every action is labeled 'permanent, not sent to the Trash'; VOLUMES require typing the exact volume name (or 'prune') before the delete fires — verified by an Opus review as a single gated choke point with no Enter/click/shortcut bypass. Listing is free (another shareable 'shocking number'); cleanup is Pro-gated like the rest. Built subagent-driven with TDD: ~65 docker tests incl. parsers, freed-bytes-honesty, license-gate, payload validation, and the typed-confirm gate; find-docker mirrors find-pnpm; execFile array args, never -f on per-item removals. Pending your review + manual daemon test", status: "done" },
{ label: "M1e — Docker tab regrouped BY PROJECT (logos + type badges + used-by association)", detail: "BUILT on feat/docker-project-grouping (stacked on the M1d PR #40; spec + plan in docs/superpowers/). Replaces the by-category Docker sections with a project-grouped list: Compose projects group at the top with their REAL logo (reusing the node_modules framework/favicon detection on the compose working_dir, generic Docker glyph when the folder is gone), then a 'Not linked to a project' section (unassociated images sub-grouped by repository, a build-cache group, and orphan volumes/containers). Each row carries a type badge (IMAGE / VOLUME / CONTAINER / CACHE), and there are Size / Name / Recent sort tabs + All/Images/Volumes/Containers/Cache filter chips mirroring the Projects tab. Association is aggressive but honest: Compose labels PLUS 'used-by' (an image a project's container runs, a volume it mounts) — an item used by >1 project stays unassociated so the size sort never double-counts; pure 'docker run' setups with no compose fall to 'Other' (a stated data-limitation ceiling). Main process adds batched docker container/volume inspect behind the existing cache, all fail-soft (any inspect error → everything unassociated, tab still lists + cleans). SAFETY UNCHANGED: presentation + enrichment only — rows still flow the real id/kind, and the typed-name volume-confirm gate is byte-identical (Opus whole-branch review verified no wrongful-delete path). Bulk-prune buttons live only on the 'Other' headers (prune is a global op, never shown as project-scoped). Built subagent-driven with TDD; the review loop caught + fixed 3 real issues (image id/ref match must be a union not a fallback; dangling-image bulk prune was unreachable; fail-soft ordering could silently hide items). Pending your review + manual daemon test", status: "done" },
{ label: "M1f — Per-tab headline data-viz + Docker auto-scan", detail: "BUILT on feat/docker-project-grouping (spec + plan docs/superpowers/2026-07-09-*, subagent-driven TDD, 8 tasks + 1 final-review fix). Each launcher tab's header slot + footer line now reflect THAT tab's core metric instead of always node_modules: Projects/Caches/Docker each fill a size-vs-limit Gauge against their OWN configurable GB limit (new cacheThresholdGB=10 / dockerThresholdGB=20 in Settings > Scanning > Storage limits, validated + clamped 0.1-1000 like the node_modules threshold); Packages shows a new compact SeverityMeter - a stacked critical/high/moderate/low advisory bar + outdated count, full breakdown in the tooltip. New TabHeadline component switches the header slot per active tab; new pure tabSummary() switches the footer line; both null-gate honestly (no gauge when a store/daemon is absent, nothing on Packages when advisory checks are off/offline). Docker auto-scans on tab open when its data is missing or >5 min stale (loop-guarded via docker.loading + fresh checkedAt, decoupled from the disk rescan; manual Cmd-R/button kept). SAFETY UNCHANGED: presentation only - the Docker typed-name confirm gate is untouched and the Projects tab renders identically (both verified in the whole-branch review). Whole-branch Opus review: 1 Important fixed - the Packages headline no longer claims 'all clear' when advisories were never checked. 277 tests, typecheck/lint/build green. Pending your review + manual pass", status: "done" },
{ label: "M1g — Docker tab: collapsible project rows (shared Accordion)", detail: "BUILT on feat/docker-project-grouping (spec + plan docs/superpowers/2026-07-09-docker-collapsible-*, subagent-driven TDD, 4 tasks + 1 final-review fix). The Docker tab was a long flat list (every group's header + all item rows always visible); now each compose-PROJECT group is a collapsed-by-default clickable row showing its logo/icon, name, item count, total size, and a green active dot when any resource is in use. Clicking reveals the project's items (accordion, one open at a time, like the Packages tab); typing in the search box expands all matching projects. The 'Other' groups (images-by-repo, build cache, orphans) are unchanged. Reuse done right: extracted a generic components/Accordion primitive (open-card wrapper, optional card prop) and retrofitted BOTH tabs onto it. Packages passes card (verified pixel-identical no-op); Docker omits it so its rows stay flat and the node_modules-style .cc-hl sliding highlight keeps working across headers + items. Two tested pure helpers (dockerGroupActive, projectRowExpanded). SAFETY UNCHANGED: presentation only, DockerItemRow + the typed-name volume confirm gate untouched. Whole-branch Opus review READY TO MERGE (0 Critical/Important; it traced the highlight offsetParent chain, the Packages no-op, and the safety gate); fixed 1 Minor (a residual em dash in the remove label). 282 tests green. Follow-up logged: an a11y pass to give the Row/PackageRow/DockerProjectRow family real aria-expanded disclosure + keyboard support. Pending your review + manual pass", status: "done" },
{ label: "M1h — Docker build cache → Caches tab + clearer deletion buttons", detail: "BUILT on feat/docker-project-grouping (spec + plan docs/superpowers/2026-07-14-docker-buildcache-*, subagent-driven TDD, 6 tasks + a final-review fix wave). You didn't like build caches listed inside the Docker tab, and the delete buttons didn't look like deletes. Fixed both. (1) groupDockerForDisplay no longer emits a build-cache group — build cache leaves the Docker list entirely (it still counts toward the Docker header gauge, since those bytes are real Docker disk) and shows up as one aggregated 'Docker build cache' row in the CACHES tab beside the pnpm store. Delete routes through the EXISTING requestDockerPrune('buildCache') (same license gate + confirm + toast; no new IPC/confirm path). The Caches tab was generalized from a hard-coded pnpm row into a list of 'live caches' with keyboard nav + Enter across them, and it auto-scans Docker on open when data is missing/stale (mirrors the Docker tab). (2) Docker bulk-prune buttons are now 'Delete unused images / dangling images / stopped containers / unused volumes' with a trash icon + red danger styling; the build-cache Delete is likewise danger-styled (CacheRow gained an opt-in danger + icon action variant); per-item unused-image/volume/container trash stays hover-revealed (already danger, like the node_modules rows). SAFETY UNCHANGED: relabel + relocation only — the typed-name volume confirm gate is byte-identical and build cache is still never individually removable (3-layer block verified). Whole-branch Opus review: 1 Important fixed — Caches Enter could fire a row that search had filtered off screen (now a tested selectedActionableCache() guard no-ops unless the selected row is visible + actionable; confirm-gated throughout, never a silent delete); also swept em dashes from the placeholder copy. 293 tests, typecheck/lint/build green. Pending your review + manual pass", status: "done" },
{ label: "M2 — npm / yarn / bun global caches", detail: "Same Caches section, after #3 lands", status: "planned" },
{ label: "M3 — per-project build outputs (.next, dist, …)", detail: "Behind a settings toggle", status: "planned" },
{ label: "M4 — beyond JS (venv, target/, DerivedData)", detail: "Only if scope grows", status: "planned" }
]
},
{
title: "Launch & marketing",
note: "Telling people it exists",
items: [
{ label: "Showcase video (Remotion, 1:1 loop)", detail: "10s square promo built in code (video/ → pnpm video:render → out/showcase.mp4): launcher fills with oversized node_modules, a one-click cascade clears them while the pixel gauge sweeps red→green and a 'Reclaimed 23 GB' badge counts up, ending on the brand card. Captions, no audio (add music). Edit live with pnpm video:studio", status: "done" },
{ label: "Landing page advertises the post-0.1.2 features", detail: "Built on feat/landing-v0.2-features (spec in docs/superpowers/specs/2026-07-01-…). Repositioned the site from 'node_modules cleaner' to 'Reclaim every byte your dependencies cost': new hero copy + a floating Packages mini-card in the hero scene, a new '04 — Every package' Features block with a faithful Packages-tab mock (unify / advisory / update pills), a refreshed 3×3 feature grid leading with Prune-the-store / real-vs-linked sizing / security advisories (3 new sprite icons), a Caches-tab Prune line + honest-sizing note in the lifecycle section, broadened How-it-works copy, a Packages nav anchor, and updated metadata + re-rendered OG card. Verified via Playwright at desktop + mobile; typecheck/lint/build green. Pending your review/merge", status: "done" },
{ label: "SEO fixes: robots, sitemap, canonical, JSON-LD", detail: "BUILT on feat/blog-seo (PR #34). robots.txt (allows all, hides /og) and sitemap.xml were both 404 in production — now generated routes with hourly revalidation. Canonical tags on every page against https://tidydisk.app; SITE_URL extracted to lib/site-url.ts. SoftwareApplication JSON-LD on the homepage (free scan + 19 euro offers), BlogPosting on articles. One conflict needs YOUR click: Vercel currently redirects apex→www while the canonical says apex (see userActions)", status: "done" },
{ label: "Blog with auto-publishing weekly articles (/blog)", detail: "BUILT on feat/blog-seo (PR #34). Markdown in site/content/blog/ + tested lib/blog pipeline (gray-matter/remark, date gating unit-tested incl. the midnight-UTC edge, malformed frontmatter fails the build). ISR everywhere (listing, article pages, sitemap, RSS revalidate hourly) so date-gated posts publish themselves — no deploys, no cron. 10 keyword-targeted articles (delete node_modules, why huge, pnpm store, find folders, npm cache, Mac dev checklist, npkill comparison, npm/yarn/pnpm disk, never rm -rf, monorepos), one per week 2026-07-04 → 2026-09-05, article 1 live at launch. RSS at /blog/rss.xml, Blog in nav/footer, landing aesthetic, TidyDisk team byline, no em dashes. 17 tests, Playwright-verified. MERGED (PR #34, 2026-07-04) — tidydisk.app/blog is live; articles release themselves weekly. Still yours: Vercel domain flip + Search Console (see userActions)", status: "done" },
{ label: "Landing vision refresh (hero, band, spacing, buy icon, OG)", detail: "BUILT on feat/landing-vision from your 2026-07-06 notes (spec + plan in docs/superpowers/). Hero repositioned on the tidy-disk aspiration you picked ('A tidy disk, without thinking about it.', node_modules gone from the hero, first appears in the band); statement-band pixel strips now converge red toward the center (right strip mirrored, unit-tested); feature-row gap 110→150px (96px stacked); buy CTAs swap the broom for your sparkles pick (Download + FinalCta; FeatureGrid keeps its broom); OG card re-captured with the new headline (you added it to scope). Bonus fixes: vitest now resolves the @/ alias, and all internal nav converted to next/link (lint was failing on main from the blog PR; CI does not run site lint). All gates + Playwright-verified. MERGED (PR #35, 2026-07-06) — tidydisk.app redeploys with the new hero + social card", status: "done" },
{ label: "Site Tailwind migration + component splits + repo cleanup", detail: "BUILT on feat/site-tailwind. landing.css (484 lines) + blog.css DELETED; everything is Tailwind v4 utilities over an @theme token block, PIXEL-VERIFIED: 12 reference screenshots (4 pages x 3 widths) compared with pixelmatch, all 0.000% except the one deliberate change (see below). New primitives: Btn, Wrap, Eyebrow, SectionHead, ui-mock kit (GlassPanel/UiRow/Pill/...); Features split into FeatureRow + 4 visual components, HeroScene extracted; unused shadcn stack removed (4 deps, ui/button, components.json). REAL BUG FOUND: the feature-row spacing has always rendered 0px (the CSS gap sat on an element with one child), so PR #35's 110-to-150px change was a no-op; the gap is now real (150px/96px). Cleanup done: handoff bundle + template svgs deleted (tracked), stray .playwright-mcp/dist/out junk removed keeping showcase.mp4, 10 provably-merged branches pruned local+remote (only main + feat/launch-assets remain). MERGED to main (PR #36, 2026-07-07); tidydisk.app redeploys on Tailwind", status: "done" },
{ label: "i18n (en/fr/es/de/pt) + godlike SEO / Lighthouse hardening", detail: "BUILT on feat/site-i18n-seo (stacked on the Tailwind branch) via subagent-driven development: 6 parallel Sonnet agents translated the site + all 10 blog articles into fr/es/de/pt (~40k words) while an Opus agent self-hosted the fonts; the routing/refactor and final review ran on Opus. Result: English unprefixed at /, fr/es/de/pt under /[locale] (no middleware, no auto-redirect); typed per-locale Dictionary (tsc enforces every field), shared HomePage/BlogIndex/BlogArticle components render both trees; hreflang + x-default on every page AND in the sitemap; per-locale RSS + localized JSON-LD (inLanguage) + localized dates; footer language switcher. Fonts now self-hosted via next/font (zero render-blocking CDN font requests). English body PIXEL-IDENTICAL (only the footer switcher differs). A build-time parity test fails if any locale's article slugs/dates drift. 33 tests green; a whole-branch Opus review found 1 Critical (/en RSS leak) + 1 Important (locale anchor slashes) + minors, ALL FIXED and re-verified. MERGED to main (PR #38, 2026-07-07) — tidydisk.app now serves en + /fr /es /de /pt with hreflang, per-locale RSS, and self-hosted fonts. Follow-ups tracked in userActions", status: "done" },
{ label: "Google Analytics + consent banner, Vercel Analytics dropped", detail: "On feat/site-analytics-consent (PR). Swapped @vercel/analytics for GA4 (G-0DW5WJLY1M via @next/third-parties), consent-gated: a new CookieConsent component in the root layout shows a localized (en/fr/es/de/pt) bottom banner and renders the GoogleAnalytics tag ONLY after the visitor clicks Accept, so no gtag script and no cookies load beforehand; Decline loads nothing; the choice persists in localStorage. Also fixed the favicon: the default Vercel favicon.ico was overriding the brand icon via Next's file convention, so it is deleted and replaced by app/icon.svg (the coral module-cube logo), and the redundant inline FAVICON metadata dropped. Verified end-to-end against a production build in a real browser: banner renders in-theme, zero GA requests before consent, gtag + a page_view fire on Accept, head links rel=icon to /icon.svg. typecheck + build green", status: "done" },
{ label: "Legal pages: privacy policy + contact/imprint (selling compliance)", detail: "BUILT on feat/site-analytics-consent (PR #44), all 5 locales. /privacy covers GA (consent-based), the app's PostHog telemetry (anonymous, opt-out), Polar (payments/merchant of record) and Vercel (hosting), with legal bases, retention, international transfers, and GDPR data-subject rights incl. the CNIL. /legal is the EU imprint: responsible person Ahmed ABOUELLEIL SAYED, address, contact@tidydisk.app, SIRET 90884903700027, plus MoR/refunds and French governing law. Factual identity lives in one constants file (untranslated), injected by the page component; long-form prose is a separate typed legal module translated by 4 parallel agents. Wired into a new footer Legal column + the sitemap + a privacy link in the cookie banner. Also fixed the reviewed GDPR Art. 7(3) gap: a footer 'Cookie preferences' control reopens the banner to change/withdraw consent, and withdrawing clears GA cookies. Verified in a real browser: EN + FR pages render, imprint details intact, consent withdrawal reopens the banner. typecheck + build green (12 new routes)", status: "done" }
]
},
{
title: "TidyDisk — monetization & first €1,000",
note: "Reposition + charge: free scan, paid clean",
items: [
{ label: "Direction decided: rename to TidyDisk + free-scan/paid-clean", detail: "Reposition from a free 'node_modules cleaner' to TidyDisk (tidydisk.app) — 'see how much disk your dev junk costs, reclaim it in one click'. Model: the scan stays free (the marketing engine), the Clean action becomes a paid lifetime unlock. Price €19 founding → €29 → €39 once Docker/Xcode/build cleanup ships. Goal: first €1,000 (≈ 35–53 sales)", status: "done" },
{ label: "Go-to-market plan (interactive)", detail: "TIDYDISK-LAUNCH.html at repo root: on-brand launch dashboard with a live price→sales calculator, pricing ladder, offer stack, competitor table, copy-paste Show HN / Reddit drafts, and a persistent 2–4 week launch checklist", status: "done" },
{ label: "Monetization + license-gate spec", detail: "docs/superpowers/specs/2026-07-01-tidydisk-gtm-and-monetization-design.md — positioning, offer, pricing, launch plan, and the one code slice: a license gate on the destructive actions", status: "done" },
{ label: "License gate (free scan / paid clean)", detail: "BUILT — feat/license-gate, PR #30. New src/main/license/*: offline-verifiable Ed25519-signed keys (TIDY-<payload>.<sig>), LicenseStore that re-verifies the signature on every load (hand-editing license.json can't unlock), enforcement inside the destructive IPC handlers (unlicensed delete/prune refuse with zero side effects), UnlockPrompt paywall in launcher + panel, Settings License row (Buy + key activation, live cross-window via license:changed), and pnpm license:issue <email> local issuance (private key gitignored; public key bundled). 10 commits, 12 new tests (134 total), typecheck/lint/build green; every task subagent-implemented via TDD + independently reviewed; final whole-branch review: ready to merge, zero Critical/Important. BUY_URL is a placeholder until #24. Awaiting your review + manual pass", status: "done" },
{ label: "Rework: Polar-validated keys + PostHog analytics", detail: "Approved addendum (docs/superpowers/specs/2026-07-03-polar-licensing-and-analytics-design.md) reworks the unmerged PR #30: Polar's native License Keys benefit (instant automated delivery) validated in-app with a 30-day offline grace + 7-day background revalidation, replacing the offline Ed25519 keys / manual issuance (deleted). Plus PostHog EU analytics: anonymous install UUID identified at activation, ~9 funnel events (scan→paywall→buy→activate), on-by-default with a Settings opt-out, no capture in dev. Privacy-absolutist copy claims retired (tracked in #26). BUILT: 8 commits on feat/license-gate (5 plan tasks, each subagent-implemented TDD + independently reviewed, trust-core task on the strongest tier), 148 tests, typecheck/lint/build green, real BUY_URL + org id + PostHog key bundled. Final delta review: ready, zero Critical; its one Important (paying customer 31+ days offline saw the Buy pitch) fixed with spec-mandated reconnect framing (needsReverify). PR #30 updated. RELEASE GATE: #26 must land before users get a build (README/site still claim no telemetry)", status: "done" },
{ label: "Rename app + landing to TidyDisk (#26) + honest copy", detail: "DONE on feat/tidydisk-rename (stacks on PR #30 + the landing branch, merged together here). App: productName/appId/artifactName + every user-visible string renamed; README rewritten (free scan, 19 euro founding lifetime, analytics + weekly license-check disclosure). Site: full rename, honest FeatureGrid lead, Download section rebuilt as $0-scan / featured-EUR19-Pro cards wired to the Polar checkout, FinalCta Buy CTA, tidydisk.app metadata + re-rendered OG card. Every user-visible em dash swept app-wide. Verified via Playwright at 1280 + 390. This LIFTS the release gate once merged. Deferred: glyph evolution, video/ assets (#28), GitHub repo rename (your call)", status: "done" },
{ label: "Payments — Merchant of Record (Polar)", detail: "Polar (polar.sh) checkout — MoR handles EU VAT. €19 one-time founding lifetime product with Polar's native License Keys benefit (instant delivery; manual issuance retired). Switched from Lemon Squeezy after their account rejection (2026-07-03); product description + images prepared (polar-product-1200x630/1200.png)", status: "user" },
{ label: "Share card — the screenshot-worthy scan result (#27)", detail: "BUILT on feat/share-card (PR #32). Post-scan 'Your damage report' reveal in the launcher (count-up total, reduced-motion aware) + header share icon; one-click Copy as image renders a 1200x675 card at 2x in a hidden offscreen window straight to the clipboard (dark glass, pixel meter, huge GB number, TidyDisk + tidydisk.app footer; numbers only, never project names). New share_card_copied funnel event (not renderer-forgeable). Plus pnpm demo / --restore: seeds the scan caches with a curated ~247 GB dataset for marketing captures, byte-identical restore. 8 new tests (156 total); final review on the strongest tier came back clean after 4 one-line fixes. MERGED (PR #32, LGTM'd 2026-07-04)", status: "done" },
{ label: "Launch push (HN / Reddit / PH / dev.to / X)", detail: "One concentrated Tue–Thu wave; reuse the Remotion showcase clip as the launch asset", status: "planned" }
]
},
{
title: "Quality & confidence",
note: "How much you can trust it without watching it",
items: [
{ label: "Unit tests — formatting & color math", detail: "vitest covers lib/format and lib/colors", status: "done" },
{ label: "CI on every PR", detail: "typecheck + tests + build + unsigned macOS packaging", status: "done" },
{ label: "Unit tests — scanner & stores", detail: "PR #3 adds du-parsing and pnpm-resolution tests; detect-kind, skip-rules and the stores are still uncovered", status: "planned" },
{ label: "Readability: design tokens across the renderer", detail: "From the June 16 senior review (verdict: sound architecture, hard to read due to inline-style walls). Color design tokens (CSS vars) now replace ~100 hand-typed rgba() literals across global.css + all renderer components/shells", status: "done" },
{ label: "Linter + CI hardening (Biome)", detail: "Biome format+lint adopted (style-matched config), wired into CI; the previously-orphaned video/ project now typechecks in CI too. Real findings fixed; a11y rules deferred as a tracked pass", status: "done" },
{ label: "Main-process hardening", detail: "IPC setSetting now validates the renderer payload (was cast as never); lifecycle teardown on quit; scanner shares its in-flight run; deduped GB/DAY constants. + validation/detect-kind/settings-store tests (48 total)", status: "done" },
{ label: "Shell decomposition", detail: "Started: extracted DiskSummary from PanelApp (345→257 lines). Remaining — LauncherApp's keyboard/effect hooks — deferred to an interactive session (needs the running app to verify behavior)", status: "progress" },
{ label: "Storybook design system for app + site", detail: "Two Storybooks: root (react-vite) documents all app components with a dark panel decorator, Intro + live Tokens docs; site/ (nextjs-vite) documents the site components against the Tailwind v4 tokens. Every component folder now requires a .stories.tsx covering its meaningful visual states; a stories-coverage.test.ts in each tree fails pnpm test on an undocumented component, with an explicit reviewable EXEMPT list for purely behavioral ones. CI builds both books on every PR alongside a new site typecheck/test job. CLAUDE.md documents the convention. MERGED to main (PR #48, 2026-08-03)", status: "done" },
{ label: "Manual end-to-end pass", detail: "Real tray, real deletion, real notifications — only you can judge this (see 'In your court')", status: "user" },
{ label: "Test coverage: CI-enforced ratchet + renderer hooks + high-risk main-process tests (deletion, scanner, scheduler, notifier)", detail: "BUILT on feat/test-coverage: vitest 3 (electron-vite pins vite 5, so vitest 4 is out) + @vitest/coverage-v8 split into node/renderer projects; a typed window.clean bridge mock (mock-clean-bridge.ts, compile-checked against CleanApi) backs new tests for all 11 renderer hooks; main-process backfill covers project-actions deletion safety, the scanner over a real fixture tree, the scan scheduler + threshold notifier, and small pure utils. Coverage thresholds now ratchet (auto-bump on pnpm test:coverage, CI fails below them, never hand-lowered): statements 75.19% / branches 88.52% / functions 85.96% / lines 75.19%, up from a 58.61% line baseline. 70 test files / 414 tests. UI (.tsx) intentionally untested here (Storybook's job) and E2E deferred per spec. MERGED to main (PR #47, 2026-08-03)", status: "done" }
]
},
{
title: "Ideas (not committed)",
note: "Future possibilities — need your vision call before any work starts",
ideas: true,
items: [
{ label: "Ignore list / pinned projects", detail: "Protect specific projects from 'Clean stale'", status: "planned" },
{ label: "Size shown in the menu bar itself", detail: "Tray title text next to the icon", status: "planned" },
{ label: "Auto-clean rules", detail: "e.g. auto-trash anything stale beyond 6 months", status: "planned" }
]
}
],
/* Things ONLY the user can do. Claude removes/updates these as they get done. */
userActions: [
{ id: "proofread-landing-repositioning", category: "review", label: "Native-speaker proofread of the new landing copy (fr/es/de/pt)", detail: "The landing-page repositioning ('all developer disk waste') added new fr/es/de/pt copy: the widened hero subhead + statement band, the four-area section, the Docker feature row, and the broadened meta description. It is high-quality machine translation, structurally validated (typecheck enforces the Dictionary shape across all 5 locales, JSX structure preserved, no em dashes, technical tokens kept), but not human-proofread. Before leaning on the non-English pages for marketing, have a native speaker skim each locale's home for tone/idiom. Copy lives in site/lib/i18n/dictionaries/<locale>.tsx." },
{ id: "release-110-publish-draft", category: "external", label: "Test the v1.1.0 draft dmg, then publish it to promote to latest", detail: "The Apple agreement cleared and the v1.1.0 release build succeeded on the third try (2026-07-20): signed + notarized, a DRAFT GitHub release now holds tidydisk-arm64.dmg + the auto-update zip/latest-mac.yml. It is NOT the latest release yet (draft, so existing users are not auto-updated). Download the draft dmg, install and run it for a bit to confirm the Docker features + the read-only panel behave on a real machine, then Publish the release on GitHub to promote v1.1.0 to latest so users get it via auto-update." },
{ id: "qa-panel-dashboard", category: "testing", label: "Manual pass of the new menu bar panel", detail: "Needs a running app + a Docker daemon (CI has neither). Open the popover: confirm the 'Tracked on disk' hero and the four rows (Projects / Caches / Packages / Docker), and that clicking each row opens the main window on THAT tab (Cmd-, still opens Settings). Confirm the popover opens instantly with a cold Docker cache (numbers fill in a moment later, they never block the open). Turn Docker off in Settings: its row disappears, its bytes leave the hero, and the hero reads 'across 2 areas'. Before ever visiting the Packages tab, its row should read 'Not checked yet' rather than claiming 'all clear', and clicking it should compute the inventory there. Finally, delete a project's node_modules in the launcher and confirm the panel's Projects row and hero both move." },
{ id: "paywall-surface-decision", category: "vision", label: "Decide where the paywall lives now the panel has none", detail: "The panel rework made the popover read-only, which removed all three paywall_shown triggers it used to fire (clean_stale, prune, affordance). The launcher is now the only paid-conversion surface in the app. Decide whether that is fine or whether the launcher needs a stronger paywall moment before this ships; the spec deliberately ruled this out of scope rather than guessing." },
{ id: "review-docker-pr", category: "review", label: "Review the Docker cleanup PR", detail: "feat/docker-cleanup — new ⌘4 Docker tab: list + per-item remove + bulk prune of images/volumes/containers/build-cache, Pro-gated, with a typed-name confirmation for volumes (permanent, no Trash). Whole-branch Opus review found no wrongful-delete path. Skim the tiered confirmation UX and merge when happy." },
{ id: "test-docker-daemon", category: "testing", label: "Manual pass of Docker cleanup against a real daemon", detail: "The unit tests mock the docker CLI (no daemon in CI), so the real behavior needs your eyes: with Docker running, open the launcher → ⌘4. Confirm images/volumes/containers/build-cache list with real sizes + in-use/unused badges; as a free user a remove shows the paywall; as Pro, removing an unused image works and the number updates; a VOLUME removal makes you type its name first; in-use items have no Remove button; the per-category Prune buttons work. Also check the 'Docker not running' state (quit Docker Desktop) and the Settings toggle hides the tab. NEW (project-grouping, PR stacked on #40): with a running docker-compose project, confirm its resources group under one header with the project's real logo, that an image its container uses appears under that project, the type badges + Size/Name/Recent sort tabs + filter chips work, and the 'Not linked to a project' section holds pulled images (by repo) + build cache." },
{ id: "proofread-translations", category: "review", label: "Native-speaker proofread of the fr/es/de/pt copy", detail: "The 4 locales (landing + 40 blog articles) are high-quality machine translations, structurally validated (typecheck + parity test + em-dash sweep) but not human-proofread. Before promoting the locale URLs in Search Console, have a native speaker skim each locale's homepage + a couple of articles for tone/idiom. Content lives in site/lib/i18n/dictionaries/<locale>.tsx and site/content/blog/<locale>/." },
{ id: "lighthouse-verify", category: "testing", label: "Run Lighthouse on the deployed i18n preview", detail: "Once feat/site-i18n-seo has a Vercel preview: run Lighthouse (desktop) on /, /blog, and one article and confirm the scores. Fonts are now self-hosted (no render-blocking CDN requests) and the a11y/hreflang work is done, so the 90s-100s should be reachable; report anything that isn't so it can be tuned." },
{ id: "vercel-primary-domain", category: "external", label: "Flip the Vercel primary domain to tidydisk.app", detail: "Today tidydisk.app 308-redirects to www.tidydisk.app, but the site's canonical/OG/sitemap URLs all say https://tidydisk.app — conflicting signals for Google. In Vercel: Project → Settings → Domains → set tidydisk.app as the primary domain (www.tidydisk.app then redirects to it). No code change needed." },
{ id: "gsc-sitemap", category: "external", label: "Add tidydisk.app to Google Search Console + submit the sitemap", detail: "After PR #34 deploys: verify the domain property in search.google.com/search-console (DNS TXT record via your registrar), then submit https://tidydisk.app/sitemap.xml. This is what gets the blog articles indexed as they release each week." },
{ id: "publish-release-100", category: "external", label: "Publish the v1.0.0 draft release 🚀", detail: "The v1.0.0 tag fired .github/workflows/release.yml: a macOS runner builds, signs and notarizes the FIRST TidyDisk build (tidydisk-arm64.dmg) onto a DRAFT release. When the run is green, open github.com/AhmedFr/clean-my-node-modules/releases, sanity-check assets + notes, and click Publish. The site's Download buttons point at releases/latest/download/tidydisk-arm64.dmg and resolve the moment you publish." },
{ id: "verify-ga-live", category: "external", label: "Confirm GA4 is receiving data on the live site", detail: "GA property G-0DW5WJLY1M is wired into the site behind a consent banner. After feat/site-analytics-consent deploys: open tidydisk.app, Accept the cookie banner, then check Google Analytics → Reports → Realtime and confirm your visit shows up." },
{ id: "legal-review", category: "vision", label: "Optional: have a professional skim the legal pages", detail: "The /privacy and /legal pages are built with your real details (name, address, contact@tidydisk.app, SIRET 90884903700027) and cover GA/PostHog/Polar/Vercel + French governing law. The copy is solid and standard, but it was written by an AI, not a lawyer. If you want certainty before launch, have an accountant or lawyer skim the privacy policy and the refunds/withdrawal wording. Two content notes: the non-English pages are machine-translated (same caveat as the rest of the site), and if you are NOT VAT-registered you may want a 'TVA non applicable, art. 293 B du CGI' line added." },
{ id: "e2e-dev-pass", category: "testing", label: "Run the app and do a real pass", detail: "pnpm dev — open the tray dropdown, check sizes look right, try the launcher (⌘O), search, keyboard nav." },
{ id: "test-real-vs-linked", category: "testing", label: "Sanity-check the new real vs linked sizes", detail: "pnpm dev — a pnpm project should show a small REAL number with a muted 'X linked' line (the .pnpm store-backed content); a plain npm project should show real ≈ apparent. Hover a size for the tooltip; check the panel total's '+ X linked in pnpm store' line and the delete-confirm 'frees …' wording." },
{ id: "test-installed-prune", category: "testing", label: "Reinstall v0.1.2 and confirm pnpm prune now shows", detail: "Download the new DMG (releases/latest/download/clean-my-node-modules-arm64.dmg), replace the installed app, and check the menu-bar panel + launcher Caches tab: the pnpm store row should now be live with a Prune action (was hidden/greyed before, the bug this release fixes)." },
{ id: "test-packages-tab", category: "testing", label: "Click through the new Packages tab", detail: "pnpm dev → ⌘O → ⌘3 (Packages). Confirm: packages list with 'N projects' + version(s), a 'unify' badge when versions diverge, size per package, an 'update available' badge, and a severity pill on any advisory. Cross-check one: installed version vs cat node_modules/<pkg>/package.json, latest vs `npm view <pkg> version`. Sort by Used/Size/Name/Updates; search filters; Enter opens npm. Toggle 'Check npm…' off in Settings (or go offline) → local data still shows with an offline note. Click a row (or ↵) to expand it — a panel lists each in-use version with its per-version severity and which projects are on it; ⌘↵ opens npm, esc collapses. Check the meta bar shows 'Scanned <time> ago' + a Re-scan button (and ⌘R) that recomputes. On launch, watch a 'pnpm' pill with a spinner appear in the footer + a ghost shimmer sweep over the unfilled part of the header gauge while the pnpm store is still being sized, then settle on the real total." },
{ id: "test-trash-delete", category: "testing", label: "Verify deletion really goes to Trash", detail: "Delete a node_modules you don't care about, then check the Trash. This is the one destructive path — it deserves your own eyes." },
{ id: "test-notification", category: "testing", label: "Check the threshold notification fires", detail: "Set the GB limit below your current total in Settings and wait for / trigger a scan." },
{ id: "test-packaged-app", category: "testing", label: "Try the packaged app", detail: "pnpm package, then launch the .app from out/ (right-click → Open, it's unsigned). Does the tray behave the same as in dev?" },
{ id: "ext-share-video", category: "external", label: "Post the showcase video on socials", detail: "out/showcase.mp4 (1080×1080, 10s loop). Add a music track, then post to X / Reels / TikTok / LinkedIn. Re-render or tweak captions any time with pnpm video:studio." },
{ id: "tidydisk-domain-tm", category: "vision", label: "Lock the TidyDisk name: domain + trademark + npm", detail: "Before any public rename, confirm tidydisk.app is available (and grab it), check there's no conflicting trademark, and that the npm package name is free. This gates the rename." },
{ id: "tidydisk-payments", category: "external", label: "Open the Polar account + create the €19 product", detail: "Polar (polar.sh) — merchant of record, handles EU VAT (Lemon Squeezy rejected the account). Create 'TidyDisk — Lifetime License' as a €19 one-time fixed-price product with the prepared description + image (polar-product-1200x630.png / -1200.png at repo root) and the native License Keys benefit (instant delivery). Then send me: the checkout URL (→ BUY_URL) and the organization ID (→ in-app key validation). Checklist: issue #24." },
{ id: "posthog-project", category: "external", label: "Create the TidyDisk PostHog project (EU)", detail: "New project on eu.posthog.com named 'TidyDisk'. Send me the project API key (phc_…) — it gets bundled in the app for the launch funnel (scan → paywall → buy → activate). Free tier covers launch volume." },
{ id: "verify-funnel-live", category: "testing", label: "Verify the funnel end to end on the published build", detail: "Once v1.0.0 is published: download tidydisk-arm64.dmg from the site like a real user, run the funnel (scan, hit the paywall, Buy click, activate a real key), and confirm every event lands in the TidyDisk PostHog EU project with your email identified after activation. This is the last dress rehearsal before the launch wave (#29)." },
{ id: "test-license-gate", category: "testing", label: "Manual pass of the license gate (after the Polar rework)", detail: "On feat/license-gate once the rework lands: pnpm dev → as a free user, try deleting a project (launcher ⌘⌫ and panel trash icon), 'Clean N stale', and the pnpm Prune — each should show the unlock bar, mutate nothing. Then activate a Polar sandbox key in launcher Settings → row flips to Pro ✓, panel updates live, delete/prune actually work. Revoke the sandbox key in Polar → relaunch → app drops to Free. Esc dismisses the unlock bar everywhere; funnel events should appear in the PostHog project." },
{ id: "test-external-drives", category: "testing", label: "Test external-drive scan coverage (needs a physical drive)", detail: "On feat/scan-coverage-and-liveness: pnpm dev, plug in an external drive that has a project with node_modules on it. Settings → Scan locations: the drive should appear as a toggle; turn it on, rescan (⌘R), and its projects should show up in the list. Also try 'Add folder…' on a normal directory. Unplug the drive and rescan → its projects drop off; replug + rescan → they return. Deleting a project whose drive you just unplugged should refuse cleanly, not error." },
{ id: "test-live-indicator", category: "testing", label: "Test the live/running indicator + delete guard", detail: "On feat/scan-coverage-and-liveness: pnpm dev, then start a dev server inside one of your scanned projects (e.g. pnpm dev in some app). Open the launcher (⌘O) / tray panel: that project should show a green dot with a tooltip naming the process (and :port if it listens), and its delete button should be disabled ('This app is running'). Stop the server, wait up to 45s or reopen the window → the dot clears and delete re-enables. Bonus: with the server running, confirm a panel 'Clean N stale' that includes it does not delete it (main refuses)." },
{ id: "test-tab-headlines", category: "testing", label: "Check the per-tab headline gauges + Docker auto-scan", detail: "On feat/docker-project-grouping: pnpm dev -> open the launcher (Cmd-O), switch tabs, and confirm the header data-viz changes per tab: Projects shows the node_modules size-vs-limit gauge (unchanged); Caches shows a gauge against the pnpm cache limit; Docker shows a gauge against the Docker limit; Packages shows the severity meter (stacked crit/high/mod/low + outdated, hover for the full breakdown). Settings -> Scanning -> Storage limits: change the pnpm cache limit and the Docker limit and watch those gauges refill. Confirm Packages shows NOTHING (not a green 'all clear') when 'Check npm for updates & advisories' is off or you are offline. Open the Docker tab cold and confirm it auto-scans; switch away and back within 5 min and confirm it does NOT rescan; Cmd-R still forces a refresh. NOTE: the main-process settings/docker changes need a full app restart (pnpm dev hot-reloads only the renderer)." },
{ id: "test-auto-update", category: "testing", label: "Manually test the auto-update flow with a signed build (PR #46 merged)", detail: "CI (typecheck/lint/tests/build + unsigned packaging) can't exercise electron-updater against a real signed release. Build a signed version + a fake higher version (pnpm release twice, bumping package.json in between), publish the higher one as a draft → publish as a FULL release (NOT a prerelease — electron-updater ignores prereleases by default, a prerelease would look like the feature is broken), install the lower one in /Applications. Confirm the panel banner appears within ~10s of launch with the right version/size/date; its ✕ hides it and it stays hidden after a restart while Settings → Updates still shows the update; clicking the banner opens the launcher on Settings → Updates; Download shows live percent and Restart and install relaunches on the new version; update_installed fires on that relaunch (PostHog); and running from ~/Downloads (translocated) shows the friendly 'move to Applications' message with no crash." },
],
log: [
{ date: "2026-06-10", text: "Built the entire app from the Claude Design handoff: scanner, tray panel, launcher window, settings, scheduler, notifications, safe-delete. Verified in dev via Playwright." },
{ date: "2026-06-11", text: "Added this status dashboard (STATUS.html) + CLAUDE.md rule to keep it updated every session." },
{ date: "2026-06-11", text: "Drafted GitHub issues: #1 business model options (rec: OSS + paid signed binary), #2 prioritized package-manager coverage plan (pnpm store first). Both await your decisions." },
{ date: "2026-06-12", text: "You decided: OSS + $9 signed binary, repo public. Shipped: clean history pushed, repo made public, CI on PRs, signing scaffolded (docs/SIGNING.md), MIT LICENSE, PR #3 (pnpm store prune), PR #4 (landing page). Your court: review both PRs, Apple credentials, Lemon Squeezy product." },
{ date: "2026-06-14", text: "Launcher list readability: list now measures a real row and caps at ~7 rows across densities (4px gap + kept y-padding), long paths truncate with an ellipsis instead of overrunning the size column, and the window is clamped to the screen so the footer no longer clips." },
{ date: "2026-06-15", text: "Built a Remotion showcase video (video/, square 1080² 10s loop): the launcher fills with oversized node_modules, a one-click cascade clears them while the pixel gauge sweeps red→green and a 'Reclaimed 23 GB' badge counts up, ending on the brand card. Rendered to out/showcase.mp4; edit live with pnpm video:studio." },
{ date: "2026-06-15", text: "Merged everything to main: PR #3 (pnpm store prune), #4 (landing page), #5 (7-row launcher list), #6 (showcase video). main is now the integrated state." },
{ date: "2026-06-15", text: "Three enhancements: panel over-limit now a two-line no-wrap stack (GB limit / GB over) so the narrow menu-bar panel never reflows; smarter project names (generic folders qualified with repo/parent); landing page gained a 'node_modules lifecycle' section with an npm-vs-pnpm duplication diagram. Apple notarization still stuck in a multi-hour backlog on Apple's side." },
{ date: "2026-06-16", text: "Apple notarization finally cleared — all submissions Accepted (~13h Apple-side backlog). Did a senior architectural review (verdict: sound architecture, readability hurt by inline-style walls) and shipped readability pass 1: color design tokens (CSS vars) across global.css + both app shells." },
{ date: "2026-06-16", text: "Worked through the full review remediation: color tokens across the whole renderer, main-process hardening (validated IPC, lifecycle teardown, in-flight scan dedup), Biome lint+format wired into CI (with video typecheck), tests up to 48, and started the shell decomposition (DiskSummary out of PanelApp). LauncherApp's hook extraction left for an interactive (running-app) session." },
{ date: "2026-06-17", text: "Fixed three launcher-window UX bugs: footer no longer clipped (auto-height now measures offsetHeight, immune to the open animation's scale transform); window dismisses on blur like Spotlight; and it's kept alive (hide, not destroy) so reopening is instant with no gray/white flash. Reopen resets to a clean Projects search. On branch feat/launcher-caches-tab." },
{ date: "2026-06-17", text: "Added a Projects / Caches tab to the launcher (brainstormed + spec'd in docs/superpowers/specs/): the Caches tab surfaces the global pnpm store with a one-click safe Prune (⌘1/⌘2 to switch, ↑↓ + ↵ to prune), and shows npm/yarn/bun as disabled 'soon' rows — the launcher home for issue #2. Reuses the existing usePnpmStore/Segmented; new CacheRow + CachesView. Tests at 50." },
{ date: "2026-06-18", text: "Shipped first-run onboarding + honest states (brainstormed with the visual companion, spec + plan in docs/superpowers/): guided 2-step setup on first launch, a 10-block pixel-stepper alert threshold replacing the range slider in both settings, an explicit scan 'Off', truthful empty/scanning/Finish-setup states with a subtle Tailwind-style ping (PingBadge), and a persisted 'onboarded' flag. New PixelStepper/PingBadge/Onboarding/PanelEmpty; tests at 56. Branch feat/onboarding-empty-states." },
{ date: "2026-06-18", text: "Fixed a macOS Spaces bug: the panel (created at launch) and launcher (created on first ⌘O) were bound to their creation Space, so clicking the tray from another desktop forced a switch. setVisibleOnAllWorkspaces(canJoinAllSpaces) + skipTransformProcessType makes them appear on the active Space with the dock-hidden process intact." },
{ date: "2026-06-18", text: "Packaging polish (PR #13): generated the .app icon from the cube glyph (scripts/make-icon.mjs reuses the tray glyph-raster + png-encode) so it matches the in-app AppIcon, and fixed 'pnpm release' — scripts/release-mac.sh now loads .env.signing and passes --publish never (was crashing on missing GH_TOKEN). You confirmed a working signed + notarized release. Next up: a GitHub Actions release pipeline (tag → signed DMG → GitHub Releases), then rebuilding the landing as a Next.js + shadcn app on Vercel with the paid tier dropped for now." },
{ date: "2026-06-18", text: "Built the GitHub release pipeline (spec + plan in docs/superpowers/): .github/workflows/release.yml fires on a vX.Y.Z tag → a macOS runner builds, signs + notarizes the arm64 app and uploads the DMG/ZIP + latest-mac.yml to a GitHub draft release (build.publish github/draft, via the built-in GITHUB_TOKEN). Release cycle: 'pnpm version patch && git push --follow-tags'. docs/SIGNING.md documents the 5 repo secrets. Branch feat/release-pipeline." },
{ date: "2026-06-19", text: "Hero now shows the REAL app UI instead of the fake mockup: recreated the live menu-bar panel as a faithful React component (AppPanel — DiskSummary limit/over stack, real pixel meter, project rows, Clean-stale CTA, pnpm Caches row, Quit; renderer design tokens scoped) and slotted it into the hero scene. Added a social-share image: a 1200×630 OG card (app/og + scripts/make-og.mjs render it to public/og.png) wired into Open Graph + Twitter metadata with a Vercel-auto-resolving metadataBase. Branch nextjs-shadcn-landing-free-tier." },
{ date: "2026-06-19", text: "Rebuilt the landing page as a Next.js 16 + TypeScript app (site/) — a faithful port of the original static design (landing.css kept verbatim; pixel meter, scroll-reveal and hero-tilt ported to React; shadcn configured, CTAs kept as the bespoke .lp-btn anchors). Removed the paid tier: the #pricing section is now a free & open-source 'Download' section (two $0 cards) and every $9/buy/Lemon-Squeezy mention is gone — CTAs point to releases/latest/download/clean-my-node-modules-arm64.dmg, and the release dmg artifactName was made stable to match. Verified faithful via a DOM class-skeleton diff (340 nodes, identical). Spec + plan in docs/superpowers/. Branch nextjs-shadcn-landing-free-tier. Your court: deploy site/ to Vercel + publish a release so the Download link resolves." },
{ date: "2026-06-19", text: "Fixed the 'pnpm store prune is missing/soon in the installed app' bug. Root cause: the feature shipped fine, but at runtime store.available came back false — Homebrew's pnpm is a '#!/usr/bin/env node' script and a Finder-launched app inherits only the bare launchd PATH, which has neither nvm's node nor Homebrew's bin, so 'pnpm store path' died with 'env: node: No such file or directory'. The binary was found but couldn't run; the panel then hid the Caches row entirely and the launcher greyed it like the 'soon' placeholders. Fix (fix/pnpm-path-detection): new find-node.ts resolves a usable node (PATH → nvm newest-first → volta/asdf/PNPM_HOME → Homebrew) and prepends its dir to PATH when spawning pnpm. Reproduced the failure and the fix under a simulated GUI env; tests at 61. Shipped in v0.1.2." },
{ date: "2026-06-20", text: "Added an in-app Uninstall button to Settings (feat/uninstall-button): a destructive red row that pops a native macOS confirm dialog then does a clean uninstall — moves the .app bundle to the Trash (resolved from app.getPath('exe'), skipped when unpackaged in dev) and removes the userData dir (settings.json + projects-cache.json) before quitting. New single-responsibility src/main/actions/app-actions.ts (uninstallApp), wired through the existing 3-layer IPC pattern (app:uninstall constant → preload bridge → ipcMain.handle). No login items to deregister. typecheck/build/61 tests all green." },
{ date: "2026-06-21", text: "Investigated 'launcher won't open in pnpm dev / seems to crash'. Reproduced with ELECTRON_ENABLE_LOGGING: no crash — main process healthy, renderer mounts with zero JS errors, and the launcher renders its full UI (confirmed by screenshot). Root cause is the intended Spotlight-style hide-on-blur (launcher-window.ts): in dev the window auto-opens but the terminal keeps focus, so it blurs and hides immediately, reading as a crash. Decision: leave behavior as-is; open the launcher from the tray panel during dev. No code change." },
{ date: "2026-06-23", text: "Made node_modules sizing honest about pnpm (real vs linked storage, branch feat/real-vs-virtual-storage). First implemented the approved plan — a per-file hard-link-count walker on a worker thread — then verification against the real 1086 MB node_modules exposed that it's wrong on the target platform: pnpm on macOS APFS clones files copy-on-write (nlink==1), and Apple exposes no way to tell a clone from a copy, so nlink/du can't see the sharing (it read 3.5% shared when 73% is store-backed). Re-planned and researched, then pivoted to a structural signal: measureNodeModules sizes node_modules and its .pnpm subtree with du, so REAL freeable = apparent − .pnpm and LINKED = .pnpm. Dropped the walker/worker/pool + the vite worker entry (simpler, faster, no native code). Added Project.uniqueSize (cache back-filled on load); scanner populates it; SizeViz/MiniRow show 'X · Y linked' + tooltip; Launcher/Panel totals = Σ real + pnpm store (each package counted once) with a 'linked in pnpm store' line; delete-confirm now says what actually frees. TDD on measureNodeModules; typecheck/lint/63 tests/build green; app boots clean in dev." },
{ date: "2026-06-24", text: "Addressed CodeRabbit review on PR #19: narrowed pnpmStoreBackedSize's stat() catch so only a genuinely missing path (ENOENT/ENOTDIR) counts as 'not pnpm-managed' — real I/O failures (EPERM/EIO/EMFILE) now propagate instead of being swallowed as 0 shared, which would overstate freeable bytes. The throw is safe: buildProject already wraps measureNodeModules in try/catch and drops the affected project rather than crashing the scan. Added two TDD tests (doMock-isolated) covering the missing-path and propagate branches. typecheck/lint/65 tests green." },
{ date: "2026-06-24", text: "Made pnpm store detection a no-fail zone (branch feat/pnpm-store-robustness). Layered resolution: manual store/binary override > run pnpm (node-prepended) > infer the store dir straight from disk (PNPM_HOME/~/Library/pnpm/~/.local/share/pnpm, newest v<N>) > none-with-reason. Shared runtime-bins discovery now finds npm-global/corepack pnpm under nvm/volta/asdf, not just standalone/Homebrew. New Settings 'pnpm store' section with native folder/file pickers + a live status line; PnpmStoreInfo gained source/reason/canPrune (sizing survives a broken pnpm; prune stays honest). Separately fixed the missing real/linked diff: stop back-filling uniqueSize=size for pre-split caches (now undefined=unknown) and show a one-click 'rescan to compute' hint in panel + launcher. TDD on runtime-bins/find-pnpm/infer-store/validate-setting/project-store; typecheck/lint/tests/build green." },
{ date: "2026-06-25", text: "Merged feat/pnpm-store-robustness to main (PR #20) — all checks green (typecheck/tests/build, macOS packaging). main is now the integrated state." },
{ date: "2026-06-25", text: "Built the Packages tab (branch feat/packages-tab): a third launcher tab (⌘3) inventorying every directly-installed package across all projects — usage count, size (per unique version, du'd from the store), version(s) in use with a 'unify' badge when they diverge, latest on npm, and a security-advisory severity pill. Computed from already-scanned projects (no re-scan): readProjectPackages → aggregatePackages → sizeEntries → enrichEntries (npm registry latest dist-tag + one bulk-advisories POST), 24h disk-cached, on by default but disableable via a new checkUpdates setting (local data works fully offline; failures surface an offline note, never break the view). On-demand compute cached to packages-cache.json; Enter opens the package on npm via a new https-guarded openExternal IPC. First network code in the app (Electron net, hardened-runtime-safe, lazily code-split). New src/main/packages/* (read-manifest/aggregate/package-size/registry/build-inventory/package-store) all TDD'd, extracted shared lib/map-limit, + PackagesView/PackageRow/usePackages + Settings toggle. typecheck/lint/114 tests/build all green. GUI click-through still on your side (userActions: test-packages-tab)." },
{ date: "2026-06-25", text: "Polished the Packages tab loading UX: replaced the plain 'Analyzing…' text with a skeleton-screen loader — shimmering placeholder rows shaped like real package rows that cascade in (staggered), plus a small spinner caption. A recompute over cached data now keeps the list visible with a quiet 'Refreshing…' caption instead of blanking. GPU-friendly (transform/opacity sweep) and respects prefers-reduced-motion. New .cc-skel/ccshimmer CSS + PackageRowSkeleton. Part of PR #21." },
{ date: "2026-06-25", text: "Packages tab: added a meta bar showing 'N packages · Scanned <time> ago' + a Re-scan button (re-runs the inventory; ⌘R still works), with sub-day 'time ago' granularity. Also surfaced ongoing background work to fix the confusing silent jump at launch (e.g. 2→10 GB once the store du finished): whenever a scan is running or the pnpm store size is still being measured, the footer shows a 'pnpm' pill with a spinning indicator (a 'scanning N folders…' pill during a disk scan) and a ghost shimmer sweeps over the unfilled portion of the header gauge. usePnpmStore exposes a loading flag; LauncherApp derives `calculating` from store-loading + live scan progress. Extracted a reusable Spinner component; new .cc-ghost shimmer overlay. Part of PR #21." },
{ date: "2026-06-25", text: "Made pnpm store sizing persistent + scan-driven. getPnpmStoreInfo now caches the size to disk (pnpm-store-cache.json) and returns it instantly on launch — no cold `du` blocking the total each start (kills the remaining 0→full jump). Recomputes only when forced, with concurrent computes coalesced. The launcher re-sizes the store automatically when a scan finishes (running→done transition triggers a forced refresh), and the 'pnpm · sizing store…' pill shows during that refresh. Dropped the old 5-min TTL in favour of explicit refresh (scan / prune / manual). Part of PR #21." },
{ date: "2026-06-25", text: "Packages list now has the same hover effect as the projects list: a sliding .cc-hl highlight that glides between rows as the cursor moves (each PackageRow's onMouseMove selects it; the highlight transitions top/height to the selected row). PackageRow drops its static per-row selected background and renders transparent above the shared highlight, matching the projects Row. Part of PR #21." },
{ date: "2026-06-25", text: "Packages rows are now expandable (chose inline expansion over a hover card — keyboard-friendly, never clips in the small launcher window, room for a real table). Click or ↵ on a row opens a detail panel listing each in-use version with its per-version advisory severity and which projects are on it (grouped by version, for the unify story); ⌘↵ opens npm, esc collapses, a rotating chevron shows state. New PackageDetails component + extracted SEVERITY_COLOR constant. Data: registry enrichment now computes advisoriesByVersion (worst advisory per in-use version, not just one per package) — TDD'd. typecheck/lint/115 tests/build green. Part of PR #21." },
{ date: "2026-06-25", text: "Reworked the expanded package to read as a true accordion: the item itself becomes one solid rounded card (header + details share a single surface, divided by a hairline) instead of a separate inset box appended below. The sliding hover highlight now skips the open row so the translucent highlight doesn't double up on its header; details align under the package name. Part of PR #21." },
{ date: "2026-06-25", text: "Multi-agent review of PR #21 (2 CLAUDE.md + 2 bug/security reviewers, each finding validated). Fixed the one real bug: aggregatePackages was building versions/multipleVersions from ALL usages including unresolved declared ranges, so a declared-but-not-installed dep showed its range as a fake version and tripped a false 'unify' badge — now versions count installed versions only (TDD'd, 116 tests). Security surface (openExternal https guard, registry URL building, untrusted-JSON handling, IPC validation) reviewed clean. Noted: LauncherApp.tsx is now 752 lines (tracked 'Shell decomposition' debt). Part of PR #21." },
{ date: "2026-06-25", text: "Extracted all Packages-tab logic out of LauncherApp into a new usePackagesTab hook (inventory + sort + search filter + accordion expansion + first-open compute) — the review's suggested decomposition. LauncherApp keeps only wiring; the hook owns the state/derived data. typecheck/lint/116 tests/build green. Part of PR #21." },
{ date: "2026-07-01", text: "Cut v0.2.0 — the first feature release since 0.1.2. Bumped package.json 0.1.2→0.2.0, tagged v0.2.0 and pushed, firing .github/workflows/release.yml (macOS runner builds/signs/notarizes the arm64 app → DMG/ZIP + latest-mac.yml on a GitHub release) with release notes highlighting the Packages tab, honest real-vs-linked sizing, robust pnpm store detection + Prune, and the in-app Uninstall. Rewrote the README to match (new Install section → latest release; the launcher's Projects/Caches/Packages tabs; real-vs-linked sizing; Uninstall; updated layout + release-cycle notes). Also spec'd the landing-page refresh that advertises the post-0.1.2 features (docs/superpowers/specs/) — Packages tab as a new hero section, pnpm store Prune, honest sizing, refreshed grid, hero panel showing the three tabs, and a reposition toward 'dependency & disk manager' — build-out deferred to a follow-up branch. Your court: publish the draft release once CI is green + deploy site/ to Vercel." },
{ date: "2026-07-01", text: "Implemented the landing-page refresh (branch feat/landing-v0.2-features, from the spec) so the site finally advertises the v0.2.0 features. Repositioned from 'node_modules cleaner' to 'Reclaim every byte your dependencies cost': new hero H1/subhead + a floating Packages mini-card in the hero scene (unify/update/advisory pills), a new '04 — Every package you've installed' Features block with a faithful Packages-tab mock (Projects/Caches/Packages tabs, sort bar, severity pills), a refreshed 3×3 feature grid led by Prune-the-store / real-vs-linked sizing / security advisories (3 new SVG-sprite icons: broom, layers, shield), a Caches-tab Prune clause + honest-sizing note in the lifecycle section, broadened How-it-works step 3, a Packages nav anchor, and updated OG/Twitter metadata + a re-rendered 1200×630 og.png. All within the existing landing.css design language (no new visual system). Verified in-browser via Playwright at 1280 + 390 widths; site typecheck/lint/build green. Opened for your review; not merged." },
{ date: "2026-07-01", text: "Business/strategy session (no app code changed): coached the monetization pivot and decided to reposition 'Clean my node_modules' as TidyDisk (tidydisk.app) — broadening from node_modules to all developer disk waste and switching from free/OSS-only to a free-scan / paid-clean model (the free scan stays the marketing engine; the Clean action becomes a €19-founding → €29 lifetime unlock; goal: first €1,000 ≈ 35–53 sales). Produced TIDYDISK-LAUNCH.html — an interactive, on-brand launch dashboard (live price→sales calculator, pricing ladder, offer stack, competitor table, Show HN / Reddit drafts, persistent launch checklist; rendering + interactivity verified via Playwright) — and docs/superpowers/specs/2026-07-01-tidydisk-gtm-and-monetization-design.md, the GTM + monetization spec whose one code slice is a license gate on the destructive actions (new src/main/license/*, offline-verifiable keys, enforced in the destructive IPC handlers, Merchant-of-Record checkout). Nothing renamed or gated in code yet — awaiting your spec review + the name/payments go-aheads." },
{ date: "2026-07-02", text: "Planned the license-gate build and broke the TidyDisk pivot into GitHub issues. Wrote the full TDD implementation plan (docs/superpowers/plans/2026-07-02-tidydisk-license-gate.md): offline-verifiable Ed25519-signed keys (TIDY-<payload>.<sig>, no server / no phone-home, MoR-agnostic so the Lemon-Squeezy-vs-Paddle choice stays open), a LicenseStore that re-verifies the signature on every load, enforcement inside the destructive IPC handlers (delete / Clean stale / prune refuse when unlicensed — with register-ipc tests), a shared UnlockPrompt component wired into launcher + panel, a Settings License row, and local key-issuance scripts (make-keypair/issue-license, private key gitignored). 8 tasks, each with failing-test-first steps. Also committed the GTM docs (TIDYDISK-LAUNCH.html + spec) and opened GitHub issues covering the rest of the launch: name lock (domain/trademark/npm), MoR account + €19 product, rename/text rewrite, screenshot-worthy scan result, launch assets, and the launch push." },
{ date: "2026-07-03", text: "Built the license gate end-to-end (feat/license-gate, PR #30) via subagent-driven development: 8 plan tasks each implemented TDD by a fresh subagent and independently reviewed (the IPC-enforcement task on the strongest review tier), plus a final whole-branch review that returned ready-to-merge with zero Critical/Important findings. Shipped: the TIDY- Ed25519 key format + never-throwing verifier; local issuance scripts (make-keypair refuses overwrite, private key gitignored and confirmed absent from all git history — the committed public key was cryptographically re-derived and matched); LicenseStore that re-verifies the signature on every load (forged license.json test-proven not to unlock); license IPC (get/activate/changed broadcast) with unlicensed delete/prune refusing side-effect-free in the main process; UnlockPrompt paywall wired into the launcher footer and tray panel (plus a soft free-tier affordance) and a Settings License row with Buy + activation. Three review-driven fixes along the way: in-flight activation race (inputs now disabled while busy), panel unlock rendering in the zero-projects state, and unlock clearing on view/tab switches + busy-guard on the Settings activator. 12 new tests (134 total), typecheck/lint/build green. Your court: back up .license-signing.pem, review PR #30, run the manual pass." },
{ date: "2026-07-03", text: "Payments pivot: Lemon Squeezy rejected the account, switched to Polar (polar.sh) — also a merchant of record (EU VAT handled), one-time fixed pricing, markdown product descriptions. Zero code rework needed: the gate was built MoR-agnostic (offline keys, BUY_URL placeholder). Prepared the Polar product config: description copy (in TIDYDISK-LAUNCH.html spirit) and two on-brand product images rendered from the design system (polar-product-1200x630.png + polar-product-1200x1200.png, repo root, untracked). Decision recorded: keep OUR offline Ed25519 keys via a Custom benefit + manual pnpm license:issue per order at launch volume (Polar's generated license keys would require online validation and bypass the offline design). Updated issue #24, the launch dashboard, and this dashboard accordingly." },
{ date: "2026-07-03", text: "Licensing pivot #2 + analytics decision (spec addendum: docs/superpowers/specs/2026-07-03-polar-licensing-and-analytics-design.md). After weighing fulfilment reality against offline purity, retired the offline Ed25519 / manual-issuance design in favour of Polar's native License Keys benefit: instant automated delivery, in-app validation against Polar's public endpoint, 30-day offline grace with 7-day background revalidation, refunds actually revoke. The expensive parts of PR #30 survive (enforcement, UnlockPrompt, Settings row, IPC); the verify internals get swapped and the issuance scripts deleted — rework happens on the unmerged branch before review. Also decided (your call): drop privacy-absolutist claims from all copy and instrument the launch funnel with PostHog EU — anonymous install UUID identified at activation, ~9 events (scan → paywall → buy → activate → retention), on by default with a Settings opt-out, no dev capture. Removed the private-key backup obligation (no keypair anymore). Your new unblockers: Polar org + product + org ID + checkout URL (#24) and a PostHog EU project API key." },
{ date: "2026-07-03", text: "Built the Polar + analytics rework end-to-end on feat/license-gate (PR #30 updated) via a second subagent-driven run: Polar validation client (net.fetch against the public license-keys endpoint, never throws, invalid vs network distinguished), LicenseStore rework (async activate, pro derived from last validation + 30-day grace, revalidateIfStale with refreshed/revoked/offline outcomes wired to launch + a 24h timer), the entire Ed25519/issuance world deleted, real BUY_URL/org-id/PostHog key bundled (all client-safe), PostHog EU analytics core (install UUID, gated wrapper, analytics setting + toggle, whitelisted analytics:track IPC) and the 9-event funnel instrumented across main + both windows with identify-at-activation. Two opus reviews (trust-core task + final delta): zero Critical/Important open after fixes — notably the spec-mandated reconnect framing so a paying customer 31+ days offline sees 'Reconnect to re-verify' instead of a Buy button. Also swept every em dash out of user-visible UI copy. 148 tests, all gates + build green. Your court: review/merge PR #30, run the manual pass (Polar sandbox key), and treat #26 as the release gate for the no-telemetry copy." },
{ date: "2026-07-04", text: "Closed #26: the app is now TidyDisk everywhere. New branch feat/tidydisk-rename merges the license-gate and landing branches into one tree, then: app rename (productName TidyDisk, appId com.ahmed.tidydisk, dmg tidydisk-arm64, tray/notifications/settings/onboarding strings), README rewritten around the honest model (free scan, 19 euro founding lifetime license via Polar, anonymous analytics + weekly license check disclosed, MIT stays), and the landing rebuilt: TidyDisk brand + tidydisk.app metadata + re-rendered OG card, the false 'nothing leaves your Mac' lead replaced with the honest scans-stay-local + opt-out line, and the old two-$0-cards Download section rebuilt as $0 Scan-everything + featured EUR19 Lifetime-cleanup cards wired to the live Polar checkout (Buy CTAs in FinalCta too; download CTAs point at the releases page until a tidydisk-named release exists). Also swept the last user-visible em dashes (notification body, pnpm store reasons). All three tasks subagent-built + reviewed; one reviewer finding waived deliberately (card bullet paraphrases beat the spec fragments), minors fixed (dead CSS, _blank on download CTAs). Verified via Playwright desktop + mobile; app + site gates green. The tidydisk.app domain is live and the release gate lifts when this merges." },
{ date: "2026-07-04", text: "🚀 SHIPPED v1.0.0. You green-lit the merge after your sandbox test pass (one fix during testing: POLAR_ORGANIZATION_ID is now env-overridable alongside POLAR_API_BASE, so sandbox validation works with two env vars). Merged everything via PR #31 (which contained the landing and license-gate branches; #22 and #30 auto-flipped to merged), restored the direct tidydisk-arm64.dmg download link on the site, bumped 0.2.0 to 1.0.0 and pushed the v1.0.0 tag, firing the signed-release pipeline for the first TidyDisk build. Vercel redeployed tidydisk.app from main. TidyDisk is now: a paid product with a live checkout, an enforced license gate, honest copy, and a launch-funnel dashboard. Remaining before the money wave: publish the v1.0.0 draft release, one live end-to-end funnel rehearsal, then the launch playbook (#27, #28, #29)." },
{ date: "2026-07-04", text: "Built #27, the share card (feat/share-card, PR #32), via the usual subagent flow: 4 tasks + a strongest-tier final review. Shipped: a pure card renderer with hard payload validation (no renderer string can reach the HTML), offscreen capture straight to the clipboard at 2x, the launcher's post-scan 'Your damage report' reveal with count-up + header share icon, the share_card_copied funnel event (main-originated only), and the pnpm demo / --restore seeder that fakes a beautiful ~247 GB machine for marketing captures with byte-identical restore. Card visually verified standalone: it is genuinely share-worthy. Final review found one real self-defeating seeder message ('hit Rescan' would wipe the seed) + three nits, all fixed. Your court: the clipboard paste check + merge PR #32. Also closed finished issues earlier today (#1, #23, #24; #25/#26 auto-closed)." },
{ date: "2026-07-04", text: "SEO + blog session (feat/blog-seo, PR #34). Audit found robots.txt and sitemap.xml 404ing in prod, no canonical tags, no structured data, and the apex→www redirect contradicting the code's apex canonical (fix is a one-click Vercel domain flip, now in your court). Shipped the fixes (robots.ts hiding /og, hourly-revalidating sitemap, canonicals everywhere, SoftwareApplication + BlogPosting JSON-LD, shared lib/site-url) plus a self-publishing blog: markdown in site/content/blog/, tested lib/blog pipeline (date gating incl. the midnight-UTC edge; bad frontmatter fails the build), ISR on every surface so date-gated posts go live on their own, RSS feed, Blog nav links. Wrote 10 keyword-targeted articles releasing weekly 2026-07-04 → 2026-09-05 (article 1 live at launch): delete node_modules safely, why node_modules is huge, pnpm store, find every folder, npm cache clean, Mac dev disk checklist, npkill vs manual vs TidyDisk, npm/yarn/pnpm disk comparison, never rm -rf, monorepos. 17 tests, build green, pages Playwright-verified (one visual bug caught and fixed: Tailwind preflight strips list markers). Your court: review/merge PR #34, flip the Vercel primary domain, then Search Console + sitemap." },
{ date: "2026-07-06", text: "Turned your landing-page notes into a reviewed-and-ready spec + implementation plan (docs/superpowers/specs/2026-07-06-landing-vision-refresh-design.md, plans/2026-07-06-landing-vision-refresh.md) on branch feat/landing-vision. Decisions made together: hero drops the bytes/dependencies framing for the tidy-disk aspiration ('A tidy disk, without thinking about it.' — your pick among three directions), buy CTAs get a sparkles icon (your pick among four). Also spec'd: statement-band pixel strips mirrored so red converges on the center, feature-row gap 110→150px (96px stacked). You then pulled the OG card into scope, which doubled as the build go-ahead: all five changes built on feat/landing-vision (hero copy, mirror prop on Pixrow with a unit test, spacing, i-sparkles sprite on both buy CTAs, OG card re-captured via make-og). Two stowaway fixes: vitest now resolves the @/ path alias (component tests could not import app code), and every internal link converted to next/link — site lint had been failing on main since the blog PR because CI does not run it. All gates green, band mirroring + sparkles + full page Playwright-verified. You merged PR #34 meanwhile, so the blog milestone flipped to merged and its review action was cleared. Merged same day as PR #35; tidydisk.app now leads with the tidy-disk vision." },
{ date: "2026-07-06", text: "Second session today: scoped and spec'd the two next site projects from your asks. (1) Tailwind migration (the site was already TypeScript — the real gap was the 484-line landing.css): pixel-faithful conversion to Tailwind v4 utilities + @theme tokens, Features/Hero split into focused subcomponents with a shared ui-mock kit, plus repo cleanup — you chose to delete the design handoff bundle and keep video/, TIDYDISK-LAUNCH.html, and the local certs; safe junk (template svgs, stray build outputs, merged branches) goes regardless. (2) i18n + godlike SEO: you picked ALL four locales (fr/es/de/pt) with everything translated including the 10 blog articles (same slugs and weekly dates), English kept at the root with /fr-style subpaths and no auto-redirect, full hreflang/x-default wiring in metadata + sitemap, per-locale RSS and OG cards, and Lighthouse-100 work led by self-hosting all three font families (currently render-blocking CDN loads). Both specs committed; worktrees from the morning cleaned up earlier; your court: review the specs, Tailwind build goes first." },
{ date: "2026-07-06", text: "You said GO GO GO: built the whole Tailwind migration on feat/site-tailwind. Methodology: captured 12 reference screenshots (/, /blog, an article, /og at 1440/900/390) from the pre-change build, then converted section by section with a pixelmatch comparison gate after every task — final state, with landing.css and blog.css DELETED, is 0.000% diff on all 12. The gate caught four real subtleties: Tailwind's max-[900px] is exclusive where the original media queries are inclusive (fixed with custom max900/max560 variants), unlayered legacy CSS was beating layered utilities (fixed by importing landing.css into a CSS layer during the transition), Tailwind's named text sizes (text-sm/text-2xl) silently set line-height where the original inherited 1.6 (switched to arbitrary sizes), and splitting a JSX text node with {\" \"} changed text shaping by a subpixel, wrapping a heading at 390px. Also discovered the feature-row gap NEVER worked (flex gap on an element with a single child) — PR #35's spacing fix was a no-op — and made the 150px/96px spacing real, the branch's only intentional visual change. Components: Btn/Wrap/Eyebrow/SectionHead primitives, ui-mock kit, Features split into FeatureRow + 4 visuals, HeroScene extracted, AppPanel kept as its documented inline-style renderer port; shadcn leftovers dropped (4 deps, dead ui/button, components.json, 129-line boilerplate globals). Cleanup: handoff bundle + template svgs git-rm'd (CLAUDE.md rule updated to document the Tailwind conventions instead), untracked dist/.playwright-mcp/out junk deleted keeping showcase.mp4, and 10 provably-merged branches (verified with git merge-base --is-ancestor per branch) deleted local + remote, leaving only main and your feat/launch-assets. Your court: review the PR." },
{ date: "2026-07-07", text: "Built the i18n + SEO project on feat/site-i18n-seo (stacked on the Tailwind branch) via subagent-driven development with parallel agents, per your request to finish fast. Sequence: an Opus agent built the i18n core (a total, tuple-enforced Dictionary type + byte-for-byte English dictionary + locale-aware blog loader with a build-time slug/date parity test); then FIVE Sonnet agents ran concurrently on disjoint files (fr/es/de/pt each translating the landing dictionary + all 10 articles, ~40k words total) plus a fonts agent self-hosting all three families via next/font (killing the render-blocking CDN font links). I verified + committed that wave centrally (typecheck, parity, lint, build, em-dash sweep; normalized one byline + fixed French apostrophe typography). The routing agent (Opus) died mid-task on an API error after refactoring the components, so I took over and finished it: shared HomePage/BlogIndex/BlogArticle components, the /[locale] route tree (English stays unprefixed; /en 404s), hreflang + x-default on every page and in the sitemap, per-locale RSS, localized JSON-LD/dates, and a footer language switcher. English body verified pixel-identical to the pre-i18n baseline (only the switcher differs), across 26 prerendered pages. A whole-branch Opus review found 1 Critical (the English RSS feed leaked at /en/blog/rss.xml because route handlers bypass the layout's locale guard) and 1 Important (localePath emitted /fr/#anchor with a stray slash that 308-redirects), plus minors — all fixed and re-verified live. 33 tests green. Your court: review/merge #36 (Tailwind) then the i18n PR; native-proofread the translations and Lighthouse the preview before promoting locale URLs." },
{ date: "2026-07-07", text: "Merged both site PRs to main: the Tailwind migration (#36) then the i18n + SEO work (#38, rebased onto main after #36 landed and GitHub auto-closed the stacked PR). tidydisk.app now serves five locales (en at /, fr/es/de/pt under their prefixes) with hreflang + x-default, per-locale sitemaps and RSS, a footer language switcher, and self-hosted fonts. Remaining are your two non-code follow-ups: a native-speaker proofread of the machine translations and a Lighthouse run on the deploy." },
{ date: "2026-07-08", text: "Two new app features on feat/scan-coverage-and-liveness (PR #39), brainstormed → spec'd → planned → built via subagent-driven TDD (15 tasks, fresh implementer + independent review each). (1) External-drive / scan-location coverage: new validated scanRoots setting + a Settings 'Scan locations' section (mounted /Volumes toggles, boot disk excluded by device id, plus an 'Add folder…' picker); resolveScanRoots feeds home + opted-in roots to the scanner each scan (mounted-only, deduped, nesting-dropped), so node_modules on an external SSD or any folder is finally scannable — unplugged drives drop off and return on replug, and a delete-time guard refuses trashing a vanished path. (2) Live/running indicator: a green dot marks any project with a process running from inside it (one lsof -d cwd sweep per refresh, best-effort :port tooltip, soft-fails to no dots on any error), its delete is disabled and main hard-refuses it re-checked at the instant of delete, so a running app's node_modules can't be trashed; polls every 45s while a window is open. Shared: unified deleteNodeModules → { freed, blocked? }, plus a batch deleteMany IPC so 'Clean N stale' runs one liveness check (the whole-branch review's one Important perf finding, fixed). The real lsof flags were smoke-tested on macOS; 183 tests + typecheck/biome/build green; final review READY TO MERGE. Your court: review/merge #39 and a manual pass (needs a physical external drive + a running dev server)." },
{ date: "2026-07-08", text: "Fixed the full launcher window refusing to open (you hit it in pnpm dev; the same root cause is the end-user's 'finish setup does nothing' report). Debugged systematically: ruled out a crash (renderer loaded React with zero JS errors, no crash reports, all three new IPCs wired), then instrumented the window lifecycle and proved it live — the window shows (isVisible=true) and stays ~300ms, then a spurious `blur` fires and the Spotlight-style dismissal hides it. Root cause: a dock-hidden (LSUIElement) accessory app can't reliably make a freshly shown window key, so macOS delivers a blur right after show() and the launcher dismisses itself the instant it opens. Fix (on feat/scan-coverage-and-liveness, PR #39, since the features' manual QA needs the launcher): app.focus({steal:true}) to activate the app on show, plus a 500ms blur grace window (new tested pure blurShouldDismiss helper) so the just-opened blur is ignored while genuine click-away still dismisses. Verified live: window now stays visible at +800ms, hide() never called. 186 tests, typecheck/biome/build green." },
{ date: "2026-07-08", text: "Fixed the launcher rendering blank (a second bug the window-hide one was masking). Debugged in-app with a lifecycle/DOM probe: window shows but React #root has 0 children, yet getSettings resolved and the preload worked and there were zero JS errors. A dynamic-import probe exposed it: the launcher was loading http://localhost:5173/assets/index-*.js (a stale production bundle) and failing to fetch it, because Docker squats on port 5173 (Vite's default). Vite fell back to another port while the Electron renderer URL still resolved to Docker's 5173, so the window loaded foreign/dead content and rendered nothing. NOT an app-code bug and NOT in the packaged app (which loads local files, no dev server). Fix: pin the renderer dev server to 127.0.0.1:5199 with strictPort in electron.vite.config.ts so it can never silently mismatch a squatted default. Verified: both windows now connect to :5199 and #root renders (rootChildren>0), no fetch errors. Best-practice hardening added on top: a root ErrorBoundary wrapping LauncherApp and PanelApp so any future render error shows a readable fallback (and logs the component stack) instead of a silent blank window. 186 tests, typecheck/biome/build green." },
{ date: "2026-07-08", text: "Three UX refinements on feat/scan-coverage-and-liveness (PR #39). (1) Disabled-delete tooltip: fixed a real bug where pointerEvents:none on the disabled trash button suppressed its own tooltip, and set the copy to 'Project currently in use, can't delete.' in both Row and MiniRow (cursor now not-allowed, click still guarded). (2) Full-window settings now tabbed: SettingsView split into Scanning / Packages / Privacy / License tabs (Segmented control) so it's no longer one long cramped column. (3) Menu-bar settings opens the full window: removed the panel's separate inline settings (deleted PanelSettings), and both the 'Settings…' menu item and ⌘, now open the launcher straight to its Settings view. New nav plumbing: openLauncher(nav?) + a one-shot consumeLauncherNav the renderer pulls on mount (race-free for a fresh window) plus an onLauncherNavigate event for the already-open case. Verified end-to-end in the running app: menu-bar settings lands on the Settings view with all four tabs rendering. 186 tests, typecheck/biome/build green." },
{ date: "2026-07-08", text: "Killed the intermittent 'can't open the full window in dev' for good (root cause was duplicate/orphaned instances, since Electron isn't single-instance by default). Added app.requestSingleInstanceLock() in main: a second launch (double-click, or a stray pnpm dev) is denied the lock, quits itself instead of spawning a duplicate tray + windows, and the first instance surfaces its full window via the second-instance handler. Verified with two real built instances: A got the lock and ran; B was denied and exited; A received second-instance and opened the launcher. Also added a pnpm dev:clean script (scripts/dev-clean.sh) that kills stray electron-vite/Electron, quits the installed v1.0.0 app, clears node_modules/.vite + out, then starts a fresh dev server. typecheck/biome/186 tests/build green." },
{ date: "2026-07-08", text: "Merged PR #39 to main (external drives + live indicator + the session's UI/dev fixes): the launcher focus/blur fix, the dev port-collision fix (strictPort 5199) + root ErrorBoundary, tabbed settings, menu-bar settings opening the full window, the disabled-delete tooltip, and app.requestSingleInstanceLock() + pnpm dev:clean. All CI green (check + package-macos + CodeRabbit + Vercel); branch deleted. These ship to the real app on the next release. Still yours: a manual pass with a physical external drive + a running dev server (userActions test-external-drives / test-live-indicator), and cutting a new signed release so users get the launcher + single-instance fixes." },
{ date: "2026-07-08", text: "Built Docker cleanup (feat/docker-cleanup) — the app's next big disk layer after the pnpm store. Brainstormed the design with you (four decisions: tiered-by-recoverability safety, all four docker categories, honest created-date + in-use status instead of a faked 'last used', per-item + bulk prune) and settled the crux: docker removals are permanent with no Trash, so the safety model carries the weight — in-use items are read-only, and volumes need a typed-name confirmation. Executed subagent-driven with TDD in two phases (read-only listing, then Pro-gated cleanup), pausing at the Phase A boundary for your go-ahead. The review loop earned its keep: it caught three latent bugs in my own plan's code (parseDate returning 0 for every timestamp; the info cache coalescing ignoring the binary-path key; the Docker tab inheriting the Projects tab's Enter/⌘⌫ so it could open the delete-node_modules dialog), plus a freed-bytes miscount that could report the whole disk as 'freed' on a measurement failure, all fixed with regression tests. A whole-branch Opus review confirmed no wrongful-delete path (the typed-volume gate is a single choke point with no bypass) and I applied its non-blocking polish (failure toast, close-confirm-on-refresh, dropped a dead docker-ps call). ~65 docker tests, 210 total green; find-docker mirrors find-pnpm; execFile array args, never -f on per-item removals; listing free, cleanup Pro-gated. PR opened; your court: review + a manual pass against a real daemon (CI can't run one). One aside: a subagent flagged a prompt-injection attempt embedded in its tool inputs and correctly ignored it." },
{ date: "2026-07-08", text: "Redesigned the Docker tab to group BY PROJECT (feat/docker-project-grouping, stacked on the #40 branch) — you asked to see which Docker stuff belongs to which project, like the node_modules view. Brainstormed the honest constraint up front: Docker resources mostly aren't tied to a folder, so only Docker Compose resources carry a project identity; we settled on a project-grouped list + type-filter chips (not category tabs, which fight the goal), and 'used-by' association (an image a project's container runs, a volume it mounts) on top of Compose labels to pull most in-use resources under their project, with the honest remainder — pulled images (by repo), build cache, orphans — in an 'Other' section. Compose groups show the project's REAL logo by reusing the node_modules framework/favicon detection on the compose working_dir. Built subagent-driven with TDD (6 tasks, fresh implementer + independent review each, final Opus whole-branch review). The review loop again earned its keep: it caught the image id/ref match being a fallback instead of a union (an image used by two projects could be misassigned), the 'Dangling images' bulk-prune button being unreachable (danglings land in 'Other', not a repo group), and a fail-soft ordering gap that could silently hide items if logo-detection ever threw — all fixed with regression tests. Whole-branch Opus review APPROVED with no wrongful-delete path: this is presentation + enrichment only, rows still flow the real id/kind and the typed-name volume-confirm gate is byte-identical. Main process adds batched docker container/volume inspect behind the existing cache, fully fail-soft. 257 tests green; typecheck/lint/build clean. PR stacked on #40; your court: review + a manual pass with a running compose project (CI has no daemon)." },
{ date: "2026-07-09", text: "Iterated on the Docker project-grouping from your real-daemon test (screenshot showed everything flat under 'Not linked to a project'). Fixed the root cause first: a crash ('info.projects is not iterable') from a docker-cache.json written by the older build with no projects field — guarded the render path + normalized the loaded cache. Then the real linking bug: the whole enrichment sat in one try/catch, so a single failing docker inspect silently dropped ALL grouping, including the repository grouping that's independent of Compose — split each docker call into its own fail-soft try with console.error logging so associateProjects always runs and repository groups always appear (the logging will also reveal whether your Supabase/custom containers carry Compose labels or their own namespace, which is what decides real project grouping). Also acted on your UI feedback: removed the type-filter chips (kept Sort), gave each type badge a distinct color, and standardized the Docker rows on the node_modules Row primitives — hover-revealed trash RowAction instead of a persistent Remove button, SizeViz for the size text — so we reuse components instead of rebuilding each screen. Each change TDD'd + independently reviewed (safety gate + buildcache-non-removable verified intact); 259 tests green. Pushed to PR #42." },
{ date: "2026-07-09", text: "Debugged the 'still no project groups' report by running your REAL docker data through the app's association logic. Found it: `docker volume inspect <names>` exits 1 whenever `docker system df -v` lists an anonymous/stale volume (one bad name fails the whole batch) — and before the resilience fix that single failure took down ALL grouping. Your containers DO carry Compose labels, so the association correctly yields three projects (lets-get-a-job, orchestrator, aprocy — aprocy has a working_dir so it gets a real logo; the two Supabase ones have empty working_dirs so they show a generic Docker icon + name). Switched volume label lookup to a single robust `docker volume ls --format` call (can't 404 on a stale volume). Also per your feedback: in-use items now show the same green LiveDot as running node_modules projects (dropped the in-use/unused text), and Docker rows got the node_modules row hover style. The main-process fixes need a FULL app restart to load (pnpm dev hot-reloads only the renderer). 265 tests green; reviewed, safety gate intact. Pushed to PR #42." },
{ date: "2026-07-09", text: "Shipped per-tab headline data-viz + Docker auto-scan (feat/docker-project-grouping) after you asked for each tab to have its own core-data visualization like the node_modules limit gauge. Brainstormed the four decisions (own configurable limit per tab; Packages gets a severity bar + outdated count rather than a limit; reuse the compact 132px header slot per-tab rather than a new band; Docker auto-scans on open when stale), spec'd + planned, then executed subagent-driven with TDD: 8 tasks, fresh implementer + independent spec/quality review each, cheap models for the pure/transcription tasks and a standard tier for the LauncherApp integration. Result: TabHeadline swaps the header gauge per active tab (Projects/Caches/Docker fill a size-vs-limit Gauge against their own new cacheThresholdGB/dockerThresholdGB settings; Packages shows a new SeverityMeter), tabSummary swaps the footer line, and a loop-guarded effect auto-refreshes Docker when its tab opens with missing or >5-min-stale data. The whole-branch Opus review earned its keep again: it caught that severityCounts() returns all-zero whenever nothing has an advisory, so with npm checks off / offline / before enrichment the Packages headline falsely rendered a green 'all clear' contradicting the tab body - fixed by gating both the header and footer on packagesCheckEnabled + packagesDataReady so they render nothing until advisories are actually trustworthy (same fix removed a first-open flash). It also confirmed the Projects tab is byte-identical and the Docker typed-name confirm safety gate is untouched. 277 tests, typecheck/lint/build green. Not pushed yet: your call whether it rides the still-pending PR #42 or gets its own branch." },
{ date: "2026-07-09", text: "Made the Docker tab's project groups collapsible (feat/docker-project-grouping). You asked for clickable project rows that reveal their items on click, like the Packages tab, since a busy daemon makes the flat list long. Brainstormed the shape (accordion, one open at a time; only PROJECT groups collapse, the 'Other' groups stay as-is) and surfaced the honest finding that there was no reusable accordion COMPONENT (it was fused into the package-specific PackageRow), so you chose to extract a generic one and retrofit both tabs. Executed subagent-driven with TDD (4 tasks): a shared components/Accordion primitive (open-card wrapper with an optional card prop), the Packages tab retrofitted onto it as a verified pixel-identical no-op, two tested pure helpers (dockerGroupActive, projectRowExpanded), and DockerView rewired so projects render through Accordion with a new collapsible DockerProjectRow header (logo, name, N items, size, active dot, rotating chevron). The key reconciliation: Docker's Accordion omits card so its wrapper stays a static div, keeping the item rows' offsetParent the outer list container so the node_modules-style .cc-hl sliding highlight still tracks across headers and items. The Opus whole-branch review traced every ancestor chain and confirmed it, plus the Packages no-op and the untouched safety gate: READY TO MERGE, 0 Critical/Important. Fixed its one Minor (a residual em dash in the Docker remove label). 282 tests, typecheck/lint/build green. Rides PR #42 per your earlier call. Logged follow-up: a proper aria-expanded/keyboard a11y pass for the Row/PackageRow/DockerProjectRow disclosure family." },
{ date: "2026-07-12", text: "Swapped the site's analytics to Google Analytics on feat/site-analytics-consent (site only, no app code). Removed @vercel/analytics and wired GA4 G-0DW5WJLY1M via @next/third-parties, consent-gated: a new CookieConsent component in the root layout shows a localized (en/fr/es/de/pt) bottom banner and renders the GoogleAnalytics tag ONLY after the visitor clicks Accept, so no gtag script and no cookies load beforehand; Decline loads nothing; the choice persists in localStorage (single-responsibility folder: index/Component/.types/.constants). Also fixed the favicon: the site showed the default Vercel favicon because app/favicon.ico (the Next.js default) overrode the metadata data-URL icon via the file convention — deleted it, added app/icon.svg (the coral module-cube brand logo), dropped the redundant inline FAVICON metadata. Verified end-to-end against a production build in a real browser: banner renders in-theme, zero GA network requests before consent, gtag/js + a page_view fire to GA on Accept, head links rel=icon to /icon.svg. typecheck + build green. Opened PR. Follow-on this session: a privacy policy + contact/imprint page for selling compliance (needs your legal/contact details, tracked in userActions)." },
{ date: "2026-07-12", text: "Added the legal pages + fixed the reviewed GDPR consent-withdrawal gap (feat/site-analytics-consent, PR #44). You flagged (correct) that once a visitor chose in the cookie banner there was no way to change or withdraw consent, violating GDPR Art. 7(3). Fixed: a footer 'Cookie preferences' control dispatches a window event that reopens the banner, and declining now also clears any GA cookies (best-effort _ga/_gid removal). Then built /privacy and /legal across all 5 locales from your details (Ahmed ABOUELLEIL SAYED, 26 rue Jean Voillot 69500 Bron France, contact@tidydisk.app, SIRET 90884903700027): /privacy documents GA (consent-based), the app's anonymous PostHog telemetry with its Settings opt-out, Polar as merchant of record, and Vercel hosting, plus legal bases, retention, international transfers via SCCs, and the full GDPR rights list incl. lodging a complaint with the CNIL; /legal is the EU imprint with the identity block, MoR/refunds, French governing law + the EU ODR link, and Vercel's host address. Architecture: factual identity in one untranslated constants file injected by the page components (so translators never touch the name/SIRET/address), long-form prose in a separate typed lib/legal module, English authored by me then fr/es/de/pt produced by 4 parallel translation agents (protected tokens preserved, GDPR->RGPD/DSGVO, no em dashes). Wired into a new footer Legal column, the sitemap (/privacy + /legal x5 locales with hreflang), and a privacy-policy link in the cookie banner. Verified against a production build in a real browser: the EN Legal Notice + FR privacy pages render correctly with the imprint intact, and the withdrawal flow reopens the banner after a prior accept. typecheck + build green (12 new routes). Pushed to PR #44. Your court: optional professional skim of the copy (userActions legal-review) and confirming GA Realtime once deployed (verify-ga-live)." },
{ date: "2026-07-14", text: "Finished the Docker feature from your two asks: build caches shouldn't live in the Docker tab, and the delete buttons should look like deletes. Brainstormed the four decisions (single aggregated 'Docker build cache' row in the Caches tab reusing the existing prune confirm/gate; build cache stays counted in the Docker gauge since it's real Docker disk; verb+trash+danger relabel of the bulk-prune buttons; keep the per-item hover trash), spec'd + planned, then executed subagent-driven with TDD (6 tasks: cheap models for the transcription tasks, a standard tier for the CachesView rewrite and the LauncherApp integration, opus for the reviews). Result: groupDockerForDisplay drops the build-cache group; the Caches tab became a generalized 'live caches' list (pnpm + Docker build cache, keyboard-navigable, auto-scans Docker on open); Docker bulk buttons now say 'Delete unused images' etc. in red with a trash icon; CacheRow gained an opt-in danger+icon action variant. SAFETY stayed frozen — the build-cache Delete routes through the unchanged requestDockerPrune('buildCache'), the typed-name volume gate is byte-identical, and build cache is still un-deletable individually (canRemove + removeDockerItem + coerceRemoveArgs all block it). Two per-task reviews flagged missed lint in the TDD-only tasks (biome format) which I fixed. The whole-branch Opus review earned its keep: it caught that Caches Enter indexed the full cache list while the rows were query-filtered, so pressing Enter with an active search could trigger a row scrolled off screen (confirm-gated, so never a silent delete) — fixed with a tested selectedActionableCache() guard, plus a footer-hint gate and an em-dash sweep of the placeholder copy. 293 tests, typecheck/lint/build green. Not pushed — your call whether it rides PR #42 or its own branch; manual daemon QA still yours." },
{ date: "2026-07-16", text: "Menu bar panel became a read-only dashboard: aggregate 'Tracked on disk' hero + four click-through rows (Projects / Caches / Packages / Docker) that open that tab in the main window; the panel's reclaimable list, Clean-stale CTA, pnpm prune row and paywall are all gone (so the launcher is now the only paid-conversion surface). Brainstormed with a visual companion (picked the aggregate-hero layout), spec'd the honest-number decisions (hero counts the pnpm store once so it is deliberately smaller than the four rows summed, since rows mirror their tabs and click-through must never lie; 'tracked' not 'reclaimable' because the store's prunable share is unknown until pruned; combined limit = sum of the three existing limits), then built subagent-driven with TDD across 9 tasks. All arithmetic + gating went into pure panelAreas.ts (hero + row gating) and launcherNav.ts (nav-target map), keeping the .tsx thin, per the pure-logic-only test setup. Freshness tuned to not regress popover open speed: Docker stale-refreshes on open (5 min, non-looping), the pnpm store re-sizes only when a scan finishes (getPnpmStoreInfo has no TTL, so a timer would du on every open), and the panel never computes the package inventory (cached read only, placeholder row clicks through to compute it in the tab). One plan-authored test bug caught + fixed (asserted 41.2 GB but the code sums 34.8+6.4, off at the IEEE 754 bit). 315 tests, typecheck/lint/build green." },
{ date: "2026-07-16", text: "Shipped the panel + the whole Docker stack to main and cut v1.1.0. You confirmed the panel looked right (after one fix: the per-row PixelMeter bars were collapsing to zero width inside AreaBar's flex column, since the meter cells are flex:1 with no basis and the root had no width, so only the GB number showed; fixed with width:100% on the PixelMeter root, a no-op in the block-context hero). Then, on your go-ahead: synced main into the branch and resolved the one STATUS.html conflict by taking the union (main's #43/#44 GA + legal work kept alongside the panel session), bumped to v1.1.0, retargeted PR #42's base from feat/docker-cleanup to main and merged it (bringing the entire Docker stack; PR #40 auto-closed), waited for CI green (check + package-macos), then tagged v1.1.0 which fired the signed-release pipeline. The build passed typecheck/tests/build and CODE-SIGNED fine, but Apple NOTARIZATION failed with HTTP 403 'a required agreement is missing or has expired' — an Apple Developer account agreement you need to accept, not a code/secrets problem (nothing published; v1.0.0 is still latest). Your court: accept the pending agreement at developer.apple.com/account then re-run the release (tag already exists), plus the still-pending manual daemon QA + the launcher-paywall decision. Tracked in userActions release-110-apple-agreement." },
{ date: "2026-07-17", text: "Repositioned the landing page from 'node_modules cleaner' to 'all developer disk waste' (feat/landing-all-disk-waste, site only). Brainstormed with a visual companion (picked a four-peer-cards 'areas' section over an aggregate bar or a tabbed app-window mock), spec'd, planned, then built subagent-driven with TDD across 7 tasks in all 5 locales. Changes: hero subhead + the 'heaviest object' band widened to name Docker/caches/dead-projects (joke kept); a new Areas section (Projects / Caches / Packages / Docker peer cards, Docker accented) between the band and the features; Docker added as a 5th feature row with a hand-built by-project visual + a new i-docker sprite icon; the 'coming soon' pill reframed to only genuinely-future items; meta.description broadened; and the hero mockup rebuilt to show the app's NEW panel dashboard (the old one recreated a reclaimable-list UI the app no longer has). The Dictionary type forced all 5 locales (features became a 5-tuple, a new areas slice), so fr/es/de/pt were translated in the same pass (per-locale tab-title choices matched each dictionary's existing naming). Reviews caught + fixed one whole-branch issue the per-task gates missed: 6 French no-unescaped-entities lint errors (straight apostrophes in JSX text where the site convention is curly) fixed by currying them. typecheck + build green (41 pages, 5 locales); site lint clean except a pre-existing CookieConsent error from #44 (not this branch, not in CI). Not pushed; your court: review/merge the PR + an optional native-speaker proofread of the new machine-translated fr/es/de/pt copy." },
{ date: "2026-07-20", text: "Merged the landing repositioning to main (PR #45) and retried the v1.1.0 release, which succeeded this time. The site work is site-only so it did not change what the v1.1.0 tag builds; merging to main also fires the Vercel production deploy, so the new 'all developer disk waste' landing (four-area section, Docker row, rebuilt hero mockup, 5 locales) is now live. The release re-run got through Apple notarization (the agreement had finally propagated), producing a signed + notarized DRAFT v1.1.0 release (tidydisk-arm64.dmg + auto-update zip/latest-mac.yml); it stays draft until you test and publish it. Your court: test + publish the draft (userAction release-110-publish-draft), the standing manual daemon QA + launcher-paywall decision, and an optional proofread of the new non-English landing copy." },
{ date: "2026-07-22", text: "Built in-app auto-update end-to-end on feat/auto-update via subagent-driven TDD (11 tasks): UpdaterService wraps electron-updater (autoDownload=false, install-on-quit after a download) behind an injectable interface with a unit-tested state machine; silent checks run at launch + every 6h in packaged builds only, and the flow stays fully user-driven (Download → progress → Restart and install, never a forced restart). Shipped: IPC/types/settings wiring, a useUpdater hook syncing state to both renderers, a Settings → Updates tab (current version, manual check, GitHub release notes), a dismissible per-version panel banner, a live deep link from the banner to Settings → Updates, translocation detection (app run outside /Applications gets a friendly fix-it message instead of a crash), and analytics (update_available deduped per version, update_download_clicked, update_installed) plus post-install version detection. A whole-branch review caught real issues — the banner not surviving background re-checks and a stale deep-link — fixed in 858da18, then returned READY FOR PR. 351 tests, typecheck/lint/build green. Opened the auto-update PR; the release process is unchanged (publishing the GitHub release draft is still the go-live switch). Your court: review the PR and a manual pass with a signed build, since CI can't exercise the real update flow (userActions test-auto-update)." },
{ date: "2026-07-29", text: "Full code review of the auto-update PR (#46): architecture, security and release plumbing all check out (release.yml publishes latest-mac.yml, so 'publish the draft = go-live' holds). The review found six small issues, all fixed in 671ad80 on the branch: Download click now transitions to 'downloading 0%' synchronously (double-click could previously start a second download + duplicate analytics), progress broadcasts are deduped by rounded percent (IPC spam), a repeated banner deep-link now always lands on the Updates tab (nav seq), 4xx errors are no longer misreported as 'offline' (a 404 = broken feed should surface its message), HTML entities decode exactly once, and package.json gained an explicit repository field so app-update.yml generation is deterministic. 355 tests, typecheck/lint green. One test-plan gotcha for your manual pass: publish the fake higher version as a FULL release, not a prerelease — electron-updater ignores prereleases by default, so a prerelease would look like the feature is broken when it isn't. Your court: PR review + the signed-build manual checklist." },
{ date: "2026-07-29", text: "You approved the review and merged PR #46: in-app auto-update is now on main (merge commit, feat/auto-update deleted) and will ship with the next tagged release. Your court unchanged: the signed-build manual checklist (userAction test-auto-update) — remember to publish the fake higher version as a full release, not a prerelease, since electron-updater ignores prereleases by default." },
{ date: "2026-07-29", text: "Built a Storybook design system for both app and site on feat/storybook via subagent-driven TDD (13 tasks): a root Storybook (react-vite) documents all 32 app components with a dark panel decorator, Intro + live Tokens docs, and a site Storybook (nextjs-vite) documents the site components against the Tailwind v4 tokens; every component folder now requires a .stories.tsx, enforced by a stories-coverage.test.ts in each tree (explicit EXEMPT list with reasons for purely behavioral components) so an undocumented component fails pnpm test; CI builds both books on every PR plus a new site typecheck/test job; CLAUDE.md documents the convention. Full verification green across both trees. Your court: review the PR." },
{ date: "2026-07-29", text: "Enterprise testing landed on feat/test-coverage: vitest 3 + coverage ratchet in CI (75.2% lines), typed window.clean mock, 19 new test files (hooks + main process). PR opened." },
{ date: "2026-08-03", text: "Addressed CodeRabbit review on PR #47: one finding was a real bug — abbreviateHome turned /Users/mean into ~an for home /Users/me (fixed test-first with a /-boundary check); also tightened the notification mock to only accept click handlers, and replied in all four threads. PR #48's red CI was an npm-registry download timeout, not code — rerun came back green. Both PRs now await your review; merge #47 first, then rebase #48 (they overlap on ci.yml, package.json, pnpm-lock)." },
{ date: "2026-08-03", text: "You approved both PRs (LGTM). Merged #47 (test coverage ratchet) to main, then brought main into feat/storybook — conflicts in package.json/.gitignore/CLAUDE.md/STATUS.html were straight unions of the Testing + Storybook additions, pnpm-lock regenerated — and merged #48 after green CI. Cleaned up branches and worktrees. Main now has the coverage ratchet AND both Storybooks building on every PR." }
]
};
</script>
<style>
:root {
--bg: #0e1014;
--panel: #161a21;
--panel2: #1c212b;
--line: rgba(255,255,255,0.07);
--text: #e8eaf0;
--dim: #8b93a3;
--faint: #5b6271;
--green: #34d399;
--amber: #f5b14c;
--red: #ff6363;
--blue: #6ca0f0;
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
padding: 48px 24px 96px;
}
.wrap { max-width: 860px; margin: 0 auto; }
/* ---------- header ---------- */
header { margin-bottom: 36px; }
.titlebar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.cube {
width: 34px; height: 34px; flex: none;
display: grid; place-items: center;
background: linear-gradient(140deg, #232a36, #161a21);
border: 1px solid var(--line); border-radius: 9px;
}
h1 { font-size: 22px; font-weight: 650; letter-spacing: -0.01em; }
h1 .nm { font-family: var(--mono); font-size: 19px; color: var(--green); }
.meta { font-family: var(--mono); font-size: 12px; color: var(--faint); margin-left: auto; text-align: right; }
.meta b { color: var(--dim); font-weight: 500; }
.phase { margin-top: 18px; font-size: 17px; font-weight: 550; letter-spacing: -0.005em; }
.summary { margin-top: 8px; color: var(--dim); max-width: 64ch; }
/* ---------- pixel meter ---------- */
.meter { margin-top: 22px; }
.meter-cells { display: flex; gap: 3px; height: 14px; }
.meter-cells i { flex: 1; border-radius: 2px; background: var(--panel2); }
.meter-cells i.on { background: var(--green); box-shadow: 0 0 6px rgba(52,211,153,0.35); }
.meter-cells i.half { background: linear-gradient(90deg, var(--green) 50%, var(--panel2) 50%); }
.meter-label { display: flex; justify-content: space-between; margin-top: 7px; font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.meter-label b { color: var(--green); font-weight: 500; }
/* ---------- sections ---------- */
section { margin-top: 44px; }
.sec-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px; }
h2 { font-size: 16px; font-weight: 650; letter-spacing: -0.005em; }
.sec-note { font-size: 13px; color: var(--faint); }
.sec-count { margin-left: auto; font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; margin-top: 10px; }
.item { display: flex; gap: 14px; padding: 13px 16px; border-top: 1px solid var(--line); align-items: baseline; }
.item:first-child { border-top: none; }
.item .label { font-weight: 550; font-size: 14.5px; }
.item .detail { color: var(--dim); font-size: 13px; margin-top: 2px; }
.item .text { flex: 1; }
.badge {
flex: none; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em;
padding: 3px 8px; border-radius: 99px; text-transform: uppercase; align-self: center;
}
.b-done { color: var(--green); background: rgba(52,211,153,0.10); }
.b-progress { color: var(--amber); background: rgba(245,177,76,0.10); }
.b-planned { color: var(--faint); background: rgba(139,147,163,0.08); }
.b-user { color: var(--red); background: rgba(255,99,99,0.10); }
.item.is-done .label { color: var(--dim); }
/* ---------- your court ---------- */
#court { scroll-margin-top: 24px; }
#court .card { border-color: rgba(255,99,99,0.25); }
#court h2 { color: var(--red); }
.court-item { display: flex; gap: 13px; padding: 14px 16px; border-top: 1px solid var(--line); }
.court-item:first-child { border-top: none; }
.court-item input[type="checkbox"] {
appearance: none; width: 18px; height: 18px; flex: none; margin-top: 2px;
border: 1.5px solid var(--faint); border-radius: 5px; cursor: pointer; position: relative;
}
.court-item input[type="checkbox"]:checked { background: var(--green); border-color: var(--green); }
.court-item input[type="checkbox"]:checked::after {
content: "✓"; position: absolute; inset: 0; display: grid; place-items: center;
color: #0e1014; font-size: 12px; font-weight: 800;
}
.court-item.is-checked .label, .court-item.is-checked .detail { text-decoration: line-through; opacity: 0.45; }
.cat {
flex: none; align-self: flex-start; margin-top: 2px;
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase;
padding: 3px 8px; border-radius: 99px;
}
.c-testing { color: var(--amber); background: rgba(245,177,76,0.10); }
.c-review { color: var(--blue); background: rgba(108,160,240,0.12); }
.c-external { color: var(--red); background: rgba(255,99,99,0.10); }
.c-vision { color: #c792ea; background: rgba(199,146,234,0.10); }
.court-hint { margin-top: 10px; font-size: 12.5px; color: var(--faint); }
/* ---------- log ---------- */
.log-item { display: flex; gap: 16px; padding: 11px 16px; border-top: 1px solid var(--line); }
.log-item:first-child { border-top: none; }
.log-date { font-family: var(--mono); font-size: 12px; color: var(--faint); flex: none; padding-top: 2px; }
.log-text { color: var(--dim); font-size: 13.5px; }
footer { margin-top: 56px; font-size: 12px; color: var(--faint); text-align: center; }
footer code { font-family: var(--mono); color: var(--dim); }
@media (prefers-color-scheme: light) {
:root {
--bg: #f4f5f7; --panel: #ffffff; --panel2: #e8eaee;
--line: rgba(15,20,30,0.08); --text: #1a1e26; --dim: #555d6b; --faint: #8a909c;
}
.meter-cells i.on { box-shadow: none; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="titlebar">
<div class="cube">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="1.6" stroke-linejoin="round">
<path d="M12 2.5 21 7.5v9L12 21.5 3 16.5v-9L12 2.5Z"/>
<path d="M3 7.5 12 12.5l9-5M12 12.5v9"/>
</svg>
</div>
<h1>Clean my <span class="nm">node_modules</span> — status</h1>
<div class="meta" id="meta"></div>
</div>
<div class="phase" id="phase"></div>
<p class="summary" id="summary"></p>
<div class="meter">
<div class="meter-cells" id="meter"></div>
<div class="meter-label"><span>roadmap progress <span style="opacity:.6">(ideas excluded)</span></span><b id="meter-pct"></b></div>
</div>
</header>
<section id="court">
<div class="sec-head">
<h2>⚑ In your court — only you can do these</h2>
<span class="sec-count" id="court-count"></span>
</div>
<div class="card" id="court-list"></div>
<p class="court-hint">Checkboxes are saved in this browser. When something is truly done (tested, decided, reviewed), tell Claude — it gets removed from this list and the roadmap moves.</p>
</section>
<div id="milestones"></div>
<section>
<div class="sec-head"><h2>Session log</h2></div>
<div class="card" id="log"></div>
</section>
<footer>
Maintained automatically by Claude Code at the end of every working session — see <code>CLAUDE.md</code>.<br>
If this page looks stale, just ask: “update STATUS.html”.
</footer>
</div>
<script>
(function render() {
const $ = (id) => document.getElementById(id);
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");
$("meta").innerHTML = `v${STATUS.version}<br><b>updated ${STATUS.updated}</b>`;
$("phase").textContent = STATUS.phase;
$("summary").textContent = STATUS.summary;
// progress across non-idea milestones; "user" items count as half-done (built or unblocked-by-you)
const tracked = STATUS.milestones.filter(m => !m.ideas).flatMap(m => m.items);
const score = tracked.reduce((n, i) => n + (i.status === "done" ? 1 : i.status === "progress" || i.status === "user" ? 0.5 : 0), 0);
const pct = Math.round((score / tracked.length) * 100);
const CELLS = 30, fill = pct / 100 * CELLS;
$("meter").innerHTML = Array.from({ length: CELLS }, (_, i) =>
`<i class="${i + 1 <= Math.floor(fill) ? "on" : i < fill ? "half" : ""}"></i>`).join("");
$("meter-pct").textContent = pct + "%";
const BADGE = { done: ["b-done", "done"], progress: ["b-progress", "in progress"], planned: ["b-planned", "planned"], user: ["b-user", "needs you"] };
$("milestones").innerHTML = STATUS.milestones.map(m => {
const done = m.items.filter(i => i.status === "done").length;
return `<section>
<div class="sec-head">
<h2>${esc(m.title)}</h2><span class="sec-note">${esc(m.note)}</span>
<span class="sec-count">${m.ideas ? m.items.length + " ideas" : done + "/" + m.items.length}</span>
</div>
<div class="card">${m.items.map(i => {
const [cls, txt] = BADGE[i.status];
return `<div class="item is-${i.status}">
<div class="text"><div class="label">${esc(i.label)}</div><div class="detail">${esc(i.detail)}</div></div>
<span class="badge ${cls}">${txt}</span>
</div>`;
}).join("")}</div>
</section>`;
}).join("");
const saved = JSON.parse(localStorage.getItem("cmnm-court") || "{}");
$("court-count").textContent = STATUS.userActions.length + " open";
$("court-list").innerHTML = STATUS.userActions.map(a =>
`<label class="court-item ${saved[a.id] ? "is-checked" : ""}" data-id="${a.id}">
<input type="checkbox" ${saved[a.id] ? "checked" : ""}>
<span class="cat c-${a.category}">${a.category}</span>
<span class="text"><span class="label">${esc(a.label)}</span><div class="detail">${esc(a.detail)}</div></span>
</label>`).join("");
$("court-list").addEventListener("change", (e) => {
const row = e.target.closest(".court-item");
saved[row.dataset.id] = e.target.checked;
row.classList.toggle("is-checked", e.target.checked);
localStorage.setItem("cmnm-court", JSON.stringify(saved));
});
$("log").innerHTML = [...STATUS.log].reverse().map(l =>
`<div class="log-item"><span class="log-date">${l.date}</span><span class="log-text">${esc(l.text)}</span></div>`).join("");
})();
</script>
</body>
</html>