# `MishkaCmsCore.Runtime.Handlers.BootHandler`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/handlers/boot_handler.ex#L5)

Application boot handler for MishkaCms runtime system initialization and asset compilation.

The `BootHandler` orchestrates the complete startup sequence for the MishkaCms runtime system,
managing the loading of sites, compilation of templates and modules, and initialization of
various runtime states. It ensures all essential components are properly loaded and compiled
before the application becomes available to serve requests.

## Features

* **Parallel Boot Process**: Executes multiple initialization tasks concurrently for optimal startup time
* **Site Management**: Loads and caches site configurations in optimized batches
* **Template Compilation**: Compiles pages, layouts, components, and error pages into runtime modules
* **Asset Pipeline Integration**: Creates compilation jobs for CSS and JavaScript assets
* **Error Recovery**: Provides detailed error reporting and graceful failure handling
* **Performance Optimization**: Uses adaptive batch sizing based on dataset size
* **Resource Cleanup**: Manages temporary files and cleans up stale Oban jobs

## Boot Process Overview

The boot process executes the following tasks in parallel:

1. **Site Loading**: Loads all active sites into `SiteState` for efficient lookups
2. **Component Compilation**: Compiles reusable UI components into runtime modules
3. **Error Page Compilation**: Builds error handling pages for each site
4. **Layout Compilation**: Compiles page layouts and templates
5. **JavaScript Hooks**: Processes JS hooks for Phoenix LiveView integration
6. **Page Compilation**: Compiles individual content pages
7. **Asset Jobs**: Schedules CSS and JavaScript compilation jobs

Once they are done, every open site's sitemap is built on this node (`build_sitemaps/0`).

## A node that does not serve the site

Every node that starts the application boots, and only one whose endpoint serves the site
(`serving?/0`) touches the compile jobs the nodes share: it cancels the ones a previous run left
(`take_over_compile_jobs/1`) and queues its sites' CSS/JS bundles (`queue_boot_jobs/0`). A
standalone node — a `mix run` script, a release's `eval`, `iex -S mix` — compiles its own sites,
pages and sitemaps as every node does, cancels no job and queues no bundle. It runs no job queue
either when the host starts Oban with `oban_config/1`, so every job it inserts is the serving
node's to run.

## Architecture Integration

The BootHandler integrates with several MishkaCms subsystems:

* **SiteState**: Caches site configurations for runtime lookups
* **WebRunTimeMetaDataState**: Stores page metadata and routing information
* **ModuleCompilerHandler**: Handles dynamic module compilation
* **RuntimeCompilerWorker**: Manages background asset compilation via Oban
* **TailwindCompiler**: Generates CSS assets for each site

## Performance Configuration

The boot process automatically adapts to dataset sizes:

    # Batch configuration scales with data volume
    # <= 1,000 items:    batch_size: 1,000,  chunk_size: 250
    # <= 5,000 items:    batch_size: 2,000,  chunk_size: 500
    # <= 20,000 items:   batch_size: 5,000,  chunk_size: 1,000
    # <= 100,000 items:  batch_size: 10,000, chunk_size: 2,000
    # > 100,000 items:   batch_size: 20,000, chunk_size: 5,000

## Template and Asset Management

The BootHandler creates template files for Tailwind CSS compilation:

    # Template files are organized by site and type:
    # priv/static/temp/runtime/
    # ├── site1/
    # │   ├── page/
    # │   ├── layout/
    # │   ├── component/
    # │   └── error/
    # ├── site2/
    # │   └── ...
    # └── __global__/
    #     └── ...

## Production Considerations

* **Startup Time**: Boot time scales with data volume; typical startup: 1-5 seconds
* **Memory Usage**: Sites and metadata are cached in memory for performance
* **Concurrency**: Uses all available CPU cores for parallel compilation
* **Resource Cleanup**: Automatically cleans up stale Oban jobs and temporary files
* **Error Recovery**: Failed individual items don't prevent overall boot success
* **Asset Pipeline**: Integrates with background job processing for asset compilation

