Skip to content

50 tag relations#120

Merged
Darkkal merged 52 commits into
masterfrom
50-tag-relations
Jul 15, 2026
Merged

50 tag relations#120
Darkkal merged 52 commits into
masterfrom
50-tag-relations

Conversation

@Darkkal

@Darkkal Darkkal commented Jul 6, 2026

Copy link
Copy Markdown
Owner

This PR is being implemented in phases according to #50.

Phase 1: Tag Categories & Custom Categories

Walkthrough: Tag Categories & Metadata Processing

We have successfully implemented Phase 1: Tag Categories & Custom Categories for Issue #50. Below is a detailed summary of the remaining commits completed in this session.

Changes Made

1. Gelbooru Auto-Import (Commit 7)

  • Extended syncLibrary() in scanner.ts to pre-load all database tag categories on scan initialization.
  • Passed categoryMap via ProcessorContext to metadata processors.
  • Updated the Gelbooru metadata processor (gelbooru.ts) to parse prefix category tags (e.g. character:, artist:, copyright:, metadata:/meta:), strip the prefix before saving, and assign the correct category ID.
  • Added comprehensive unit tests in gelbooru.test.ts.

2. E-Hentai Auto-Import (Commit 8)

  • Updated the E-Hentai metadata processor (ehentai.ts) to extract prefix categories (e.g., artist:, character:, cosplayer:, group:, circle:, parody:, language:, reclass:, and female:/male: namespace prefix selectors) and assign the matching category ID.
  • Added unit tests in ehentai.test.ts.

3. UI - Tags Page Enhancements (Commit 9)

  • Integrated category filter tab pills into the Server Component at page.tsx to filter top tags server-side without client-side JS overhead.
  • Enhanced tag card layout to show category badges with custom HSL variables on the card metadata section.
  • Added color styles and layout adjustments in page.module.css.

4. UI - Lightbox Detail Panel (Commit 10)

  • Updated Lightbox.tsx to retrieve TagWithCategory objects from getPostTags.
  • Updated createOrFindTag() in tags.ts to eager-load the category relation on tag creation.
  • Styled tags inside the details sidebar in Lightbox.module.css with their category HSL colors for high visual hierarchy.

5. UI - Settings Category Management (Commit 11)

  • Added a "Tag Categories" tab panel in the system settings page (page-client.tsx).
  • Added sliders for Hue, Saturation, and Lightness settings to dynamically preview custom color tokens before creating or updating them.
  • Rendered list cards showing existing system/custom categories, allowing dynamic modifications or deletion.
  • Added styles in page.module.css.

6. Documentation (Commit 12)

  • Documented the database schema changes, tables, and relationships inside ARCHITECTURE.md.
  • Updated coding conventions and prefix mapping guidelines under CONTRIBUTING.md.

7. Removal of "General" Category

  • Removed the default "general" category from system/test category seeding.
  • Updated metadata scanners (gelbooru.ts and ehentai.ts) so that tags without specific mappings or with unrecognized prefixes (including E-Hentai namespaces like male/female) do not get assigned a category (i.e. categoryId remains null / uncategorized).
  • Updated unit tests, repository validations, and confirmation messages to align with optional tag categorization.

Verification & Test Results

1. Unit & Integration Tests

All 87 tests compile and pass successfully, confirming that all repository and action operations function correctly.

> web-gallery@0.5.0 test:unit
> vitest run

 Test Files  12 passed (12)
      Tests  87 passed (87)
   Start at  17:39:18
   Duration  1.07s

2. Next.js Production Build

Verified that a production build of the Next.js application compiles successfully with all routes optimized.

▲ Next.js 16.1.1 (Turbopack)
- Environments: .env
  Creating an optimized production build ...
✓ Compiled successfully in 3.8s
✓ Finished TypeScript in 5.4s

Phase 2: Tag Management Page

Walkthrough - Phase 2 (Tag Management Page)

We have successfully completed all implementation and verification steps for Phase 2 of Issue #50 (Tag Relations & Management Page).

Changes Made

