commits
The rekey flow only handled a corrupted state (garden exists but OAuth
client doesn't) that isn't a normal operational scenario. Simplify the
client-side fallback: when reauthentication fails, clear credentials
and re-register as a new garden.
Removes: rekey API endpoint, GardenRekey schema, rekey_garden/2,
Registration.rekey/3, and all client-side rekey logic.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Narrow token invalidation to only fire when the server returns 401/403
(upgrade_failure), indicating the token or OAuth client is invalid.
Normal disconnects (server restart, network blip) no longer force an
unnecessary HTTP reauthentication.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden is disconnected (e.g. server rejected auth because the
OAuth client was deleted), the cached access token is invalidated.
This forces the next reconnect to go through reauthenticate → rekey
instead of reusing a token the server will reject again.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden has stored credentials but the OAuth client no longer
exists on the server, reauthentication fails. Previously this was a
hard error that left the garden retrying forever. Now it falls through
to the rekey flow, which can recover the garden's identity.
If the rekey also fails with garden_not_found (garden was deleted),
it falls through to fresh registration.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gardens with stored credentials that fail reauthentication no longer
silently re-register as new gardens. Instead they attempt a rekey via
the new POST /api/v1/gardens/:sid/rekey endpoint, which creates a new
OAuth client for the existing garden identity.
Server-side changes:
- Add rekey_garden/2 to Sower.Orchestration.Garden
- Add rekey action to GardenController with garden:register permission
- Rescue ArgumentError in GardenSocket when Boruta token references a
deleted OAuth client
Client-side changes:
- Add SowerClient.Registration.rekey/3
- Add try_http_rekey path in resolve_connect_token when garden has a
stored garden_sid but no usable credentials
- Fresh registration only happens for truly new gardens (no garden_sid)
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the garden process starts and cannot obtain a token (e.g. server
not yet ready), it now starts with an unconnected socket and schedules
reconnection with backoff, instead of returning :ignore which left
the garden permanently stranded.
Token resolution functions now return {:ok, token}/{:error, reason}
tuples instead of token/nil, so failures propagate clearly through
build_connect_config and do_connect.
sow-158
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the dynamic add/remove dropdown pattern for permissions with
simple checkboxes. Fix modal and form dark mode backgrounds (zinc-300
was too bright, now zinc-800) and text color.
SOW-31
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer sends garden:hello over websocket. After HTTP
registration it joins the private channel directly with stored
garden_sid.
Server channel join now dispatches on access_token type: boruta-
authenticated gardens are authorized by matching garden_id from the
OAuth context, while legacy registration-token gardens still use
local_sid matching.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run reconcile_deployments in a background task instead of blocking the
channel process, preventing assert_reply timeouts in slow environments
- Drain TaskSupervisor children in channel_case on_exit to prevent
sandbox teardown while tasks are still running
- Increase assert_reply timeouts from 100ms default to 1000ms
- Use wait_until_succeeds for garden deploy RPC in e2e test to handle
subscription sync race
sow-125
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use progressive backoff delays (200ms, 500ms, 1s, 2s) when the garden
socket disconnects, instead of immediately retrying. Resets the backoff
counter on successful connection.
sow-142
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer falls back to sending the registration token
over the websocket. HTTP registration is now the only path for new
gardens without stored OAuth credentials.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add POST /api/v1/gardens/register endpoint so gardens can register via
HTTP before connecting the websocket, moving registration tokens off
the websocket path.
Server: new GardenController with OpenApiSpex operation, permission
check consistent with other API controllers, delegates to existing
register_new_garden/1.
Client: SowerClient.Registration HTTP client module, Garden.Socket
tries HTTP registration before falling back to websocket registration
token for backward compat.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move auth token from URL query param to x-auth-token header (sow-147)
Server accepts both header and query param for backward compat.
Garden client now sends via header only.
- Remove longpoll transport from garden socket (sow-144)
- Use Base.decode64 instead of decode64! for untrusted tokens (sow-145)
- Use exact scope matching instead of String.contains? (sow-146)
sow-144, sow-145, sow-146, sow-147
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the unmaintained Durable workflow library (GitHub-only dep) with
Oban for the RealtimeDeploy workflow. Improves the design by fanning out
to per-subscription deploy jobs instead of retrying the entire batch.
sow-143
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden's websocket disconnects, Slipstream reconnects using the
original URI from init time, which contains an expired boruta token.
Override handle_disconnect to call connect() with a freshly built config
that reads the current token from storage.
sow-141
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The StateDirectory defaulted to 0755, exposing the garden state
directory to other users on the system. Set StateDirectoryMode to
0700 so only the sower-garden user can access it.
sow-138
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an existing garden reconnects with a public_key but has no
oauth_client_id, create a Boruta client and return the client_id.
This allows existing gardens to adopt private_key_jwt auth without
losing their identity.
sow-105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace client_credentials + refresh_token flow with client_credentials +
private_key_jwt. Gardens generate a 4096-bit RSA keypair and send the public
key during registration. Token acquisition uses RS512-signed JWT assertions.
No refresh tokens — gardens reauthenticate with fresh assertions when access
tokens expire. Fully RFC 6749 §4.4.3 compliant.
sow-105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Boruta OAuth library for per-garden token management. Each garden
gets its own OAuth client at registration time with short-lived access
tokens (15 min) and refresh tokens (30 days), replacing reliance on
the static registration token for ongoing auth.
Server-side:
- Sower.GardenAuth module wraps Boruta's token pipeline for
issue (client_credentials) and refresh (refresh_token) grants
- GardenSocket.connect supports dual auth: boruta:<token> prefix for
OAuth tokens, existing base64 for registration tokens
- GardenChannel returns oauth_credentials in hello reply for new
registrations, adds token:refresh handler for mid-session rotation
- POST /api/oauth/token endpoint for HTTP-based refresh before connect
- Boruta tables exempted from org_id enforcement in Repo
- GardenAuth.Context + permissions for OAuth-authenticated gardens
Garden-side:
- Storage gains oauth_credentials field (ETF-persisted)
- Boot sequence: try stored access token → HTTP refresh → registration
token fallback
- Token refresh scheduled at 80% of TTL via channel, with retry
fix: handle storage schema evolution for oauth_credentials field
Old storage.etf files deserialized via binary_to_term produce structs
missing the new oauth_credentials key. Add ensure_fields/1 migration
step that re-structs to fill in missing fields with defaults.
sow-105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
inputs:
- flake-parts: f20dc5d (2026-03-01) → 3107b77 (2026-04-01)
- nixpkgs: b63fe7f (2026-03-28) → 8d8c1fa (2026-04-02)
- nixpkgs-lib: c185c7a (2026-02-28) → 333c4e0 (2026-03-28)
register_subscription was generating a random SID as the name fallback
when the garden didn't provide one. This defeated the upsert on
(garden_id, org_id, name), causing every re-sync to create a new
subscription record and orphan the subscriptions_deployments links.
Now falls through to create_subscription's existing default of
seed_name, which is stable across re-syncs.
sow-134
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace separate "Matching Seeds" and "Deployments" sections with a
single seed-centric table showing generation info, deployment status,
and active/pending indicators. Adds Flop pagination (10 per page).
SOW-1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Durable workflow engine and a RealtimeDeploy workflow that fires
after Seed.create/2. It finds matching subscriptions with
allow_realtime enabled, checks window constraints, and triggers
deploy_subscription for each.
- Add durable dependency with migration and supervision tree setup
- Add within_window? helper to Subscription for time window evaluation
- Add find_realtime_subscriptions to filter by allow_realtime flag
- Skip org_id check in Repo for Durable's internal schema prefix
- Update nix deps lock
SOW-130
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display schedule, activation_args, reboot_policy, allow_realtime, and
window on the subscription show page. Fields with default values are
hidden to reduce noise.
SOW-131
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add allow_realtime (boolean) and window (days, time_start, time_end, tz)
fields to subscription schema on both client and server sides, with
migration to persist them. These fields flow through subscriptions:sync
from garden to server.
SOW-4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fail evaluation if users provide:
- subscriptions as a list (now a map in 0.8.0)
- deployment_profiles (removed, use activation_args/reboot_policy on subscriptions)
- default_deployment_profile (removed)
SOW-129
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display real-time seed deployment state (pending/downloading/activating/
completed) on the deployment show page. Broadcasts seed status changes
via PubSub so the LiveView updates without page refresh.
sow-72
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace LiveStream-based garden list with Flop-powered pagination and
sorting. Adds Flop.Schema derive to Garden with name sorting and 20-item
pages. Switches the LiveView from stream assigns to plain assigns with
Flop metadata for pagination controls.
sow-126
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace dynamic string interpolation in table component's hide_on and
action_hide_on with a helper that returns complete static class strings.
Tailwind's purge scanner cannot detect dynamically constructed classes
like "hidden #{breakpoint}:table-cell", causing them to be stripped from
the CSS output.
sow-127
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the seed type check before any reboot decision logic so
non-rebootable deployments skip the entire reboot path, including
the "reboot skipped" decision line for failed deployments.
sow-128
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded "nixos" checks in maybe_reboot/3 and reboot_reason/3
with a @rebootable_seed_types module attribute, allowing reboot logic
to be skipped entirely for non-rebootable seed types.
sow-128
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
inputs:
- nixpkgs: e607cb5 (2026-03-09) → b63fe7f (2026-03-28)
activation_args and reboot_policy are now fields on the subscription
schema instead of a separate DeploymentProfile struct. The deployer
and seed activation code read these directly from the subscription
looked up by sid.
- Client schema: added activation_args (default []) and reboot_policy
(default "never") to Subscription
- Server schema: added activation_args and reboot_policy columns
- Garden.Seed: accepts Subscription instead of DeploymentProfile
- Deployer: uses find_subscription_fun instead of get_deployment_profile_fun
- Deleted DeploymentProfile schema and all references
SOW-121
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Subscriptions are now configured as a map (name -> config) instead of a
list. The map key becomes the subscription name. This is a breaking
change to the config file format.
Also removes deployment_profiles and default_deployment_profile from
the Config schema since the deployer already uses defaults.
refactor: remove deployment profile lookup chain from garden deployer
get_deployment_profile, find_deployment_profile, default_deployment_profile,
and find_subscription were all dead code after removing deployment_profiles
from config. The deployer now uses %DeploymentProfile{} defaults directly.
SOW-119
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the name field on subscription show page (as header, with sid
as subtitle), subscription index table, and garden show subscriptions
table.
SOW-118
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Decouple subscription identity from (seed_name, seed_type) by adding a
name column as the new unique key. The unique constraint is now
(garden_id, org_id, name) instead of (garden_id, org_id, seed_name,
seed_type).
- DB migration adds name column (not null, backfilled from seed_name)
- Server derives name from client struct, generates sid if absent
- Client schema adds optional name field (required in 0.9.0)
- Removes deployment_profile from client schema; deployer always uses
default profile
SOW-118
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a new deployment is created for a subscription, any existing stale
deployments for that subscription are transitioned to a new :canceled state.
Also updates reconcile_deployments_on_connect to use :canceled instead of
:stale for explicitly superseded deployments.
sow-117
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking the expanded log itself was triggering collapse, preventing text
selection and other interactions. Move phx-click from article to the
header div so only the header row toggles the log.
sow-115
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pre-start migration hooks that renamed sower-agent state directories
to sower-garden did not work due to permissions issues. Remove them from
both the NixOS module and the home-manager module.
sow-113
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add aria-label to online/1 SVG (Online/Offline)
- Add aria-label to result/1 SVG (Success/Failed/No result)
- Show full text label in deployment_status/1 completed state,
matching the other states (Created, Dispatched, etc.)
- Fix empty column label on deployments index table to 'Status'
fix: compact deploy status on mobile and seed deployments
- Add compact attr to deployment_status component to render icon-only
- Use compact mode on mobile in garden show deployments table
- Use compact mode for seed deployment results on deployment show
- Add aria-labels to dot indicators in deployment_status
- Add Status column label to garden show deployments table
- Hide Retry action on mobile in garden show deployments table
- Truncate SID on mobile in garden show deployments table
fix: remove broken action_hide_on from garden deployments table
action_hide_on generates Tailwind classes dynamically via string
interpolation, but Tailwind's purge doesn't detect them so the
responsive classes are never generated. This is a pre-existing
bug in the table component. Removing it restores the Retry button
visibility that existed before the previous commit.
SOW-109
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests were sharing _build/storage.etf with the dev server, risking
cross-contamination. Now uses a unique tmp dir per test run, cleaned
up via ExUnit.after_suite.
sow-89
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the OAuth callback URL as a read-only field so users know what
to configure in their forge's OAuth application settings. Extract
redirect_url/0 in Sower.Forge.Oauth to avoid duplicating the URL
construction.
sow-114
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sow-116
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sow-115
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The rekey flow only handled a corrupted state (garden exists but OAuth
client doesn't) that isn't a normal operational scenario. Simplify the
client-side fallback: when reauthentication fails, clear credentials
and re-register as a new garden.
Removes: rekey API endpoint, GardenRekey schema, rekey_garden/2,
Registration.rekey/3, and all client-side rekey logic.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Narrow token invalidation to only fire when the server returns 401/403
(upgrade_failure), indicating the token or OAuth client is invalid.
Normal disconnects (server restart, network blip) no longer force an
unnecessary HTTP reauthentication.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden is disconnected (e.g. server rejected auth because the
OAuth client was deleted), the cached access token is invalidated.
This forces the next reconnect to go through reauthenticate → rekey
instead of reusing a token the server will reject again.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden has stored credentials but the OAuth client no longer
exists on the server, reauthentication fails. Previously this was a
hard error that left the garden retrying forever. Now it falls through
to the rekey flow, which can recover the garden's identity.
If the rekey also fails with garden_not_found (garden was deleted),
it falls through to fresh registration.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gardens with stored credentials that fail reauthentication no longer
silently re-register as new gardens. Instead they attempt a rekey via
the new POST /api/v1/gardens/:sid/rekey endpoint, which creates a new
OAuth client for the existing garden identity.
Server-side changes:
- Add rekey_garden/2 to Sower.Orchestration.Garden
- Add rekey action to GardenController with garden:register permission
- Rescue ArgumentError in GardenSocket when Boruta token references a
deleted OAuth client
Client-side changes:
- Add SowerClient.Registration.rekey/3
- Add try_http_rekey path in resolve_connect_token when garden has a
stored garden_sid but no usable credentials
- Fresh registration only happens for truly new gardens (no garden_sid)
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the garden process starts and cannot obtain a token (e.g. server
not yet ready), it now starts with an unconnected socket and schedules
reconnection with backoff, instead of returning :ignore which left
the garden permanently stranded.
Token resolution functions now return {:ok, token}/{:error, reason}
tuples instead of token/nil, so failures propagate clearly through
build_connect_config and do_connect.
sow-158
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer sends garden:hello over websocket. After HTTP
registration it joins the private channel directly with stored
garden_sid.
Server channel join now dispatches on access_token type: boruta-
authenticated gardens are authorized by matching garden_id from the
OAuth context, while legacy registration-token gardens still use
local_sid matching.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run reconcile_deployments in a background task instead of blocking the
channel process, preventing assert_reply timeouts in slow environments
- Drain TaskSupervisor children in channel_case on_exit to prevent
sandbox teardown while tasks are still running
- Increase assert_reply timeouts from 100ms default to 1000ms
- Use wait_until_succeeds for garden deploy RPC in e2e test to handle
subscription sync race
sow-125
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add POST /api/v1/gardens/register endpoint so gardens can register via
HTTP before connecting the websocket, moving registration tokens off
the websocket path.
Server: new GardenController with OpenApiSpex operation, permission
check consistent with other API controllers, delegates to existing
register_new_garden/1.
Client: SowerClient.Registration HTTP client module, Garden.Socket
tries HTTP registration before falling back to websocket registration
token for backward compat.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move auth token from URL query param to x-auth-token header (sow-147)
Server accepts both header and query param for backward compat.
Garden client now sends via header only.
- Remove longpoll transport from garden socket (sow-144)
- Use Base.decode64 instead of decode64! for untrusted tokens (sow-145)
- Use exact scope matching instead of String.contains? (sow-146)
sow-144, sow-145, sow-146, sow-147
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden's websocket disconnects, Slipstream reconnects using the
original URI from init time, which contains an expired boruta token.
Override handle_disconnect to call connect() with a freshly built config
that reads the current token from storage.
sow-141
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace client_credentials + refresh_token flow with client_credentials +
private_key_jwt. Gardens generate a 4096-bit RSA keypair and send the public
key during registration. Token acquisition uses RS512-signed JWT assertions.
No refresh tokens — gardens reauthenticate with fresh assertions when access
tokens expire. Fully RFC 6749 §4.4.3 compliant.
sow-105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Boruta OAuth library for per-garden token management. Each garden
gets its own OAuth client at registration time with short-lived access
tokens (15 min) and refresh tokens (30 days), replacing reliance on
the static registration token for ongoing auth.
Server-side:
- Sower.GardenAuth module wraps Boruta's token pipeline for
issue (client_credentials) and refresh (refresh_token) grants
- GardenSocket.connect supports dual auth: boruta:<token> prefix for
OAuth tokens, existing base64 for registration tokens
- GardenChannel returns oauth_credentials in hello reply for new
registrations, adds token:refresh handler for mid-session rotation
- POST /api/oauth/token endpoint for HTTP-based refresh before connect
- Boruta tables exempted from org_id enforcement in Repo
- GardenAuth.Context + permissions for OAuth-authenticated gardens
Garden-side:
- Storage gains oauth_credentials field (ETF-persisted)
- Boot sequence: try stored access token → HTTP refresh → registration
token fallback
- Token refresh scheduled at 80% of TTL via channel, with retry
fix: handle storage schema evolution for oauth_credentials field
Old storage.etf files deserialized via binary_to_term produce structs
missing the new oauth_credentials key. Add ensure_fields/1 migration
step that re-structs to fill in missing fields with defaults.
sow-105
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
register_subscription was generating a random SID as the name fallback
when the garden didn't provide one. This defeated the upsert on
(garden_id, org_id, name), causing every re-sync to create a new
subscription record and orphan the subscriptions_deployments links.
Now falls through to create_subscription's existing default of
seed_name, which is stable across re-syncs.
sow-134
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Durable workflow engine and a RealtimeDeploy workflow that fires
after Seed.create/2. It finds matching subscriptions with
allow_realtime enabled, checks window constraints, and triggers
deploy_subscription for each.
- Add durable dependency with migration and supervision tree setup
- Add within_window? helper to Subscription for time window evaluation
- Add find_realtime_subscriptions to filter by allow_realtime flag
- Skip org_id check in Repo for Durable's internal schema prefix
- Update nix deps lock
SOW-130
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add allow_realtime (boolean) and window (days, time_start, time_end, tz)
fields to subscription schema on both client and server sides, with
migration to persist them. These fields flow through subscriptions:sync
from garden to server.
SOW-4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace LiveStream-based garden list with Flop-powered pagination and
sorting. Adds Flop.Schema derive to Garden with name sorting and 20-item
pages. Switches the LiveView from stream assigns to plain assigns with
Flop metadata for pagination controls.
sow-126
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace dynamic string interpolation in table component's hide_on and
action_hide_on with a helper that returns complete static class strings.
Tailwind's purge scanner cannot detect dynamically constructed classes
like "hidden #{breakpoint}:table-cell", causing them to be stripped from
the CSS output.
sow-127
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
activation_args and reboot_policy are now fields on the subscription
schema instead of a separate DeploymentProfile struct. The deployer
and seed activation code read these directly from the subscription
looked up by sid.
- Client schema: added activation_args (default []) and reboot_policy
(default "never") to Subscription
- Server schema: added activation_args and reboot_policy columns
- Garden.Seed: accepts Subscription instead of DeploymentProfile
- Deployer: uses find_subscription_fun instead of get_deployment_profile_fun
- Deleted DeploymentProfile schema and all references
SOW-121
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Subscriptions are now configured as a map (name -> config) instead of a
list. The map key becomes the subscription name. This is a breaking
change to the config file format.
Also removes deployment_profiles and default_deployment_profile from
the Config schema since the deployer already uses defaults.
refactor: remove deployment profile lookup chain from garden deployer
get_deployment_profile, find_deployment_profile, default_deployment_profile,
and find_subscription were all dead code after removing deployment_profiles
from config. The deployer now uses %DeploymentProfile{} defaults directly.
SOW-119
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Decouple subscription identity from (seed_name, seed_type) by adding a
name column as the new unique key. The unique constraint is now
(garden_id, org_id, name) instead of (garden_id, org_id, seed_name,
seed_type).
- DB migration adds name column (not null, backfilled from seed_name)
- Server derives name from client struct, generates sid if absent
- Client schema adds optional name field (required in 0.9.0)
- Removes deployment_profile from client schema; deployer always uses
default profile
SOW-118
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a new deployment is created for a subscription, any existing stale
deployments for that subscription are transitioned to a new :canceled state.
Also updates reconcile_deployments_on_connect to use :canceled instead of
:stale for explicitly superseded deployments.
sow-117
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add aria-label to online/1 SVG (Online/Offline)
- Add aria-label to result/1 SVG (Success/Failed/No result)
- Show full text label in deployment_status/1 completed state,
matching the other states (Created, Dispatched, etc.)
- Fix empty column label on deployments index table to 'Status'
fix: compact deploy status on mobile and seed deployments
- Add compact attr to deployment_status component to render icon-only
- Use compact mode on mobile in garden show deployments table
- Use compact mode for seed deployment results on deployment show
- Add aria-labels to dot indicators in deployment_status
- Add Status column label to garden show deployments table
- Hide Retry action on mobile in garden show deployments table
- Truncate SID on mobile in garden show deployments table
fix: remove broken action_hide_on from garden deployments table
action_hide_on generates Tailwind classes dynamically via string
interpolation, but Tailwind's purge doesn't detect them so the
responsive classes are never generated. This is a pre-existing
bug in the table component. Removing it restores the Retry button
visibility that existed before the previous commit.
SOW-109
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>