# `boot_id`

```elixir
@spec boot_id() :: String.t() | nil
```

This running instance's own id, made when `:mishka_cms_core` starts (`new_boot_id/0`) and `nil`
before then.

Every bundle job a serving instance (`serving?/0`) queues carries it (`asset_job/4`), and
`MishkaCmsCore.Runtime.Workers.RuntimeCompilerWorker` builds the job on the instance it names: the
CSS, JS and sitemap a bundle job builds go into that instance's own
`MishkaCmsCore.Runtime.States.AssetsState`. A deploy that starts the next instance beside the old
one gives both the same `node()` and the same Oban queue; this id is what tells them apart. A
standalone node's bundle job names no instance, and a serving one builds it.

# `booted_at`

```elixir
@spec booted_at() :: DateTime.t()
```

When this node started — the moment every compile job created before it stopped describing
anything: a serving node's boot cancels those still pending (`take_over_compile_jobs/1`) and
compiles from the database itself, without a job.

# `build_component_class_content`

Aggregates every class-bearing field of a Component into one blob for
Tailwind to scan: `template`, `body`, helper code, the templates
+ bodies of multi-clause variants stored in `extra.clauses`, and the content
an author places with it — its `examples` and the `extra.furnishing` it
arrives with, which the content editor offers as ready blocks.

It ends with the attribute lines the CSS closure reads to follow a component named through an
attribute — `MishkaCmsCore.Runtime.Compilers.CssClosure.attribute_marks/1`.

# `build_error_template_content`

Builds comprehensive template content for error pages by aggregating all related templates.

This function constructs the complete template content for error pages by combining
the error page templates with their associated layouts. This ensures that Tailwind
CSS compilation captures all classes used across the entire error page rendering chain.

## Parameters

* `err` - Error page struct containing error page data and associated layouts

## Return Value

* `binary()` - Combined template content as a single string

## Template Aggregation Process

1. **Root Layout Template**: Includes the base layout template
2. **Error Page Templates**: Adds all error page template variants
3. **Associated Layouts**: Includes templates from nested layout structures
4. **Content Filtering**: Removes nil/empty templates and combines valid content

## Template Structure Processing

The function handles nested template structures and extracts content from:
* Direct template strings
* Nested layout template references
* Complex error page hierarchies
* Multiple error page variants per error configuration

# `build_resource_class_content`

Aggregate template + helper code for Page/Layout resources. Tailwind
scans the result, so unparsed Elixir snippets are fine.

It is the COMPILED template, not the stored one. A page carrying a form compiles with the
confirmation region in front of it
(`MishkaCmsCore.Runtime.Compilers.LiveViewCompiler.confirmable/2`), and a class that only ever
exists after this file is written gets no rule — the visitor read the success message as bare
unstyled body text with no box, no padding and no centring.

# `build_sitemaps`

```elixir
@spec build_sitemaps() :: :ok | {:error, {:sitemaps_failed, [term()]}}
```

Builds every open site's sitemap into THIS node's `AssetsState` — the boot counterpart of the
site's `"sitemap"` job (see `MishkaCmsCore.Runtime.Compilers.SitemapCompiler`).

Two sites at a time, whatever the node's size, so a node booting with many sites reads their rows
a couple at a time instead of all at once. Each is built in its own task under
`MishkaCmsCore.Runtime.TaskSupervisor`, unlinked, so a site whose build raises or runs past a
minute is left without one and the rest are still built. The first request for it enqueues its
job.

## Return Values

* `:ok` - Every open site's sitemap is stored
* `{:error, {:sitemaps_failed, failed}}` - One entry per site not built: `{:ok, {name, {:error,
  reason}}}`, or `{:exit, {name, reason}}` for a build that raised or timed out

