# Deploy a Caddy static site with HostKit

Start with the small set of notebook dependencies. `kino` gives us friendly inputs, `req` verifies the deployed site, and `host_kit` provides the deployment DSL and runtime APIs.

```elixir
Mix.install([
  {:kino, "~> 0.14"},
  {:req, "~> 0.5"},
  {:host_kit, github: "elixir-vibe/host_kit"}
])
```

Use short aliases in the rest of the notebook so the code reads like a walkthrough instead of a wall of module names.

```elixir
alias HostKit, as: HK
alias HostKit.{Apply, Host, Project}
alias HostKit.Apply.Event, as: ApplyEvent
alias HostKit.Plan.{Artifact, Format}
alias HostKit.Providers.Caddy
```

This notebook is a small, readable HostKit demo:

1. collect target and site settings,
2. declare a target and a static site,
3. inspect the plan,
4. optionally apply it,
5. verify the site.

The deployment declaration lives directly in the notebook. The integration test extracts the marked DSL cell below.

## Demo settings

First, expose the values someone actually cares about. Advanced SSH values still come from ordinary environment variables so the demo form stays approachable.

```elixir
settings_form =
  Kino.Control.form(
    [
      server: Kino.Input.text("Server", default: System.get_env("HOSTKIT_TARGET_HOST", "127.0.0.1")),
      user: Kino.Input.text("SSH user", default: System.get_env("HOSTKIT_TARGET_USER", "root")),
      public_port: Kino.Input.number("Public port", default: 18_080),
      message: Kino.Input.text("Message", default: "Deployed by HostKit"),
      apply?: Kino.Input.checkbox("Apply now", default: false),
      verify?: Kino.Input.checkbox("Verify after apply", default: false)
    ],
    submit: "Plan deployment"
  )

Kino.render(settings_form)
```

Read the submitted form. From here on, everything is plain Elixir data that the HostKit declaration can reference.

```elixir
settings = Kino.Control.await(settings_form)
```

Turn the UI fields into explicit target settings. This keeps credentials and transport details visible without mixing them into the deployment declaration.

```elixir
target = %{
  host: settings.server,
  user: settings.user,
  sudo: true,
  ssh: [
    port: String.to_integer(System.get_env("HOSTKIT_TARGET_PORT", "22")),
    identity_file: Path.expand(System.get_env("HOSTKIT_IDENTITY_FILE", "~/.ssh/id_ed25519")),
    silently_accept_hosts: true
  ]
}
```

Describe the tiny site we want to publish.

```elixir
site = %{
  address: ":#{settings.public_port}",
  message: settings.message
}

apply? = settings.apply?
verify? = settings.verify?
```

Derive stable names and paths once. The declaration below can now focus on resources instead of string building.

```elixir
deployment_name = "hostkit-caddy-demo"

target_host = target.host
target_user = target.user
target_sudo = target.sudo
ssh_opts = target.ssh

site_address = site.address
message = site.message
acme_email = "admin@example.com"
site_root = "/srv/#{deployment_name}"
caddy_config_path = "/etc/#{deployment_name}/Caddyfile"
caddy_config_dir = Path.dirname(caddy_config_path)
caddy_sites_dir = "/etc/#{deployment_name}/sites"
caddy_service_name = "#{deployment_name}.service"
artifact_path = "/tmp/#{deployment_name}.plan.json"
verify_url = "http://127.0.0.1#{site_address}"
public_url = "http://#{target_host}#{site_address}"
```

## Deployment declaration

This is the important part: ordinary HostKit DSL, not a generated script. The same declaration works in tests, scripts, Mix tasks, and this Livebook.

```elixir
# hostkit:deploy-caddy-site-dsl
html = """
<!doctype html>
<html>
  <head><meta charset=\"utf-8\"><title>Hello from HostKit</title></head>
  <body>
    <h1>Hello from HostKit</h1>
    <p>#{message}</p>
  </body>
</html>
"""

caddyfile = """
{
  admin off
  email #{acme_email}
}

import #{caddy_sites_dir}/*.caddy
"""

alias HostKit.Providers.Caddy

use HostKit.DSL, providers: [Caddy]

project =
  project :deploy_caddy_site do
    host :target do
      hostname target_host
      user target_user
      sudo target_sudo
      ssh ssh_opts
    end

    provider :caddy, Caddy do
      set :sites_dir, caddy_sites_dir
    end

    service :hello_site do
      package :caddy, as: "caddy"

      directory site_root, owner: "root", group: "root", mode: 0o755
      directory caddy_config_dir, owner: "root", group: "root", mode: 0o755
      directory caddy_sites_dir, owner: "root", group: "root", mode: 0o755

      file Path.join(site_root, "index.html"),
        content: html,
        owner: "root",
        group: "root",
        mode: 0o644

      file caddy_config_path,
        content: caddyfile,
        owner: "root",
        group: "root",
        mode: 0o644

      daemon caddy_service_name do
        description "HostKit demo Caddy site"
        exec_start ["/usr/bin/caddy", "run", "--config", caddy_config_path]
        restart :on_failure
        wanted_by :multi_user
      end

      caddy_site :hello, site_address do
        root site_root
        file_server()
      end

      ready :hello_site do
        systemd caddy_service_name, restart: true
        http verify_url
      end
    end
  end

project
```

## Plan

Build an inspectable plan for the selected host. Nothing has been changed on the target yet.

```elixir
host = hd(project.hosts)
target_opts = Host.target_opts(host)
{:ok, plan} = HK.plan(project, target_opts)
```

Render the plan as a human-readable checklist.

```elixir
plan |> Format.format() |> Kino.Markdown.new()
```

## Inspect generated resources

The DSL compiles to plain structs. You can inspect the resources before applying them.

```elixir
Project.resources(project)
```

## Save an artifact

Plans can be saved as artifacts for review, CI, or later rollback workflows.

```elixir
:ok = Artifact.save(artifact_path, plan)
artifact_path
```

## Apply

Applying is explicit. Set `Apply now` in the settings form when you are ready.

```elixir
reporter = self()

apply_result =
  if apply? do
    HK.apply(plan, Keyword.merge(target_opts, confirm: true, reporter: reporter))
  else
    {:skipped, :set_apply_to_true}
  end
```

HostKit sends progress events to the notebook process, so the UI does not need callback plumbing.

```elixir
progress =
  Stream.repeatedly(fn ->
    receive do
      {Apply, event} -> ApplyEvent.format(event)
    after
      0 -> nil
    end
  end)
  |> Enum.take_while(& &1)

{apply_result, progress}
```

## Verify

Readiness checks in the declaration already restart and wait for the service during apply. Verification can stay simple: fetch the public URL.

```elixir
if verify? do
  response = Req.get!(public_url)
  {response.status, response.body, public_url}
else
  {:skipped, public_url}
end
```
