diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index fa6ff29..06a8a56 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -62,9 +62,9 @@ { label: 'Audit Logs', icon: IconAudit, href: '/dashboard/audit' }, ...(currentTeamIsByoc ? [{ - label: 'BYOC', + label: 'Hosts', icon: IconServer, - href: '/dashboard/byoc', + href: '/dashboard/hosts', disabled: auth.role === 'member', disabledHint: 'Available to team owners and admins only' }] @@ -262,29 +262,29 @@ {/if} {#if !collapsed}Docs{/if} - - + {#if !collapsed}Notifications{/if} - - +
- + {#if !collapsed}Settings{/if} - +
diff --git a/frontend/src/routes/dashboard/billing/+page.svelte b/frontend/src/routes/dashboard/billing/+page.svelte new file mode 100644 index 0000000..2ba5cd6 --- /dev/null +++ b/frontend/src/routes/dashboard/billing/+page.svelte @@ -0,0 +1,131 @@ + + + + Wrenn — Billing + + +
+ + +
+
+ +
+

+ Billing +

+

+ Subscription, invoices, and payment methods for your team. +

+
+ +
+ + +
+ {#if status === 'loading'} +
+
+ + + + Loading billing data... +
+
+ {:else if status === 'error'} +
+ {errorMsg} + +
+ {:else if status === 'not_available'} +
+ +
+
+
+ + + + + +
+
+

+ Enterprise feature +

+

+ Billing management is available on Wrenn Cloud. +

+ + +
+ + + + + + + This instance is running in self-hosted mode + +
+
+ {:else} + +
+ Billing data will be displayed here. +
+ {/if} +
+
+ + +
+
diff --git a/frontend/src/routes/dashboard/byoc/+page.svelte b/frontend/src/routes/dashboard/byoc/+page.svelte deleted file mode 100644 index acd682f..0000000 --- a/frontend/src/routes/dashboard/byoc/+page.svelte +++ /dev/null @@ -1,587 +0,0 @@ - - -
- - -
- -
-
-
-

- BYOC Hosts -

-

- Your own compute, running Wrenn capsules. -

-
- {#if canManage} - - {/if} -
- - - {#if !loading && !error && hosts.length > 0} -
-
- {hosts.length} - total -
-
- - - - - {onlineCount} - online -
-
- {/if} -
- - -
- {#if loading} - {@render skeletonRows()} - {:else if error} -
- {error} -
- {:else if hosts.length === 0} - {@render emptyState()} - {:else} -
- - - - - - - - - {#if canManage} - - {/if} - - - - {#each hosts as host (host.id)} - - - - - - - {#if canManage} - - {/if} - - {/each} - -
HostStatus
-
{host.id}
- {#if host.address} -
{host.address}
- {/if} - {#if host.provider || host.availability_zone} -
- {[host.provider, host.availability_zone].filter(Boolean).join(' · ')} -
- {/if} -
- - {#if host.status === 'online'} - - - - - {:else} - - {/if} - {host.status} - - - -
-
- {/if} -
-
-
- -{#snippet skeletonRows()} -
- - - - - - - - - - - - {#each Array(4) as _, i} - - - - - - - - {/each} - -
HostStatus
-
-
-
-
-
-
-{/snippet} - -{#snippet emptyState()} -
-
- -
- {#if canManage} -

- No hosts yet. -

-

- Register a server and Wrenn will schedule capsules on your own infrastructure. -

- - {:else} -

- No hosts registered. -

-

- Ask a team owner or admin to register a BYOC host for your team. -

- {/if} -
-{/snippet} - - -{#if showCreate} -
-
{ if (!creating) showCreate = false; }} - onkeydown={(e) => { if (e.key === 'Escape' && !creating) showCreate = false; }} - >
-
-

- Register Host -

-

- Add a server to your team's BYOC pool. You'll receive a one-time registration token. -

- - {#if createError} -
- {createError} -
- {/if} - -
-
- - -
-
- - -
-
- -
- - -
-
-
-{/if} - - -{#if createdResult} -
-
-
- -
- - - -
- -

- Host registered -

-

- Pass this token to the host agent to complete registration. It expires in - 1 hour and is single-use. -

- -
-
- - {createdResult.registration_token} - - -
-
- -
- -

- This token will not be shown again. Store it safely before closing. -

-
- -
- -
-
-
-{/if} - - -{#if deleteTarget} -
-
{ if (!deleting) deleteTarget = null; }} - onkeydown={(e) => { if (e.key === 'Escape' && !deleting) deleteTarget = null; }} - >
-
-

- Delete Host -

-

- Remove {deleteTarget.id} from your BYOC pool. -

- - {#if deletePreviewLoading} -
- - Checking active capsules… -
- {:else if deletePreviewSandboxes.length > 0} -
-

- {deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed. -

-

- All running workloads on this host will be terminated immediately. -

-
- {/if} - - {#if deleteError} -
- {deleteError} -
- {/if} - -
- - -
-
-
-{/if} - - diff --git a/frontend/src/routes/dashboard/hosts/+page.svelte b/frontend/src/routes/dashboard/hosts/+page.svelte new file mode 100644 index 0000000..8e657d6 --- /dev/null +++ b/frontend/src/routes/dashboard/hosts/+page.svelte @@ -0,0 +1,728 @@ + + + + Wrenn — Hosts + + +
+ + +
+
+ +
+
+
+

+ Hosts +

+

+ Your own compute, running Wrenn capsules. +

+
+ + {#if canManage} + + {/if} +
+ + + {#if !loading && !error && hosts.length > 0} +
+
+ {hosts.length} + total +
+
+ + + + + {onlineCount} + online +
+ {#if offlineCount > 0} +
+ + {offlineCount} + offline +
+ {/if} +
+ {/if} + +
+
+ + +
+ {#if error} +
+ {error} + +
+ {/if} + + {#if loading} + {@render skeletonRows()} + {:else if hosts.length === 0} + {@render emptyState()} + {:else} +
+ +
+
Host
+
Status
+ + + + {#if canManage} +
+ {/if} +
+ + + {#each hosts as host, i (host.id)} +
+ +
+ + +
+ {host.id} + {#if host.address} +
{host.address}
+ {/if} + {#if host.provider || host.availability_zone} +
+ {#if host.provider} + {host.provider} + {/if} + {#if host.availability_zone} + {host.availability_zone} + {/if} +
+ {/if} +
+ + +
+ + {#if host.status === 'online'} + + + + + {:else} + + {/if} + {statusLabel(host.status)} + +
+ + + + + + + + + + + + {#if canManage} +
+ +
+ {/if} +
+ {/each} +
+ +

+ {hosts.length} {hosts.length === 1 ? 'host' : 'hosts'} registered +

+ {/if} +
+
+ + +
+
+ +{#snippet skeletonRows()} +
+
+
Host
+
Status
+ + + +
+ {#each Array(4) as _, i} +
+
+
+
+
+
+
+
+ + + +
+ {/each} +
+{/snippet} + +{#snippet emptyState()} +
+
+
+
+ + + + + + +
+
+ {#if canManage} +

No hosts yet

+

+ Register a server and Wrenn will schedule capsules on your own infrastructure. +

+ + {:else} +

No hosts registered

+

+ Ask a team owner or admin to register a host for your team. +

+ {/if} +
+{/snippet} + + +{#if showCreate} +
+ +
{ if (!creating) showCreate = false; }} + onkeydown={(e) => { if (e.key === 'Escape' && !creating) showCreate = false; }} + >
+ +
+

Register Host

+

+ Add a server to your team's host pool. You'll receive a one-time registration token. +

+ + {#if createError} +
+ {createError} +
+ {/if} + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+{/if} + + +{#if createdResult} +
+ +
{ if (e.key === 'Escape') closeTokenReveal(); }} + >
+ +
+ +
+ + + + + + Host registered successfully +
+ +

Registration Token

+

+ Pass this token to the host agent to complete registration. It expires in + 1 hour and is single-use. +

+ + +
+
+ + {createdResult.registration_token} + + {#key copyCount} + + {/key} +
+
+ + +
+ + + + +

+ This token will not be shown again. Store it in your secrets manager — not a note, not a chat message, not a commit. +

+
+ +
+ +
+
+
+{/if} + + +{#if deleteTarget} +
+ +
{ if (!deleting) deleteTarget = null; }} + onkeydown={(e) => { if (e.key === 'Escape' && !deleting) deleteTarget = null; }} + >
+ +
+

Delete Host

+

+ Remove {deleteTarget.id} from your host pool. +

+ + {#if deletePreviewLoading} +
+ + Checking active capsules… +
+ {:else if deletePreviewSandboxes.length > 0} +
+

+ {deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed. +

+

+ All running workloads on this host will be terminated immediately. +

+
+ {/if} + + {#if deleteError} +
+ {deleteError} +
+ {/if} + +
+ + +
+
+
+{/if} + + diff --git a/frontend/src/routes/dashboard/usage/+page.svelte b/frontend/src/routes/dashboard/usage/+page.svelte new file mode 100644 index 0000000..e7fdbed --- /dev/null +++ b/frontend/src/routes/dashboard/usage/+page.svelte @@ -0,0 +1,130 @@ + + + + Wrenn — Usage + + +
+ + +
+
+ +
+

+ Usage +

+

+ Resource consumption and execution metrics across your team. +

+
+ +
+ + +
+ {#if status === 'loading'} +
+
+ + + + Loading usage data... +
+
+ {:else if status === 'error'} +
+ {errorMsg} + +
+ {:else if status === 'not_available'} +
+ +
+
+
+ + + + +
+
+

+ Enterprise feature +

+

+ Usage tracking is available on Wrenn Cloud. +

+ + +
+ + + + + + + This instance is running in self-hosted mode + +
+
+ {:else} + +
+ Usage data will be displayed here. +
+ {/if} +
+
+ + +
+
diff --git a/internal/audit/logger.go b/internal/audit/logger.go index e60d8a6..72d4c4f 100644 --- a/internal/audit/logger.go +++ b/internal/audit/logger.go @@ -369,6 +369,7 @@ func (l *AuditLogger) LogHostDelete(ctx context.Context, ac auth.AuthContext, ho } // LogHostMarkedDown records a system-initiated host status transition to unreachable. +// Scoped to "team" so BYOC team members can see when their hosts go down. func (l *AuditLogger) LogHostMarkedDown(ctx context.Context, teamID, hostID pgtype.UUID) { if !teamID.Valid { return @@ -382,13 +383,14 @@ func (l *AuditLogger) LogHostMarkedDown(ctx context.Context, teamID, hostID pgty ResourceType: "host", ResourceID: optText(id.FormatHostID(hostID)), Action: "marked_down", - Scope: "admin", + Scope: "team", Status: "error", Metadata: []byte("{}"), }) } // LogHostMarkedUp records a system-initiated host status transition back to online. +// Scoped to "team" so BYOC team members can see when their hosts recover. func (l *AuditLogger) LogHostMarkedUp(ctx context.Context, teamID, hostID pgtype.UUID) { if !teamID.Valid { return @@ -402,7 +404,7 @@ func (l *AuditLogger) LogHostMarkedUp(ctx context.Context, teamID, hostID pgtype ResourceType: "host", ResourceID: optText(id.FormatHostID(hostID)), Action: "marked_up", - Scope: "admin", + Scope: "team", Status: "success", Metadata: []byte("{}"), })