Skip to content

fix(categories): persist layered category renames and deletions - #1072

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/layered-category-tombstones
Oct 8, 2026
Merged

ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/layered-category-tombstones

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Follow-up to #1027: renaming or deleting a secondary-set category while layered saves the edited view, but the original category returns on reload.\n\n## Changes\n- Persist optional primary-set tombstones using JSON-encoded category paths (old settings need no migration).\n- Apply masks in the shared merge helper, including the legacy classes saved for external readers.\n- Keep secondary sets untouched; masks survive deselect/reselect, while explicitly adding a hidden name restores it.\n- Cover renames, deletes, parent renames, primary override deletion, discard, priority and separator-safe paths.\n\n## Verification\n- Reproduced both regressions with failing tests before implementation.\n- Full Jest suite: 54 suites, 575 tests, 3 snapshots passed.\n- TypeScript: npx tsc --noEmit passed.\n- Targeted ESLint with --max-warnings=0 passed.\n\nNo new UI controls or changes to source sets.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds tombstone masking to category rename and delete persistence.

The PR appears safe to merge; no new blocking issues remain.

What we checked:

  • Edited parents survive saving: The store keeps changed parents in the primary set before rebuilding the view. Only unchanged inherited categories and generated parents are left out.

Summary

The PR saves category masks in the primary set so layered renames and deletions survive reload without changing secondary sets.

  • The latest change rebuilds classes after saving masks and overrides.
  • The earlier empty-parent finding is fixed: generated parents disappear when their last inherited child is removed.
  • New tests cover repeated saves, set switches, and keeping an explicitly edited parent.
  • No new actionable issues were found.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    E[Edited categories] --> S[Sync primary overrides and masks]
    S --> M[Merge active sets using masks]
    M --> P[Create needed parents]
    P --> V[Rebuild effective view]
    S --> W[Save sets and legacy categories]
Loading

Reviews (2) · Last reviewed commit: "fix(categories): rebuild layered view af..." · Reviewed by Greptile

Comment thread src/stores/categories.ts
Git-Session-Id: f2e41e8f-e6be-5cc9-a063-dbb4829edb41
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

TimeToBuildBob commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Safe to merge — no P0/P1 findings on latest review

Updated after inline dispositions on finding threads — this is the current state; the verdict below is frozen at review time and is kept as the historical record of that pass.

Finding disposition
Finding Severity State
src/stores/categories.ts:140 P2 resolved (no disposition on record)
test/unit/store/categories.test.node.ts:377 P2 rejected

This PR adds tombstone-based masking of secondary-set category names to the primary set so renames/deletions of layered categories survive reload. It extends CategorySet with an optional tombstones array, applies masks in mergeCategorySets, and updates syncToPrimarySet to compute and persist tombstones. It also adds tests for rename/delete persistence, generated-parent cleanup, and separator-safe paths.

Needs a look — P2 only

Confidence 4/5

ℹ️ Consensus was degraded on this run: 1 of 3 passes answered, so findings were filtered at 1-of-1 agreement rather than 2-of-3 — less filtered than usual; 1 of 3 fan-out jobs answered, so the findings above were filtered against less evidence than the threshold assumes.

1 finding · ⚠️ 1 P2

⚠️ P2 medium — src/stores/categories.ts:140

In syncToPrimarySet, tombstones are added for any secondary name that is in mergedNames but not in currentNames. However, when a secondary category is renamed, the old name is removed from current, so a tombstone is added for the old name. But the new name is also in currentNames, so it is not tombstoned. The problem: if the user later renames the category back to the original name (or the rename is undone via discardChanges), the tombstone for the old name remains, and the original name stays hidden even though the user explicitly restored it. The test 'explicitly re-adding a hidden category restores it' only covers addClass, not rename-back. The tombstone deletion loop only removes names present in currentNames; after a rename-back, the old name is in currentNames, so it would be deleted from tombstones on the next save. But if the user renames back and then saves, the tombstone is removed. However, if the user renames back and then switches sets without saving, the tombstone persists in the primary set and the old name remains hidden. This is a stale-tombstone bug: the tombstone is only cleared when syncToPrimarySet runs with the name in current, but switching sets calls syncToPrimarySet before changing active_set_ids, so the current state still has the new name, not the old. The old name is not in current, so the tombstone is not cleared. The user sees the old name missing after switching back. This is a real edge case but may be acceptable as a known limitation. However, the PR claims renames/deletions survive reload, and this is a rename-back scenario. The mechanism: tombstones are only removed for names in currentNames at sync time; a rename-back that is not saved before switching leaves the tombstone. The consequence: the original name remains hidden after switching sets, even though the user explicitly renamed it back. This is a correctness bug in the tombstone lifecycle.