1. Database & Repository Layer

  • Implemented core tag manipulation functions in tags.ts:
    • getTagsPaginated: Cursor-based dynamic pagination with filters (search query, category name, uncategorized) and sorting (Most Popular, Alphabetical).
      • Bugfix: Resolved a filter issue where selecting "Uncategorized" did not display uncategorized tags. The client sent "none" as the category parameter, while the backend expected "uncategorized". The query filter condition now checks for both "none" and "uncategorized" to return categoryId IS NULL records correctly.
    • renameTag: Rename validation and execution.
    • mergeTags: Transfer all post-tag relations from multiple source tags to a single target tag, cascading database entries, and purging source tags without breaking transaction safety.
    • deleteTag / deleteTags: Cascade delete post-tag relations and tags.
    • cleanupOrphanedTags: Automatically clean up unused tags with 0 post associations.

2. Server Actions & Types

  • Exposed clean, validation-wrapped server actions in tags.ts:
    • renameTag
    • mergeTags (with library stats adjustment: totalTags counts are decremented when tags are merged).
    • deleteTag / deleteTags (with library stats adjustment).
    • cleanupOrphanedTags (with library stats adjustment).
    • bulkSetTagCategory
    • getOrCreateTagByName (facilitates auto-creating the target tag if a user types a new tag name in the merge modal).
  • Declared the TagManageItem interface to structure properties in the dashboard table.

3. API Route Handler

  • Created route.ts under /api/tags to serve client-side cursor-paginated GET queries.

4. Tag Category Relocation

  • Extracted and deleted the Tag Category CRUD tabs and logic from page-client.tsx and page.tsx.

5. Tag Management UI & Dashboards

  • Added a clean Manage Tags button next to the title on the Tag Statistics page.
  • Created a gorgeous, interactive Tag Management Dashboard under /tags/manage:
    • Server loader page.tsx pre-fetches custom and built-in categories.
    • Client component page-client.tsx handles searching, category filters, selection, sorting, and pagination.
      • Infinite Scrolling: Replaced the explicit "Load More" button pagination with the shared InfiniteScrollSentinel component to allow tags to scroll infinitely as the user reaches the bottom of the table.
    • Bulk operations bar appears dynamically when items are checked, allowing bulk category assignment, bulk deletion, or bulk merging.
    • Relocated Category Manager Modal allows creating, updating HSL colors with range sliders/live badge preview, or deleting custom categories.
    • Autocomplete Merge Modal allows merging selected tags into an existing tag (via autocomplete querying /api/autocomplete?column=tag&q=...) or a brand-new tag.

Verification Results

Automated Unit Tests

  • Updated the repository test suite tags.test.ts to verify all new pagination, renaming, merging, deleting, and cleanup helper functions. Added tests to explicitly assert both "none" and "uncategorized" query parameters return uncategorized tags (33/33 passed).
  • Updated the server actions test suite tags.test.ts to test validations and statistics integration (24/24 passed).
  • Ran all unit tests across the entire workspace (109/109 passed).

Production Build Validation

  • Executed npm run build which verified successful TypeScript compilation and compiled route layouts:
    Route (app)
    ...
    ├ ƒ /api/tags
    ├ ƒ /tags
    ├ ƒ /tags/manage
    

Phase 3: Tag Aliases

