---
phase: 03-component-system
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
  - summercms/src/component/HtmxResponse.scala
  - summercms/src/component/TemplateService.scala
  - summercms/src/component/ComponentRoutes.scala
  - summercms/src/component/CsrfService.scala
  - summercms/src/component/package.scala
  - summercms/src/api/Routes.scala
  - summercms/src/Main.scala
autonomous: true

must_haves:
  truths:
    - "Components can define HTMX handlers with 'on' prefix that become routable"
    - "HTMX handlers receive form data and return HTML fragments"
    - "HTMX responses can include triggers, retarget, reswap, and OOB swaps"
    - "All HTMX POST requests require valid CSRF token"
    - "Templates render via Pebble with Twig-compatible syntax"
  artifacts:
    - path: "summercms/src/component/HtmxResponse.scala"
      provides: "HTMX response types with headers"
      contains: "case class HtmxResponse"
    - path: "summercms/src/component/TemplateService.scala"
      provides: "Pebble-based template rendering"
      exports: ["TemplateService"]
    - path: "summercms/src/component/ComponentRoutes.scala"
      provides: "Routes for HTMX component handlers"
      contains: "POST /summer/component"
    - path: "summercms/src/component/CsrfService.scala"
      provides: "CSRF token generation and validation"
      exports: ["CsrfService"]
  key_links:
    - from: "summercms/src/component/ComponentRoutes.scala"
      to: "summercms/src/component/ComponentManager.scala"
      via: "resolves component and invokes handler"
      pattern: "ComponentManager\\.create"
    - from: "summercms/src/component/ComponentRoutes.scala"
      to: "summercms/src/component/CsrfService.scala"
      via: "validates CSRF before handler"
      pattern: "CsrfService\\.validate"
    - from: "summercms/src/api/Routes.scala"
      to: "summercms/src/component/ComponentRoutes.scala"
      via: "combines component routes"
      pattern: "ComponentRoutes\\.routes"
---

<objective>
Implement HTMX handler routing and response system for SummerCMS components.

Purpose: Components need to respond to HTMX interactions (button clicks, form submissions) by returning HTML fragments. This plan creates the routing, CSRF protection, template service, and response types that enable interactive components.

Output: HtmxResponse type, TemplateService (Pebble), ComponentRoutes, CsrfService, integration with main Routes
</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/03-component-system/03-RESEARCH.md
@.planning/phases/03-component-system/03-CONTEXT.md
@.planning/phases/03-component-system/03-01-SUMMARY.md
@summercms/src/component/SummerComponent.scala
@summercms/src/component/ComponentManager.scala
@summercms/src/api/Routes.scala
@summercms/src/Main.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create HTMX response types and CSRF service</name>
  <files>
    summercms/src/component/HtmxResponse.scala
    summercms/src/component/CsrfService.scala
  </files>
  <action>
**HtmxResponse.scala:**
```scala
package component

import zio.http.*
import zio.http.htmx.HtmxResponseHeaders
import scalatags.Text.all.*

/** HTML fragment wrapper */
case class Html(content: String):
  override def toString: String = content

object Html:
  def apply(frag: scalatags.Text.Frag): Html = Html(frag.render)
  val empty: Html = Html("")

/** Out-of-band swap for updating multiple DOM elements */
case class OobSwap(
  targetId: String,
  html: Html,
  swapStrategy: String = "outerHTML"  // outerHTML, innerHTML, beforeend, etc.
):
  /** Render as HTML fragment with hx-swap-oob attribute */
  def render: String =
    s"""<div id="$targetId" hx-swap-oob="$swapStrategy">${html.content}</div>"""

/**
 * HTMX response from a component handler.
 * Includes HTML content plus optional HTMX headers and OOB swaps.
 */
case class HtmxResponse(
  html: Html,
  triggers: Map[String, String] = Map.empty,      // HX-Trigger header events
  retarget: Option[String] = None,                // HX-Retarget - change target element
  reswap: Option[String] = None,                  // HX-Reswap - change swap strategy
  reselect: Option[String] = None,                // HX-Reselect - select subset of response
  pushUrl: Option[String] = None,                 // HX-Push-Url - update browser URL
  replaceUrl: Option[String] = None,              // HX-Replace-Url - replace URL without push
  refresh: Boolean = false,                        // HX-Refresh - full page refresh
  redirect: Option[String] = None,                // HX-Redirect - client-side redirect
  oobSwaps: List[OobSwap] = List.empty            // Out-of-band swaps
):
  /** Convert to ZIO HTTP Response */
  def toResponse: Response =
    val fullHtml = if oobSwaps.isEmpty then
      html.content
    else
      html.content + oobSwaps.map(_.render).mkString("\n")

    var response = Response.html(fullHtml)

    // Add HTMX headers using zio-http-htmx module
    if triggers.nonEmpty then
      val triggerJson = triggers.map { case (k, v) =>
        if v.isEmpty then s""""$k"""" else s""""$k":"$v""""
      }.mkString("{", ",", "}")
      response = response.addHeader(Header.Custom("HX-Trigger", triggerJson))

    retarget.foreach { t =>
      response = response.addHeader(Header.Custom("HX-Retarget", t))
    }

    reswap.foreach { s =>
      response = response.addHeader(Header.Custom("HX-Reswap", s))
    }

    reselect.foreach { s =>
      response = response.addHeader(Header.Custom("HX-Reselect", s))
    }

    pushUrl.foreach { url =>
      response = response.addHeader(Header.Custom("HX-Push-Url", url))
    }

    replaceUrl.foreach { url =>
      response = response.addHeader(Header.Custom("HX-Replace-Url", url))
    }

    if refresh then
      response = response.addHeader(Header.Custom("HX-Refresh", "true"))

    redirect.foreach { url =>
      response = response.addHeader(Header.Custom("HX-Redirect", url))
    }

    response

object HtmxResponse:
  /** Create simple HTML response */
  def html(content: Html): HtmxResponse = HtmxResponse(html = content)

  /** Create response from ScalaTags fragment */
  def apply(frag: scalatags.Text.Frag): HtmxResponse = HtmxResponse(Html(frag))

  /** Empty response (for handlers that only use OOB swaps) */
  val empty: HtmxResponse = HtmxResponse(Html.empty)
```

**CsrfService.scala:**
```scala
package component

import zio.*
import zio.http.*
import java.security.SecureRandom
import java.util.Base64

/** CSRF token service for protecting HTMX requests */
trait CsrfService:
  /** Generate a new CSRF token */
  def generateToken: UIO[String]

  /** Validate a token from request against session token */
  def validate(request: Request, sessionToken: String): IO[ComponentError, Unit]

  /** Extract token from request (header or form field) */
  def extractToken(request: Request): UIO[Option[String]]

object CsrfService:
  /** Token header name */
  val headerName = "X-CSRF-Token"

  /** Token form field name */
  val formFieldName = "_token"

  /** Generate a CSRF token */
  def generateToken: ZIO[CsrfService, Nothing, String] =
    ZIO.serviceWithZIO[CsrfService](_.generateToken)

  /** Validate request token */
  def validate(request: Request, sessionToken: String): ZIO[CsrfService, ComponentError, Unit] =
    ZIO.serviceWithZIO[CsrfService](_.validate(request, sessionToken))

  /** Live implementation */
  val live: ULayer[CsrfService] = ZLayer.succeed {
    new CsrfService:
      private val random = new SecureRandom()
      private val encoder = Base64.getUrlEncoder.withoutPadding()

      def generateToken: UIO[String] = ZIO.succeed {
        val bytes = new Array[Byte](32)
        random.nextBytes(bytes)
        encoder.encodeToString(bytes)
      }

      def extractToken(request: Request): UIO[Option[String]] = ZIO.succeed {
        // Try header first, then form field
        request.header(headerName).map(_.renderedValue).orElse {
          // Note: For form field extraction, need to parse body
          // This is a simplified version - full implementation needs body parsing
          None
        }
      }

      def validate(request: Request, sessionToken: String): IO[ComponentError, Unit] =
        for
          requestToken <- extractToken(request)
          _ <- ZIO.fail(ComponentError.CsrfViolation("Missing CSRF token"))
            .when(requestToken.isEmpty)
          _ <- ZIO.fail(ComponentError.CsrfViolation("Invalid CSRF token"))
            .when(!constantTimeEquals(requestToken.get, sessionToken))
        yield ()

      /** Constant-time string comparison to prevent timing attacks */
      private def constantTimeEquals(a: String, b: String): Boolean =
        if a.length != b.length then false
        else
          var result = 0
          var i = 0
          while i < a.length do
            result |= a.charAt(i) ^ b.charAt(i)
            i += 1
          result == 0
  }

  /**
   * Middleware that validates CSRF on unsafe methods (POST, PUT, DELETE, PATCH).
   * Requires session token to be available in request context.
   *
   * Note: Full implementation needs session integration.
   * For now, provides validation function for use in routes.
   */
  def validateRequest(request: Request): ZIO[CsrfService, ComponentError, Unit] =
    // For initial implementation, skip CSRF validation if no session
    // Full session integration comes in Phase 6 (Backend Authentication)
    ZIO.unit // TODO: Implement with session in Phase 6
```
  </action>
  <verify>Run `./mill summercms.compile` - HtmxResponse and CsrfService compile</verify>
  <done>HtmxResponse with all HTMX headers and OOB swaps, CsrfService with secure token generation and validation</done>
