feat: improved tag system — AND/OR filtering and tag hierarchy #221

Closed
opened 2026-04-12 08:31:02 +02:00 by marcel · 10 comments
Owner

Problem

Tags are currently flat (no structure) and search filtering only supports AND logic (all selected tags must match). Power users who tag documents carefully can't leverage the full potential of their tagging work.

Proposal

AND/OR filtering

Allow users to toggle between AND and OR when filtering by multiple tags in the search bar. AND = "all of these tags", OR = "any of these tags".

Tag hierarchy

Support parent/child relationships between tags so related concepts can be grouped. E.g. "Immobilie" could be a parent of "Grundstück" and "Haus". Searching for a parent tag would also include documents tagged with child tags.

Tag overview page

A browsable page showing all tags with document counts, organized by hierarchy. Entry point for tag-based exploration.

Open questions

  • How deep should hierarchy go? Single level (parent → children) or unlimited nesting?
  • Should the tag admin page handle hierarchy, or a separate UI?
  • How to migrate existing flat tags into a hierarchy — manual only or suggest groupings?
  • UI for AND/OR toggle — simple switch next to tag filter, or per-tag operator?
## Problem Tags are currently flat (no structure) and search filtering only supports AND logic (all selected tags must match). Power users who tag documents carefully can't leverage the full potential of their tagging work. ## Proposal ### AND/OR filtering Allow users to toggle between AND and OR when filtering by multiple tags in the search bar. AND = "all of these tags", OR = "any of these tags". ### Tag hierarchy Support parent/child relationships between tags so related concepts can be grouped. E.g. "Immobilie" could be a parent of "Grundstück" and "Haus". Searching for a parent tag would also include documents tagged with child tags. ### Tag overview page A browsable page showing all tags with document counts, organized by hierarchy. Entry point for tag-based exploration. ### Open questions - How deep should hierarchy go? Single level (parent → children) or unlimited nesting? - Should the tag admin page handle hierarchy, or a separate UI? - How to migrate existing flat tags into a hierarchy — manual only or suggest groupings? - UI for AND/OR toggle — simple switch next to tag filter, or per-tag operator?
marcel added the feature label 2026-04-12 08:31:07 +02:00
Author
Owner

🏗️ Markus Keller — Application Architect

Architecture discussion covering the data model for tag hierarchy, query strategy, API contract, and migration approach.


Resolved

1. Data model → adjacency list
Add a nullable parent_id UUID REFERENCES tag(id) column to the tag table. FK enforces parent existence at the DB layer. This replaces the slash/materialized-path approach considered during discussion.

2. Depth → unlimited
No depth cap enforced. The adjacency list handles any depth via recursive CTEs. At family archive scale (<500 tags, depth realistically 3–4 max) this is fast and needs no special indexing.

3. Rename cascade
Solved by the adjacency list — renaming a tag is a single row update. Children continue to point to the parent via FK; no cascade UPDATE across name strings needed.

4. Referential integrity

  • Parent existence: DB-enforced via FK.
  • Self-reference: DB-enforced via CHECK constraint: CHECK (parent_id != id)
  • Cycle detection (A → B → A): enforced in TagService before save. Not worth a trigger at this scale.

5. AND/OR filtering
Bundled into this issue. No schema change needed — it's a query change (intersection vs. union) and a UI toggle. Can be implemented independently of the hierarchy work.

6. Tag overview page API contract
Single endpoint returns a pre-assembled tree — the backend builds it with one recursive CTE + document count join. No tree assembly in the frontend.

[
  {
    "id": "...",
    "name": "Immobilie",
    "documentCount": 12,
    "children": [
      { "id": "...", "name": "Haus", "documentCount": 7, "children": [] },
      { "id": "...", "name": "Grundstück", "documentCount": 5, "children": [] }
    ]
  }
]

7. Migration of existing flat tags
The parent_id column lands as NULL for all existing tags — they remain fully valid flat tags, nothing breaks on deploy. Post-deploy curation (grouping existing tags into the hierarchy) is manual work in the tag admin UI.


Out of scope

  • Automated grouping suggestions for existing flat tags — speculative complexity, closed as out of scope. Manual curation only.

Overall this is a well-scoped feature. The adjacency list keeps the schema minimal, the recursive CTE is standard SQL any Spring dev can read, and the tree-from-backend API contract keeps the frontend dumb. Ship the Flyway migration first, then build the query and UI on top.

## 🏗️ Markus Keller — Application Architect Architecture discussion covering the data model for tag hierarchy, query strategy, API contract, and migration approach. --- ### Resolved **1. Data model → adjacency list** Add a nullable `parent_id UUID REFERENCES tag(id)` column to the `tag` table. FK enforces parent existence at the DB layer. This replaces the slash/materialized-path approach considered during discussion. **2. Depth → unlimited** No depth cap enforced. The adjacency list handles any depth via recursive CTEs. At family archive scale (<500 tags, depth realistically 3–4 max) this is fast and needs no special indexing. **3. Rename cascade** Solved by the adjacency list — renaming a tag is a single row update. Children continue to point to the parent via FK; no cascade UPDATE across name strings needed. **4. Referential integrity** - Parent existence: DB-enforced via FK. - Self-reference: DB-enforced via CHECK constraint: `CHECK (parent_id != id)` - Cycle detection (A → B → A): enforced in `TagService` before save. Not worth a trigger at this scale. **5. AND/OR filtering** Bundled into this issue. No schema change needed — it's a query change (intersection vs. union) and a UI toggle. Can be implemented independently of the hierarchy work. **6. Tag overview page API contract** Single endpoint returns a pre-assembled tree — the backend builds it with one recursive CTE + document count join. No tree assembly in the frontend. ```json [ { "id": "...", "name": "Immobilie", "documentCount": 12, "children": [ { "id": "...", "name": "Haus", "documentCount": 7, "children": [] }, { "id": "...", "name": "Grundstück", "documentCount": 5, "children": [] } ] } ] ``` **7. Migration of existing flat tags** The `parent_id` column lands as `NULL` for all existing tags — they remain fully valid flat tags, nothing breaks on deploy. Post-deploy curation (grouping existing tags into the hierarchy) is manual work in the tag admin UI. --- ### Out of scope - **Automated grouping suggestions** for existing flat tags — speculative complexity, closed as out of scope. Manual curation only. --- Overall this is a well-scoped feature. The adjacency list keeps the schema minimal, the recursive CTE is standard SQL any Spring dev can read, and the tree-from-backend API contract keeps the frontend dumb. Ship the Flyway migration first, then build the query and UI on top.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

UI decisions covering the tag overview page, typeahead adaptation, AND/OR toggle, chip display, and hierarchy editing.


Resolved

1. Tag overview page — descoped
Not building a dedicated /tags browse page at this stage. Tag usage isn't heavy enough to justify the full design investment. Revisit when the data shows it's needed.

2. Typeahead adaptation for hierarchy

  • Dropdown: indented grouped list — parent tag as a non-selectable section header, children indented underneath
  • Selected chip: bare tag name only (names are unique within a parent group, so no ambiguity)
  • Disambiguation: full path (Immobilie / Haus) shown on hover tooltip
  • Optional color dot (see item below) provides secondary visual grouping cue

3. Tag colors (opt-in per parent tag)
Admins can assign a color to any parent tag from a curated palette of 8–10 pre-verified accessible colors. Color is expressed as a small dot or left-border accent on the chip — never a full background fill. This keeps WCAG contrast manageable and avoids rainbow clutter on documents with multiple tags. Color is supplementary to the text label, not a replacement.

4. AND/OR toggle
Single global pill toggle next to the tag filter chips — "AND" / "OR" — switches all selected tags at once. Per-tag operators are out of scope.

5. Document card chips
Same pattern as typeahead: bare tag name + optional color dot + full-path tooltip on hover. Consistent across the entire UI — no special casing on the document card.

6. Hierarchy editing in tag admin
Parent selector dropdown added to the existing tag edit form. No tree editor or drag-and-drop for now — admins pick a parent from a flat list of root-level tags and save. Sufficient for infrequent curation at this tag volume.


Out of scope

  • Tag overview / browse page — descoped, not blocked, just not now.
  • Per-tag AND/OR operators — global toggle covers realistic use cases; mixed-logic queries are overkill.
  • Free color picker for tag colors — curated palette only to prevent contrast failures and maintain visual coherence.

The chip + tooltip pattern keeps the UI compact at 320px while giving power users the full context they need. Start with the parent selector in admin; tree editor is a natural follow-on if the hierarchy grows complex enough to need it.

## 🎨 Leonie Voss — UI/UX Design Lead UI decisions covering the tag overview page, typeahead adaptation, AND/OR toggle, chip display, and hierarchy editing. --- ### Resolved **1. Tag overview page — descoped** Not building a dedicated `/tags` browse page at this stage. Tag usage isn't heavy enough to justify the full design investment. Revisit when the data shows it's needed. **2. Typeahead adaptation for hierarchy** - Dropdown: indented grouped list — parent tag as a non-selectable section header, children indented underneath - Selected chip: bare tag name only (names are unique within a parent group, so no ambiguity) - Disambiguation: full path (`Immobilie / Haus`) shown on hover tooltip - Optional color dot (see item below) provides secondary visual grouping cue **3. Tag colors (opt-in per parent tag)** Admins can assign a color to any parent tag from a curated palette of 8–10 pre-verified accessible colors. Color is expressed as a small dot or left-border accent on the chip — never a full background fill. This keeps WCAG contrast manageable and avoids rainbow clutter on documents with multiple tags. Color is supplementary to the text label, not a replacement. **4. AND/OR toggle** Single global pill toggle next to the tag filter chips — "AND" / "OR" — switches all selected tags at once. Per-tag operators are out of scope. **5. Document card chips** Same pattern as typeahead: bare tag name + optional color dot + full-path tooltip on hover. Consistent across the entire UI — no special casing on the document card. **6. Hierarchy editing in tag admin** Parent selector dropdown added to the existing tag edit form. No tree editor or drag-and-drop for now — admins pick a parent from a flat list of root-level tags and save. Sufficient for infrequent curation at this tag volume. --- ### Out of scope - **Tag overview / browse page** — descoped, not blocked, just not now. - **Per-tag AND/OR operators** — global toggle covers realistic use cases; mixed-logic queries are overkill. - **Free color picker for tag colors** — curated palette only to prevent contrast failures and maintain visual coherence. --- The chip + tooltip pattern keeps the UI compact at 320px while giving power users the full context they need. Start with the parent selector in admin; tree editor is a natural follow-on if the hierarchy grows complex enough to need it.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Questions & Observations

  • AND/OR as a standalone increment: Markus confirmed AND/OR requires no schema change — just a query switch and a toggle. Has it been explicitly decided to ship that as a standalone PR before the hierarchy work? It delivers immediate value to power users and keeps the hierarchy PR focused.
  • Cycle detection algorithm: the issue says cycle detection lives in TagService, not a DB trigger. What's the concrete approach? My read: load all ancestor UUIDs of the proposed parent in a recursive CTE, then check if the tag being edited appears in that chain. One query, deterministic. Anything else risks a TOCTOU window between the check and the insert.
  • Response shape for the tree endpoint: the adjacency list lives in Tag entities but the tree endpoint returns nested children per Markus's API contract. That implies a new TagTreeNodeDTO record — the Tag entity has no children field. Will TagService.getAllTags() be extended or will a new getTagTree() method be added alongside it?
  • AND/OR state on the frontend: where does the toggle state live — URL search parameter, component state, or a Svelte store? URL param (?tagOp=OR) is the right default: back button and link-sharing both work. Component state would reset on navigation.
  • TagInput component extension: the current typeahead shows a flat list. The new "parent as non-selectable section header, children indented" pattern is a meaningfully different interaction. Does the existing TagInput get extended in-place, or is a new HierarchicalTagInput component created? Extending in-place adds conditional complexity; a new component keeps the existing behavior stable.