# `cancel_leftover_jobs`

```elixir
@spec cancel_leftover_jobs(Oban.name()) :: {:ok, non_neg_integer()}
```

Cancels every runtime compile job a previous run left pending or executing, in whichever queue it
was put — the boot rebuilds what they would have. Other workers' jobs (mail, notifications) are
left to `Oban.Lifeline`. Takes the Oban instance, `Oban` by default.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `classify_compile_stream_result`

```elixir
@spec classify_compile_stream_result({:ok, term()} | {:exit, term()}) :: term()
```

Maps one `Task.async_stream` element from a boot compile pass to a compile outcome.

Every pass runs with `on_timeout: :kill_task`, so an item that exceeds the pass timeout arrives
here as `{:exit, :timeout}` (never a raised crash) — this keeps one pathological template from
aborting the whole boot. A successful item is either `nil` (compiled cleanly, nothing to report)
or an `{:error, _}` tuple for a broken-but-skipped item.

## Examples

    classify_compile_stream_result({:ok, nil})
    #=> nil

    classify_compile_stream_result({:ok, {:error, :not_found}})
    #=> {:error, :not_found}

    classify_compile_stream_result({:exit, :timeout})
    #=> {:compilation_exit, :timeout}

# `compile_site`

Recompiles every active dependent part of a single site — the per-site
counterpart to the boot passes. Writes templates + routing for active records,
compiles the precompiled modules, and rebuilds the site's CSS/JS bundle and its
sitemap. Used by the Site runtime worker on activate / unarchive / rename.

# `create_assets_compilation_jobs`

Creates background jobs for asset compilation across all active sites.

This function schedules Oban jobs to compile CSS and JavaScript assets for all
active sites in the system. It provides a scalable approach to asset generation
by distributing the work across background workers, preventing boot process delays.

## Parameters

* `type` - String indicating the asset type to compile (defaults to "asset")
  * `"asset"` - Standard asset compilation (CSS, JS)
  * `"css"` - CSS-only compilation
  * `"js"` - JavaScript-only compilation
  * `"sitemap"` - the site's sitemap (`MishkaCmsCore.Runtime.Compilers.SitemapCompiler`)

## Return Values

* `:ok` - Compilation jobs successfully created
* `{:error, reason}` - Job creation failed with error details

## Job Creation Process

1. **Site Discovery**: Queries all active sites from SiteState
2. **Batch Processing**: Processes sites in optimal chunks for job creation
3. **Job Scheduling**: Creates Oban jobs for each site's asset compilation
4. **Queue Management**: Distributes jobs across available worker queues

## Examples

    # Create standard asset compilation jobs
    :ok = BootHandler.create_assets_compilation_jobs()

    # Create CSS-only compilation jobs
    :ok = BootHandler.create_assets_compilation_jobs("css")

    # Handle job creation failures
    case BootHandler.create_assets_compilation_jobs("js") do
      :ok ->
        Logger.info("JavaScript compilation jobs scheduled")

      {:error, reason} ->
        Logger.error("Failed to schedule JS jobs: #{inspect(reason)}")
    end

## Job Configuration

    # Each job is configured with:
    %{
      resource: "MishkaCmsCore.Runtime.Handlers.BootHandler",
      action: "boot_handler",
      # `nil` on a node that does not serve the site
      boot_id: BootHandler.boot_id(),
      data: %{site_name: "example_site", site_id: "<uuid>", type: "asset"}
    }

## Worker Integration

    # Jobs are processed by RuntimeCompilerWorker
    # Worker handles actual asset compilation using:
    # - TailwindCompiler for CSS generation
    # - JavaScript bundling and minification
    # - Asset optimization and compression

# `create_js_hooks_template`

Processes JavaScript hooks and creates template files for CSS class extraction.

This function handles Phoenix LiveView JavaScript hooks by creating template files
that allow the Tailwind compiler to discover CSS classes used within the JavaScript
code. This ensures that styles referenced in JS hooks are included in the compiled CSS.

