---
phase: 08-admin-dashboard
plan: 04
type: execute
wave: 2
depends_on: ["08-02"]
files_modified:
  - summercms/admin/src/settings/SettingsController.scala
  - summercms/admin/src/settings/SettingsIndex.scala
autonomous: false

must_haves:
  truths:
    - "Admin can navigate to settings index and see categories"
    - "Admin can open a plugin's settings page"
    - "Settings form renders from YAML using Phase 7 form system"
    - "Saving settings persists values to system_settings table"
  artifacts:
    - path: "summercms/admin/src/settings/SettingsController.scala"
      provides: "HTTP routes for settings CRUD"
      exports: ["SettingsController"]
    - path: "summercms/admin/src/settings/SettingsIndex.scala"
      provides: "Settings index page with categories"
      contains: "def renderSettingsIndex"
  key_links:
    - from: "SettingsController"
      to: "SettingsManager"
      via: "looks up registered settings"
      pattern: "settingsManager\\.(listItems|findItem)"
    - from: "SettingsController"
      to: "SettingsModelService"
      via: "loads and saves settings values"
      pattern: "settingsModelService\\.(get|set)"
    - from: "SettingsController"
      to: "FormRenderer"
      via: "renders form from YAML using Phase 7 system"
      pattern: "formRenderer\\.render"
---

<objective>
Create the settings controller with generic CRUD operations and settings index page. Settings pages use the Phase 7 YAML-driven form system.

Purpose: Enable plugins to have settings pages accessible from admin menu
Output: SettingsController, SettingsIndex rendering, integration with Phase 7 forms
</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

# Depends on 08-02 output
@summercms/admin/src/settings/SettingsManager.scala
@summercms/admin/src/settings/SettingsModel.scala

# Phase 7 form system (assumed available)
# @summercms/admin/src/forms/FormRenderer.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Settings Index Page</name>
  <files>
    summercms/admin/src/settings/SettingsIndex.scala
  </files>
  <action>
Create the settings index page that displays all registered settings grouped by category:

**SettingsIndex.scala** - Settings landing page:

```scala
package summercms.admin.settings

import scalatags.Text.all._

object SettingsIndex:
  /** Render settings index with categories and items */
  def renderSettingsIndex(
    itemsByCategory: Map[String, List[SettingsItem]],
    userPermissions: Set[String]
  ): Frag =
    div(cls := "settings-index",
      h2(cls := "mb-4", "System Settings"),

      // Category navigation tabs
      ul(cls := "nav nav-tabs mb-4", id := "settings-tabs", role := "tablist",
        sortedCategories(itemsByCategory.keys).zipWithIndex.map { case (catCode, idx) =>
          val category = SettingsCategory.byCode(catCode).getOrElse(
            CategoryDef(catCode, catCode.capitalize, 999)
          )
          li(cls := "nav-item", role := "presentation",
            button(
              cls := s"nav-link ${if idx == 0 then "active" else ""}",
              id := s"$catCode-tab",
              attr("data-bs-toggle") := "tab",
              attr("data-bs-target") := s"#$catCode-pane",
              `type` := "button",
              role := "tab",
              category.label
            )
          )
        }
      ),

      // Category content panes
      div(cls := "tab-content", id := "settings-content",
        sortedCategories(itemsByCategory.keys).zipWithIndex.map { case (catCode, idx) =>
          val items = itemsByCategory.getOrElse(catCode, Nil)
          val filteredItems = items.filter { item =>
            item.permissions.isEmpty || item.permissions.exists(userPermissions.contains)
          }

          div(
            cls := s"tab-pane fade ${if idx == 0 then "show active" else ""}",
            id := s"$catCode-pane",
            role := "tabpanel",
            attr("aria-labelledby") := s"$catCode-tab",

            if filteredItems.isEmpty then
              p(cls := "text-muted", "No settings available in this category.")
            else
              div(cls := "row g-3",
                filteredItems.map(renderSettingsCard)
              )
          )
        }
      )
    )

  /** Render a single settings item card */
  private def renderSettingsCard(item: SettingsItem): Frag =
    div(cls := "col-md-6 col-lg-4",
      div(cls := "card h-100 settings-card",
        div(cls := "card-body",
          div(cls := "d-flex align-items-start",
            item.icon.map(iconClass =>
              div(cls := "settings-icon me-3",
                i(cls := s"$iconClass fs-3 text-muted")
              )
            ).getOrElse(frag()),
            div(cls := "flex-grow-1",
              h5(cls := "card-title mb-1",
                a(href := item.url.getOrElse("#"), item.label)
              ),
              item.description.map(desc =>
                p(cls := "card-text text-muted small mb-0", desc)
              ).getOrElse(frag())
            )
          )
        ),
        div(cls := "card-footer bg-transparent",
          a(href := item.url.getOrElse("#"), cls := "btn btn-sm btn-outline-primary",
            "Configure"
          )
        )
      )
    )

  /** Sort categories by their defined order */
  private def sortedCategories(codes: Iterable[String]): List[String] =
    codes.toList.sortBy { code =>
      SettingsCategory.byCode(code).map(_.order).getOrElse(999)
    }

  /** Render settings form page */
  def renderSettingsForm(
    item: SettingsItem,
    formHtml: Frag,
    backUrl: String = "/admin/system/settings"
  ): Frag =
    div(cls := "settings-form",
      // Breadcrumb
      nav(attr("aria-label") := "breadcrumb",
        ol(cls := "breadcrumb",
          li(cls := "breadcrumb-item", a(href := "/admin", "Dashboard")),
          li(cls := "breadcrumb-item", a(href := backUrl, "Settings")),
          li(cls := "breadcrumb-item active", attr("aria-current") := "page", item.label)
        )
      ),

      // Header
      div(cls := "d-flex justify-content-between align-items-center mb-4",
        div(
          h2(cls := "mb-1", item.label),
          item.description.map(desc =>
            p(cls := "text-muted mb-0", desc)
          ).getOrElse(frag())
        ),
        div(
          a(href := backUrl, cls := "btn btn-secondary me-2",
            i(cls := "icon-arrow-left me-1"), "Back"
          )
        )
      ),

      // Form
      form(
        method := "POST",
        attr("hx-post") := item.url.getOrElse("#"),
        attr("hx-target") := "#form-container",
        attr("hx-swap") := "innerHTML",

        div(id := "form-container",
          formHtml
        ),

        // Actions
        div(cls := "form-actions mt-4 pt-3 border-top",
          button(`type` := "submit", cls := "btn btn-primary",
            i(cls := "icon-check me-1"), "Save Settings"
          ),
          button(`type` := "button", cls := "btn btn-outline-secondary ms-2",
            attr("hx-post") := s"${item.url.getOrElse("#")}/reset",
            attr("hx-confirm") := "Reset settings to defaults?",
            i(cls := "icon-refresh me-1"), "Reset to Default"
          )
        )
      )
    )

  /** Render success message after save */
  def renderSaveSuccess(item: SettingsItem, formHtml: Frag): Frag =
    frag(
      div(cls := "alert alert-success alert-dismissible fade show", role := "alert",
        "Settings saved successfully.",
        button(`type` := "button", cls := "btn-close", attr("data-bs-dismiss") := "alert")
      ),
      formHtml
    )
```

The index page:
- Groups settings by category using tabs
- Sorts categories by their defined order
- Filters items by user permissions
- Each item shows as a card with icon, label, description
- Clicking "Configure" navigates to the settings form
  </action>
  <verify>
    mill summercms.admin.compile succeeds
    SettingsIndex.renderSettingsIndex compiles
    Categories sorted by order when rendered
  </verify>
  <done>
    SettingsIndex renders tabbed view of settings grouped by category
    Items filtered by user permissions
    Settings cards show icon, label, description, configure link
    Form page has breadcrumb, header, form container, save/reset buttons
  </done>
</task>

<task type="auto">
  <name>Task 2: Settings Controller</name>
  <files>
    summercms/admin/src/settings/SettingsController.scala
  </files>
  <action>
Create the controller for settings CRUD operations:

**SettingsController.scala** - HTTP routes for settings:

```scala
package summercms.admin.settings

import zio._
import zio.http._
import io.circe._
import io.circe.syntax._
import scalatags.Text.all._

class SettingsController(
  settingsManager: SettingsManager,
  settingsModelService: SettingsModelService,
  formRenderer: FormRenderer  // From Phase 7
):
  def routes: Routes[Any, Nothing] =
    Routes(
      // Settings index
      Method.GET / "admin" / "system" / "settings" -> handler { (req: Request) =>
        renderIndex(req)
      },

      // Settings update page (GET)
      Method.GET / "admin" / "system" / "settings" / "update" / string("author") / string("plugin") / string("code") ->
        handler { (author: String, plugin: String, code: String, req: Request) =>
          showUpdateForm(author, plugin, code, req)
        },

      // Settings save (POST)
      Method.POST / "admin" / "system" / "settings" / "update" / string("author") / string("plugin") / string("code") ->
        handler { (author: String, plugin: String, code: String, req: Request) =>
          saveSettings(author, plugin, code, req)
        },

      // Settings reset (POST)
      Method.POST / "admin" / "system" / "settings" / "update" / string("author") / string("plugin") / string("code") / "reset" ->
        handler { (author: String, plugin: String, code: String, req: Request) =>
          resetSettings(author, plugin, code, req)
        }
    )

  private def renderIndex(req: Request): ZIO[Any, Nothing, Response] =
    (for
      userPerms <- extractUserPermissions(req)
      items <- settingsManager.listItems
      // Filter by permissions
      filteredItems = items.view.mapValues { categoryItems =>
        categoryItems.filter { item =>
          item.permissions.isEmpty || item.permissions.exists(userPerms.contains)
        }
      }.filterNot(_._2.isEmpty).toMap
      html = SettingsIndex.renderSettingsIndex(filteredItems, userPerms)
    yield Response.html(wrapInLayout(html, "Settings"))).catchAll { e =>
      ZIO.succeed(Response.text(s"Error: ${e.getMessage}").status(Status.InternalServerError))
    }

  private def showUpdateForm(
    author: String,
    plugin: String,
    code: String,
    req: Request
  ): ZIO[Any, Nothing, Response] =
    val owner = s"$author.$plugin"
    (for
      userPerms <- extractUserPermissions(req)
      item <- settingsManager.findItem(owner, code)
                .someOrFail(new RuntimeException(s"Settings not found: $owner.$code"))

      // Check permissions
      _ <- ZIO.when(item.permissions.nonEmpty && !item.permissions.exists(userPerms.contains)) {
        ZIO.fail(new RuntimeException("Access denied"))
      }

      // Set context for settings manager
      _ <- settingsManager.setContext(owner, code)

      // Load settings model
      model <- loadSettingsModel(item)

      // Get current values
      values <- settingsModelService.get(model)

      // Parse YAML fields definition and render form
      fieldsYaml <- loadFieldsYaml(model.settingsFields)
      formHtml <- formRenderer.render(fieldsYaml, jsonMapToFormValues(values))

      html = SettingsIndex.renderSettingsForm(item, formHtml)
    yield Response.html(wrapInLayout(html, item.label))).catchAll { e =>
      ZIO.succeed(Response.text(s"Error: ${e.getMessage}").status(Status.InternalServerError))
    }

  private def saveSettings(
    author: String,
    plugin: String,
    code: String,
    req: Request
  ): ZIO[Any, Nothing, Response] =
    val owner = s"$author.$plugin"
    (for
      userPerms <- extractUserPermissions(req)
      item <- settingsManager.findItem(owner, code)
                .someOrFail(new RuntimeException(s"Settings not found: $owner.$code"))

      // Check permissions
      _ <- ZIO.when(item.permissions.nonEmpty && !item.permissions.exists(userPerms.contains)) {
        ZIO.fail(new RuntimeException("Access denied"))
      }

      // Load model
      model <- loadSettingsModel(item)

      // Parse form data
      body <- req.body.asString
      formData = parseFormData(body)

      // Convert to JSON values
      jsonValues = formData.view.mapValues(stringToJson).toMap

      // Save settings
      _ <- settingsModelService.set(model, jsonValues)

      // Re-render form with success message
      fieldsYaml <- loadFieldsYaml(model.settingsFields)
      currentValues <- settingsModelService.get(model)
      formHtml <- formRenderer.render(fieldsYaml, jsonMapToFormValues(currentValues))

      // Return updated form with success message (HTMX swap)
      html = SettingsIndex.renderSaveSuccess(item, formHtml)
    yield Response.html(html.render)).catchAll { e =>
      ZIO.succeed(
        Response.html(
          div(cls := "alert alert-danger", s"Error saving settings: ${e.getMessage}").render
        ).status(Status.BadRequest)
      )
    }

  private def resetSettings(
    author: String,
    plugin: String,
    code: String,
    req: Request
  ): ZIO[Any, Nothing, Response] =
    val owner = s"$author.$plugin"
    (for
      item <- settingsManager.findItem(owner, code)
                .someOrFail(new RuntimeException(s"Settings not found: $owner.$code"))
      model <- loadSettingsModel(item)
      _ <- settingsModelService.resetDefault(model)
    yield Response.redirect(URL.decode(item.url.getOrElse("/admin/system/settings")).toOption.get)
    ).catchAll { e =>
      ZIO.succeed(Response.text(s"Error: ${e.getMessage}").status(Status.InternalServerError))
    }

  // Helper methods

  private def extractUserPermissions(req: Request): Task[Set[String]] =
    // TODO: Extract from session/JWT via Phase 6 auth
    ZIO.succeed(Set("system.access_settings", "system.manage_settings"))

  private def loadSettingsModel(item: SettingsItem): Task[SettingsModel] =
    item.settingsClass match
      case Some(className) =>
        ZIO.attempt {
          val clazz = Class.forName(className)
          clazz.getDeclaredConstructor().newInstance().asInstanceOf[SettingsModel]
        }.mapError(e => new RuntimeException(s"Failed to load settings model: $className", e))
      case None =>
        // Create anonymous settings model from item
        ZIO.succeed(new SettingsModel {
          val settingsCode = s"${item.owner}_${item.code}".replace(".", "_").toLowerCase
          val settingsFields = s"$$/plugins/${item.owner.replace(".", "/")}/${item.code}/fields.yaml"
        })

  private def loadFieldsYaml(path: String): Task[FormDefinition] =
    // TODO: Use actual YAML loading from Phase 7
    // For now, return minimal form
    ZIO.succeed(FormDefinition(
      fields = Map(
        "placeholder" -> FormField(
          label = Some("Settings"),
          `type` = Some("text"),
          comment = Some(s"Fields loaded from: $path")
        )
      )
    ))

  private def parseFormData(body: String): Map[String, String] =
    body.split("&").flatMap { pair =>
      pair.split("=", 2) match
        case Array(k, v) =>
          Some(java.net.URLDecoder.decode(k, "UTF-8") -> java.net.URLDecoder.decode(v, "UTF-8"))
        case _ => None
    }.toMap

  private def stringToJson(s: String): Json =
    // Try to parse as number or boolean, fall back to string
    s.toBooleanOption.map(Json.fromBoolean)
      .orElse(s.toIntOption.map(Json.fromInt))
      .orElse(s.toDoubleOption.map(Json.fromDouble).flatten)
      .getOrElse(Json.fromString(s))

  private def jsonMapToFormValues(m: Map[String, Json]): Map[String, Any] =
    m.view.mapValues { json =>
      json.fold(
        jsonNull = "",
        jsonBoolean = _.toString,
        jsonNumber = _.toString,
        jsonString = identity,
        jsonArray = _.map(_.noSpaces).mkString(","),
        jsonObject = _.noSpaces
      )
    }.toMap

  private def wrapInLayout(content: Frag, title: String): String =
    // TODO: Use admin layout from Phase 7
    s"""<!DOCTYPE html>
    <html>
    <head>
      <title>$title - SummerCMS</title>
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
      <div class="container-fluid py-4">
        ${content.render}
      </div>
      <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
      <script src="https://unpkg.com/htmx.org@2.0.0"></script>
    </body>
    </html>"""

// Temporary placeholder for Phase 7 form types
case class FormDefinition(fields: Map[String, FormField])
case class FormField(
  label: Option[String] = None,
  `type`: Option[String] = Some("text"),
  comment: Option[String] = None
)

// Temporary placeholder for Phase 7 form renderer
trait FormRenderer:
  def render(definition: FormDefinition, values: Map[String, Any]): Task[Frag]
```