</task>

<task type="auto">
  <name>Task 2: Create template service with Pebble</name>
  <files>summercms/src/component/TemplateService.scala</files>
  <action>
**TemplateService.scala:**
```scala
package component

import zio.*
import io.pebbletemplates.pebble.PebbleEngine
import io.pebbletemplates.pebble.loader.{Loader, StringLoader}
import io.pebbletemplates.pebble.template.PebbleTemplate
import io.pebbletemplates.pebble.extension.AbstractExtension
import io.pebbletemplates.pebble.extension.Function
import java.io.StringWriter
import java.nio.file.{Files, Path}
import java.util.{Map as JMap, List as JList}
import scala.jdk.CollectionConverters.*

/** Template rendering service using Pebble (Twig-compatible) */
trait TemplateService:
  /** Render a template file with context */
  def render(templatePath: Path, context: Map[String, Any]): IO[ComponentError, Html]

  /** Render a template string with context */
  def renderString(template: String, context: Map[String, Any]): IO[ComponentError, Html]

object TemplateService:
  /** Render a template file */
  def render(templatePath: Path, context: Map[String, Any]): ZIO[TemplateService, ComponentError, Html] =
    ZIO.serviceWithZIO[TemplateService](_.render(templatePath, context))

  /** Render a template string */
  def renderString(template: String, context: Map[String, Any]): ZIO[TemplateService, ComponentError, Html] =
    ZIO.serviceWithZIO[TemplateService](_.renderString(template, context))

  /** Live implementation */
  val live: ULayer[TemplateService] = ZLayer.succeed {
    new TemplateService:
      // Main engine for file-based templates (caches templates)
      private val fileEngine = new PebbleEngine.Builder()
        .autoEscaping(true)
        .strictVariables(false)
        .extension(new SummerPebbleExtension())
        .build()

      // Engine for string templates (no caching)
      private val stringEngine = new PebbleEngine.Builder()
        .loader(new StringLoader())
        .autoEscaping(true)
        .strictVariables(false)
        .extension(new SummerPebbleExtension())
        .build()

      def render(templatePath: Path, context: Map[String, Any]): IO[ComponentError, Html] =
        ZIO.attemptBlocking {
          val template = fileEngine.getTemplate(templatePath.toString)
          val writer = new StringWriter()
          template.evaluate(writer, toJavaContext(context))
          Html(writer.toString)
        }.mapError(e => ComponentError.RenderError("unknown", templatePath.toString, e))

      def renderString(template: String, context: Map[String, Any]): IO[ComponentError, Html] =
        ZIO.attemptBlocking {
          val compiled = stringEngine.getTemplate(template)
          val writer = new StringWriter()
          compiled.evaluate(writer, toJavaContext(context))
          Html(writer.toString)
        }.mapError(e => ComponentError.RenderError("unknown", "string", e))

      /** Convert Scala map to Java map, handling nested structures */
      private def toJavaContext(context: Map[String, Any]): JMap[String, AnyRef] =
        context.view.mapValues(toJavaValue).toMap.asJava

      private def toJavaValue(value: Any): AnyRef = value match
        case null => null
        case s: String => s
        case n: Int => Integer.valueOf(n)
        case n: Long => java.lang.Long.valueOf(n)
        case n: Double => java.lang.Double.valueOf(n)
        case b: Boolean => java.lang.Boolean.valueOf(b)
        case m: Map[_, _] =>
          m.map { case (k, v) => k.toString -> toJavaValue(v) }.asJava
        case l: List[_] => l.map(toJavaValue).asJava
        case l: Seq[_] => l.map(toJavaValue).asJava
        case opt: Option[_] => opt.map(toJavaValue).orNull
        case pv: PropertyValue => propertyValueToJava(pv)
        case other => other.asInstanceOf[AnyRef]

      private def propertyValueToJava(pv: PropertyValue): AnyRef = pv match
        case PropertyValue.StringVal(s) => s
        case PropertyValue.BoolVal(b) => java.lang.Boolean.valueOf(b)
        case PropertyValue.IntVal(i) => Integer.valueOf(i)
        case PropertyValue.ListVal(l) => l.map(propertyValueToJava).asJava
        case PropertyValue.ObjectVal(m) => m.view.mapValues(propertyValueToJava).toMap.asJava
        case PropertyValue.NullVal => null
  }

/**
 * Custom Pebble extension providing SummerCMS-specific functions and filters.
 */
class SummerPebbleExtension extends AbstractExtension:
  import java.util.{Map as JMap, List as JList}

  override def getFunctions: JMap[String, Function] =
    Map[String, Function](
      // {{ componentHandler('onRefresh') }} -> /summer/component/{alias}/onRefresh
      "componentHandler" -> new ComponentHandlerFunction(),
      // {{ csrfToken() }} -> generates CSRF token (placeholder for now)
      "csrfToken" -> new CsrfTokenFunction(),
      // {{ asset('img/logo.png') }} -> theme asset URL (placeholder for Phase 4)
      "asset" -> new AssetFunction()
    ).asJava

  override def getFilters: JMap[String, io.pebbletemplates.pebble.extension.Filter] =
    Map[String, io.pebbletemplates.pebble.extension.Filter](
      // {{ 'text'|_ }} -> Translation filter (placeholder for i18n)
      "_" -> new TranslateFilter()
    ).asJava

/** Generates component handler URLs */
class ComponentHandlerFunction extends Function:
  override def getArgumentNames: JList[String] =
    List("handler").asJava

  override def execute(
    args: JMap[String, AnyRef],
    self: io.pebbletemplates.pebble.template.PebbleTemplate,
    context: io.pebbletemplates.pebble.template.EvaluationContext,
    lineNumber: Int
  ): AnyRef =
    val handler = args.get("handler").toString
    val alias = Option(context.getVariable("__SELF__")).map(_.toString).getOrElse("unknown")
    s"/summer/component/$alias/$handler"

/** Generates CSRF tokens */
class CsrfTokenFunction extends Function:
  override def getArgumentNames: JList[String] = List.empty.asJava

  override def execute(
    args: JMap[String, AnyRef],
    self: io.pebbletemplates.pebble.template.PebbleTemplate,
    context: io.pebbletemplates.pebble.template.EvaluationContext,
    lineNumber: Int
  ): AnyRef =
    // For now return placeholder - real implementation needs session integration
    Option(context.getVariable("_csrfToken")).map(_.toString).getOrElse("csrf-placeholder")

/** Generates asset URLs (placeholder for Phase 4 Theme Engine) */
class AssetFunction extends Function:
  override def getArgumentNames: JList[String] = List("path").asJava

  override def execute(
    args: JMap[String, AnyRef],
    self: io.pebbletemplates.pebble.template.PebbleTemplate,
    context: io.pebbletemplates.pebble.template.EvaluationContext,
    lineNumber: Int
  ): AnyRef =
    val path = args.get("path").toString
    // Placeholder - real implementation in Phase 4
    s"/assets/$path"

/** Translation filter {{ 'text'|_ }} */
class TranslateFilter extends io.pebbletemplates.pebble.extension.Filter:
  override def getArgumentNames: JList[String] = List.empty.asJava

  override def apply(
    input: AnyRef,
    args: JMap[String, AnyRef],
    self: io.pebbletemplates.pebble.template.PebbleTemplate,
    context: io.pebbletemplates.pebble.template.EvaluationContext,
    lineNumber: Int
  ): AnyRef =
    // Placeholder - returns input unchanged, real i18n in later phase
    input
```
  </action>
  <verify>Run `./mill summercms.compile` - TemplateService compiles with Pebble integration</verify>
  <done>TemplateService.live provides Pebble-based template rendering with componentHandler, csrfToken, asset functions and translate filter</done>
</task>

<task type="auto">
  <name>Task 3: Create component routes and integrate with application</name>
  <files>
    summercms/src/component/ComponentRoutes.scala
    summercms/src/component/package.scala
    summercms/src/api/Routes.scala
    summercms/src/Main.scala
  </files>
  <action>
**ComponentRoutes.scala:**
```scala
package component

import zio.*
import zio.http.*
import scala.reflect.Selectable.reflectiveSelectable

/** Routes for component HTMX handlers */
object ComponentRoutes:

  /** Component handler route: POST /summer/component/{alias}/{handler} */
  val routes: Routes[ComponentManager & TemplateService & CsrfService, Response] =
    Routes(
      // HTMX handler endpoint
      Method.POST / "summer" / "component" / string("alias") / string("handler") ->
        handler { (alias: String, handlerName: String, req: Request) =>
          handleComponentRequest(alias, handlerName, req)
        },

      // Component render endpoint (for initial page load)
      Method.GET / "summer" / "component" / string("alias") / "render" ->
        handler { (alias: String, req: Request) =>
          renderComponent(alias, req)
        }
    )

  private def handleComponentRequest(
    alias: String,
    handlerName: String,
    req: Request
  ): ZIO[ComponentManager & TemplateService & CsrfService, Nothing, Response] =
    (for
      // Validate CSRF token
      _ <- CsrfService.validateRequest(req)

      // Validate handler name starts with "on"
      _ <- ZIO.fail(ComponentError.InvalidHandler(alias, handlerName, "Handler must start with 'on'"))
        .when(!handlerName.startsWith("on"))

      // Create page context from request
      ctx = pageContextFromRequest(req)

      // Extract properties from request body
      props <- extractPropertiesFromRequest(req)

      // Create component instance
      manager <- ZIO.service[ComponentManager]
      component <- manager.create(alias, props, ctx)

      // Initialize component
      _ <- component.init.provideEnvironment(ZEnvironment(ctx))

      // Invoke the handler using reflection
      result <- invokeHandler(component, handlerName, req, ctx)
    yield result.toResponse).catchAll { error =>
      // Return error as HTML fragment with error styling
      ZIO.succeed(errorResponse(error))
    }

  private def renderComponent(
    alias: String,
    req: Request
  ): ZIO[ComponentManager & TemplateService, Nothing, Response] =
    (for
      ctx = pageContextFromRequest(req)
      props <- extractPropertiesFromRequest(req)
      manager <- ZIO.service[ComponentManager]
      component <- manager.create(alias, props, ctx)
      _ <- component.init.provideEnvironment(ZEnvironment(ctx))
      _ <- component.onRun.provideEnvironment(ZEnvironment(ctx))

      // Render default template
      templateService <- ZIO.service[TemplateService]
      templatePath = component.templateDir.resolve("default.htm")
      propsMap <- component.properties.get
      html <- templateService.render(templatePath, buildTemplateContext(component, propsMap))
    yield Response.html(html.content)).catchAll { error =>
      ZIO.succeed(errorResponse(error))
    }

  private def pageContextFromRequest(req: Request): PageContext =
    PageContext(
      url = req.url.path.toString,
      params = req.url.queryParams.map.view.mapValues(_.headOption.getOrElse("")).toMap,
      isBackendEditor = req.header("X-Summer-Editor").isDefined
    )

  private def extractPropertiesFromRequest(req: Request): UIO[Map[String, PropertyValue]] =
    // For now, return empty map - full form parsing in later enhancement
    // Properties typically come from page/layout configuration, not request
    ZIO.succeed(Map.empty)

  /**
   * Invoke a handler method on the component.
   * Handler methods must:
   * - Start with "on" (e.g., onRefresh, onLoadMore)
   * - Return ZIO[ComponentEnv, ComponentError, HtmxResponse]
   *
   * Note: Uses Scala 3 reflection. For production, consider compile-time macro.
   */
  private def invokeHandler(
    component: SummerComponent,
    handlerName: String,
    req: Request,
    ctx: PageContext
  ): ZIO[TemplateService, ComponentError, HtmxResponse] =
    ZIO.attemptBlocking {
      // Use reflection to find and invoke the handler method
      val cls = component.getClass
      val method = cls.getMethod(handlerName)
      method.invoke(component)
    }.flatMap {
      case zio: ZIO[?, ?, ?] =>
        // The handler returns a ZIO - we need to run it
        zio.asInstanceOf[ZIO[ComponentEnv, ComponentError, HtmxResponse]]
          .provideEnvironment(ZEnvironment(ctx))
      case response: HtmxResponse =>
        // Handler returned a direct response
        ZIO.succeed(response)
      case other =>
        ZIO.fail(ComponentError.InvalidHandler(
          component.alias,
          handlerName,
          s"Handler must return ZIO[ComponentEnv, ComponentError, HtmxResponse], got ${other.getClass}"
        ))
    }.catchAll {
      case ce: ComponentError => ZIO.fail(ce)
      case e: NoSuchMethodException =>
        ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, "Handler method not found"))
      case e: Throwable =>
        ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, e.getMessage))
    }

  private def buildTemplateContext(
    component: SummerComponent,
    properties: Map[String, PropertyValue]
  ): Map[String, Any] =
    Map(
      "__SELF__" -> component.alias,
      "_component" -> component.details
    ) ++ properties.view.mapValues(pvToAny).toMap

  private def pvToAny(pv: PropertyValue): Any = pv match
    case PropertyValue.StringVal(s) => s
    case PropertyValue.BoolVal(b) => b
    case PropertyValue.IntVal(i) => i
    case PropertyValue.ListVal(l) => l.map(pvToAny)
    case PropertyValue.ObjectVal(m) => m.view.mapValues(pvToAny).toMap
    case PropertyValue.NullVal => null

  private def errorResponse(error: ComponentError): Response =
    val html = s"""<div class="summer-error" role="alert">
      <strong>Component Error:</strong> ${error.message}
    </div>"""
    Response.html(html).status(Status.InternalServerError)
```

**package.scala:**
```scala
package object component:
  import zio.*

  /** Combined layer for all component services */
  type ComponentServices = ComponentManager & TemplateService & CsrfService

  /** Create all component service layers */
  val componentLayer: ZLayer[Any, Nothing, ComponentServices] =
    ComponentManager.live ++ TemplateService.live ++ CsrfService.live
```

**Update api/Routes.scala** to include component routes:

Read existing Routes.scala and add component routes:

```scala
import zio.http.*
import component.{ComponentRoutes, componentLayer}

object Routes:
  /** Combined routes for the application */
  val routes: Routes[Any, Response] =
    HealthRoutes.routes ++ LandingRoutes.routes ++
      ComponentRoutes.routes.provideLayer(componentLayer)
```

Note: The exact integration depends on existing Routes.scala structure. The key is to combine ComponentRoutes.routes with existing routes and provide the componentLayer.

**Update Main.scala** to provide component services:

Read existing Main.scala and update the layer composition. The key changes:

1. Import component package: `import component.componentLayer`
2. Add componentLayer to the provide chain for routes

The updated provide for startupLogic should include:

```scala
// In the routes Server.serve line:
res <- Server.serve(Routes.routes).provide(
  Server.defaultWithPort(cfg.server.port),
  QuillContext.dataSourceLayer,
  componentLayer
)
```