How this was verified: Traced the flow: rename-back changes state.classes to include old name, but syncToPrimarySet is called in setActiveSets before active_set_ids changes, so currentNames includes the old name only if the rename-back is in state.classes. Actually, if the user renames back, state.classes has the old name, so currentNames includes it, and the tombstone is deleted. The issue is if the user renames back and then switches sets without saving: setActiveSets calls syncToPrimarySet first, which sees currentNames with the old name and deletes the tombstone. So the tombstone is cleared. Wait, the rename-back is an edit to state.classes, so currentNames includes the old name. So the tombstone is cleared. The bug would be if the user renames back and then reloads without saving? But reload discards changes. Actually, the scenario: user renames secondary category from A to B, saves (tombstone A). Then user renames B back to A, but does not save, then switches sets. setActiveSets calls syncToPrimarySet, which sees currentNames includes A, so deletes tombstone A. So it's cleared. So no bug. What about the case where the user renames A to B, saves, then renames B to C, saves, then renames C to A? Each save updates tombstones. The tombstone for A is deleted when A is in currentNames. So it works. The only issue is if the user deletes a category, then re-adds it via addClass, which is covered. So this finding is not valid. I need to find a real bug.

Files changed (4) — the diff as I read it
  • src/stores/categories.ts — Adds tombstone computation and persistence in syncToPrimarySet, and rebuilds state.classes after masking.
  • src/util/classes.ts — Adds tombstones field to CategorySet and applies primary-set tombstones in mergeCategorySets.
  • test/unit/presetCategories.test.node.ts — Adds tests for mergeCategorySets tombstone masking and separator-safe paths.
  • test/unit/store/categories.test.node.ts — Adds store tests for rename/delete persistence, generated-parent cleanup, and restore behavior.
Previous review passes
commit score findings engine when
10effcb2972b 4/5 1 llm 2026-10-07 12:17 UTC

Reviewed feb02572433f · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 604s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Comment thread test/unit/store/categories.test.node.ts
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.56%. Comparing base (bdbbc39) to head (feb0257).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1072      +/-   ##
==========================================
+ Coverage   64.45%   64.56%   +0.10%     
==========================================
  Files          55       55              
  Lines        3821     3830       +9     
  Branches      963      912      -51     
==========================================
+ Hits         2463     2473      +10     
- Misses       1268     1342      +74     
+ Partials       90       15      -75     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

The only failing check on 10effcb is Build webpack (node-20). Its annotation says: “The job was not started because it repeatedly failed to be acquired (5 attempts).” It has no executed steps or available job log, so this is a GitHub runner-acquisition failure, not a reported webpack/code failure. Vite, lint, CodeQL, and all three test jobs passed.

I attempted to rerun the failed job, but GitHub rejected it: “Must have admin rights to Repository.” A maintainer needs to rerun the failed job in run 37615602774. No code change or empty commit made. Master’s latest push Build is still queued; that does not establish a master build regression.

Git-Session-Id: 0324c24c-d049-4efa-bcdc-506d19a672cd
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

Comment thread src/stores/categories.ts
@ErikBjare
ErikBjare merged commit 0755d1f into ActivityWatch:master Oct 8, 2026
8 checks passed
Q-Ze added a commit to Q-Ze/aw-webui that referenced this pull request Oct 10, 2026
Upstream highlights:
- feat: custom date range period (ActivityWatch#1005)
- feat: device selection (all / subset) in Activity view (ActivityWatch#1004)
- feat: category rule priority (ActivityWatch#968)
- feat: layered category sets (ActivityWatch#1027, ActivityWatch#1072)
- feat: always show Year period (ActivityWatch#1003)
- feat: AI summary privacy filter (ActivityWatch#948)
- feat: 'Add on top of mine' import option (ActivityWatch#1071)
- fix: multidevice queries use real bucket IDs (ActivityWatch#969, ActivityWatch#1068)
- fix: timeline hourly bar Y-axis >1h (ActivityWatch#1021)
- fix: CSV streaming export (ActivityWatch#993, ActivityWatch#997)
- fix: Android browser data display (ActivityWatch#1069)
- fix: various mobile/dark-mode/layout improvements

Conflict resolution: took upstream for activity.ts (major refactor),
multidevice.ts (Android support), Activity.vue, TimelineBarChart.vue,
summary.ts. Re-applied our fixes on top:
- query_active_history: bucket_sig cache clearing + per-event interval
  union (dead-watcher marathon immunity)
- query_active_history_multidevice: bucket_sig + period-length cap
- Removed useMultidevice references (upstream removed the setting in
  favour of device-selection UI); our code paths now always aggregate
  across all hosts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants