---
phase: 08-admin-dashboard
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - summercms/admin/src/settings/SettingsManager.scala
  - summercms/admin/src/settings/categories.scala
  - summercms/admin/src/settings/SettingsModel.scala
  - summercms/db/src/main/resources/db/migration/V006__system_settings.sql
autonomous: true

must_haves:
  truths:
    - "Plugins can register settings pages with category and permissions"
    - "Settings are grouped by category when displayed"
    - "Settings models persist values to database using YAML-driven forms"
  artifacts:
    - path: "summercms/admin/src/settings/SettingsManager.scala"
      provides: "Registry for plugin settings pages"
      exports: ["SettingsManager", "SettingsItem"]
    - path: "summercms/admin/src/settings/categories.scala"
      provides: "Standard settings categories"
      contains: "object SettingsCategory"
    - path: "summercms/admin/src/settings/SettingsModel.scala"
      provides: "Behavior for YAML-driven settings persistence"
      contains: "trait SettingsModel"
    - path: "summercms/db/src/main/resources/db/migration/V006__system_settings.sql"
      provides: "System settings storage table"
      contains: "CREATE TABLE system_settings"
  key_links:
    - from: "SettingsManager"
      to: "SettingsItem"
      via: "registerSettings accepts items"
      pattern: "def registerSettings.*SettingsItem"
    - from: "SettingsModel"
      to: "system_settings table"
      via: "loadFromDatabase/saveToDatabase"
      pattern: "system_settings"
---

<objective>
Create the plugin settings system: a manager for registering settings pages, standard categories for grouping, and a model behavior for YAML-driven settings persistence.

Purpose: Enable plugins to define settings pages using the same YAML-driven form system from Phase 7
Output: SettingsManager, SettingsCategory, SettingsModel, migration
</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/phases/08-admin-dashboard/08-RESEARCH.md

# Phase 1 Quill patterns
@summercms/db/src/main/scala/summercms/db/QuillContext.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Settings Manager and Categories</name>
  <files>
    summercms/admin/src/settings/SettingsManager.scala
    summercms/admin/src/settings/categories.scala
  </files>
  <action>
Create the settings registration system:

**categories.scala** - Standard settings categories with sort order:

```scala
package summercms.admin.settings

/** Standard settings categories following WinterCMS conventions */
object SettingsCategory:
  val System = CategoryDef("system", "System", 100)
  val CMS    = CategoryDef("cms", "CMS", 200)
  val Users  = CategoryDef("users", "Users", 300)
  val Mail   = CategoryDef("mail", "Mail", 400)
  val Misc   = CategoryDef("misc", "Miscellaneous", 900)

  val all: List[CategoryDef] = List(System, CMS, Users, Mail, Misc)

  def byCode(code: String): Option[CategoryDef] =
    all.find(_.code == code)

case class CategoryDef(
  code: String,
  label: String,
  order: Int
)
```

**SettingsManager.scala** - Registry for plugin settings:

```scala
package summercms.admin.settings

import zio._

/** A registered settings page */
case class SettingsItem(
  code: String,
  owner: String,              // Plugin ID: "golem15.blog"
  label: String,              // Display name or translation key
  description: Option[String] = None,
  category: String,           // Category code (system, cms, users, etc.)
  icon: Option[String] = None,
  url: Option[String] = None, // Custom URL or auto-generated
  settingsClass: Option[String] = None,  // Settings model class name
  permissions: List[String] = Nil,
  order: Int = 500,
  keywords: Option[String] = None  // For search
)

/** Context tracking current settings page being edited */
case class SettingsContext(
  owner: String,
  code: String
)

/** Registry for plugin settings pages */
trait SettingsManager:
  /** Register settings items from a plugin */
  def registerSettings(owner: String, items: List[SettingsItem]): UIO[Unit]

  /** List all items grouped by category */
  def listItems: UIO[Map[String, List[SettingsItem]]]

  /** Find a specific settings item */
  def findItem(owner: String, code: String): UIO[Option[SettingsItem]]

  /** Set current context for settings operations */
  def setContext(owner: String, code: String): UIO[Unit]

  /** Get current context */
  def getContext: UIO[Option[SettingsContext]]

object SettingsManager:
  val live: ZLayer[Any, Nothing, SettingsManager] =
    ZLayer.fromZIO {
      for
        itemsRef   <- Ref.make(Map.empty[String, SettingsItem])
        contextRef <- Ref.make(Option.empty[SettingsContext])
      yield new SettingsManager:
        def registerSettings(owner: String, items: List[SettingsItem]): UIO[Unit] =
          itemsRef.update { current =>
            items.foldLeft(current) { case (acc, item) =>
              val key = s"${owner.toUpperCase}.${item.code.toUpperCase}"
              val itemWithOwner = item.copy(
                owner = owner,
                url = item.url.orElse(Some(generateSettingsUrl(owner, item.code)))
              )
              acc + (key -> itemWithOwner)
            }
          }

        def listItems: UIO[Map[String, List[SettingsItem]]] =
          itemsRef.get.map { items =>
            items.values.toList
              .groupBy(_.category)
              .view
              .mapValues(_.sortBy(_.order))
              .toMap
          }

        def findItem(owner: String, code: String): UIO[Option[SettingsItem]] =
          itemsRef.get.map(_.get(s"${owner.toUpperCase}.${code.toUpperCase}"))

        def setContext(owner: String, code: String): UIO[Unit] =
          contextRef.set(Some(SettingsContext(owner.toLowerCase, code.toLowerCase)))

        def getContext: UIO[Option[SettingsContext]] =
          contextRef.get

        private def generateSettingsUrl(owner: String, code: String): String =
          val parts = owner.split('.')
          s"/admin/system/settings/update/${parts.mkString("/")}/$code"
    }

  // Accessor methods
  def registerSettings(owner: String, items: List[SettingsItem]): ZIO[SettingsManager, Nothing, Unit] =
    ZIO.serviceWithZIO[SettingsManager](_.registerSettings(owner, items))

  def listItems: ZIO[SettingsManager, Nothing, Map[String, List[SettingsItem]]] =
    ZIO.serviceWithZIO[SettingsManager](_.listItems)

  def findItem(owner: String, code: String): ZIO[SettingsManager, Nothing, Option[SettingsItem]] =
    ZIO.serviceWithZIO[SettingsManager](_.findItem(owner, code))
```

Categories follow WinterCMS pattern. Plugins can use existing categories or define custom ones (just use any string code).
  </action>
  <verify>
    mill summercms.admin.compile succeeds
    SettingsCategory.all contains System, CMS, Users, Mail, Misc
    SettingsManager.live layer constructs
  </verify>
  <done>
    SettingsCategory defines standard categories with order
    SettingsItem captures all metadata for a settings page
    SettingsManager provides registration and lookup with ZIO Ref
    Auto-generated URLs follow /admin/system/settings/update/{author}/{plugin}/{code} pattern
  </done>
</task>

<task type="auto">
  <name>Task 2: Settings Model Behavior</name>
  <files>
    summercms/admin/src/settings/SettingsModel.scala
    summercms/db/src/main/resources/db/migration/V006__system_settings.sql
  </files>
  <action>
Create the settings persistence layer:

**V006__system_settings.sql** - Migration for settings storage:

```sql
-- System settings storage (key-value with JSON)
CREATE TABLE system_settings (
    id BIGSERIAL PRIMARY KEY,
    item VARCHAR(255) NOT NULL UNIQUE,
    value JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_system_settings_item ON system_settings(item);

-- Trigger to update updated_at
CREATE TRIGGER update_system_settings_updated_at
    BEFORE UPDATE ON system_settings
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
```

**SettingsModel.scala** - Trait for settings persistence:

```scala
package summercms.admin.settings

import io.circe._
import io.circe.syntax._
import zio._
import summercms.db.QuillContext
import java.time.OffsetDateTime

/** Database row for system_settings */
case class SystemSetting(
  id: Long,
  item: String,
  value: Json,
  createdAt: OffsetDateTime,
  updatedAt: OffsetDateTime
)

/** Base trait for plugin settings models */
trait SettingsModel:
  /** Unique code identifying this settings group */
  def settingsCode: String

  /** Path to fields.yaml defining the form */
  def settingsFields: String

  /** Initialize default values (override in subclass) */
  def initSettingsData(): Map[String, Json] = Map.empty

/** Service for settings model operations */
trait SettingsModelService:
  /** Get settings values, loading from DB or defaults */
  def get(model: SettingsModel): Task[Map[String, Json]]

  /** Get a single setting value */
  def getValue[T: Decoder](model: SettingsModel, key: String, default: T): Task[T]

  /** Set settings values */
  def set(model: SettingsModel, values: Map[String, Json]): Task[Unit]

  /** Reset to defaults by deleting from DB */
  def resetDefault(model: SettingsModel): Task[Unit]

object SettingsModelService:
  val live: ZLayer[QuillContext, Nothing, SettingsModelService] =
    ZLayer.fromZIO {
      ZIO.service[QuillContext].map { quill =>
        import quill.ctx._

        // Cache for loaded settings
        val cache = new java.util.concurrent.ConcurrentHashMap[String, Map[String, Json]]()

        new SettingsModelService:
          def get(model: SettingsModel): Task[Map[String, Json]] =
            // Check cache first
            Option(cache.get(model.settingsCode)) match
              case Some(values) => ZIO.succeed(values)
              case None =>
                for
                  rows <- run(
                    query[SystemSetting]
                      .filter(_.item == lift(model.settingsCode))
                  )
                  values <- rows.headOption match
                    case Some(row) =>
                      ZIO.fromEither(row.value.as[Map[String, Json]])
                        .mapError(e => new RuntimeException(s"Failed to decode settings: ${e.getMessage}"))
                    case None =>
                      ZIO.succeed(model.initSettingsData())
                  _ <- ZIO.succeed(cache.put(model.settingsCode, values))
                yield values

          def getValue[T: Decoder](model: SettingsModel, key: String, default: T): Task[T] =
            get(model).map { values =>
              values.get(key)
                .flatMap(_.as[T].toOption)
                .getOrElse(default)
            }

          def set(model: SettingsModel, values: Map[String, Json]): Task[Unit] =
            val valuesJson = values.asJson
            val now = OffsetDateTime.now()
            for
              existing <- run(
                query[SystemSetting]
                  .filter(_.item == lift(model.settingsCode))
              )
              _ <- existing.headOption match
                case Some(row) =>
                  run(
                    query[SystemSetting]
                      .filter(_.id == lift(row.id))
                      .update(
                        _.value -> lift(valuesJson),
                        _.updatedAt -> lift(now)
                      )
                  ).unit
                case None =>
                  run(
                    query[SystemSetting]
                      .insert(
                        _.item -> lift(model.settingsCode),
                        _.value -> lift(valuesJson),
                        _.createdAt -> lift(now),
                        _.updatedAt -> lift(now)
                      )
                  ).unit
              // Invalidate cache
              _ <- ZIO.succeed(cache.remove(model.settingsCode))
            yield ()

          def resetDefault(model: SettingsModel): Task[Unit] =
            for
              _ <- run(
                query[SystemSetting]
                  .filter(_.item == lift(model.settingsCode))
                  .delete
              )
              _ <- ZIO.succeed(cache.remove(model.settingsCode))
            yield ()
      }
    }

  // Accessor methods
  def get(model: SettingsModel): ZIO[SettingsModelService, Throwable, Map[String, Json]] =
    ZIO.serviceWithZIO[SettingsModelService](_.get(model))

  def set(model: SettingsModel, values: Map[String, Json]): ZIO[SettingsModelService, Throwable, Unit] =
    ZIO.serviceWithZIO[SettingsModelService](_.set(model, values))
```

The SettingsModelService provides caching to avoid repeated DB hits. Cache is invalidated on write. Plugin settings models implement SettingsModel trait with their settingsCode and fields path.

Example usage by a plugin:

```scala
// In a plugin
object BlogSettings extends SettingsModel:
  val settingsCode = "golem15_blog_settings"
  val settingsFields = "$/plugins/golem15/blog/models/settings/fields.yaml"

  override def initSettingsData(): Map[String, Json] = Map(
    "posts_per_page" -> Json.fromInt(10),
    "show_author" -> Json.fromBoolean(true)
  )
```
  </action>
  <verify>
    mill summercms.db.flywayMigrate runs migration V006 successfully
    mill summercms.admin.compile succeeds
    SettingsModelService compiles with get/set/resetDefault
  </verify>
  <done>
    Migration V006 creates system_settings table with JSONB value column
    SystemSetting model maps to table
    SettingsModel trait defines settingsCode and settingsFields
    SettingsModelService provides get/set with caching
    Cache invalidation on write prevents stale reads
  </done>
</task>

</tasks>

<verification>
1. `mill summercms.admin.compile` succeeds
2. `mill summercms.db.flywayMigrate` applies V006 migration
3. Database has system_settings table with correct schema
4. SettingsCategory.byCode("system") returns System category
5. SettingsManager and SettingsModelService layers construct
</verification>

<success_criteria>
- SettingsCategory provides standard categories (System, CMS, Users, Mail, Misc)
- SettingsManager registers and groups settings by category
- SettingsModel trait established for plugin settings
- SettingsModelService persists to system_settings table with caching
- Migration creates system_settings with JSONB support
</success_criteria>

<output>
After completion, create `.planning/phases/08-admin-dashboard/08-02-SUMMARY.md`
</output>