Walkthrough: Tag Aliases (Issue #50 Phase 3)

This walkthrough documents the completion of Phase 3 of Issue #50, which implements self-referencing tag aliases, query-time bi-directional search expansion, tag category inheritance, and UI management/statistics updates.

Changes Made

1. Database Schema

  • File: schema.ts
    • Added self-referencing column aliasOfTagId on the tags table referencing tags.id with onDelete: "set null".
    • Added totalCanonicalTags column to both libraryStatistics and statisticsHistory tables to track tags that are not aliases.
  • Migration: Created SQL migration drizzle/0014_polite_mandarin.sql.

2. Repository Layer & Logic

  • File: tags.ts
    • Updated getTagsPaginated to left-join tags on itself to fetch aliasName and aliasOfTagId for UI rendering.
    • Implemented setTagAlias(tagId, aliasOfTagId) with flat hierarchy validation (disallowing circular alias definitions or aliasing to tags that are themselves aliases). Setting an alias automatically syncs its category with the canonical tag. Clearing an alias preserves the inherited category.
    • Implemented bulkSetTagAlias(tagIds, aliasOfTagId) with batch operations and category inheritance syncing.
    • Updated setTagCategory and bulkSetTagCategory to block category changes directly on alias tags (throwing an error) and instead propagate category updates on canonical tags automatically to all of their aliases.
  • File: statistics.ts
    • Updated recomputeStatistics, incrementStatistics, recordHistorySnapshot, and getHistory to calculate and maintain totalCanonicalTags counters.
  • File: posts.ts & media.ts
    • Implemented expandSearchAliases(query) to resolve tag synonyms. Before matching FTS, the code extracts tag_names:<name> filters, fetches equivalent tags (canonical and sibling aliases) from the DB, and rewrites the FTS clauses into bi-directional OR structures (e.g. (tag_names:car OR tag_names:automobile)).
  • File: autocomplete.ts
    • Updated autocompleteTag to query aliasOfTagId and aliasName using a self-join. It maps matching tags to redirect formats (car → automobile).

3. Server Actions & UI

  • File: tags.ts Actions
    • Exposed setTagAlias and bulkSetTagAlias as server actions.
    • Modified deletions and merges (deleteTag, deleteTags, mergeTags, cleanupOrphanedTags) to call recomputeStatistics() to keep all canonical tag counts perfectly in sync after database cascading foreign key events (onDelete: "set null").
  • File: Tags Dashboard page-client.tsx & page.module.css
    • Added a "Set Alias" action button to individual tag rows.
    • Added an "Alias Selected" action button to bulk operations.
    • Rendered a visual badge indicator next to tag names showing their redirection (e.g., ↳ automobile with a redirect icon).
    • Created a modal for bulk/single tag alias selection with autocomplete and flat hierarchy validations.
    • Replaced the native <select> dropdown for category selection with a dedicated "Set Category" button in bulk operations and a matching icon button (FolderEdit) on individual canonical tag rows.
    • Designed and built a new Category Selection Modal that allows users to type in a category name and autocomplete based on existing categories in the system.
    • Configured category assignment to automatically filter out alias tags (since category inheritance is managed via the canonical tag) and show an informative message notifying the user that they inherit categories.
    • Resolved a UX positioning issue on autocomplete lists by setting .modal and .modalBody overflow values to visible for standard modals, letting autocomplete list overlays float cleanly in front of confirmation buttons and outside the modal bounds without causing container resizing or scrollbars. Added border radius inheritance to header/footer blocks to prevent corner background clipping issues.
  • File: Stats Dashboard page-client.tsx & StatCard.tsx
    • Modified StatCard to accept custom action items (inline buttons).
    • Added a segmented-toggle button ("All" vs. "Canonical") inside the "Total Tags" card headers to switch dynamically between totalTags and totalCanonicalTags.

Verification Results

Unit Tests

Ran npm run test:unit and verified all 121 tests passed successfully:

  • Added tags.test.ts repository tests verifying tag alias validation (throws on circular references, flat validation check, canonical statistics delta calculation).
  • Added tags.test.ts repository tests verifying tag alias category inheritance (setting alias inherits category, clearing preserves category, updating canonical tag category propagates to all aliases, and setting category directly on an alias throws an error).
  • Added posts.test.ts repository tests verifying FTS search expansion expands searches to include both aliases and canonical equivalents.
  • Added autocomplete.test.ts repository tests verifying autocompleteTag suggests formatted redirects.
  • Added server actions tests verifying setTagAlias and bulkSetTagAlias actions.

Build Check

Ran npx tsc --noEmit and confirmed clean compilation with zero TypeScript errors.


Phase 4: Tag Hierarchy

Walkthrough: Tag Hierarchy Implementation (Phase 4 of Issue #50)

We have successfully implemented and verified the final phase of the Tag Hierarchy feature. This establishes a fully functional self-referencing hierarchy system for tags with implicit search expansion, settings management, repository validations, server actions, unit tests, and multiple interactive UI layouts.

Changes Implemented

1. Database & Schema

  • Added parentTagId column to the tags table in schema.ts.
  • Configured a self-referencing relationship in tagsRelations defining parentTag and childTags.
  • Generated a migration file under drizzle/ to apply the database changes.

2. Settings & Configuration

  • Added implicitHierarchyFiltering to AppSettings in types/settings.ts (defaulting to true).
  • Exposed a premium toggle switch in the "App" tab of /settings to enable/disable implicit search expansion.

3. Repository & API Layer

  • Tag Hierarchy Management:
    • Implemented setTagParent and bulkSetTagParent in src/lib/db/repositories/tags.ts.
    • Added validations: prevents tags from referencing themselves, prevents alias tags from being assigned parents (or acting as parents), and runs a validation traversal to prevent circular parent chains (e.g. A -> B -> C -> A).
    • Updated getTagsPaginated to retrieve parentTagId and parentName for the tag list layout.
  • Search Expansion:
    • Renamed expandSearchAliases to expandSearchTags in src/lib/db/repositories/posts.ts and media.ts.
    • Leveraged a WITH RECURSIVE CTE in Drizzle to resolve descendants of queried tags. When a query is made on a parent (e.g. tag:animal), it expands to include all direct and indirect descendants (e.g. (tag:animal OR tag:dog OR tag:cat)) alongside their aliases.
  • Children API with Sorting and Filtering:
    • Updated /api/tags/children to calculate direct childCount for each tag.
    • Added support for sorting by childCount DESC (the number of direct sub-tags at each level).
    • Added a hierarchiesFirst=true query option to order tags that have nested child tags to the top of the level.
    • Added a hideOrphans=true query option to hide top-level tags that have no children (orphans).
    • Upgraded pagination from cursor to offset-based pagination to support stable sorting by child counts and custom order rules.
  • Autocomplete:
    • Updated autocompleteTag in src/lib/db/repositories/autocomplete.ts to recursively resolve the list of ancestor tag names.

4. Server Actions

  • Exposed setTagParent and bulkSetTagParent as Next.js server actions in src/app/actions/tags.ts.

5. Frontend & UI Elements

  • Tag Management Dashboard (/tags/manage):
    • Added parent tag indicators in the table rows next to tag names.
    • Added a "Set Parent" row action icon (ChevronUp) and bulk action button to assign parent tags.
    • Implemented SetParentModal supporting autocomplete for parent selection and parent clearance.
  • Tag Statistics Page (/tags):
    • Integrated a view mode toggle: "Flat View" and "Tree View".
    • Implemented a highly responsive, lazy-loaded interactive nested tree view. Expanding a parent node fetches direct children via /api/tags/children.
    • Added a sub-header panel in Tree View with custom controls:
      • Sorting selector: Choose between Alphabetical sorting and sorting descending "By Subtag Count" (sorting by number of tags in each level).
      • Hierarchies First checkbox: Move tags with hierarchies to the top of their levels.
      • Hide Flat Tags checkbox: Hide tags without hierarchies entirely, showing only the active hierarchical tree structures.
    • Added nested category badges displaying the number of subtags (e.g. 3 subtags).
  • Tagging Workflow / Lightbox (/gallery lightbox):
    • Updated TagAutocompleteInput to pass ancestor suggestions.
    • Added a suggestion block in the Lightbox sidebar. Adding a child tag (e.g. cat) prompts the user with button suggestions to easily add unassigned ancestor tags (e.g. + feline, + animal) with a single click.

Verification & Testing

Automated Tests

We added unit tests verifying all new logic, and all tests pass successfully (npm run test:unit):

  • tests/unit/api/tags-children.test.ts (NEW):
    • Verified default alphabetical children sorting.
    • Verified sorting by child count descending when sortBy=childCount is active.
    • Verified ordering of hierarchies to the top when hierarchiesFirst=true is active.
    • Verified filtering of orphans when hideOrphans=true is active.
  • tests/unit/actions/tags.test.ts & src/lib/db/repositories/tags.test.ts:
    • Verified setting parent, bulk setting parent, and circular parent dependency checks.
    • Verified validations for aliases and non-existent parents.
  • src/lib/db/repositories/posts.test.ts:
    • Verified expandSearchTags expands child tag matches when implicitHierarchyFiltering is true.
    • Verified no expansion occurs when implicitHierarchyFiltering is false.
  • src/lib/db/repositories/autocomplete.test.ts:
    • Verified tag autocomplete correctly returns ancestor chains for suggestions.

Production Build Verification

The complete application successfully compiled for production via npm run build.


Phase 5: Related Tags

Walkthrough: Related Tags (Issue #50 Phase 5)

We have successfully implemented and verified Phase 5 (Related Tags) of Issue #50. This completes the final phase of the tag relations system on the 50-tag-relations branch.

Below is a detailed summary of the changes made and verified in this session.

Changes Made

1. Database Schema & Migrations

  • File: schema.ts
    • Created the tag_relations join table.
    • Configured a compound Primary Key on (tag_id, related_tag_id) to prevent duplicate links.
    • Implemented the database CHECK (tag_id < related_tag_id) constraint via Drizzle to enforce ordering of symmetric pairs directly in SQLite.
    • Configured cascade delete foreign keys pointing to tags.id.
    • Declared relatedTags and relatedToTags relations in tagsRelations and tagRelationsRelations.
  • Migration: Generated and applied SQL migration drizzle/0016_oval_corsair.sql.

2. Repository Layer

  • File: tags.ts
    • addRelatedTag: Creates a relation. Automatically orders tagId1 and tagId2 to satisfy the database check constraint, prevents self-relating, and throws an error if either tag is an alias.
    • removeRelatedTag: Deletes a relation.
    • getRelatedTags: Retrieves a list of related tag objects.
    • getSuggestedRelatedTags: Given post tags, suggests related tags sorted by association frequency, excluding tags already present on the post.

3. Server Actions

  • File: tags.ts (Actions)
    • Exposed addRelatedTag, removeRelatedTag, getRelatedTags, and getSuggestedRelatedTags as React server actions.

4. UI - Tag Management Page

  • File: page-client.tsx
    • Added a "Manage Related" icon button (Network icon) to individual tag rows in the dashboard table.
    • Created the RelatedTagsModal:
      • Displays currently related tags as dismissible badges.
      • Provides a debounced autocomplete input to search and select tags to relate.
      • Autocomplete suggestions exclude the tag itself and already related tags.
      • Clicking the close button on badges triggers removeRelatedTag and updates the state.
  • File: page.module.css
    • Added styles for the related tag badge layout and remove button hover states.

5. UI - Lightbox Suggestions

  • File: Lightbox.tsx
    • Added state and a useEffect hook to fetch related tag suggestions based on currently active tags on the post.
    • Rendered a "Related Suggestions" panel in the tagging sidebar showing clickable HSL-colored category chips to add related tags with a single click.

Verification & Test Results

1. Automated Tests

All unit tests compile and pass successfully, confirming that all new relation operations, validations, suggestions, and UI actions function correctly.

 Test Files  13 passed (13)
      Tests  146 passed (146)
   Start at  11:19:32
   Duration  1.23s
  • Added repository tests verifying symmetric behavior, duplicate prevention, alias validation, and suggestion sorting.
  • Added actions tests verifying action wrapper functions.

2. Next.js Production Build

Verified that a production build of the Next.js application compiles successfully with all routes optimized.

▲ Next.js 16.1.1 (Turbopack)
- Environments: .env
  Creating an optimized production build ...
✓ Compiled successfully in 3.8s
✓ Finished TypeScript in 6.0s
✓ Collecting page data using 23 workers in 1231.0ms
✓ Generating static pages using 23 workers (11/11) in 136.2ms
✓ Finalizing page optimization in 473.6ms

@Darkkal Darkkal linked an issue Jul 6, 2026 that may be closed by this pull request
@Darkkal
Darkkal marked this pull request as ready for review July 15, 2026 18:29
@Darkkal

Darkkal commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

PR #120 Review: Tag Relations (50-tag-relationsmaster)

PR: #120 — 50 tag relations
Author: Darkkal | Status: Open, not draft, mergeable state clean
Branch: 50-tag-relationsmaster
Scope: +15,125 / -123 across 52 files, 47 commits
Implements: Issue #50 across 5 phases


CI / Automated Checks

All 6 check runs pass

Check Status Duration
CodeQL ✅ success ~2s
lint (Biome) ✅ success ~22s
test (unit) ✅ success ~4m 7s
Analyze (actions) ✅ success ~39s
Analyze (javascript-typescript) ✅ success ~1m 6s
test (unit #2) ✅ success ~25s

Unit tests: 146 tests across 13 test files — all passing.
Production build: compiles successfully with TypeScript and route optimization.


Commit Message Compliance (Conventional Commits)

Tip

Overall the commit messages are well-structured and follow Conventional Commits.

The 47 commits follow the convention spec correctly with appropriate scopes:

Convention Count Examples
feat(...) 37 feat(db): add tag_categories table and tags.categoryId FK
fix(...) 4 fix(tags): resolve category filtering for uncategorized tags
test(...) 3 test(db): add unit tests for tag category repository
refactor(...) 2 refactor(settings): relocate tag category management out of settings
docs(...) 1 docs(tags): document tag category relationships and styling variables
style(...) 1 style(ui): make modal autocomplete dropdown visible

Commits are logically incremental, organized per-phase and per-layer (DB → actions → UI), which aligns with CONTRIBUTING.md §3.1 (plan per page, not per layer) and the user's commit strategy rules.

Note

One commit (8ba96705) uses a non-conventional format: 110 add unitintegration testing for server actions and repository layer (#116) — however, this is a merge commit from a prior PR and is not part of this branch's authored work.


Contributor Guideline Compliance

✅ Patterns Correctly Followed

Guideline Status Notes
§1.1 Server + Client split /tags/manage/page.tsx (server) + page-client.tsx (client)
§1.2 Drizzle + async All repository functions use await, relational queries with with:
§1.3 Cursor-based pagination getTagsPaginated returns { items, nextCursor }
§1.4 Strategy + Factory Gelbooru/E-Hentai processors updated, no if/else in base
§1.5 CSS Modules + tokens New pages use co-located .module.css, HSL variables
§1.6 Shared hooks Reuses usePaginatedData, useSelection, useInfiniteScroll
§1.8 Component extraction Sub-components import parent page.module.css
§1.9 Actions split by domain All new actions in src/app/actions/tags.ts
§1.11 Security No sql.raw(), parameterized queries only
§1.14 Statistics incrementStatistics/recomputeStatistics called after mutations
§2.2 Path aliases All imports use @/ aliases
§2.7 Per-page metadata /tags/manage/page.tsx exports metadata
§2.8 Conventional commits See above analysis
§4 Testing Co-located repo tests + centralized action tests

⚠️ Findings Requiring Attention


Finding 1: ARCHITECTURE.md missing documentation for Phases 3–5 (HIGH)

Important

Per CONTRIBUTING.md §2.9: "When implementing new features, modifications to subsystems, or introducing new architectural patterns, you MUST update both ARCHITECTURE.md and CONTRIBUTING.md."

The ARCHITECTURE.md was updated for Phase 1 only (tag categories — directory map, schema diagram, and key relationships). However, the following major architectural additions from Phases 3–5 are not documented:

Feature Schema/Code Change Documented?
tags.aliasOfTagId (self-ref FK) Migration 0014, schema.ts ❌ Not in ARCHITECTURE.md
tags.parentTagId (self-ref FK) Migration 0015, schema.ts ❌ Not in ARCHITECTURE.md
tag_relations join table Migration 0016, schema.ts ❌ Not in ARCHITECTURE.md
totalCanonicalTags counter Added to library_statistics + statistics_history ❌ Not in ARCHITECTURE.md
implicitHierarchyFiltering setting Added to AppSettings ❌ Not in ARCHITECTURE.md
Recursive CTE search expansion expandSearchTags in posts.ts / media.ts ❌ Not in ARCHITECTURE.md
/api/tags/children route New route handler with offset pagination ❌ Not in ARCHITECTURE.md or directory map

Specifically missing from ARCHITECTURE.md:

  1. §2 Directory Map: Missing /api/tags/children/ entry
  2. §5 Database Schema: The schema diagram should show tags.aliasOfTagId → tags.id, tags.parentTagId → tags.id, and the tag_relations join table
  3. §5 Key Relationships: Missing self-referencing FK descriptions and the symmetric pair constraint
  4. §3.7 Settings: Missing implicitHierarchyFiltering in the settings description
  5. §3.9 Statistics: Missing totalCanonicalTags in the statistics description

Finding 2: CONTRIBUTING.md missing rules for tag aliases, hierarchy, and relations (MEDIUM)

The CONTRIBUTING.md was updated with two new rules (§1.4 tag category prefix stripping, §1.5 dynamic HSL category variables). However, no guidelines were added for:

  1. Tag alias rules: Flat alias hierarchy constraint (no chaining), category inheritance semantics, alias blocking on category changes
  2. Tag hierarchy rules: Circular dependency prevention, alias-parent mutual exclusion, implicitHierarchyFiltering behavior
  3. Tag relations rules: Symmetric pair constraint (tag_id < related_tag_id), alias exclusion
  4. Search expansion pattern: How expandSearchTags works with recursive CTEs and when it applies

Finding 3: Static inline style in StatCard.tsx (LOW)

Per CONTRIBUTING.md §1.5: "Only use inline style={{}} for truly dynamic runtime values. All static styling belongs in CSS Modules."

StatCard.tsx L57 introduces a static inline style:

<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>

This is a fixed layout style and should be a CSS Module class. Suggested fix: add a .statCardHeaderRight class to the statistics page.module.css.


Finding 4: as any type cast in tags route handler (LOW)

Per CONTRIBUTING.md §2.1: "Avoid any — if a type is unknown, use unknown and narrow it."

route.ts L8:

const sortBy = (searchParams.get("sortBy") as any) || "count";

Should be typed as a union:

const sortBy = (searchParams.get("sortBy") as "count" | "name" | "recent") || "count";

Finding 5: rgba(0, 0, 0, ...) in settings page.module.css (LOW — pre-existing)

page.module.css L405:

box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

This is a rgba hardcoded shadow color in the new category card styles. While the existing code (L30, L306) already uses rgba(0,0,0,...) for shadows, the CONTRIBUTING.md rule states no hardcoded colors. This is a pre-existing pattern, but the new code perpetuates it.

Note

This is a systemic issue across the codebase and not unique to this PR. Consider addressing it as a separate cleanup task.


Finding 6: Missing loading.tsx / error.tsx for /tags/manage/ (MEDIUM)

Per CONTRIBUTING.md §1.10: "Every route segment should have loading.tsx and error.tsx."

The /tags/manage/ route has page.tsx and page-client.tsx but the subagent research confirmed these boundaries exist at the parent /tags/ level. However, per the convention, the manage/ sub-route should have its own boundaries for deterministic loading states, especially given it loads categories server-side.

Check needed: Verify whether /tags/manage/ inherits from /tags/loading.tsx or needs its own. If the parent covers it, this is acceptable.


Finding 7: Offset-based pagination in /api/tags/children (OBSERVATION)

Per CONTRIBUTING.md §1.3: "List endpoints use cursor-based pagination, not offset-based."

The /api/tags/children route handler uses offset-based pagination (cursor is actually an offset integer). The PR description explicitly documents this decision:

"Upgraded pagination from cursor to offset-based pagination to support stable sorting by child counts and custom order rules."

This is a justified deviation — offset pagination is necessary here because the sort order includes computed values (childCount) that would make cursor-based pagination unstable. The PR description explains the rationale clearly.

Tip

Consider adding this exception to CONTRIBUTING.md §1.3 to prevent future confusion: "Exception: tree-structured endpoints that sort by computed aggregates (e.g., /api/tags/children sorted by child count) may use offset-based pagination when cursor stability cannot be guaranteed."


PR Description Quality

Tip

The PR description is exceptionally thorough. It includes per-phase walkthroughs, file-level change descriptions, verification results (test counts, build output), and links to affected files. This is exemplary.

One suggestion: The PR description could be structured with GitHub's <details> collapse blocks for each phase to improve readability, as it's currently ~400+ lines long.


Summary of Action Items

Must Fix Before Merge

# Severity Finding Action
1 🔴 HIGH ARCHITECTURE.md missing Phases 3–5 documentation Add schema diagram entries for aliasOfTagId, parentTagId, tag_relations, totalCanonicalTags. Update directory map with /api/tags/children. Document settings addition and search expansion pattern.
2 🟡 MEDIUM CONTRIBUTING.md missing alias/hierarchy/relation rules Add rules for alias flat hierarchy constraint, hierarchy circular prevention, relation symmetry, and search expansion behavior.
3 🟡 MEDIUM Missing /tags/manage/ loading/error boundaries Add loading.tsx and error.tsx to /tags/manage/, or document that parent route boundaries cover it.

Should Fix (Non-blocking)

# Severity Finding Action
4 🟢 LOW Static inline style in StatCard.tsx Move display: flex; align-items: center; gap: 0.5rem to a CSS Module class.
5 🟢 LOW as any type cast in tags route Replace with proper union type.
6 🟢 LOW rgba() in new CSS styles Pre-existing pattern — track as future cleanup.
7 — INFO Offset pagination deviation Add exception note to CONTRIBUTING.md §1.3.

@Darkkal

Darkkal commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

Walkthrough - PR #120 Action Items Resolution

I have completed the implementation plan to address the action items from the code review of PR #120 (Tag Relations).

Changes Made

1. Documentation Updates

  • ARCHITECTURE.md:
    • Updated the directory map with /api/tags/children/.
    • Documented implicitHierarchyFiltering in the Settings subsystem description.
    • Documented totalCanonicalTags in the Statistics subsystem description.
    • Updated the Database Schema diagram to show aliasOfTagId and parentTagId self-referencing foreign keys, as well as the tag_relations join table.
    • Added details to the "Key relationships" section explaining self-referencing FKs and the symmetric join constraints.
    • Added a subsection under Database Layer detailing recursive CTE search expansion via expandSearchTags.
  • CONTRIBUTING.md:
    • Documented the offset pagination exception for tree-structured endpoints that sort by computed aggregates in §1.3.
    • Added new guidelines in §1.4 outlining the rules for flat aliases (no chaining), circular dependency prevention, symmetric tag relations, and search expansion.

2. UI & Route Boundaries

  • src/app/tags/manage/loading.tsx:
    • Created a skeleton loading dashboard utilizing standard classes from loading.module.css.
    • Added a data-testid="loading-skeleton" attribute to ensure deterministic E2E test waiting.
    • We intentionally deferred the creation of /tags/manage/error.tsx, relying on the parent root-level error boundary (matching the rest of the application routes).

3. Code Quality & Style Adjustments

  • src/app/statistics/page.module.css:
    • Extracted the layout CSS rules display: flex; align-items: center; gap: 0.5rem; into a .statCardHeaderRight class.
  • src/app/statistics/components/StatCard.tsx:
    • Replaced the inline layout styles at line 57 with the new .statCardHeaderRight class.
  • src/app/api/tags/route.ts:
    • Replaced the unsafe as any type cast on line 8 with a union type cast restricting options to "name" | "name-desc" | "count" | "count-asc".

Verification Results

Automated Checks & Tests

  • Vitest Unit/Integration Suite:
    • Run command: npm run test:unit
    • Results: All 146 tests across 13 test files passed successfully.
  • Production Build:
    • Run command: npm run build
    • Results: Compiled successfully without any TypeScript or Turbopack errors. All static and dynamic pages resolved correctly, including /tags/manage.
  • Biome Linter/Formatter (via husky/lint-staged on commit):
    • Checked all modified files for formatting and syntax issues, all passed successfully.

@Darkkal
Darkkal merged commit 21643b0 into master Jul 15, 2026
6 checks passed
@Darkkal
Darkkal deleted the 50-tag-relations branch July 15, 2026 19:04
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.

Tag relations

1 participant