Files
Jakub Zych b05e85cd55 fix(10): revise plans based on checker feedback
- Plan 10-02: Added explicit UserMailService wiring in Account.scala
  onRegister handler for activation emails when activateMode == User
- Plan 10-04: Split into backend-only (models, services, admin controller)
  reducing from 16 to 15 files, estimated context ~50%
- Plan 10-05: New plan for frontend components (Posts, Post, Categories,
  RelatedPosts), Wave 3 depends on 10-04
- Updated ROADMAP.md to reflect 5 plans for Phase 10
2026-02-05 16:13:31 +01:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
10-core-plugins 04 execute 2
10-03
plugins/golem15/blog/models/Category.scala
plugins/golem15/blog/models/Tag.scala
plugins/golem15/blog/models/PostCategory.scala
plugins/golem15/blog/models/PostTag.scala
plugins/golem15/blog/repositories/CategoryRepository.scala
plugins/golem15/blog/repositories/TagRepository.scala
plugins/golem15/blog/services/CategoryService.scala
plugins/golem15/blog/services/TagService.scala
plugins/golem15/blog/services/NestedTree.scala
plugins/golem15/blog/services/BlogPostService.scala
plugins/golem15/blog/repositories/BlogPostRepository.scala
plugins/golem15/blog/controllers/Posts.scala
plugins/golem15/blog/controllers/Categories.scala
plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
plugins/golem15/blog/resources/controllers/posts/fields.yaml
plugins/golem15/blog/resources/controllers/categories/fields.yaml
plugins/golem15/blog/resources/controllers/categories/columns.yaml
true
truths artifacts key_links
Blog posts can be assigned to categories
Categories are hierarchical (parent/child)
Blog posts can have multiple tags
Admin can manage categories in tree view
Related posts calculated by shared categories/tags
path provides contains
plugins/golem15/blog/models/Category.scala Hierarchical category model with nested set case class Category
path provides contains
plugins/golem15/blog/models/Tag.scala Tag model for free-form labeling case class Tag
path provides exports
plugins/golem15/blog/services/CategoryService.scala Category CRUD with tree operations
CategoryService
getTree
getAllChildrenAndSelf
path provides exports
plugins/golem15/blog/controllers/Categories.scala Admin controller for category management
CategoriesController
path provides contains
plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql Categories, tags, and pivot tables CREATE TABLE blog_categories
from to via pattern
plugins/golem15/blog/services/BlogPostService.scala CategoryService category filtering categoryService.getAllChildrenAndSelf
from to via pattern
plugins/golem15/blog/services/CategoryService.scala NestedTree tree operations NestedTree.
from to via pattern
plugins/golem15/blog/controllers/Categories.scala CategoryService CRUD operations categoryService.
Add categories and tags to the Blog plugin with admin management.

Purpose: Enable content organization with hierarchical categories and free-form tags, providing the data layer for frontend components.

Output: Blog plugin extended with:

  • Hierarchical categories using nested set model
  • Tags for free-form labeling
  • Admin controller for category management
  • BlogPostService updated with category/tag relations
  • Related posts algorithm based on shared categories/tags

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-core-plugins/10-CONTEXT.md @.planning/phases/10-core-plugins/10-RESEARCH.md

Prior plan in this phase

@.planning/phases/10-core-plugins/10-03-SUMMARY.md

Reference: Phase 7 admin controller patterns

Task 1: Category and tag models with services plugins/golem15/blog/models/Category.scala plugins/golem15/blog/models/Tag.scala plugins/golem15/blog/models/PostCategory.scala plugins/golem15/blog/models/PostTag.scala plugins/golem15/blog/repositories/CategoryRepository.scala plugins/golem15/blog/repositories/TagRepository.scala plugins/golem15/blog/services/CategoryService.scala plugins/golem15/blog/services/TagService.scala plugins/golem15/blog/services/NestedTree.scala plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql Create category and tag models with nested tree support:
**Category.scala (research Pattern 6):**
- Case class with fields:
  id, name, slug (unique), description (Option), parentId (Option),
  nestLeft (Int), nestRight (Int), nestDepth (Int),
  published, publishedAt, createdAt, updatedAt
- Quill table mapping to `blog_categories`
- CategoryTreeNode case class: category, children (List), postCount

**Tag.scala (research Pattern 7):**
- Case class: id, name, slug (unique), createdAt, updatedAt
- Quill table mapping to `blog_tags`
- TagWithCount case class: tag, postCount

**PostCategory.scala & PostTag.scala:**
- Pivot case classes for many-to-many relationships
- PostCategory: postId, categoryId
- PostTag: postId, tagId

**NestedTree.scala (utility object):**
- Functions for nested set operations:
  - rebuild(categories: List[Category]): List[Category] - Recalculate left/right
  - descendants(category, all): List[Category] - Get all children
  - ancestors(category, all): List[Category] - Get all parents
  - moveNode(category, newParent, all): List[Category] - Move category in tree
  - insertNode(category, parent, all): List[Category] - Add new category
  - deleteNode(category, all): List[Category] - Remove category and adjust tree

**CategoryRepository.scala:**
- Trait:
  - findById(id): Option[Category]
  - findBySlug(slug): Option[Category]
  - listAll: List[Category]
  - create(category): Category
  - update(category): Category
  - delete(id): Unit
  - getPostCount(categoryId): Int
  - getNestedPostCount(categoryId): Int (includes children)
- Live implementation using QuillContext

**TagRepository.scala:**
- Trait:
  - findById(id): Option[Tag]
  - findBySlug(slug): Option[Tag]
  - findByName(name): Option[Tag]
  - listAll: List[Tag]
  - create(tag): Tag
  - listForPost(postId): List[Tag]
  - getPopular(limit): List[TagWithCount]