## Return Values

* `:ok` - All JS hooks processed successfully
* `{:error, reason}` - Processing failed with error details

# `create_template_file`

Creates a template file in the appropriate directory structure for CSS compilation.

This function organizes template files by site and type, creating the necessary
directory structure and writing template content to disk. The files are used
by the Tailwind compiler to scan for CSS classes during asset generation.

## Parameters

* `site_name` - String name of the site (nil becomes "__global__")
* `type` - String indicating template type ("page", "layout", "component", etc.)
* `name` - String name of the template (will be slugified for filename)
* `content` - String content of the template

## Directory Structure

Templates are organized in the following structure:
```
priv/static/temp/runtime/
├── site_name/
│   ├── page/
│   ├── layout/
│   ├── component/
│   ├── error/
│   └── js_hook/
└── __global__/
    └── ...
```

# `delete_template_file`

Deletes a template file from the organized directory structure.

This function removes template files that are no longer needed, such as when
pages, components, or layouts are deleted from the CMS. It helps maintain
a clean template directory and prevents stale CSS classes from being included
in compiled assets.

## Parameters

* `site_name` - String name of the site (nil becomes `__global__`)
* `type` - String indicating template type (`page`, `layout`, `component`, etc.)
* `name` - String name of the template to delete (will be slugified)

## Return Value

* `:ok` - File successfully deleted or didn't exist
* `{:error, reason}` - File deletion failed

## File Safety

This function is safe to call multiple times or on non-existent files.
It will not raise exceptions for missing files, making it suitable for
cleanup operations where the file state is uncertain.

# `get_optimal_batch_config`

Determines optimal batch and chunk sizes based on dataset size for performance tuning.

This function provides adaptive batch configuration that scales with the number of items
being processed. It optimizes memory usage and processing time by selecting appropriate
batch sizes for different dataset volumes.

## Parameters

* `site_count` - Integer representing the total number of items to process

## Return Value

Returns a tuple `{batch_size, chunk_size}` where:
* `batch_size` - Number of items to load from database in each query
* `chunk_size` - Number of items to process together in parallel tasks

## Scaling Strategy

* **≤ 1,000 items**: `{1_000, 250}` - Small datasets, conservative batching
* **≤ 5,000 items**: `{2_000, 500}` - Medium datasets, moderate batching
* **≤ 20,000 items**: `{5_000, 1_000}` - Large datasets, aggressive batching
* **≤ 100,000 items**: `{10_000, 2_000}` - Very large datasets, high-volume batching
* **> 100,000 items**: `{20_000, 5_000}` - Massive datasets, maximum batching

## Examples

    # Small site configuration
    {batch_size, chunk_size} = BootHandler.get_optimal_batch_config(500)
    # Returns: {1_000, 250}

    # Large enterprise configuration
    {batch_size, chunk_size} = BootHandler.get_optimal_batch_config(50_000)
    # Returns: {10_000, 2_000}

## Performance Considerations

* **Memory Usage**: Larger batch sizes use more memory but reduce database roundtrips
* **Concurrency**: Smaller chunk sizes enable better parallelization
* **Database Load**: Batch sizes balance query efficiency with resource usage
* **System Resources**: Configuration adapts to available CPU cores and memory

# `get_status`

Retrieves the current boot status of the application.

This function provides real-time information about the boot process state,
which is essential for health checks, monitoring systems, and ensuring the
application is ready to serve requests.

## Return Values

* `:ready` - Boot process completed successfully, application is ready
* `:initializing` - Boot process is currently running
* `{:failed, reason}` - Boot process failed with specific error information

## Monitoring Integration

    # Continuous monitoring in a GenServer
    def handle_info(:check_status, state) do
      status = BootHandler.get_status()

      # Update external monitoring systems
      :telemetry.execute([:boot, :status], %{}, %{status: status})

      {:noreply, %{state | boot_status: status}}
    end