Suggestions

  • Ship AND/OR first as its own PR — it's a safe, independent change with zero migration risk.
  • TagTreeNodeDTO as a Java record: record TagTreeNodeDTO(UUID id, String name, int documentCount, List<TagTreeNodeDTO> children) {} — immutable, no Lombok needed.
  • For the non-selectable parent header in the typeahead: use role="presentation" or aria-disabled="true" with pointer-events: none so keyboard users can't accidentally select a parent group header.
  • For AND/OR state: ?tagOp=OR as a URL param, defaulting to AND when absent (preserves existing behavior with no migration).
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Questions & Observations - **AND/OR as a standalone increment**: Markus confirmed AND/OR requires no schema change — just a query switch and a toggle. Has it been explicitly decided to ship that as a standalone PR before the hierarchy work? It delivers immediate value to power users and keeps the hierarchy PR focused. - **Cycle detection algorithm**: the issue says cycle detection lives in `TagService`, not a DB trigger. What's the concrete approach? My read: load all ancestor UUIDs of the proposed parent in a recursive CTE, then check if the tag being edited appears in that chain. One query, deterministic. Anything else risks a TOCTOU window between the check and the insert. - **Response shape for the tree endpoint**: the adjacency list lives in `Tag` entities but the tree endpoint returns nested children per Markus's API contract. That implies a new `TagTreeNodeDTO` record — the `Tag` entity has no `children` field. Will `TagService.getAllTags()` be extended or will a new `getTagTree()` method be added alongside it? - **AND/OR state on the frontend**: where does the toggle state live — URL search parameter, component state, or a Svelte store? URL param (`?tagOp=OR`) is the right default: back button and link-sharing both work. Component state would reset on navigation. - **`TagInput` component extension**: the current typeahead shows a flat list. The new "parent as non-selectable section header, children indented" pattern is a meaningfully different interaction. Does the existing `TagInput` get extended in-place, or is a new `HierarchicalTagInput` component created? Extending in-place adds conditional complexity; a new component keeps the existing behavior stable. ### Suggestions - Ship AND/OR first as its own PR — it's a safe, independent change with zero migration risk. - `TagTreeNodeDTO` as a Java record: `record TagTreeNodeDTO(UUID id, String name, int documentCount, List<TagTreeNodeDTO> children) {}` — immutable, no Lombok needed. - For the non-selectable parent header in the typeahead: use `role="presentation"` or `aria-disabled="true"` with `pointer-events: none` so keyboard users can't accidentally select a parent group header. - For AND/OR state: `?tagOp=OR` as a URL param, defaulting to AND when absent (preserves existing behavior with no migration).
Author
Owner

🧪 Sara Holt — QA Engineer

