docs(10): create phase plan
Phase 10: Core Plugins - 4 plans in 2 waves - Wave 1: 10-01 (User auth), 10-03 (Blog posts) - parallel - Wave 2: 10-02 (User profiles), 10-04 (Blog categories/tags) - sequential - Ready for execution
This commit is contained in:
@@ -195,13 +195,13 @@ Plans:
|
|||||||
6. Blog posts can be organized into categories
|
6. Blog posts can be organized into categories
|
||||||
7. Blog posts can be tagged with multiple tags
|
7. Blog posts can be tagged with multiple tags
|
||||||
8. Frontend displays blog post listings via components
|
8. Frontend displays blog post listings via components
|
||||||
**Plans**: TBD
|
**Plans**: 4 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 10-01: User plugin - registration and authentication
|
- [ ] 10-01-PLAN.md - User plugin models, services, and authentication components
|
||||||
- [ ] 10-02: User plugin - profiles and password reset
|
- [ ] 10-02-PLAN.md - User profile management and password reset flow
|
||||||
- [ ] 10-03: Blog plugin - posts and editor
|
- [ ] 10-03-PLAN.md - Blog post model, services, and admin controller with TinyMCE
|
||||||
- [ ] 10-04: Blog plugin - categories, tags, listing components
|
- [ ] 10-04-PLAN.md - Blog categories, tags, and frontend listing components
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
|
|||||||
| 7. Admin Forms & Lists | 0/3 | Planned | - |
|
| 7. Admin Forms & Lists | 0/3 | Planned | - |
|
||||||
| 8. Admin Dashboard | 0/4 | Planned | - |
|
| 8. Admin Dashboard | 0/4 | Planned | - |
|
||||||
| 9. Content Management | 0/7 | Planned | - |
|
| 9. Content Management | 0/7 | Planned | - |
|
||||||
| 10. Core Plugins | 0/4 | Not started | - |
|
| 10. Core Plugins | 0/4 | Planned | - |
|
||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-02-04*
|
*Roadmap created: 2026-02-04*
|
||||||
|
|||||||
333
.planning/phases/10-core-plugins/10-01-PLAN.md
Normal file
333
.planning/phases/10-core-plugins/10-01-PLAN.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
---
|
||||||
|
phase: 10-core-plugins
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- plugins/golem15/user/plugin.yaml
|
||||||
|
- plugins/golem15/user/Plugin.scala
|
||||||
|
- plugins/golem15/user/models/FrontendUser.scala
|
||||||
|
- plugins/golem15/user/models/UserGroup.scala
|
||||||
|
- plugins/golem15/user/models/UserThrottle.scala
|
||||||
|
- plugins/golem15/user/models/UserSettings.scala
|
||||||
|
- plugins/golem15/user/services/FrontendUserService.scala
|
||||||
|
- plugins/golem15/user/services/FrontendAuthService.scala
|
||||||
|
- plugins/golem15/user/services/ThrottleService.scala
|
||||||
|
- plugins/golem15/user/components/Session.scala
|
||||||
|
- plugins/golem15/user/components/Account.scala
|
||||||
|
- plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql
|
||||||
|
- plugins/golem15/user/resources/views/account/login.peb
|
||||||
|
- plugins/golem15/user/resources/views/account/register.peb
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Frontend visitor can register with email, password, name, surname"
|
||||||
|
- "Registered user receives activation email if activation mode is user"
|
||||||
|
- "User can log in with email and password"
|
||||||
|
- "User can log out and session is terminated"
|
||||||
|
- "Session component restricts page access based on security mode"
|
||||||
|
- "Login throttling prevents brute force attacks"
|
||||||
|
artifacts:
|
||||||
|
- path: "plugins/golem15/user/models/FrontendUser.scala"
|
||||||
|
provides: "Frontend user domain model with validation"
|
||||||
|
contains: "case class FrontendUser"
|
||||||
|
- path: "plugins/golem15/user/services/FrontendAuthService.scala"
|
||||||
|
provides: "Authentication logic with JWT session tokens"
|
||||||
|
exports: ["FrontendAuthService", "login", "logout", "getCurrentUser"]
|
||||||
|
- path: "plugins/golem15/user/components/Account.scala"
|
||||||
|
provides: "Registration and login HTMX handlers"
|
||||||
|
exports: ["AccountComponent", "onSignin", "onRegister"]
|
||||||
|
- path: "plugins/golem15/user/components/Session.scala"
|
||||||
|
provides: "Page access control component"
|
||||||
|
exports: ["SessionComponent", "onLogout"]
|
||||||
|
- path: "plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql"
|
||||||
|
provides: "Database schema for frontend users"
|
||||||
|
contains: "CREATE TABLE frontend_users"
|
||||||
|
key_links:
|
||||||
|
- from: "plugins/golem15/user/components/Account.scala"
|
||||||
|
to: "FrontendAuthService"
|
||||||
|
via: "ZIO service injection"
|
||||||
|
pattern: "ZIO\\.service\\[FrontendAuthService\\]"
|
||||||
|
- from: "plugins/golem15/user/services/FrontendAuthService.scala"
|
||||||
|
to: "FrontendUserRepository"
|
||||||
|
via: "repository lookup"
|
||||||
|
pattern: "userRepo\\.findByEmail"
|
||||||
|
- from: "plugins/golem15/user/components/Session.scala"
|
||||||
|
to: "pageContext"
|
||||||
|
via: "user injection"
|
||||||
|
pattern: "pageContext\\.set\\(\"user\""
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the User plugin foundation with registration and authentication functionality.
|
||||||
|
|
||||||
|
Purpose: Establish frontend user management as the first core plugin, demonstrating the complete plugin pattern with models, services, components, and migrations.
|
||||||
|
|
||||||
|
Output: Working User plugin with:
|
||||||
|
- FrontendUser model with validation rules
|
||||||
|
- Registration flow with configurable activation
|
||||||
|
- Login/logout with JWT cookie sessions
|
||||||
|
- Session component for page access control
|
||||||
|
- Login throttling for security
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
# Reference patterns from prior phases
|
||||||
|
# Phase 2: Plugin manifest and lifecycle patterns
|
||||||
|
# Phase 3: Component system with HTMX handlers
|
||||||
|
# Phase 6: Authentication patterns (Argon2id, JWT)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: User plugin models and database schema</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/user/plugin.yaml
|
||||||
|
plugins/golem15/user/Plugin.scala
|
||||||
|
plugins/golem15/user/models/FrontendUser.scala
|
||||||
|
plugins/golem15/user/models/UserGroup.scala
|
||||||
|
plugins/golem15/user/models/UserThrottle.scala
|
||||||
|
plugins/golem15/user/models/UserSettings.scala
|
||||||
|
plugins/golem15/user/resources/db/migration/V10_1_1__user_plugin.sql
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create User plugin structure following Phase 2 plugin patterns:
|
||||||
|
|
||||||
|
**plugin.yaml:**
|
||||||
|
- name: Golem15.User
|
||||||
|
- description: Frontend user authentication and management
|
||||||
|
- author: Golem15
|
||||||
|
- version: 1.0.0
|
||||||
|
- require: [] (no plugin dependencies)
|
||||||
|
|
||||||
|
**Plugin.scala:**
|
||||||
|
- Extend SummerPlugin trait
|
||||||
|
- Register components: Session, Account (ResetPassword in plan 02)
|
||||||
|
- Register settings model
|
||||||
|
- Boot method for any initialization
|
||||||
|
|
||||||
|
**FrontendUser.scala:**
|
||||||
|
- Case class with fields from research Pattern 1:
|
||||||
|
id, email, passwordHash, name, surname, username (Option), avatarPath (Option),
|
||||||
|
isActivated, activationCode, activatedAt, persistCode, resetPasswordCode,
|
||||||
|
lastLogin, lastSeen, createdIpAddress, lastIpAddress, isGuest, isSuperuser,
|
||||||
|
deletedAt (soft delete), createdAt, updatedAt
|
||||||
|
- Validation rules object: email (required, unique, 6-255), password (required, 8-255),
|
||||||
|
name (required, 2-100), surname (required, 2-100)
|
||||||
|
- Quill table mapping to `frontend_users`
|
||||||
|
|
||||||
|
**UserGroup.scala:**
|
||||||
|
- Case class: id, name, code (unique), description, createdAt, updatedAt
|
||||||
|
- Quill table mapping to `user_groups`
|
||||||
|
- UsersGroups pivot case class for many-to-many
|
||||||
|
|
||||||
|
**UserThrottle.scala:**
|
||||||
|
- Case class: id, userId (Option), ipAddress (Option), attempts, lastAttemptAt,
|
||||||
|
isSuspended, suspendedAt, isBanned, bannedAt
|
||||||
|
- Quill table mapping to `user_throttle`
|
||||||
|
|
||||||
|
**UserSettings.scala:**
|
||||||
|
- Sealed traits: ActivateMode (Auto, User, Admin), RememberLogin (Always, Never, Ask)
|
||||||
|
- Case class UserPluginSettings with fields from research Pattern 2:
|
||||||
|
allowRegistration, requireActivation, activateMode, useThrottle,
|
||||||
|
useRegisterThrottle, blockPersistence, rememberLogin, minPasswordLength
|
||||||
|
- Default values matching WinterCMS
|
||||||
|
|
||||||
|
**V10_1_1__user_plugin.sql:**
|
||||||
|
- CREATE TABLE frontend_users (all fields with proper types, indexes)
|
||||||
|
- CREATE TABLE user_groups (id, name, code unique, description, timestamps)
|
||||||
|
- CREATE TABLE users_groups (user_id, group_id, composite PK)
|
||||||
|
- CREATE TABLE user_throttle (id, user_id nullable, ip_address inet, attempts, timestamps, ban flags)
|
||||||
|
- Indexes on email, username, persist_code, throttle lookups
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds with new models
|
||||||
|
SQL migration syntax valid (no syntax errors in IDE)
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
FrontendUser, UserGroup, UserThrottle, UserSettings models exist with Quill mappings.
|
||||||
|
Migration creates all required tables with proper indexes.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Authentication and user services</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/user/repositories/FrontendUserRepository.scala
|
||||||
|
plugins/golem15/user/services/FrontendUserService.scala
|
||||||
|
plugins/golem15/user/services/FrontendAuthService.scala
|
||||||
|
plugins/golem15/user/services/ThrottleService.scala
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create service layer for user operations:
|
||||||
|
|
||||||
|
**FrontendUserRepository.scala:**
|
||||||
|
- Trait with ZIO effects following Phase 1 repository pattern
|
||||||
|
- Methods: findById, findByEmail, findByPersistCode, create, update, delete (soft)
|
||||||
|
- Live implementation using QuillContext
|
||||||
|
- Error handling with RepositoryError ADT
|
||||||
|
|
||||||
|
**FrontendUserService.scala:**
|
||||||
|
- Trait following research Pattern 1
|
||||||
|
- Methods:
|
||||||
|
- findByEmail(email): Option[FrontendUser]
|
||||||
|
- findById(id): Option[FrontendUser]
|
||||||
|
- register(data: RegistrationData): FrontendUser (handles activation code generation)
|
||||||
|
- activate(userId, code): FrontendUser
|
||||||
|
- updateProfile(userId, data: ProfileUpdate): FrontendUser
|
||||||
|
- touchLastSeen(userId): Unit
|
||||||
|
- touchIpAddress(userId, ip): Unit
|
||||||
|
- RegistrationData case class: email, password, name, surname, ipAddress
|
||||||
|
- ProfileUpdate case class: name, surname, newPassword (Option)
|
||||||
|
- Use Password4j for hashing (Argon2id, matching Phase 6 patterns)
|
||||||
|
- Live implementation with ZLayer
|
||||||
|
|
||||||
|
**FrontendAuthService.scala:**
|
||||||
|
- Trait following research Pattern 10
|
||||||
|
- Methods:
|
||||||
|
- login(user, remember): String (returns JWT token)
|
||||||
|
- logout: Unit (clears session, rotates persistCode)
|
||||||
|
- getCurrentUser: Option[FrontendUser]
|
||||||
|
- isAuthenticated: Boolean
|
||||||
|
- verifyPassword(userId, password): Boolean
|
||||||
|
- validateAndSetSession(token): Unit (for middleware)
|
||||||
|
- JWT token generation using jwt-scala (15min session, 7day remember)
|
||||||
|
- Cookie configuration helper (httpOnly, secure in prod, SameSite Lax)
|
||||||
|
- Session state via Ref[Option[FrontendUser]]
|
||||||
|
- Live implementation with ZLayer
|
||||||
|
|
||||||
|
**ThrottleService.scala:**
|
||||||
|
- Trait for login/registration throttling
|
||||||
|
- Methods:
|
||||||
|
- isThrottled(ip): Boolean
|
||||||
|
- recordAttempt(ip, success): Unit
|
||||||
|
- clearAttempts(ip): Unit
|
||||||
|
- isBanned(ip): Boolean
|
||||||
|
- Configurable thresholds (5 attempts, 15 min lockout)
|
||||||
|
- Live implementation with ZLayer
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
All service traits have Live implementations
|
||||||
|
ZLayers compose correctly (no missing dependencies)
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
FrontendUserRepository provides data access.
|
||||||
|
FrontendUserService handles registration with Argon2id password hashing.
|
||||||
|
FrontendAuthService manages JWT sessions with cookie helpers.
|
||||||
|
ThrottleService prevents brute force attacks.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Session and Account components with templates</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/user/components/Session.scala
|
||||||
|
plugins/golem15/user/components/Account.scala
|
||||||
|
plugins/golem15/user/resources/views/session/default.peb
|
||||||
|
plugins/golem15/user/resources/views/account/login.peb
|
||||||
|
plugins/golem15/user/resources/views/account/register.peb
|
||||||
|
plugins/golem15/user/resources/views/account/success.peb
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create frontend components following Phase 3 component patterns:
|
||||||
|
|
||||||
|
**Session.scala (research Pattern 3):**
|
||||||
|
- Extend SummerComponent trait
|
||||||
|
- ComponentDetails: name "Session", description "User session and access control"
|
||||||
|
- Property schema:
|
||||||
|
- security: Dropdown (all/user/guest), default "all"
|
||||||
|
- allowedUserGroups: Set, optional group filtering
|
||||||
|
- redirect: Dropdown, page to redirect unauthorized users
|
||||||
|
- onRun lifecycle:
|
||||||
|
- Check security mode against current auth state
|
||||||
|
- If unauthorized and redirect set, fail with ComponentRedirect
|
||||||
|
- If unauthorized and no redirect, fail with AccessDenied
|
||||||
|
- Inject "user" into pageContext (null if guest)
|
||||||
|
- Touch lastSeen for authenticated users
|
||||||
|
- HTMX handler onLogout:
|
||||||
|
- Call authService.logout
|
||||||
|
- Return HtmxResponse with HX-Redirect to configured page
|
||||||
|
- Trigger "userLoggedOut" event
|
||||||
|
|
||||||
|
**Account.scala (research Pattern 4):**
|
||||||
|
- Extend SummerComponent trait
|
||||||
|
- ComponentDetails: name "Account", description "Registration, login, profile"
|
||||||
|
- Property schema:
|
||||||
|
- redirect: Dropdown, page after login/register
|
||||||
|
- paramCode: String, activation code URL param name, default "code"
|
||||||
|
- requirePassword: Checkbox, require current password for profile update
|
||||||
|
- HTMX handlers:
|
||||||
|
- onSignin: Parse login/password from form, validate length, call authService.authenticate,
|
||||||
|
handle errors (InvalidCredentials, AccountBanned, NotActivated) with generic message,
|
||||||
|
record IP, return HX-Redirect on success
|
||||||
|
- onRegister: Check allowRegistration setting, check throttle, parse form data,
|
||||||
|
call userService.register, send activation email if mode is User,
|
||||||
|
auto-login if auto-activated, return HX-Redirect on success
|
||||||
|
- onActivate: Get code from URL param, call userService.activate, auto-login, redirect
|
||||||
|
|
||||||
|
**Templates (Pebble):**
|
||||||
|
- session/default.peb: Empty by default (component injects user context only)
|
||||||
|
- account/login.peb:
|
||||||
|
- Form with hx-post to onSignin handler
|
||||||
|
- Email input, password input, remember checkbox (if settings allow)
|
||||||
|
- Error display area with hx-swap="innerHTML"
|
||||||
|
- CSRF token hidden field
|
||||||
|
- account/register.peb:
|
||||||
|
- Form with hx-post to onRegister handler
|
||||||
|
- Email, password, password_confirmation, name, surname inputs
|
||||||
|
- Error display area
|
||||||
|
- CSRF token hidden field
|
||||||
|
- account/success.peb:
|
||||||
|
- Success message partial for HTMX responses
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
Templates have valid Pebble syntax (no unclosed tags)
|
||||||
|
Components registered in Plugin.scala boot method
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Session component controls page access with security modes.
|
||||||
|
Account component handles login and registration via HTMX.
|
||||||
|
Templates render forms with CSRF protection and error handling.
|
||||||
|
User plugin fully functional for registration and authentication flows.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
After all tasks complete:
|
||||||
|
1. User plugin compiles: `./mill summercms.compile`
|
||||||
|
2. Migration valid: Check SQL syntax for frontend_users, user_groups, users_groups, user_throttle
|
||||||
|
3. Component registration: Plugin.scala registers Session and Account components
|
||||||
|
4. Service wiring: All ZLayers compose without missing dependencies
|
||||||
|
5. Template syntax: Pebble templates parse without errors
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Frontend visitor can submit registration form (model, service, component exist)
|
||||||
|
- Registration creates FrontendUser with hashed password
|
||||||
|
- Login authenticates user and sets JWT cookie
|
||||||
|
- Session component restricts page access based on security mode
|
||||||
|
- Logout clears session and rotates persist code
|
||||||
|
- Throttle service tracks failed login attempts
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/10-core-plugins/10-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
267
.planning/phases/10-core-plugins/10-02-PLAN.md
Normal file
267
.planning/phases/10-core-plugins/10-02-PLAN.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
---
|
||||||
|
phase: 10-core-plugins
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["10-01"]
|
||||||
|
files_modified:
|
||||||
|
- plugins/golem15/user/components/ResetPassword.scala
|
||||||
|
- plugins/golem15/user/services/FrontendUserService.scala
|
||||||
|
- plugins/golem15/user/services/UserMailService.scala
|
||||||
|
- plugins/golem15/user/resources/views/account/profile.peb
|
||||||
|
- plugins/golem15/user/resources/views/account/avatar.peb
|
||||||
|
- plugins/golem15/user/resources/views/resetpassword/request.peb
|
||||||
|
- plugins/golem15/user/resources/views/resetpassword/reset.peb
|
||||||
|
- plugins/golem15/user/resources/views/mail/activation.peb
|
||||||
|
- plugins/golem15/user/resources/views/mail/reset.peb
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Logged-in user can view and edit their profile"
|
||||||
|
- "User can change password with current password verification"
|
||||||
|
- "User can upload and change avatar"
|
||||||
|
- "Visitor can request password reset via email"
|
||||||
|
- "User can reset password using email link with token"
|
||||||
|
- "Activation email sent when activation mode is user"
|
||||||
|
artifacts:
|
||||||
|
- path: "plugins/golem15/user/components/ResetPassword.scala"
|
||||||
|
provides: "Password reset flow component"
|
||||||
|
exports: ["ResetPasswordComponent", "onRequest", "onReset"]
|
||||||
|
- path: "plugins/golem15/user/services/UserMailService.scala"
|
||||||
|
provides: "Email sending for activation and reset"
|
||||||
|
exports: ["UserMailService", "sendActivationEmail", "sendResetEmail"]
|
||||||
|
- path: "plugins/golem15/user/resources/views/account/profile.peb"
|
||||||
|
provides: "Profile edit form template"
|
||||||
|
contains: "hx-post"
|
||||||
|
key_links:
|
||||||
|
- from: "plugins/golem15/user/components/Account.scala"
|
||||||
|
to: "UserMailService"
|
||||||
|
via: "activation email on register"
|
||||||
|
pattern: "mailService\\.sendActivationEmail"
|
||||||
|
- from: "plugins/golem15/user/components/ResetPassword.scala"
|
||||||
|
to: "FrontendUserService"
|
||||||
|
via: "password reset"
|
||||||
|
pattern: "userService\\.resetPassword"
|
||||||
|
- from: "plugins/golem15/user/services/FrontendUserService.scala"
|
||||||
|
to: "MediaLibrary"
|
||||||
|
via: "avatar upload"
|
||||||
|
pattern: "mediaLibrary\\.upload"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Complete the User plugin with profile management and password reset functionality.
|
||||||
|
|
||||||
|
Purpose: Enable users to manage their accounts and recover access, completing the frontend user experience.
|
||||||
|
|
||||||
|
Output: Working profile and reset features:
|
||||||
|
- Profile edit form with name/surname/password change
|
||||||
|
- Avatar upload using media library
|
||||||
|
- Password reset request and completion flow
|
||||||
|
- Email templates for activation and reset
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<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-01-SUMMARY.md
|
||||||
|
|
||||||
|
# Reference: Phase 9 media library for avatar upload
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Profile management with avatar upload</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/user/components/Account.scala (modify)
|
||||||
|
plugins/golem15/user/services/FrontendUserService.scala (modify)
|
||||||
|
plugins/golem15/user/resources/views/account/profile.peb
|
||||||
|
plugins/golem15/user/resources/views/account/avatar.peb
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Add profile management to Account component:
|
||||||
|
|
||||||
|
**Account.scala additions:**
|
||||||
|
- Add onUpdate HTMX handler (research Pattern 4):
|
||||||
|
- Get current user from authService, fail if not authenticated
|
||||||
|
- If requirePassword property set, validate current password
|
||||||
|
- Parse ProfileUpdate from form (name, surname, newPassword optional)
|
||||||
|
- Validate new password length if provided (min from settings)
|
||||||
|
- Call userService.updateProfile
|
||||||
|
- If password changed, re-authenticate user (new JWT)
|
||||||
|
- Return success partial with "Profile updated" message
|
||||||
|
- Add onUploadAvatar HTMX handler:
|
||||||
|
- Get current user, fail if not authenticated
|
||||||
|
- Get uploaded file from multipart form
|
||||||
|
- Validate file type (image/jpeg, image/png, image/gif, image/webp)
|
||||||
|
- Validate file size (max 2MB from settings or default)
|
||||||
|
- Call userService.updateAvatar
|
||||||
|
- Return updated avatar partial with new image
|
||||||
|
|
||||||
|
**FrontendUserService.scala additions:**
|
||||||
|
- Add updateAvatar(userId, file: UploadedFile): FrontendUser
|
||||||
|
- Use MediaLibrary from Phase 9 to store file
|
||||||
|
- Generate path: users/{userId}/avatar.{ext}
|
||||||
|
- Update user.avatarPath
|
||||||
|
- Delete old avatar if exists
|
||||||
|
- Modify updateProfile to handle password change:
|
||||||
|
- If newPassword provided, hash with Argon2id
|
||||||
|
- Update passwordHash field
|
||||||
|
- Rotate persistCode to invalidate other sessions
|
||||||
|
|
||||||
|
**profile.peb template:**
|
||||||
|
- Form with hx-post to onUpdate handler, hx-target for response
|
||||||
|
- Name input (pre-filled from user)
|
||||||
|
- Surname input (pre-filled from user)
|
||||||
|
- Current password input (if requirePassword property)
|
||||||
|
- New password input (optional)
|
||||||
|
- Confirm new password input
|
||||||
|
- Submit button
|
||||||
|
- Error/success display area
|
||||||
|
- CSRF token
|
||||||
|
|
||||||
|
**avatar.peb template:**
|
||||||
|
- Current avatar display (or placeholder)
|
||||||
|
- Form with hx-post to onUploadAvatar, hx-encoding="multipart/form-data"
|
||||||
|
- File input with accept="image/*"
|
||||||
|
- Upload button
|
||||||
|
- Preview area that updates on success
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
Account component has onUpdate and onUploadAvatar handlers
|
||||||
|
Templates have valid Pebble syntax
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Users can edit profile (name, surname, password).
|
||||||
|
Password change requires current password if configured.
|
||||||
|
Avatar upload stores image via media library.
|
||||||
|
Templates render forms with proper HTMX attributes.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Password reset flow and email service</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/user/components/ResetPassword.scala
|
||||||
|
plugins/golem15/user/services/UserMailService.scala
|
||||||
|
plugins/golem15/user/services/FrontendUserService.scala (modify)
|
||||||
|
plugins/golem15/user/resources/views/resetpassword/request.peb
|
||||||
|
plugins/golem15/user/resources/views/resetpassword/reset.peb
|
||||||
|
plugins/golem15/user/resources/views/mail/activation.peb
|
||||||
|
plugins/golem15/user/resources/views/mail/reset.peb
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create password reset component and email service:
|
||||||
|
|
||||||
|
**UserMailService.scala:**
|
||||||
|
- Trait with ZIO effects:
|
||||||
|
- sendActivationEmail(user: FrontendUser, code: String): IO[MailError, Unit]
|
||||||
|
- sendResetEmail(user: FrontendUser, code: String): IO[MailError, Unit]
|
||||||
|
- Live implementation using ZIO email service (or stub for now):
|
||||||
|
- Load Pebble template for email body
|
||||||
|
- Include activation/reset URL with code
|
||||||
|
- Send via configured SMTP or email service
|
||||||
|
- MailError ADT: SendFailed, TemplateError, ConfigurationError
|
||||||
|
|
||||||
|
**FrontendUserService.scala additions:**
|
||||||
|
- requestPasswordReset(email: String): Unit
|
||||||
|
- Find user by email (don't reveal if not found - timing attack)
|
||||||
|
- Generate secure random reset code (32 bytes hex)
|
||||||
|
- Set resetPasswordCode on user with expiry tracking
|
||||||
|
- Call mailService.sendResetEmail
|
||||||
|
- Always succeed (don't reveal user existence)
|
||||||
|
- resetPassword(code: String, newPassword: String): FrontendUser
|
||||||
|
- Find user by resetPasswordCode
|
||||||
|
- Validate code not expired (24 hours)
|
||||||
|
- Hash new password with Argon2id
|
||||||
|
- Clear resetPasswordCode
|
||||||
|
- Rotate persistCode (invalidate other sessions)
|
||||||
|
- Return updated user
|
||||||
|
|
||||||
|
**ResetPassword.scala component:**
|
||||||
|
- Extend SummerComponent trait
|
||||||
|
- ComponentDetails: name "ResetPassword", description "Password recovery flow"
|
||||||
|
- Property schema:
|
||||||
|
- paramCode: String, URL parameter for reset code, default "code"
|
||||||
|
- redirect: Dropdown, page after successful reset
|
||||||
|
- HTMX handlers:
|
||||||
|
- onRequest: Parse email from form, call userService.requestPasswordReset,
|
||||||
|
always show success message (no email enumeration)
|
||||||
|
- onReset: Get code from URL param, parse new password + confirmation,
|
||||||
|
validate password length and match, call userService.resetPassword,
|
||||||
|
auto-login user, redirect to configured page
|
||||||
|
|
||||||
|
**Templates:**
|
||||||
|
- resetpassword/request.peb:
|
||||||
|
- Form with hx-post to onRequest
|
||||||
|
- Email input
|
||||||
|
- Submit button "Send Reset Link"
|
||||||
|
- Success message: "If an account exists, you will receive an email"
|
||||||
|
- resetpassword/reset.peb:
|
||||||
|
- Form with hx-post to onReset
|
||||||
|
- Hidden input with reset code from URL
|
||||||
|
- New password input
|
||||||
|
- Confirm password input
|
||||||
|
- Submit button "Reset Password"
|
||||||
|
- Error display for invalid/expired code
|
||||||
|
- mail/activation.peb (Pebble email template):
|
||||||
|
- Subject: "Activate your account"
|
||||||
|
- Body: Welcome message, activation link with code
|
||||||
|
- Plain text fallback
|
||||||
|
- mail/reset.peb (Pebble email template):
|
||||||
|
- Subject: "Reset your password"
|
||||||
|
- Body: Reset link with code, expiry notice (24 hours)
|
||||||
|
- Plain text fallback
|
||||||
|
|
||||||
|
**Register ResetPassword component in Plugin.scala**
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
ResetPassword component registered in Plugin.scala
|
||||||
|
Email templates have valid Pebble syntax
|
||||||
|
Reset code generation uses SecureRandom
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Password reset request sends email (or stubs if no SMTP).
|
||||||
|
Reset link with token allows setting new password.
|
||||||
|
Activation email template ready for registration flow.
|
||||||
|
No email enumeration possible (generic success messages).
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
After all tasks complete:
|
||||||
|
1. User plugin compiles: `./mill summercms.compile`
|
||||||
|
2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar
|
||||||
|
3. ResetPassword component handles: onRequest, onReset
|
||||||
|
4. Email service can send activation and reset emails
|
||||||
|
5. Templates render valid forms with HTMX
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Logged-in user can view profile form pre-filled with their data
|
||||||
|
- Profile update saves name/surname changes
|
||||||
|
- Password change requires current password confirmation
|
||||||
|
- Avatar upload stores image and updates user record
|
||||||
|
- Password reset request always shows success (no enumeration)
|
||||||
|
- Reset link with valid code allows new password entry
|
||||||
|
- User automatically logged in after successful reset
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/10-core-plugins/10-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
421
.planning/phases/10-core-plugins/10-03-PLAN.md
Normal file
421
.planning/phases/10-core-plugins/10-03-PLAN.md
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
---
|
||||||
|
phase: 10-core-plugins
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- plugins/golem15/blog/plugin.yaml
|
||||||
|
- plugins/golem15/blog/Plugin.scala
|
||||||
|
- plugins/golem15/blog/models/BlogPost.scala
|
||||||
|
- plugins/golem15/blog/models/BlogSettings.scala
|
||||||
|
- plugins/golem15/blog/repositories/BlogPostRepository.scala
|
||||||
|
- plugins/golem15/blog/services/BlogPostService.scala
|
||||||
|
- plugins/golem15/blog/services/ContentService.scala
|
||||||
|
- plugins/golem15/blog/controllers/Posts.scala
|
||||||
|
- plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql
|
||||||
|
- plugins/golem15/blog/resources/controllers/posts/fields.yaml
|
||||||
|
- plugins/golem15/blog/resources/controllers/posts/columns.yaml
|
||||||
|
- plugins/golem15/blog/resources/views/posts/form.peb
|
||||||
|
- build.mill
|
||||||
|
- package.json
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Admin can create a new blog post"
|
||||||
|
- "Admin can edit existing blog posts"
|
||||||
|
- "Blog post has title, slug, content with WYSIWYG editor"
|
||||||
|
- "Blog post content supports both Markdown and HTML"
|
||||||
|
- "Blog posts have published/draft state with scheduled publishing"
|
||||||
|
- "Blog posts can be marked as featured"
|
||||||
|
artifacts:
|
||||||
|
- path: "plugins/golem15/blog/models/BlogPost.scala"
|
||||||
|
provides: "Blog post domain model"
|
||||||
|
contains: "case class BlogPost"
|
||||||
|
- path: "plugins/golem15/blog/services/BlogPostService.scala"
|
||||||
|
provides: "Blog post CRUD operations"
|
||||||
|
exports: ["BlogPostService", "create", "update", "findBySlug"]
|
||||||
|
- path: "plugins/golem15/blog/controllers/Posts.scala"
|
||||||
|
provides: "Admin controller for post management"
|
||||||
|
exports: ["PostsController"]
|
||||||
|
- path: "plugins/golem15/blog/resources/controllers/posts/fields.yaml"
|
||||||
|
provides: "YAML-driven form definition"
|
||||||
|
contains: "content:"
|
||||||
|
- path: "plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql"
|
||||||
|
provides: "Blog posts table schema"
|
||||||
|
contains: "CREATE TABLE blog_posts"
|
||||||
|
key_links:
|
||||||
|
- from: "plugins/golem15/blog/controllers/Posts.scala"
|
||||||
|
to: "BlogPostService"
|
||||||
|
via: "ZIO service injection"
|
||||||
|
pattern: "ZIO\\.service\\[BlogPostService\\]"
|
||||||
|
- from: "plugins/golem15/blog/services/BlogPostService.scala"
|
||||||
|
to: "BlogPostRepository"
|
||||||
|
via: "data access"
|
||||||
|
pattern: "postRepo\\."
|
||||||
|
- from: "plugins/golem15/blog/services/ContentService.scala"
|
||||||
|
to: "commonmark"
|
||||||
|
via: "Markdown parsing"
|
||||||
|
pattern: "Parser\\.builder"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the Blog plugin foundation with post management and WYSIWYG editor integration.
|
||||||
|
|
||||||
|
Purpose: Establish blog content management as the second core plugin, demonstrating admin CRUD patterns with YAML-driven forms.
|
||||||
|
|
||||||
|
Output: Working Blog plugin with:
|
||||||
|
- BlogPost model with content, publishing state, and metadata
|
||||||
|
- Admin controller for post CRUD
|
||||||
|
- TinyMCE WYSIWYG editor integration
|
||||||
|
- Markdown and HTML content support
|
||||||
|
- Draft/published state with scheduled publishing
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
# Reference patterns from prior phases
|
||||||
|
# Phase 2: Plugin manifest and lifecycle patterns
|
||||||
|
# Phase 7: Admin forms with YAML definitions
|
||||||
|
# Phase 9: Media library for image uploads in editor
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Blog post model and database schema</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/blog/plugin.yaml
|
||||||
|
plugins/golem15/blog/Plugin.scala
|
||||||
|
plugins/golem15/blog/models/BlogPost.scala
|
||||||
|
plugins/golem15/blog/models/BlogSettings.scala
|
||||||
|
plugins/golem15/blog/resources/db/migration/V10_2_1__blog_posts.sql
|
||||||
|
build.mill
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create Blog plugin structure and post model:
|
||||||
|
|
||||||
|
**build.mill additions:**
|
||||||
|
- Add slugify library: mvn"com.github.slugify:slugify:3.0.7"
|
||||||
|
- Add commonmark: mvn"org.commonmark:commonmark:0.22.0"
|
||||||
|
|
||||||
|
**plugin.yaml:**
|
||||||
|
- name: Golem15.Blog
|
||||||
|
- description: Blog posts with categories and tags
|
||||||
|
- author: Golem15
|
||||||
|
- version: 1.0.0
|
||||||
|
- require: [] (no plugin dependencies for posts)
|
||||||
|
|
||||||
|
**Plugin.scala:**
|
||||||
|
- Extend SummerPlugin trait
|
||||||
|
- Register controllers: Posts (Categories in plan 04)
|
||||||
|
- Register components: Posts, Post (in plan 04)
|
||||||
|
- Register settings model
|
||||||
|
- Boot method for initialization
|
||||||
|
|
||||||
|
**BlogPost.scala (research Pattern 5):**
|
||||||
|
- Case class with fields:
|
||||||
|
id, authorId (BackendUser reference), title, slug (unique), excerpt (Option),
|
||||||
|
content (raw Markdown/HTML), contentHtml (rendered), charactersCount,
|
||||||
|
published (Boolean), publishedAt (Option[Instant]), isFeatured,
|
||||||
|
mainCategoryId (Option, for plan 04), metadata (Map[String, Json]),
|
||||||
|
createdAt, updatedAt
|
||||||
|
- Validation rules: title required, slug required + unique + pattern, content required
|
||||||
|
- allowedSortingOptions map (title asc/desc, created_at, published_at, random)
|
||||||
|
- Quill table mapping to `blog_posts`
|
||||||
|
|
||||||
|
**BlogSettings.scala:**
|
||||||
|
- Case class BlogPluginSettings:
|
||||||
|
postsPerPage (Int, default 10), defaultSorting (String),
|
||||||
|
urlPattern (String, e.g., "/blog/:slug"), enableComments (Boolean, false),
|
||||||
|
excerptLength (Int, default 200)
|
||||||
|
- Settings YAML schema definition
|
||||||
|
|
||||||
|
**V10_2_1__blog_posts.sql:**
|
||||||
|
- CREATE TABLE blog_posts with all fields from research:
|
||||||
|
id BIGSERIAL PRIMARY KEY
|
||||||
|
author_id BIGINT NOT NULL REFERENCES backend_users(id)
|
||||||
|
title VARCHAR(255) NOT NULL
|
||||||
|
slug VARCHAR(255) NOT NULL UNIQUE
|
||||||
|
excerpt TEXT
|
||||||
|
content TEXT NOT NULL
|
||||||
|
content_html TEXT NOT NULL
|
||||||
|
characters_count INT DEFAULT 0
|
||||||
|
published BOOLEAN DEFAULT false
|
||||||
|
published_at TIMESTAMPTZ
|
||||||
|
is_featured BOOLEAN DEFAULT false
|
||||||
|
main_category_id BIGINT (FK added in plan 04)
|
||||||
|
metadata JSONB DEFAULT '{}'
|
||||||
|
created_at, updated_at TIMESTAMPTZ
|
||||||
|
- Indexes on slug, published+published_at, author_id, is_featured
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds with new dependencies
|
||||||
|
SQL migration syntax valid
|
||||||
|
BlogPost model has Quill mappings
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Blog plugin structure created with manifest.
|
||||||
|
BlogPost model ready with all fields.
|
||||||
|
Migration creates blog_posts table.
|
||||||
|
Dependencies added for slug generation and Markdown.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Post service with content processing</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/blog/repositories/BlogPostRepository.scala
|
||||||
|
plugins/golem15/blog/services/BlogPostService.scala
|
||||||
|
plugins/golem15/blog/services/ContentService.scala
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create service layer for post operations:
|
||||||
|
|
||||||
|
**BlogPostRepository.scala:**
|
||||||
|
- Trait with ZIO effects following repository pattern:
|
||||||
|
- findById(id): Option[BlogPost]
|
||||||
|
- findBySlug(slug): Option[BlogPost]
|
||||||
|
- create(post): BlogPost
|
||||||
|
- update(post): BlogPost
|
||||||
|
- delete(id): Unit (hard delete or soft)
|
||||||
|
- listPublished(options): PaginatedResult[BlogPost]
|
||||||
|
- listAll(options): PaginatedResult[BlogPost] (admin)
|
||||||
|
- countByAuthor(authorId): Int
|
||||||
|
- Live implementation using QuillContext
|
||||||
|
- Published filter: published = true AND published_at <= NOW()
|
||||||
|
|
||||||
|
**ContentService.scala:**
|
||||||
|
- Trait for content processing:
|
||||||
|
- renderMarkdown(content): String (Markdown to HTML)
|
||||||
|
- sanitizeHtml(html): String (XSS protection)
|
||||||
|
- generateExcerpt(contentHtml, maxLength): String
|
||||||
|
- calculateReadingTime(contentHtml): Int (minutes)
|
||||||
|
- countCharacters(content): Int
|
||||||
|
- Live implementation:
|
||||||
|
- Use CommonMark parser for Markdown (research Pattern 5)
|
||||||
|
- Use Jsoup with Safelist.relaxed() for sanitization
|
||||||
|
- Excerpt: Split on <!-- more --> marker or truncate to maxLength
|
||||||
|
- Reading time: Strip HTML, count words, divide by 200 WPM, round up
|
||||||
|
|
||||||
|
**BlogPostService.scala (research Pattern 5):**
|
||||||
|
- Trait:
|
||||||
|
- create(data: PostCreateData): BlogPost
|
||||||
|
- update(id, data: PostUpdateData): BlogPost
|
||||||
|
- delete(id): Unit
|
||||||
|
- findBySlug(slug): Option[BlogPostWithRelations]
|
||||||
|
- listFrontend(options: PostListOptions): PaginatedResult[BlogPostWithRelations]
|
||||||
|
- publish(id): BlogPost (sets published=true, publishedAt=now if null)
|
||||||
|
- unpublish(id): BlogPost (sets published=false)
|
||||||
|
- PostCreateData case class: title, slug (Option - auto-generate if empty),
|
||||||
|
content, excerpt (Option), published, publishedAt (Option), isFeatured, authorId
|
||||||
|
- PostUpdateData case class: title, slug, content, excerpt, published, publishedAt, isFeatured
|
||||||
|
- PostListOptions from research: page, perPage, sort, published, exceptPost, exceptCategories, search
|
||||||
|
- Before save:
|
||||||
|
- If slug empty, generate from title using Slugify
|
||||||
|
- Render content to contentHtml using ContentService
|
||||||
|
- Calculate charactersCount
|
||||||
|
- Sanitize HTML content
|
||||||
|
- BlogPostWithRelations case class for joins (categories, tags, author in plan 04)
|
||||||
|
- Live implementation with ZLayer
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
ContentService renders Markdown correctly
|
||||||
|
BlogPostService generates slugs from titles
|
||||||
|
Sanitization strips dangerous HTML
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
BlogPostRepository provides data access.
|
||||||
|
ContentService handles Markdown rendering and XSS sanitization.
|
||||||
|
BlogPostService manages CRUD with auto-slug and content processing.
|
||||||
|
Reading time and character count auto-calculated.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Admin controller with TinyMCE editor</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/blog/controllers/Posts.scala
|
||||||
|
plugins/golem15/blog/resources/controllers/posts/fields.yaml
|
||||||
|
plugins/golem15/blog/resources/controllers/posts/columns.yaml
|
||||||
|
plugins/golem15/blog/resources/views/posts/form.peb
|
||||||
|
plugins/golem15/blog/resources/views/posts/list.peb
|
||||||
|
package.json
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create admin controller for post management:
|
||||||
|
|
||||||
|
**package.json additions:**
|
||||||
|
- Add TinyMCE: "tinymce": "^7.0.0"
|
||||||
|
|
||||||
|
**Posts.scala controller:**
|
||||||
|
- Extend AdminController (Phase 7 pattern)
|
||||||
|
- Configure: modelClass = BlogPost, listUrl = "/admin/blog/posts", formUrl = "/admin/blog/posts/:id/edit"
|
||||||
|
- Actions:
|
||||||
|
- index: List posts using BlogPostService.listAll, render list.peb
|
||||||
|
- create: New post form
|
||||||
|
- store: Parse form, call BlogPostService.create, redirect to edit
|
||||||
|
- edit: Load post by ID, render form.peb
|
||||||
|
- update: Parse form, call BlogPostService.update, return success response
|
||||||
|
- delete: Call BlogPostService.delete, redirect to list
|
||||||
|
- publish/unpublish: Toggle published state
|
||||||
|
- HTMX handlers for inline actions (delete confirmation, publish toggle)
|
||||||
|
|
||||||
|
**fields.yaml (Phase 7 form definition):**
|
||||||
|
```yaml
|
||||||
|
fields:
|
||||||
|
title:
|
||||||
|
type: text
|
||||||
|
label: Title
|
||||||
|
required: true
|
||||||
|
span: full
|
||||||
|
|
||||||
|
slug:
|
||||||
|
type: text
|
||||||
|
label: Slug
|
||||||
|
span: auto
|
||||||
|
comment: "Leave blank to auto-generate from title"
|
||||||
|
preset:
|
||||||
|
field: title
|
||||||
|
type: slug
|
||||||
|
|
||||||
|
published:
|
||||||
|
type: checkbox
|
||||||
|
label: Published
|
||||||
|
span: auto
|
||||||
|
|
||||||
|
published_at:
|
||||||
|
type: datepicker
|
||||||
|
label: Publish Date
|
||||||
|
mode: datetime
|
||||||
|
span: auto
|
||||||
|
dependsOn: published
|
||||||
|
|
||||||
|
is_featured:
|
||||||
|
type: checkbox
|
||||||
|
label: Featured Post
|
||||||
|
span: auto
|
||||||
|
|
||||||
|
content:
|
||||||
|
type: richeditor
|
||||||
|
label: Content
|
||||||
|
size: huge
|
||||||
|
span: full
|
||||||
|
|
||||||
|
excerpt:
|
||||||
|
type: textarea
|
||||||
|
label: Excerpt
|
||||||
|
size: small
|
||||||
|
span: full
|
||||||
|
comment: "Leave blank to auto-generate from content"
|
||||||
|
|
||||||
|
tabs:
|
||||||
|
fields:
|
||||||
|
tab: Content
|
||||||
|
_categories:
|
||||||
|
tab: Categories
|
||||||
|
_tags:
|
||||||
|
tab: Tags
|
||||||
|
```
|
||||||
|
|
||||||
|
**columns.yaml:**
|
||||||
|
```yaml
|
||||||
|
columns:
|
||||||
|
title:
|
||||||
|
label: Title
|
||||||
|
sortable: true
|
||||||
|
|
||||||
|
slug:
|
||||||
|
label: Slug
|
||||||
|
sortable: true
|
||||||
|
|
||||||
|
author:
|
||||||
|
label: Author
|
||||||
|
relation: author
|
||||||
|
select: name
|
||||||
|
|
||||||
|
published:
|
||||||
|
label: Status
|
||||||
|
type: switch
|
||||||
|
|
||||||
|
published_at:
|
||||||
|
label: Published
|
||||||
|
type: datetime
|
||||||
|
sortable: true
|
||||||
|
|
||||||
|
updated_at:
|
||||||
|
label: Updated
|
||||||
|
type: datetime
|
||||||
|
sortable: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**form.peb template:**
|
||||||
|
- Form structure following Phase 7 patterns
|
||||||
|
- Include TinyMCE initialization script for content field:
|
||||||
|
```javascript
|
||||||
|
initBlogEditor('#Form-field-Post-content');
|
||||||
|
```
|
||||||
|
- Reference research Pattern 9 for TinyMCE config
|
||||||
|
- Image upload handler pointing to media library endpoint
|
||||||
|
- Save button with HTMX submit
|
||||||
|
- Publish/Unpublish button
|
||||||
|
|
||||||
|
**list.peb template:**
|
||||||
|
- List structure following Phase 7 patterns
|
||||||
|
- Columns from columns.yaml
|
||||||
|
- Row actions: Edit, Delete, Publish/Unpublish
|
||||||
|
- Bulk actions: Delete selected, Publish/Unpublish selected
|
||||||
|
- Filters: published, author, date range
|
||||||
|
|
||||||
|
**Register controller in Plugin.scala**
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
npm install succeeds with TinyMCE
|
||||||
|
YAML files parse correctly
|
||||||
|
Controller registered in plugin
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Admin can access /admin/blog/posts to list posts.
|
||||||
|
Create/edit forms render with TinyMCE editor.
|
||||||
|
Posts can be saved, published, unpublished, deleted.
|
||||||
|
List shows posts with sorting and filtering.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
After all tasks complete:
|
||||||
|
1. Blog plugin compiles: `./mill summercms.compile`
|
||||||
|
2. Dependencies installed: `npm install` includes TinyMCE
|
||||||
|
3. Migration creates blog_posts table: Check SQL syntax
|
||||||
|
4. Controller routes registered: Posts controller in Plugin.scala
|
||||||
|
5. Form fields render TinyMCE: richeditor type maps to TinyMCE
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Admin can navigate to /admin/blog/posts and see post list
|
||||||
|
- Create post form shows title, slug, content editor, excerpt, publish options
|
||||||
|
- TinyMCE editor loads for content field with image upload
|
||||||
|
- Saving post generates slug from title if empty
|
||||||
|
- Content rendered to HTML with Markdown support
|
||||||
|
- Published posts have published_at timestamp
|
||||||
|
- Featured flag can be toggled
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/10-core-plugins/10-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
512
.planning/phases/10-core-plugins/10-04-PLAN.md
Normal file
512
.planning/phases/10-core-plugins/10-04-PLAN.md
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
---
|
||||||
|
phase: 10-core-plugins
|
||||||
|
plan: 04
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["10-03"]
|
||||||
|
files_modified:
|
||||||
|
- plugins/golem15/blog/models/Category.scala
|
||||||
|
- plugins/golem15/blog/models/Tag.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/BlogPostService.scala
|
||||||
|
- plugins/golem15/blog/controllers/Categories.scala
|
||||||
|
- plugins/golem15/blog/components/Posts.scala
|
||||||
|
- plugins/golem15/blog/components/Post.scala
|
||||||
|
- plugins/golem15/blog/components/Categories.scala
|
||||||
|
- plugins/golem15/blog/components/RelatedPosts.scala
|
||||||
|
- plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
|
||||||
|
- plugins/golem15/blog/resources/controllers/categories/fields.yaml
|
||||||
|
- plugins/golem15/blog/resources/views/posts/default.peb
|
||||||
|
- plugins/golem15/blog/resources/views/posts/item.peb
|
||||||
|
- plugins/golem15/blog/resources/views/post/default.peb
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Blog posts can be assigned to categories"
|
||||||
|
- "Categories are hierarchical (parent/child)"
|
||||||
|
- "Blog posts can have multiple tags"
|
||||||
|
- "Frontend displays post listings with pagination"
|
||||||
|
- "Frontend displays individual post pages"
|
||||||
|
- "Related posts shown based on shared categories/tags"
|
||||||
|
artifacts:
|
||||||
|
- path: "plugins/golem15/blog/models/Category.scala"
|
||||||
|
provides: "Hierarchical category model with nested set"
|
||||||
|
contains: "case class Category"
|
||||||
|
- path: "plugins/golem15/blog/models/Tag.scala"
|
||||||
|
provides: "Tag model for free-form labeling"
|
||||||
|
contains: "case class Tag"
|
||||||
|
- path: "plugins/golem15/blog/components/Posts.scala"
|
||||||
|
provides: "Frontend post listing component"
|
||||||
|
exports: ["PostsComponent", "onLoadMore"]
|
||||||
|
- path: "plugins/golem15/blog/components/Post.scala"
|
||||||
|
provides: "Single post display component"
|
||||||
|
exports: ["PostComponent"]
|
||||||
|
- path: "plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql"
|
||||||
|
provides: "Categories, tags, and pivot tables"
|
||||||
|
contains: "CREATE TABLE blog_categories"
|
||||||
|
key_links:
|
||||||
|
- from: "plugins/golem15/blog/components/Posts.scala"
|
||||||
|
to: "BlogPostService"
|
||||||
|
via: "post listing"
|
||||||
|
pattern: "postService\\.listFrontend"
|
||||||
|
- from: "plugins/golem15/blog/services/BlogPostService.scala"
|
||||||
|
to: "CategoryService"
|
||||||
|
via: "category filtering"
|
||||||
|
pattern: "categoryService\\.getAllChildrenAndSelf"
|
||||||
|
- from: "plugins/golem15/blog/services/CategoryService.scala"
|
||||||
|
to: "NestedTree"
|
||||||
|
via: "tree operations"
|
||||||
|
pattern: "NestedTree\\."
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Complete the Blog plugin with categories, tags, and frontend listing components.
|
||||||
|
|
||||||
|
Purpose: Enable content organization and frontend display, demonstrating hierarchical data structures and frontend component patterns.
|
||||||
|
|
||||||
|
Output: Complete Blog plugin with:
|
||||||
|
- Hierarchical categories using nested set model
|
||||||
|
- Tags for free-form labeling
|
||||||
|
- Frontend Posts component with pagination/infinite scroll
|
||||||
|
- Frontend Post component for single post display
|
||||||
|
- Related posts based on shared categories/tags
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<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 3 component patterns for HTMX
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Category and tag models with services</name>
|
||||||
|
<files>
|
||||||
|
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
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
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
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
NestedTree rebuild produces valid left/right sequence
|
||||||
|
SQL migration syntax valid
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
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.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Update BlogPostService for relations</name>
|
||||||
|
<files>
|
||||||
|
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
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
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**
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
Related posts algorithm returns posts with shared categories/tags
|
||||||
|
Category tree drag-drop works in admin
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
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.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Frontend listing components</name>
|
||||||
|
<files>
|
||||||
|
plugins/golem15/blog/components/Posts.scala
|
||||||
|
plugins/golem15/blog/components/Post.scala
|
||||||
|
plugins/golem15/blog/components/Categories.scala
|
||||||
|
plugins/golem15/blog/components/RelatedPosts.scala
|
||||||
|
plugins/golem15/blog/resources/views/posts/default.peb
|
||||||
|
plugins/golem15/blog/resources/views/posts/item.peb
|
||||||
|
plugins/golem15/blog/resources/views/post/default.peb
|
||||||
|
plugins/golem15/blog/resources/views/categories/default.peb
|
||||||
|
plugins/golem15/blog/resources/views/relatedposts/default.peb
|
||||||
|
plugins/golem15/blog/Plugin.scala (modify)
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create frontend components for blog display:
|
||||||
|
|
||||||
|
**Posts.scala (research Pattern 8):**
|
||||||
|
- Extend SummerComponent trait
|
||||||
|
- ComponentDetails: name "Posts", description "Blog post listing"
|
||||||
|
- Property schema:
|
||||||
|
- pageNumber: String, URL param for pagination, default "{{ :page }}"
|
||||||
|
- categoryFilter: String, filter by category slug
|
||||||
|
- postsPerPage: String, default "10", validation 1-100
|
||||||
|
- noPostsMessage: String, default "No posts found."
|
||||||
|
- sortOrder: Dropdown with BlogPost.allowedSortingOptions
|
||||||
|
- throwNotFound: Checkbox, 404 on empty results
|
||||||
|
- Page variables: posts (PaginatedResult), category (Option), noPostsMessage
|
||||||
|
- onRun lifecycle:
|
||||||
|
- Load category if categoryFilter set
|
||||||
|
- Build PostListOptions from properties
|
||||||
|
- Call postService.listFrontend or listByCategory
|
||||||
|
- Handle empty results per throwNotFound
|
||||||
|
- Set page context variables
|
||||||
|
- onLoadMore HTMX handler:
|
||||||
|
- Parse page number from form
|
||||||
|
- Load next page of posts
|
||||||
|
- Return items partial
|
||||||
|
- Trigger "postsExhausted" when no more
|
||||||
|
|
||||||
|
**Post.scala component:**
|
||||||
|
- ComponentDetails: name "Post", description "Single post display"
|
||||||
|
- Property schema:
|
||||||
|
- slug: String, URL param for post slug, default "{{ :slug }}"
|
||||||
|
- notFoundMessage: String
|
||||||
|
- Page variables: post (BlogPostWithRelations)
|
||||||
|
- onRun lifecycle:
|
||||||
|
- Get slug from URL
|
||||||
|
- Call postService.findBySlug
|
||||||
|
- 404 if not found
|
||||||
|
- Set page context: post, author, categories, tags, readingTime
|
||||||
|
- Set OpenGraph meta tags for social sharing
|
||||||
|
|
||||||
|
**Categories.scala component:**
|
||||||
|
- ComponentDetails: name "Categories", description "Category listing/tree"
|
||||||
|
- Property schema:
|
||||||
|
- displayMode: Dropdown (tree/flat), default "tree"
|
||||||
|
- showPostCount: Checkbox, default true
|
||||||
|
- includeEmpty: Checkbox, show categories with no posts
|
||||||
|
- Page variables: categories (tree or flat list)
|
||||||
|
- onRun: Load category tree, filter empty if needed
|
||||||
|
|
||||||
|
**RelatedPosts.scala component:**
|
||||||
|
- ComponentDetails: name "RelatedPosts", description "Related posts by category/tag"
|
||||||
|
- Property schema:
|
||||||
|
- limit: String, max posts to show, default "3"
|
||||||
|
- postSlug: String, current post slug (from context)
|
||||||
|
- Page variables: relatedPosts (List)
|
||||||
|
- onRun: Call postService.getRelatedPosts, set context
|
||||||
|
|
||||||
|
**Templates:**
|
||||||
|
|
||||||
|
**posts/default.peb (research code example):**
|
||||||
|
```html
|
||||||
|
<div class="blog-posts" id="posts-{{ __SELF__ }}">
|
||||||
|
{% if posts.items is empty %}
|
||||||
|
<div class="no-posts">{{ noPostsMessage }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="posts-list">
|
||||||
|
{% for post in posts.items %}
|
||||||
|
{% include "posts/item" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if posts.hasNext %}
|
||||||
|
<div class="load-more">
|
||||||
|
<button
|
||||||
|
hx-post="{{ componentHandler('onLoadMore') }}"
|
||||||
|
hx-vals='{"page": {{ posts.currentPage + 1 }}}'
|
||||||
|
hx-target="#posts-{{ __SELF__ }} .posts-list"
|
||||||
|
hx-swap="beforeend"
|
||||||
|
hx-indicator=".htmx-indicator">
|
||||||
|
Load More
|
||||||
|
<span class="htmx-indicator">Loading...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**posts/item.peb:**
|
||||||
|
- Article element with post data
|
||||||
|
- Featured image (lazy load)
|
||||||
|
- Title as link to post page
|
||||||
|
- Excerpt or auto-generated summary
|
||||||
|
- Author name, published date, reading time
|
||||||
|
- Category and tag links
|
||||||
|
|
||||||
|
**post/default.peb:**
|
||||||
|
- Full post display
|
||||||
|
- OpenGraph meta tags in head
|
||||||
|
- Title, featured image, author, date
|
||||||
|
- Content HTML
|
||||||
|
- Categories and tags
|
||||||
|
- Share buttons (copy link, Twitter, Facebook)
|
||||||
|
- Include RelatedPosts component placeholder
|
||||||
|
|
||||||
|
**categories/default.peb:**
|
||||||
|
- Tree or flat list based on displayMode
|
||||||
|
- Category name as link
|
||||||
|
- Post count if showPostCount
|
||||||
|
|
||||||
|
**relatedposts/default.peb:**
|
||||||
|
- Grid of related post cards
|
||||||
|
- Image, title, excerpt snippet
|
||||||
|
|
||||||
|
**Register all components in Plugin.scala boot method**
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
./mill summercms.compile succeeds
|
||||||
|
Components registered in Plugin.scala
|
||||||
|
Templates have valid Pebble syntax
|
||||||
|
HTMX attributes properly formatted
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
Posts component displays paginated listings with infinite scroll.
|
||||||
|
Post component displays single post with meta tags.
|
||||||
|
Categories component shows category tree/list.
|
||||||
|
RelatedPosts component shows related content.
|
||||||
|
All templates render with HTMX interactions.
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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 component loads and paginates
|
||||||
|
5. Post component displays with OpenGraph meta
|
||||||
|
6. Related posts algorithm returns relevant content
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<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
|
||||||
|
- Frontend /blog shows post listing with load more
|
||||||
|
- Frontend /blog/:slug shows full post with related posts
|
||||||
|
- Category pages filter posts by category and children
|
||||||
|
- Tag pages filter posts by tag
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user