# `lifeline?`

```elixir
@spec lifeline?() :: boolean()
```

Whether the running Oban rescues jobs a stopped node left executing (`lifeline:` in the app's
Oban config). Without it, a mail or notification job interrupted by a restart stays `executing`.

# `load_page_editions`

```elixir
@spec load_page_editions() :: :ok | {:error, term()}
```

Streams the language editions of every site's active pages into `SiteState`'s editions index —
what `MishkaCmsCore.Runtime.PageEditions.editions/2` reads on every render instead of a query.

Part of `load_sites/0`, so it runs at boot and whenever `SiteState` restarts, and from there on
every write of a page keeps the index itself — on the node that wrote it through
`MishkaCmsCore.Runtime.Resources.Notifiers.PageEditions`, and in `SiteState` on the others.
`MishkaCmsCore.Runtime.PageLanguageBackfill` runs before it at boot, so a page given a group
there is read with it. Only the fields an edition carries are read, never a template. A load
error is logged and returned as `{:error, reason}`, as in `load_sites/0`.

# `load_sites`

```elixir
@spec load_sites() :: :ok | {:error, term()}
```

Streams every site from the DB in tuned batches and upserts them into `SiteState`'s ETS cache,
then the editions of their pages (`load_page_editions/0`).

This is the single site-cache loader: `perform_boot/0` runs it at startup, and
`SiteState.handle_continue/2` reuses it to self-heal after an owner crash. Streaming (rather than
materializing all rows at once) keeps memory bounded and startup fast even with tens of thousands
of sites. A load error is logged and returned as `{:error, reason}` — a failure here must not
abort boot or crash-loop the cache owner.

# `load_stylesheets`

Boot-time pass for `Runtime.Stylesheet` rows.

CSS content lives in the DB and is inlined at compile time by
`TailwindCompiler.read_global_stylesheets/0` and `read_site_stylesheets/1`.
This pass:

  1. Streams every active stylesheet row (global + per-site).
  2. Writes the CSS content to a `.template` file under
     `priv/static/temp/runtime/<site>/stylesheet/<name>.template`.
     Tailwind scans those files via `@source` directives, so any
     utility-class references inside the CSS (e.g. `@apply bg-primary-light`,
     or class names mentioned in comments) get picked up. Without this
     step, classes only present in stylesheet content would be missing
     from the compiled CSS.
  3. Logs the count.

This runs in parallel with `compile_components`, so total boot time is
unchanged.

# `load_web_metadata`

```elixir
@spec load_web_metadata() :: :ok | {:error, term()}
```

Repopulates `WebRunTimeMetaDataState`'s routing rows from the DB — the crash/boot self-heal path
reused by `WebRunTimeMetaDataState.handle_continue/2`.

Streams active Page/Layout/ErrorPage rows in tuned batches and upserts ONLY their routing
metadata (id/site/path/title/section/priority) via `save_batch/1` — it never writes templates or
recompiles modules (that stays in the boot passes). A DB error is logged and returned rather than
raised: this runs once per (re)start, not on the hot path, so a transient outage must not
crash-loop the owner.

## Examples

    # after a WebRunTimeMetaDataState crash wiped the routing tables, or at boot
    BootHandler.load_web_metadata()
    #=> :ok

# `new_boot_id`

```elixir
@spec new_boot_id() :: :ok
```

Gives this instance a new `boot_id/0`. Called once, by `MishkaCmsCore.Application.start/2`.

# `oban_config`

```elixir
@spec oban_config(keyword()) :: keyword()
```

The Oban config to start this node with: `config` as it is on a node that serves the site
(`serving?/0`), and with no queue on one that does not. A `mix run` script, a release's `eval` or
`iex -S mix` then runs none of the jobs the serving node is there to run: a page compile built into
its own memory, a stylesheet into its own `AssetsState`, a share card drawn on it. A job it inserts
still lands in the table they share, and a serving node takes it.