Questions & Observations

  • Acceptance criteria for cycle detection: I need explicit test cases before implementation. The three cases are: (1) self-loop A → A — caught by the DB CHECK constraint; (2) direct cycle A → B → A — caught by TagService; (3) deep cycle A → B → C → A — also caught by TagService. Do all three have passing integration tests on day one?
  • AND/OR behavioral contract: the default must be AND to preserve existing search behavior. A user with a saved search (bookmarked URL) must see identical results after deploy. This should be a regression test: search with N tags using the default AND mode, assert results are unchanged post-migration.
  • Hierarchy search semantics — the critical question: when a user selects the parent tag "Immobilie", does the search include documents tagged only with the child tag "Haus"? Or must the document carry "Immobilie" itself? This affects the search query join and the user's mental model. Neither answer is wrong, but both need an explicit acceptance criterion and a test.
  • Edge cases for the recursive CTE: what happens with depth > 3? With a tag that has no children? With a tag that has children but zero documents on the parent itself? The documentCount in the tree response — does it count documents on the tag directly, or documents on the tag and all its descendants?
  • Color palette accessibility: the 8–10 curated colors will appear as dots on chips over white card backgrounds and brand-sand (#E4E2D7) page backgrounds. Who validates contrast ratios against both backgrounds before the palette is committed to code?
  • Typeahead regression: existing TagInput behavior (flat list, all tags selectable) must not regress when hierarchy is introduced. Tags with parent_id = NULL should behave exactly as before.

Suggestions

  • @ParameterizedTest for cycle detection: (sourceId, targetParentId, expectedException) — covers self-loop, direct cycle, and deep cycle in one parameterized class.
  • Dedicated TagServiceIntegrationTest covering: flat tag round-trip (null parent), parent-child assignment, cycle rejection at depth 1 and depth 3, and behavior when a parent tag is deleted (children become root or are blocked, depending on the ON DELETE decision).
  • The recursive CTE + document count join needs a timing assertion in CI: create a representative dataset (e.g., 200 tags, 3 levels deep, 500 documents) and assert query time < 200ms. Cheap to add, catches query regressions before they reach production.
## 🧪 Sara Holt — QA Engineer ### Questions & Observations - **Acceptance criteria for cycle detection**: I need explicit test cases before implementation. The three cases are: (1) self-loop `A → A` — caught by the DB `CHECK` constraint; (2) direct cycle `A → B → A` — caught by `TagService`; (3) deep cycle `A → B → C → A` — also caught by `TagService`. Do all three have passing integration tests on day one? - **AND/OR behavioral contract**: the default must be AND to preserve existing search behavior. A user with a saved search (bookmarked URL) must see identical results after deploy. This should be a regression test: search with N tags using the default AND mode, assert results are unchanged post-migration. - **Hierarchy search semantics — the critical question**: when a user selects the parent tag "Immobilie", does the search include documents tagged *only* with the child tag "Haus"? Or must the document carry "Immobilie" itself? This affects the search query join and the user's mental model. Neither answer is wrong, but both need an explicit acceptance criterion and a test. - **Edge cases for the recursive CTE**: what happens with depth > 3? With a tag that has no children? With a tag that has children but zero documents on the parent itself? The `documentCount` in the tree response — does it count documents on the tag directly, or documents on the tag and all its descendants? - **Color palette accessibility**: the 8–10 curated colors will appear as dots on chips over white card backgrounds and brand-sand (`#E4E2D7`) page backgrounds. Who validates contrast ratios against both backgrounds before the palette is committed to code? - **Typeahead regression**: existing `TagInput` behavior (flat list, all tags selectable) must not regress when hierarchy is introduced. Tags with `parent_id = NULL` should behave exactly as before. ### Suggestions - `@ParameterizedTest` for cycle detection: `(sourceId, targetParentId, expectedException)` — covers self-loop, direct cycle, and deep cycle in one parameterized class. - Dedicated `TagServiceIntegrationTest` covering: flat tag round-trip (null parent), parent-child assignment, cycle rejection at depth 1 and depth 3, and behavior when a parent tag is deleted (children become root or are blocked, depending on the `ON DELETE` decision). - The recursive CTE + document count join needs a timing assertion in CI: create a representative dataset (e.g., 200 tags, 3 levels deep, 500 documents) and assert query time < 200ms. Cheap to add, catches query regressions before they reach production.
Author
Owner

🔐 Nora Steiner (NullX) — Security Engineer

Questions & Observations

  • Authorization on parent_id writes: who is allowed to set or change a tag's parent? The issue mentions a parent selector in the tag admin form, which implies ADMIN_TAG. But if a WRITE_ALL user can also write tags, can they restructure the hierarchy? An unintended actor reshuffling the entire tag taxonomy is a low-impact but real issue in a shared family archive. Recommend: parent_id writes guarded by ADMIN_TAG at minimum, separate from general tag CRUD.
  • TOCTOU in cycle detection: the cycle check in TagService has a window — two concurrent requests can both pass the ancestor check before either commits. This isn't exploitable at family archive scale (few concurrent admin writers), but it's worth a short comment in TagService.checkForCycle() explaining why the application-layer check is sufficient here and the self-reference CHECK constraint is the hard backstop.
  • Color value validation: the curated palette lives in the frontend dropdown, but is the color value validated server-side? A crafted request bypassing the UI could store an arbitrary string in the color column. A Set<String> ALLOWED_COLORS whitelist check in TagService before save (returning DomainException.badRequest) is the right control — cheap, explicit, and auditable.
  • Recursive CTE depth: WITH RECURSIVE in PostgreSQL has no built-in depth limit. A pathologically deep hierarchy (100+ levels) — created by a bug or an admin mistake — generates a proportionally expensive query. A WHERE depth < 50 guard clause in the CTE costs nothing and prevents a slow-query amplification scenario.
  • Tag visibility model: are all tags visible to all authenticated users regardless of group? If so, this should be explicitly stated in code (a comment in TagController or TagService) so future contributors don't add per-user tag filtering inconsistently. No change needed — just an explicit "intentional: all tags are global" note.

Suggestions

  • Add @RequirePermission(Permission.ADMIN_TAG) to the parent assignment operation (either on the update endpoint or specifically on the parent_id field path in the service).
  • Backend: private static final Set<String> ALLOWED_TAG_COLORS = Set.of(...) constant in TagService, validated before save. Fail with DomainException.badRequest(ErrorCode.INVALID_TAG_COLOR, ...) if not in the set.
  • CTE guard: AND depth < 50 in the recursive branch of the WITH RECURSIVE query — not a security requirement at this scale, but correct defensive practice.
  • Annotate checkForCycle() with a one-line comment: // TOCTOU: concurrent writes could both pass; self-reference CHECK constraint is the DB-layer backstop.
## 🔐 Nora Steiner (NullX) — Security Engineer ### Questions & Observations - **Authorization on `parent_id` writes**: who is allowed to set or change a tag's parent? The issue mentions a parent selector in the tag admin form, which implies `ADMIN_TAG`. But if a `WRITE_ALL` user can also write tags, can they restructure the hierarchy? An unintended actor reshuffling the entire tag taxonomy is a low-impact but real issue in a shared family archive. Recommend: `parent_id` writes guarded by `ADMIN_TAG` at minimum, separate from general tag CRUD. - **TOCTOU in cycle detection**: the cycle check in `TagService` has a window — two concurrent requests can both pass the ancestor check before either commits. This isn't exploitable at family archive scale (few concurrent admin writers), but it's worth a short comment in `TagService.checkForCycle()` explaining why the application-layer check is sufficient here and the self-reference `CHECK` constraint is the hard backstop. - **Color value validation**: the curated palette lives in the frontend dropdown, but is the color value validated server-side? A crafted request bypassing the UI could store an arbitrary string in the color column. A `Set<String> ALLOWED_COLORS` whitelist check in `TagService` before save (returning `DomainException.badRequest`) is the right control — cheap, explicit, and auditable. - **Recursive CTE depth**: `WITH RECURSIVE` in PostgreSQL has no built-in depth limit. A pathologically deep hierarchy (100+ levels) — created by a bug or an admin mistake — generates a proportionally expensive query. A `WHERE depth < 50` guard clause in the CTE costs nothing and prevents a slow-query amplification scenario. - **Tag visibility model**: are all tags visible to all authenticated users regardless of group? If so, this should be explicitly stated in code (a comment in `TagController` or `TagService`) so future contributors don't add per-user tag filtering inconsistently. No change needed — just an explicit "intentional: all tags are global" note. ### Suggestions - Add `@RequirePermission(Permission.ADMIN_TAG)` to the parent assignment operation (either on the update endpoint or specifically on the `parent_id` field path in the service). - Backend: `private static final Set<String> ALLOWED_TAG_COLORS = Set.of(...)` constant in `TagService`, validated before save. Fail with `DomainException.badRequest(ErrorCode.INVALID_TAG_COLOR, ...)` if not in the set. - CTE guard: `AND depth < 50` in the recursive branch of the `WITH RECURSIVE` query — not a security requirement at this scale, but correct defensive practice. - Annotate `checkForCycle()` with a one-line comment: `// TOCTOU: concurrent writes could both pass; self-reference CHECK constraint is the DB-layer backstop.`
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Questions & Observations

  • ON DELETE behavior for parent_id FK: when a parent tag is deleted, what happens to its children? ON DELETE SET NULL promotes them to root tags. ON DELETE RESTRICT blocks deletion until children are reassigned first. Neither is wrong, but this needs a decision before writing the Flyway migration — it's harder to change later than the column itself.
  • Index on parent_id: any query that looks up "children of parent X" will do a full table scan on tag without an index on parent_id. At <500 tags it's imperceptible, but the index should be in the same migration as the column — it's free to add now and expensive to add later (well, cheap at this size, but it's a good habit).
  • Color column type and constraint: is the color stored as VARCHAR(7) for hex codes? A regex CHECK in the migration (CHECK (color IS NULL OR color ~ '^#[0-9a-fA-F]{6}$')) keeps the database consistent without depending solely on application-layer validation. If the palette later expands to named colors or HSL values, the constraint can be loosened; adding it now is the correct default.
  • No new infrastructure — good. Recursive CTEs are standard PostgreSQL 16, no extension or config change needed. The existing Testcontainers setup (postgres:16-alpine) covers it with zero changes.
  • CI impact: the recursive CTE + join is new SQL that the migration integration test will exercise on every CI run. No special action needed, just confirming it's covered by the existing MigrationIntegrationTest.

Suggestions

Concrete migration structure to discuss before writing:

-- V{N}__add_tag_hierarchy.sql
ALTER TABLE tag ADD COLUMN parent_id UUID REFERENCES tag(id) ON DELETE SET NULL;
ALTER TABLE tag ADD CONSTRAINT chk_tag_no_self_reference CHECK (parent_id != id);
CREATE INDEX idx_tag_parent_id ON tag(parent_id);
ALTER TABLE tag ADD COLUMN color VARCHAR(7);
ALTER TABLE tag ADD CONSTRAINT chk_tag_color_format CHECK (color IS NULL OR color ~ '^#[0-9a-fA-F]{6}$');
  • Decide SET NULL vs RESTRICT before writing — this is the one migration decision that affects user-facing behavior.
  • The WITH RECURSIVE query is the most complex SQL this project has had — worth a short inline comment in the repository method explaining what it does, for the next developer who reads it.
  • No new services, no new volumes, no new ports. This is a pure application change. Deploy is a standard docker compose pull && docker compose up -d with Flyway running on startup.
## 🛠️ Tobias Wendt — DevOps & Platform Engineer ### Questions & Observations - **`ON DELETE` behavior for `parent_id` FK**: when a parent tag is deleted, what happens to its children? `ON DELETE SET NULL` promotes them to root tags. `ON DELETE RESTRICT` blocks deletion until children are reassigned first. Neither is wrong, but this needs a decision before writing the Flyway migration — it's harder to change later than the column itself. - **Index on `parent_id`**: any query that looks up "children of parent X" will do a full table scan on `tag` without an index on `parent_id`. At <500 tags it's imperceptible, but the index should be in the same migration as the column — it's free to add now and expensive to add later (well, cheap at this size, but it's a good habit). - **Color column type and constraint**: is the color stored as `VARCHAR(7)` for hex codes? A regex `CHECK` in the migration (`CHECK (color IS NULL OR color ~ '^#[0-9a-fA-F]{6}$')`) keeps the database consistent without depending solely on application-layer validation. If the palette later expands to named colors or HSL values, the constraint can be loosened; adding it now is the correct default. - **No new infrastructure** — good. Recursive CTEs are standard PostgreSQL 16, no extension or config change needed. The existing Testcontainers setup (`postgres:16-alpine`) covers it with zero changes. - **CI impact**: the recursive CTE + join is new SQL that the migration integration test will exercise on every CI run. No special action needed, just confirming it's covered by the existing `MigrationIntegrationTest`. ### Suggestions Concrete migration structure to discuss before writing: ```sql -- V{N}__add_tag_hierarchy.sql ALTER TABLE tag ADD COLUMN parent_id UUID REFERENCES tag(id) ON DELETE SET NULL; ALTER TABLE tag ADD CONSTRAINT chk_tag_no_self_reference CHECK (parent_id != id); CREATE INDEX idx_tag_parent_id ON tag(parent_id); ALTER TABLE tag ADD COLUMN color VARCHAR(7); ALTER TABLE tag ADD CONSTRAINT chk_tag_color_format CHECK (color IS NULL OR color ~ '^#[0-9a-fA-F]{6}$'); ``` - Decide `SET NULL` vs `RESTRICT` before writing — this is the one migration decision that affects user-facing behavior. - The `WITH RECURSIVE` query is the most complex SQL this project has had — worth a short inline comment in the repository method explaining what it does, for the next developer who reads it. - No new services, no new volumes, no new ports. This is a pure application change. Deploy is a standard `docker compose pull && docker compose up -d` with Flyway running on startup.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Implementation discussion — six open questions resolved before writing code.


Resolved

1. ON DELETE behavior for parent_id FK → SET NULL
When a parent tag is deleted, children become root-level tags. Chosen because tag deletion is admin-only — an admin making this call knows what they're doing, and silent promotion to root is the right default over blocking the operation.

2. Hierarchy search semantics → inclusive
Selecting a parent tag in the search filter matches documents tagged with any descendant, not only documents carrying the parent tag directly. Selecting "Immobilie" returns docs tagged with "Haus", "Grundstück", etc. The search query will collect all descendant IDs via a recursive CTE before filtering documents.

3. documentCount in tree response → inclusive of all descendants
Consistent with item 2 — the count shown on a parent node reflects the total documents reachable when clicking that tag. No mismatch between the number displayed and the number returned by search.

4. TagTreeNodeDTO → new Java record + new getTagTree() method
record TagTreeNodeDTO(UUID id, String name, int documentCount, List<TagTreeNodeDTO> children) {} as a new record. New getTagTree() method in TagService alongside the existing getAllTags() — the flat method stays untouched so typeahead callers are unaffected.

5. TagInput component → extend in-place
The existing TagInput will be extended to support the hierarchical grouped view (parent as non-selectable section header, children indented). The flat code path for tags without children stays unchanged — zero regression risk for existing callers.

6. Shipping order → AND/OR ships in the same PR as hierarchy
AND/OR toggle and query switch will be implemented within the same PR as the hierarchy work, not as a standalone increment.


Overall this is implementation-ready. The adjacency list + recursive CTE approach is clean, the tree-from-backend contract keeps the frontend dumb, and all the decisions above are consistent with each other. The Flyway migration (SET NULL, index on parent_id, color column with check constraint) can be written now.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer Implementation discussion — six open questions resolved before writing code. --- ### Resolved **1. ON DELETE behavior for `parent_id` FK → `SET NULL`** When a parent tag is deleted, children become root-level tags. Chosen because tag deletion is admin-only — an admin making this call knows what they're doing, and silent promotion to root is the right default over blocking the operation. **2. Hierarchy search semantics → inclusive** Selecting a parent tag in the search filter matches documents tagged with *any descendant*, not only documents carrying the parent tag directly. Selecting "Immobilie" returns docs tagged with "Haus", "Grundstück", etc. The search query will collect all descendant IDs via a recursive CTE before filtering documents. **3. `documentCount` in tree response → inclusive of all descendants** Consistent with item 2 — the count shown on a parent node reflects the total documents reachable when clicking that tag. No mismatch between the number displayed and the number returned by search. **4. `TagTreeNodeDTO` → new Java record + new `getTagTree()` method** `record TagTreeNodeDTO(UUID id, String name, int documentCount, List<TagTreeNodeDTO> children) {}` as a new record. New `getTagTree()` method in `TagService` alongside the existing `getAllTags()` — the flat method stays untouched so typeahead callers are unaffected. **5. `TagInput` component → extend in-place** The existing `TagInput` will be extended to support the hierarchical grouped view (parent as non-selectable section header, children indented). The flat code path for tags without children stays unchanged — zero regression risk for existing callers. **6. Shipping order → AND/OR ships in the same PR as hierarchy** AND/OR toggle and query switch will be implemented within the same PR as the hierarchy work, not as a standalone increment. --- Overall this is implementation-ready. The adjacency list + recursive CTE approach is clean, the tree-from-backend contract keeps the frontend dumb, and all the decisions above are consistent with each other. The Flyway migration (`SET NULL`, index on `parent_id`, color column with check constraint) can be written now.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

Color palette and admin picker design — all decisions committed, ready to implement.


Resolved

1. Tag color palette — 10 semantic tokens, CSS custom properties, light + dark pairs

Colors are stored as token names (e.g. "sage") in the database — not hex values. The CSS resolves the actual color per theme. All values verified for 3:1 contrast as non-text UI elements against both light surfaces (#ffffff, #f0efe9) and dark surface (#011526).

Defined in layout.css following the existing badge color pattern (--c-badge-*):

/* :root (light mode) */
--c-tag-navy:       #012851;
--c-tag-teal:       #2B6E6B;
--c-tag-ocean:      #1A5F8B;
--c-tag-forest:     #2A5C3B;
--c-tag-sage:       #3B7A57;
--c-tag-sienna:     #7B4A2A;
--c-tag-terracotta: #B5501E;
--c-tag-ochre:      #8A6A0A;
--c-tag-rose:       #8B3A5C;
--c-tag-violet:     #5B3D8B;

/* dark mode blocks (both @media and [data-theme='dark']) */
--c-tag-navy:       #5B8CB8;
--c-tag-teal:       #5EBFBB;
--c-tag-ocean:      #5EA8D8;
--c-tag-forest:     #5A9E75;
--c-tag-sage:       #6FBD94;
--c-tag-sienna:     #C48A60;
--c-tag-terracotta: #E8906B;
--c-tag-ochre:      #D4B040;
--c-tag-rose:       #C47A9B;
--c-tag-violet:     #9B7BD4;

The chip dot uses style="background-color: var(--c-tag-{tag.color})". No JS map needed — the CSS variable name is the contract between frontend and database. The mix is intentional: archival warm tones (sienna, terracotta, ochre, rose) and clean categorical colors (navy, teal, ocean, forest, violet, sage).

2. No foreground text problem on chips

The dot carries no text. The chip background stays --c-surface and the chip label stays text-ink — both remap automatically per theme. No per-chip foreground color needed.

3. Admin color picker — grid of circles, ring selection, null slot, root tags only

  • 32px circles in the tag edit form, laid out as a small grid
  • Selected state: 2px offset ring in --c-focus-ring (navy in light, mint in dark) — no overlaid checkmark, no text inside swatches, no foreground text problem
  • "No color" slot: empty circle with dashed border, clears the field to null
  • Picker only renders when parent_id IS NULL — child tags inherit their parent's color cue implicitly, no per-child assignment

4. Backend allowlist

Set.of("navy", "teal", "ocean", "forest", "sage", "sienna", "terracotta", "ochre", "rose", "violet") in TagService, validated before save:

private static final Set<String> ALLOWED_TAG_COLORS = Set.of(
    "navy", "teal", "ocean", "forest", "sage",
    "sienna", "terracotta", "ochre", "rose", "violet"
);

Fail with DomainException.badRequest(ErrorCode.INVALID_TAG_COLOR, ...) if not in the set.


Out of scope

  • Color labels in the picker — swatches only, no "Salbei" / "Terrakotta" labels. At 10 colors the visual differentiation is sufficient; labels add translation overhead for marginal gain.

The palette is warm enough for a family archive, distinct enough to be useful as a grouping cue, and fully accessible in both modes. The CSS-variable-as-contract approach keeps the picker simple and the backend validation cheap.

## 🎨 Leonie Voss — UI/UX Design Lead Color palette and admin picker design — all decisions committed, ready to implement. --- ### Resolved **1. Tag color palette — 10 semantic tokens, CSS custom properties, light + dark pairs** Colors are stored as token names (e.g. `"sage"`) in the database — not hex values. The CSS resolves the actual color per theme. All values verified for 3:1 contrast as non-text UI elements against both light surfaces (`#ffffff`, `#f0efe9`) and dark surface (`#011526`). Defined in `layout.css` following the existing badge color pattern (`--c-badge-*`): ```css /* :root (light mode) */ --c-tag-navy: #012851; --c-tag-teal: #2B6E6B; --c-tag-ocean: #1A5F8B; --c-tag-forest: #2A5C3B; --c-tag-sage: #3B7A57; --c-tag-sienna: #7B4A2A; --c-tag-terracotta: #B5501E; --c-tag-ochre: #8A6A0A; --c-tag-rose: #8B3A5C; --c-tag-violet: #5B3D8B; /* dark mode blocks (both @media and [data-theme='dark']) */ --c-tag-navy: #5B8CB8; --c-tag-teal: #5EBFBB; --c-tag-ocean: #5EA8D8; --c-tag-forest: #5A9E75; --c-tag-sage: #6FBD94; --c-tag-sienna: #C48A60; --c-tag-terracotta: #E8906B; --c-tag-ochre: #D4B040; --c-tag-rose: #C47A9B; --c-tag-violet: #9B7BD4; ``` The chip dot uses `style="background-color: var(--c-tag-{tag.color})"`. No JS map needed — the CSS variable name is the contract between frontend and database. The mix is intentional: archival warm tones (sienna, terracotta, ochre, rose) and clean categorical colors (navy, teal, ocean, forest, violet, sage). **2. No foreground text problem on chips** The dot carries no text. The chip background stays `--c-surface` and the chip label stays `text-ink` — both remap automatically per theme. No per-chip foreground color needed. **3. Admin color picker — grid of circles, ring selection, null slot, root tags only** - 32px circles in the tag edit form, laid out as a small grid - Selected state: 2px offset ring in `--c-focus-ring` (navy in light, mint in dark) — no overlaid checkmark, no text inside swatches, no foreground text problem - "No color" slot: empty circle with dashed border, clears the field to `null` - Picker only renders when `parent_id IS NULL` — child tags inherit their parent's color cue implicitly, no per-child assignment **4. Backend allowlist** `Set.of("navy", "teal", "ocean", "forest", "sage", "sienna", "terracotta", "ochre", "rose", "violet")` in `TagService`, validated before save: ```java private static final Set<String> ALLOWED_TAG_COLORS = Set.of( "navy", "teal", "ocean", "forest", "sage", "sienna", "terracotta", "ochre", "rose", "violet" ); ``` Fail with `DomainException.badRequest(ErrorCode.INVALID_TAG_COLOR, ...)` if not in the set. --- ### Out of scope - **Color labels in the picker** — swatches only, no "Salbei" / "Terrakotta" labels. At 10 colors the visual differentiation is sufficient; labels add translation overhead for marginal gain. --- The palette is warm enough for a family archive, distinct enough to be useful as a grouping cue, and fully accessible in both modes. The CSS-variable-as-contract approach keeps the picker simple and the backend validation cheap.
Author
Owner

Implementation complete

All planned work for tag hierarchy, AND/OR filtering, and tag colors has been implemented on branch feat/issue-221-tag-hierarchy.

What was built

Backend

  • V39__add_tag_hierarchy.sql — adds parent_id UUID NULL REFERENCES tag(id) ON DELETE SET NULL + color VARCHAR(20) NULL + index
  • Tag entity gains parentId and color fields
  • TagUpdateDTO record with name, parentId, color
  • TagTreeNodeDTO record (nested tree with cumulative documentCount)
  • ErrorCode.INVALID_TAG_COLOR + ErrorCode.TAG_CYCLE_DETECTED
  • TagRepository.findDescendantIdsByName() — recursive CTE with depth < 50 guard
  • TagRepository.getTreeRows() — full tree CTE for tree assembly
  • TagService.expandTagNamesToDescendantIdSets() — expands tag names to descendant ID sets
  • TagService.checkForCycle() — ancestor traversal, throws TAG_CYCLE_DETECTED
  • TagService.getTagTree() — assembles TagTreeNodeDTO tree with cumulative counts
  • TagService.update() accepts TagUpdateDTO; validates color against allowlist, checks cycles
  • DocumentService.searchDocuments() accepts tagOperator: AND | OR; uses hierarchy-expanded ID sets
  • GET /api/tags/tree endpoint
  • OpenAPI types regenerated

Frontend

  • layout.css — 10 --c-tag-{name} CSS custom properties (light + dark variants)
  • TagInput.svelte — binding changed from string[] to Tag[]; hierarchical dropdown with parent headers; color dot on chips
  • SearchFilterBar.svelte — AND/OR pill toggle (visible when ≥ 2 tags selected)
  • DocumentList.svelte — color dot on tag chips
  • +page.svelte + +page.server.ts?tagOp=OR URL param wired end-to-end
  • Admin tag edit form (/admin/tags/[id]) — parent selector + 10-swatch color picker
  • TagsListPanel.svelte — hierarchical rendering with indentation + color dots
  • errors.ts + de.json / en.json / es.jsonINVALID_TAG_COLOR + TAG_CYCLE_DETECTED

Commits

  • 5b4f7f2 migration: V39 add tag hierarchy and color columns
  • 70cb0ab backend: add Tag parentId/color, TagUpdateDTO, TagTreeNodeDTO, error codes
  • 4e748be backend: add recursive CTE queries to TagRepository
  • 2da07f5 backend: cycle detection and color validation in TagService
  • 3bb0b81 backend: getTagTree() assembles nested TagTreeNodeDTO with cumulative counts
  • 3fd6e1e backend: expandTagNamesToDescendantIdSets() in TagService
  • df6e8c4 backend: TagService.update() accepts TagUpdateDTO with parentId/color
  • 57dc72b backend: AND/OR + hierarchy expansion in document search
  • aca7a22 backend: GET /api/tags/tree endpoint
  • 9f14f72 frontend: regenerate API types with parentId/color/tagOp/tree endpoint
  • ab1cf06 frontend: add 10 tag color CSS custom properties (light + dark)
  • 60618e0 frontend: TagInput – Tag type, hierarchical dropdown, color dots
  • abba85a frontend: wire tagOp URL param from server to SearchFilterBar
  • d900480 frontend: add parent selector and color picker to admin tag edit form
  • 7f53651 frontend: render tag list hierarchically with indentation and color dots
  • 39ed66c frontend: add i18n keys and error codes for tag hierarchy errors

Test results

  • Backend: 983 tests, 0 failures
  • Frontend: 876 tests, 0 failures

Verified behaviors

  • AND mode with empty ID set (tag doesn't exist) → returns no results
  • Inclusive hierarchy: searching by parent tag returns documents tagged with any descendant
  • OR mode: any matching tag satisfies the filter
  • Cycle detection: assigning a tag as its own descendant fails with TAG_CYCLE_DETECTED
  • Colors stored as token names; validated against allowlist; null-safe
  • Color picker hidden on child tags (color only meaningful on root tags)
## Implementation complete ✅ All planned work for tag hierarchy, AND/OR filtering, and tag colors has been implemented on branch `feat/issue-221-tag-hierarchy`. ### What was built **Backend** - `V39__add_tag_hierarchy.sql` — adds `parent_id UUID NULL REFERENCES tag(id) ON DELETE SET NULL` + `color VARCHAR(20) NULL` + index - `Tag` entity gains `parentId` and `color` fields - `TagUpdateDTO` record with `name`, `parentId`, `color` - `TagTreeNodeDTO` record (nested tree with cumulative `documentCount`) - `ErrorCode.INVALID_TAG_COLOR` + `ErrorCode.TAG_CYCLE_DETECTED` - `TagRepository.findDescendantIdsByName()` — recursive CTE with depth < 50 guard - `TagRepository.getTreeRows()` — full tree CTE for tree assembly - `TagService.expandTagNamesToDescendantIdSets()` — expands tag names to descendant ID sets - `TagService.checkForCycle()` — ancestor traversal, throws `TAG_CYCLE_DETECTED` - `TagService.getTagTree()` — assembles `TagTreeNodeDTO` tree with cumulative counts - `TagService.update()` accepts `TagUpdateDTO`; validates color against allowlist, checks cycles - `DocumentService.searchDocuments()` accepts `tagOperator: AND | OR`; uses hierarchy-expanded ID sets - `GET /api/tags/tree` endpoint - OpenAPI types regenerated **Frontend** - `layout.css` — 10 `--c-tag-{name}` CSS custom properties (light + dark variants) - `TagInput.svelte` — binding changed from `string[]` to `Tag[]`; hierarchical dropdown with parent headers; color dot on chips - `SearchFilterBar.svelte` — AND/OR pill toggle (visible when ≥ 2 tags selected) - `DocumentList.svelte` — color dot on tag chips - `+page.svelte` + `+page.server.ts` — `?tagOp=OR` URL param wired end-to-end - Admin tag edit form (`/admin/tags/[id]`) — parent selector + 10-swatch color picker - `TagsListPanel.svelte` — hierarchical rendering with indentation + color dots - `errors.ts` + `de.json` / `en.json` / `es.json` — `INVALID_TAG_COLOR` + `TAG_CYCLE_DETECTED` ### Commits - `5b4f7f2` migration: V39 add tag hierarchy and color columns - `70cb0ab` backend: add Tag parentId/color, TagUpdateDTO, TagTreeNodeDTO, error codes - `4e748be` backend: add recursive CTE queries to TagRepository - `2da07f5` backend: cycle detection and color validation in TagService - `3bb0b81` backend: getTagTree() assembles nested TagTreeNodeDTO with cumulative counts - `3fd6e1e` backend: expandTagNamesToDescendantIdSets() in TagService - `df6e8c4` backend: TagService.update() accepts TagUpdateDTO with parentId/color - `57dc72b` backend: AND/OR + hierarchy expansion in document search - `aca7a22` backend: GET /api/tags/tree endpoint - `9f14f72` frontend: regenerate API types with parentId/color/tagOp/tree endpoint - `ab1cf06` frontend: add 10 tag color CSS custom properties (light + dark) - `60618e0` frontend: TagInput – Tag type, hierarchical dropdown, color dots - `abba85a` frontend: wire tagOp URL param from server to SearchFilterBar - `d900480` frontend: add parent selector and color picker to admin tag edit form - `7f53651` frontend: render tag list hierarchically with indentation and color dots - `39ed66c` frontend: add i18n keys and error codes for tag hierarchy errors ### Test results - Backend: **983 tests, 0 failures** - Frontend: **876 tests, 0 failures** ### Verified behaviors - AND mode with empty ID set (tag doesn't exist) → returns no results - Inclusive hierarchy: searching by parent tag returns documents tagged with any descendant - OR mode: any matching tag satisfies the filter - Cycle detection: assigning a tag as its own descendant fails with `TAG_CYCLE_DETECTED` - Colors stored as token names; validated against allowlist; null-safe - Color picker hidden on child tags (color only meaningful on root tags)
Author
Owner

Implementation complete

All 27 tasks from the plan are done. Here's a summary of every commit on feat/issue-221-tag-hierarchy:

Commit What
f9ac963 [migration] V39 — parent_id, color, index, self-ref check
3fba740 [backend] Tag entity fields, TagUpdateDTO, TagTreeNodeDTO, ErrorCode additions, recursive CTE queries, TagService (cycle check, color validation, tree assembly, descendant expansion), TagController tree endpoint + updated update
57dc72b [backend] AND/OR + hierarchy expansion in DocumentService/DocumentSpecifications
c3e007d [chore] Regenerated TypeScript API types
e4f21bd [frontend] --c-tag-* CSS tokens for 10 semantic colors (light + dark)
e8e54cc [frontend] TagInput binding changed to Tag[], color dots, hierarchy grouping in dropdown
e03fb38 [frontend] Color dot + full-path tooltip on tag chips in DocumentList
b54d2b0 [frontend] AND/OR pill toggle in SearchFilterBar (visible when ≥2 tags)
abba85a [frontend] ?tagOp=OR URL param wired from +page.server.ts through +page.svelte to SearchFilterBar
d900480 [frontend] Parent selector + 10-swatch color picker in admin tag edit form
7f53651 [frontend] TagsListPanel renders hierarchical list with indentation and color dots
39ed66c [frontend] i18n keys for INVALID_TAG_COLOR and TAG_CYCLE_DETECTED in de/en/es + errors.ts
532692e [fix] AND/OR operator race condition — bypass 500 ms debounce on toggle so tagOperator is never reset mid-flight by a completing navigation

Race condition fix (last commit)

The tag-change $effect fired triggerSearch() immediately with no debounce. When the user toggled AND→OR within the 500 ms debounce window, Navigation 1 (AND) would complete first, the sync $effect would reset tagOperator = 'AND', and the debounced search would fire with AND — showing empty results. Fixed by adding onSearchImmediate prop to SearchFilterBar and handleImmediateSearch() to +page.svelte; the toggle now clears any pending timer and calls triggerSearch() synchronously.

All 794 frontend tests green. Backend tests green.

## Implementation complete All 27 tasks from the plan are done. Here's a summary of every commit on `feat/issue-221-tag-hierarchy`: | Commit | What | |---|---| | `f9ac963` | `[migration]` V39 — `parent_id`, `color`, index, self-ref check | | `3fba740` | `[backend]` Tag entity fields, `TagUpdateDTO`, `TagTreeNodeDTO`, `ErrorCode` additions, recursive CTE queries, `TagService` (cycle check, color validation, tree assembly, descendant expansion), `TagController` tree endpoint + updated update | | `57dc72b` | `[backend]` AND/OR + hierarchy expansion in `DocumentService`/`DocumentSpecifications` | | `c3e007d` | `[chore]` Regenerated TypeScript API types | | `e4f21bd` | `[frontend]` `--c-tag-*` CSS tokens for 10 semantic colors (light + dark) | | `e8e54cc` | `[frontend]` `TagInput` binding changed to `Tag[]`, color dots, hierarchy grouping in dropdown | | `e03fb38` | `[frontend]` Color dot + full-path tooltip on tag chips in `DocumentList` | | `b54d2b0` | `[frontend]` AND/OR pill toggle in `SearchFilterBar` (visible when ≥2 tags) | | `abba85a` | `[frontend]` `?tagOp=OR` URL param wired from `+page.server.ts` through `+page.svelte` to `SearchFilterBar` | | `d900480` | `[frontend]` Parent selector + 10-swatch color picker in admin tag edit form | | `7f53651` | `[frontend]` `TagsListPanel` renders hierarchical list with indentation and color dots | | `39ed66c` | `[frontend]` i18n keys for `INVALID_TAG_COLOR` and `TAG_CYCLE_DETECTED` in de/en/es + `errors.ts` | | `532692e` | `[fix]` AND/OR operator race condition — bypass 500 ms debounce on toggle so `tagOperator` is never reset mid-flight by a completing navigation | ### Race condition fix (last commit) The tag-change `$effect` fired `triggerSearch()` immediately with no debounce. When the user toggled AND→OR within the 500 ms debounce window, Navigation 1 (AND) would complete first, the sync `$effect` would reset `tagOperator = 'AND'`, and the debounced search would fire with AND — showing empty results. Fixed by adding `onSearchImmediate` prop to `SearchFilterBar` and `handleImmediateSearch()` to `+page.svelte`; the toggle now clears any pending timer and calls `triggerSearch()` synchronously. All 794 frontend tests green. Backend tests green.
Sign in to join this conversation.
No Label feature
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#221