Note: This controller has placeholder code for Phase 7 form rendering (`FormRenderer`, `FormDefinition`). The actual integration will use the real types from Phase 7 when both phases are complete.

**Integration points to wire up:**
1. Replace placeholder `FormRenderer` with actual Phase 7 `FormRenderer`
2. Replace placeholder `FormDefinition`/`FormField` with Phase 7 YAML types
3. Replace `loadFieldsYaml` with actual YAML parsing from Phase 7
4. Replace `extractUserPermissions` with Phase 6 auth integration
  </action>
  <verify>
    mill summercms.admin.compile succeeds
    SettingsController routes compile
    GET /admin/system/settings returns index page
    GET /admin/system/settings/update/author/plugin/code returns form
  </verify>
  <done>
    SettingsController provides index, update (GET/POST), reset routes
    Index filters items by user permissions
    Form page loads settings model and renders form
    Save parses form data and persists to system_settings
    Reset deletes settings and redirects
    Placeholder form types ready for Phase 7 integration
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <what-built>
    Settings management system with:
    - Settings index page with tabbed categories
    - Plugin settings registration and lookup
    - Settings form page using YAML-driven forms (placeholder for Phase 7)
    - Settings persistence to system_settings table
    - Reset to defaults functionality
  </what-built>
  <how-to-verify>
    1. Start the application: `mill summercms.run`
    2. Navigate to http://localhost:8080/admin/system/settings
    3. Verify: Settings index page shows with category tabs
    4. Register a test settings item (manually add to SettingsManager in code)
    5. Verify: Test settings item appears in appropriate category
    6. Click "Configure" on settings item
    7. Verify: Settings form page loads
    8. Save the form
    9. Verify: Success message displays, values persisted
    10. Click "Reset to Default"
    11. Verify: Settings reset, redirects to index
  </how-to-verify>
  <resume-signal>Type "approved" or describe issues found</resume-signal>
</task>

</tasks>

<verification>
1. `mill summercms.admin.compile` succeeds
2. Settings index page renders with category tabs
3. Settings form page loads for registered items
4. Form submission saves to system_settings table
5. Reset clears settings and returns to index
6. Permission filtering works on index page
</verification>

<success_criteria>
- Settings index displays all registered settings grouped by category
- Category tabs sorted by defined order
- Settings filtered by user permissions
- Form page renders with breadcrumb and save/reset actions
- Form submission persists values via SettingsModelService
- Reset clears settings from database
- Ready for integration with Phase 7 form system
</success_criteria>

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