The host wraps what it starts Oban with, after anything that reads its queues, such as
`AshOban.config/2`:

    {Oban, BootHandler.oban_config(AshOban.config(domains, Application.fetch_env!(:my_app, Oban)))}

A queue signal cannot do this once Oban runs: even a `local_only` one is addressed to the node's
Oban name, and the serving node carries the same one — every BEAM on one machine does, and every
instance of a deploy whose release names its node the same.

# `queue_boot_jobs`

```elixir
@spec queue_boot_jobs() :: :ok | {:error, term()}
```

Queues every open site's CSS/JS bundle job at the end of a serving node's boot
(`create_assets_compilation_jobs/0`), and none on a standalone one (`serving?/0`).

# `serving?`

```elixir
@spec serving?() :: boolean()
```

Whether this node serves the site: the host's endpoint (`config :mishka_cms_core, endpoint:`, in
the `host_app:` it names) is set to serve, as `Phoenix.Endpoint.server?/2` reads it — `mix
phx.server`, or `server: true`, which a release's `PHX_SERVER` sets. A node that does not is
standalone: a `mix run` script, a release's `eval`, `iex -S mix`, a test run, and a node whose app
names no endpoint.

# `site_priority_to_oban`

Maps a site priority value to an Oban job priority (0–9, lower = higher priority).

Site priorities are positive integers where higher values mean more important.
Oban priorities are 0–9 where 0 is the most important.

## Mapping

* Site priority >= 100 → Oban 0 (highest)
* Site priority >= 50  → Oban 1
* Site priority >= 20  → Oban 2
* Site priority >= 10  → Oban 3
* Site priority >= 5   → Oban 4
* Site priority >= 1   → Oban 5
* Everything else      → Oban 6

# `start_link`

Starts the BootHandler GenServer as part of the application supervision tree.

This function is typically called automatically by the application supervisor.
The BootHandler runs as a named process and immediately begins the boot sequence
upon initialization.

## Parameters

* `opts` - Optional GenServer options (defaults to empty list)

## Return Values

* `{:ok, pid}` - Successfully started the GenServer
* `{:error, reason}` - Failed to start the GenServer

## Examples

    # Manual start (usually not needed as it's supervised)
    {:ok, pid} = BootHandler.start_link()

    # Start with custom GenServer options
    {:ok, pid} = BootHandler.start_link(debug: [:trace])

## Supervision Tree Integration

    # In your application.ex
    children = [
      # ... other processes
      MishkaCmsCore.Runtime.Handlers.BootHandler
    ]

# `take_over_compile_jobs`

```elixir
@spec take_over_compile_jobs(keyword()) :: {:ok, non_neg_integer()}
```

What a boot does to the compile jobs already queued, before it compiles anything.

A serving node (`serving?/0`) cancels every one a previous run left (`cancel_leftover_jobs/1`) and
builds what they would have from the database, so a new instance supersedes the one it replaces. A
standalone node cancels none, because the jobs it finds are the serving node's, and sends no queue
signal: Oban addresses even a `local_only` one by the node's Oban name, which every BEAM on one
machine shares, and so does every instance of a deploy. It says so in the log when it runs a queue
anyway, which `oban_config/1` is the way to stop. Returns how many were cancelled.

Options: `:oban`, the Oban instance, `Oban` by default.

# `web_metadata_row`

Builds one routing-metadata row for `WebRunTimeMetaDataState` from a Page/Layout/ErrorPage
record (its `:site` must be loaded). `section` is the row kind — `"Page"`, `"Layout"`, or
`"ErrorPage"`. Shared by the boot passes and `load_web_metadata/0`.

## Examples

    web_metadata_row(page, "Page")
    #=> %{id: page.id, site: "example", path: "/about", title: "About", ...}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