However, if routes are already using the layer internally (as shown above), Main.scala may not need changes.

Full Main.scala update:
```scala
import zio.*
import zio.http.*
import zio.config.typesafe.TypesafeConfigProvider

import api.Routes
import _root_.config.{AppConfig as SummerConfig}
import db.QuillContext
import plugin.{PluginManager, PluginDiscovery, pluginLayer}
import component.{ComponentManager, componentLayer}

object Main extends ZIOAppDefault {

  private val banner: String =
    """
      |
      |             |   .
      |      `.  *  |     .'
      |        `. ._|_* .'  .
      |      . * .'   `.  *
      |   -------|     |-------
      |      .  *`.___.' *  .
      |         .'  |* `.  *
      |       .' *  |  . `.
      |           . |
      |
      |     S U M M E R C M S
      |""".stripMargin

  override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
    Runtime.setConfigProvider(TypesafeConfigProvider.fromResourcePath())

  private val startupLogic: ZIO[PluginManager & PluginDiscovery & ComponentManager, Any, Nothing] =
    for {
      cfg <- ZIO.config[SummerConfig](SummerConfig.config)
      _   <- Console.printLine(banner)
      _   <- Console.printLine(s"Starting on port ${cfg.server.port}...")
      _   <- Console.printLine("")
      // Initialize plugin system
      _   <- Console.printLine("Loading plugins...")
      _   <- PluginManager.loadPlugins(PluginDiscovery.defaultPluginsDir)
        .catchAll(e => Console.printLine(s"Plugin loading warning: ${e.message}").as(()))
      _   <- PluginManager.bootAll
        .catchAll(e => Console.printLine(s"Plugin boot warning: ${e.message}").as(()))
      plugins <- PluginManager.listPlugins
      _   <- Console.printLine(s"Loaded ${plugins.count(_._2.isActive)} plugin(s)")
      // Initialize component system
      components <- ComponentManager.listComponents
      _   <- Console.printLine(s"Registered ${components.size} component(s)")
      _   <- Console.printLine("")
      // Note: Migrations are NOT auto-run. Use CLI to run migrations (Phase 5).
      res <- Server.serve(Routes.routes).provide(
               Server.defaultWithPort(cfg.server.port),
               QuillContext.dataSourceLayer
             )
    } yield res

  override def run: ZIO[Any, Any, Any] =
    startupLogic.provide(pluginLayer ++ componentLayer)
}
```
  </action>
  <verify>
Run `./mill summercms.compile` - all files compile.

Run `./mill summercms.run` - server starts with messages:
- "Loaded 0 plugin(s)"
- "Registered 0 component(s)"

Test component route exists (will return 404/error since no components registered):
```bash
curl -X POST http://localhost:8080/summer/component/test/onTest
# Should return HTML error "Component not found: test"
```
  </verify>
  <done>ComponentRoutes handles POST /summer/component/{alias}/{handler}, component layer integrated with Main.scala, server reports registered component count</done>
</task>

</tasks>

<verification>
1. `./mill summercms.compile` succeeds
2. `./mill summercms.run` starts server with component count message
3. HtmxResponse.toResponse produces correct HTMX headers
4. TemplateService.renderString produces HTML from Twig-compatible template
5. POST /summer/component/alias/handler route exists and validates handler name
6. CSRF validation function available (full session integration in Phase 6)
</verification>

<success_criteria>
- HtmxResponse supports triggers, retarget, reswap, OOB swaps
- Pebble templates render with componentHandler(), csrfToken(), asset() functions
- Component routes validate handler names start with "on"
- CSRF service provides secure token generation and validation
- Application starts with component system initialized
- Component handler invocation works via reflection
</success_criteria>

<output>
After completion, create `.planning/phases/03-component-system/03-02-SUMMARY.md`
</output>