- Live implementation

**CategoryService.scala:**
- Trait:
  - create(data): Category (auto-generate slug, rebuild tree)
  - update(id, data): Category (rebuild tree if parent changed)
  - delete(id): Unit (rebuild tree after)
  - findBySlug(slug): Option[Category]
  - listAll: List[Category]
  - getTree: List[CategoryTreeNode]
  - getChildren(parentId): List[Category]
  - getAllChildrenAndSelf(categoryId): List[Category] (for nested queries)
  - getPostCount(categoryId): Int
  - getNestedPostCount(categoryId): Int
- Live implementation with NestedTree for tree operations

**TagService.scala:**
- Trait:
  - findOrCreate(name): Tag (create with auto-slug if not exists)
  - findBySlug(slug): Option[Tag]
  - listAll: List[Tag]
  - listForPost(postId): List[Tag]
  - getPopularTags(limit): List[TagWithCount]
  - syncPostTags(postId, tagNames): List[Tag] (update pivot table)
- Live implementation

**V10_2_2__blog_categories_tags.sql:**
- CREATE TABLE blog_categories (all fields, nested set indexes)
- CREATE TABLE blog_tags (id, name, slug unique, timestamps)
- CREATE TABLE blog_posts_categories (post_id, category_id, composite PK, FKs)
- CREATE TABLE blog_posts_tags (post_id, tag_id, composite PK, FKs)
- ALTER TABLE blog_posts ADD CONSTRAINT fk_main_category REFERENCES blog_categories
- Indexes on slugs, nested set columns, pivot table lookups
./mill summercms.compile succeeds NestedTree rebuild produces valid left/right sequence SQL migration syntax valid Category model with nested set tree support. Tag model for free-form labeling. Services handle CRUD with automatic tree maintenance. Pivot tables link posts to categories and tags. Task 2: Update BlogPostService and admin for relations plugins/golem15/blog/services/BlogPostService.scala (modify) plugins/golem15/blog/repositories/BlogPostRepository.scala (modify) plugins/golem15/blog/controllers/Posts.scala (modify) plugins/golem15/blog/controllers/Categories.scala plugins/golem15/blog/resources/controllers/posts/fields.yaml (modify) plugins/golem15/blog/resources/controllers/categories/fields.yaml plugins/golem15/blog/resources/controllers/categories/columns.yaml Integrate categories and tags with blog posts:
**BlogPostRepository.scala additions:**
- findBySlugWithRelations(slug): Option[BlogPostWithRelations]
- listFrontendWithRelations(options): PaginatedResult[BlogPostWithRelations]
- listByCategory(categoryId, options): PaginatedResult[BlogPostWithRelations]
- listByTag(tagSlug, options): PaginatedResult[BlogPostWithRelations]
- getRelatedPosts(postId, limit): List[BlogPostWithRelations]
- Join queries to load categories, tags, author in single query

**BlogPostService.scala modifications:**
- BlogPostWithRelations case class:
  post: BlogPost, categories: List[Category], tags: List[Tag],
  featuredImages: List[MediaItem], author: Option[BackendUser],
  readingTime: Int
- Update create/update to handle categories and tags:
  - PostCreateData/UpdateData add: categoryIds (List[Long]), tagNames (List[String])
  - On save: Sync pivot tables via services
  - Set mainCategoryId if categories provided
- Update listFrontend to use listFrontendWithRelations
- Add listByCategory(categoryId, options)
- Add listByTag(tagSlug, options)
- Add getRelatedPosts(postId, limit):
  - Score = 2 * shared_categories + 1 * shared_tags
  - Exclude current post
  - Return top N by score

**Posts.scala controller modifications:**
- Update store/update actions to handle categories and tags from form
- Add category/tag selection to form context

**Categories.scala controller (new):**
- Admin controller for category management
- Actions: index, create, store, edit, update, delete, reorder (tree)
- HTMX handler for drag-drop tree reordering

**fields.yaml modifications (Posts):**
- Add categories tab fields:
  ```yaml
  _categories:
    type: relation
    label: Categories
    nameFrom: name
    list: true
    tree: true
    tab: Categories
  _tags:
    type: taglist
    label: Tags
    mode: array
    separator: ','
    tab: Tags
  ```

**categories/fields.yaml:**
```yaml
fields:
  name:
    type: text
    label: Name
    required: true

  slug:
    type: text
    label: Slug
    preset:
      field: name
      type: slug

  parent_id:
    type: relation
    label: Parent Category
    emptyOption: -- None --
    nameFrom: name

  description:
    type: textarea
    label: Description
    size: small

  published:
    type: checkbox
    label: Published
```

**categories/columns.yaml:**
```yaml
columns:
  name:
    label: Name
    sortable: true
    tree: true

  slug:
    label: Slug

  published:
    label: Published
    type: switch

  post_count:
    label: Posts
    type: number
```

**Register Categories controller in Plugin.scala**
./mill summercms.compile succeeds Related posts algorithm returns posts with shared categories/tags Category tree drag-drop works in admin Posts can be assigned categories and tags. Admin can manage categories in tree view. Related posts calculated by shared categories/tags. Post queries include relations efficiently. After all tasks complete: 1. Blog plugin compiles: `./mill summercms.compile` 2. Migration creates all tables: blog_categories, blog_tags, pivot tables 3. Category nested tree rebuilds correctly on insert/delete 4. Posts admin form shows category tree selector and tag input 5. Related posts algorithm returns relevant content

<success_criteria>

  • Categories form hierarchical tree structure
  • Nested set left/right values correct after operations
  • Tags can be created on-the-fly when typing
  • Post admin form shows category tree selector and tag input
  • Admin can manage categories with tree view and drag-drop
  • Related posts calculated by shared categories/tags </success_criteria>
After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`