1
0
forked from wrenn/wrenn
Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com>
Reviewed-on: wrenn/sandbox#8
This commit is contained in:
2026-04-09 19:24:49 +00:00
parent 32e5a5a715
commit d3e4812e46
199 changed files with 24552 additions and 2776 deletions

View File

@ -0,0 +1,38 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type AuditLog = {
id: string;
actor_type: 'user' | 'api_key' | 'system';
actor_id?: string;
actor_name?: string;
resource_type: string;
resource_id?: string;
action: string;
scope: 'team' | 'admin';
status: 'success' | 'info' | 'warning' | 'error';
metadata?: Record<string, unknown>;
created_at: string;
};
export type AuditListResponse = {
items: AuditLog[];
next_before?: string;
next_before_id?: string;
};
export async function listAuditLogs(params?: {
before?: string;
before_id?: string;
resource_types?: string[];
actions?: string[];
limit?: number;
}): Promise<ApiResult<AuditListResponse>> {
const q = new URLSearchParams();
if (params?.before) q.set('before', params.before);
if (params?.before_id) q.set('before_id', params.before_id);
params?.resource_types?.forEach((t) => q.append('resource_type', t));
params?.actions?.forEach((a) => q.append('action', a));
if (params?.limit != null) q.set('limit', String(params.limit));
const qs = q.toString();
return apiFetch('GET', `/api/v1/audit-logs${qs ? '?' + qs : ''}`);
}

View File

@ -3,6 +3,7 @@ export type AuthResponse = {
user_id: string;
team_id: string;
email: string;
name: string;
};
export type AuthResult = { ok: true; data: AuthResponse } | { ok: false; error: string };
@ -11,8 +12,8 @@ export async function apiLogin(email: string, password: string): Promise<AuthRes
return authFetch('/api/v1/auth/login', { email, password });
}
export async function apiSignup(email: string, password: string): Promise<AuthResult> {
return authFetch('/api/v1/auth/signup', { email, password });
export async function apiSignup(email: string, password: string, name: string): Promise<AuthResult> {
return authFetch('/api/v1/auth/signup', { email, password, name });
}
async function authFetch(url: string, body: Record<string, string>): Promise<AuthResult> {

View File

@ -0,0 +1,76 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type BuildLogEntry = {
step: number;
phase: string; // "pre-build", "recipe", or "post-build"
cmd: string;
stdout: string;
stderr: string;
exit: number;
ok: boolean;
elapsed_ms: number;
};
export type Build = {
id: string;
name: string;
base_template: string;
recipe: string[];
healthcheck?: string;
vcpus: number;
memory_mb: number;
status: string;
current_step: number;
total_steps: number;
logs: BuildLogEntry[];
error?: string;
sandbox_id?: string;
host_id?: string;
created_at: string;
started_at?: string;
completed_at?: string;
};
export type CreateBuildParams = {
name: string;
base_template?: string;
recipe: string[];
healthcheck?: string;
vcpus?: number;
memory_mb?: number;
skip_pre_post?: boolean;
};
export async function createBuild(params: CreateBuildParams): Promise<ApiResult<Build>> {
return apiFetch('POST', '/api/v1/admin/builds', params);
}
export async function listBuilds(): Promise<ApiResult<Build[]>> {
return apiFetch('GET', '/api/v1/admin/builds');
}
export async function getBuild(id: string): Promise<ApiResult<Build>> {
return apiFetch('GET', `/api/v1/admin/builds/${id}`);
}
export type AdminTemplate = {
name: string;
type: string;
vcpus: number;
memory_mb: number;
size_bytes: number;
team_id: string;
created_at: string;
};
export async function listAdminTemplates(): Promise<ApiResult<AdminTemplate[]>> {
return apiFetch('GET', '/api/v1/admin/templates');
}
export async function deleteAdminTemplate(name: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/admin/templates/${name}`);
}
export async function cancelBuild(id: string): Promise<ApiResult<void>> {
return apiFetch('POST', `/api/v1/admin/builds/${id}/cancel`);
}

View File

@ -20,6 +20,10 @@ export async function listCapsules(): Promise<ApiResult<Capsule[]>> {
return apiFetch('GET', '/api/v1/sandboxes');
}
export async function getCapsule(id: string): Promise<ApiResult<Capsule>> {
return apiFetch('GET', `/api/v1/sandboxes/${id}`);
}
export type CreateCapsuleParams = {
template?: string;
vcpus?: number;
@ -50,6 +54,7 @@ export type Snapshot = {
memory_mb?: number;
size_bytes: number;
created_at: string;
platform: boolean;
};
export async function createSnapshot(sandboxId: string, name?: string): Promise<ApiResult<Snapshot>> {

View File

@ -0,0 +1,72 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type Channel = {
id: string;
team_id: string;
name: string;
provider: string;
events: string[];
created_at: string;
updated_at: string;
secret?: string; // only present immediately after creation (webhook provider)
};
export const PROVIDERS = [
{ value: 'discord', label: 'Discord', fields: ['webhook_url'] },
{ value: 'slack', label: 'Slack', fields: ['webhook_url'] },
{ value: 'teams', label: 'Teams', fields: ['webhook_url'] },
{ value: 'googlechat', label: 'Google Chat', fields: ['webhook_url'] },
{ value: 'telegram', label: 'Telegram', fields: ['bot_token', 'chat_id'] },
{ value: 'matrix', label: 'Matrix', fields: ['homeserver_url', 'access_token', 'room_id'] },
{ value: 'webhook', label: 'Webhook', fields: ['url'] }
] as const;
export const EVENT_TYPES = [
{ value: 'capsule.created', group: 'Capsule' },
{ value: 'capsule.running', group: 'Capsule' },
{ value: 'capsule.paused', group: 'Capsule' },
{ value: 'capsule.destroyed', group: 'Capsule' },
{ value: 'template.snapshot.created', group: 'Template' },
{ value: 'template.snapshot.deleted', group: 'Template' },
{ value: 'host.up', group: 'Host' },
{ value: 'host.down', group: 'Host' }
] as const;
export async function listChannels(): Promise<ApiResult<Channel[]>> {
return apiFetch('GET', '/api/v1/channels');
}
export async function createChannel(
name: string,
provider: string,
config: Record<string, string>,
events: string[]
): Promise<ApiResult<Channel>> {
return apiFetch('POST', '/api/v1/channels', { name, provider, config, events });
}
export async function updateChannel(
id: string,
name: string,
events: string[]
): Promise<ApiResult<Channel>> {
return apiFetch('PATCH', `/api/v1/channels/${id}`, { name, events });
}
export async function deleteChannel(id: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/channels/${id}`);
}
export async function rotateConfig(
id: string,
config: Record<string, string>
): Promise<ApiResult<Channel>> {
return apiFetch('PUT', `/api/v1/channels/${id}/config`, { config });
}
export async function testChannel(
provider: string,
config: Record<string, string>
): Promise<ApiResult<{ status: string }>> {
return apiFetch('POST', '/api/v1/channels/test', { provider, config });
}

View File

@ -0,0 +1,84 @@
import { apiFetch } from './client';
export type Host = {
id: string;
type: 'regular' | 'byoc';
team_id?: string;
team_name?: string;
provider?: string;
availability_zone?: string;
arch?: string;
cpu_cores?: number;
memory_mb?: number;
disk_gb?: number;
address?: string;
status: 'pending' | 'online' | 'offline' | 'unreachable' | 'draining';
last_heartbeat_at?: string;
created_by: string;
created_at: string;
updated_at: string;
};
export type CreateHostParams = {
type: 'regular' | 'byoc';
team_id?: string;
provider?: string;
availability_zone?: string;
};
export type CreateHostResult = {
host: Host;
registration_token: string;
};
export async function listHosts(): Promise<{ ok: true; data: Host[] } | { ok: false; error: string }> {
return apiFetch<Host[]>('GET', '/api/v1/hosts');
}
export async function createHost(
params: CreateHostParams
): Promise<{ ok: true; data: CreateHostResult } | { ok: false; error: string }> {
return apiFetch<CreateHostResult>('POST', '/api/v1/hosts', params);
}
export async function deleteHost(
id: string,
force = false
): Promise<{ ok: true } | { ok: false; error: string; sandbox_ids?: string[] }> {
const url = `/api/v1/hosts/${id}${force ? '?force=true' : ''}`;
const res = await apiFetch<void>('DELETE', url);
if (!res.ok) {
return res as { ok: false; error: string };
}
return { ok: true };
}
export async function getDeletePreview(
id: string
): Promise<{ ok: true; data: { host: Host; sandbox_ids: string[] } } | { ok: false; error: string }> {
return apiFetch<{ host: Host; sandbox_ids: string[] }>('GET', `/api/v1/hosts/${id}/delete-preview`);
}
export function statusColor(status: Host['status']): string {
switch (status) {
case 'online':
return 'var(--color-accent)';
case 'pending':
return 'var(--color-amber)';
case 'offline':
case 'unreachable':
return 'var(--color-red)';
case 'draining':
return 'var(--color-blue)';
default:
return 'var(--color-text-muted)';
}
}
export function formatSpecs(host: Host): string {
const parts: string[] = [];
if (host.cpu_cores) parts.push(`${host.cpu_cores} vCPU`);
if (host.memory_mb) parts.push(`${Math.round(host.memory_mb / 1024)}GB RAM`);
if (host.disk_gb) parts.push(`${host.disk_gb}GB disk`);
return parts.join(' · ') || '—';
}

View File

@ -0,0 +1,25 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type MetricRange = '5m' | '10m' | '1h' | '6h' | '24h';
export type MetricPoint = {
timestamp_unix: number;
cpu_pct: number;
mem_bytes: number;
disk_bytes: number;
};
export type MetricsResponse = {
sandbox_id: string;
range: MetricRange;
points: MetricPoint[];
};
export async function fetchSandboxMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
return apiFetch('GET', `/api/v1/sandboxes/${id}/metrics?range=${range}`);
}
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h'];
// All ranges poll every 10 seconds.
export const METRIC_POLL_INTERVAL = 10_000;

View File

@ -0,0 +1,44 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type TimeRange = '5m' | '1h' | '6h' | '24h' | '30d';
export type StatsResponse = {
range: TimeRange;
current: {
running_count: number;
vcpus_reserved: number;
memory_mb_reserved: number;
sampled_at?: string;
};
peaks: {
running_count: number;
vcpus: number;
memory_mb: number;
};
series: {
labels: string[];
running: number[];
vcpus: number[];
memory_mb: number[];
};
};
export async function fetchStats(range: TimeRange): Promise<ApiResult<StatsResponse>> {
return apiFetch('GET', `/api/v1/sandboxes/stats?range=${range}`);
}
export const POLL_INTERVALS: Record<TimeRange, number> = {
'5m': 15_000,
'1h': 30_000,
'6h': 60_000,
'24h': 120_000,
'30d': 300_000,
};
export const RANGE_LABELS: Record<TimeRange, string> = {
'5m': '5m',
'1h': '1h',
'6h': '6h',
'24h': '24h',
'30d': '30d',
};

View File

@ -0,0 +1,85 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
export type TeamMember = {
user_id: string;
name: string;
email: string;
role: 'owner' | 'admin' | 'member';
joined_at: string;
};
export type TeamInfo = {
id: string;
name: string;
slug: string;
created_at: string;
};
export type TeamDetail = {
team: TeamInfo;
members: TeamMember[];
};
export type UserSearchResult = {
user_id: string;
email: string;
};
export type TeamWithRole = {
id: string;
name: string;
slug: string;
is_byoc: boolean;
created_at: string;
role: string;
};
export async function listTeams(): Promise<ApiResult<TeamWithRole[]>> {
return apiFetch('GET', '/api/v1/teams');
}
export async function createTeam(name: string): Promise<ApiResult<TeamWithRole>> {
return apiFetch('POST', '/api/v1/teams', { name });
}
export async function switchTeam(
teamId: string
): Promise<ApiResult<{ token: string; user_id: string; team_id: string; email: string; name: string }>> {
return apiFetch('POST', '/api/v1/auth/switch-team', { team_id: teamId });
}
export async function getTeam(id: string): Promise<ApiResult<TeamDetail>> {
return apiFetch('GET', `/api/v1/teams/${id}`);
}
export async function updateTeam(id: string, name: string): Promise<ApiResult<void>> {
return apiFetch('PATCH', `/api/v1/teams/${id}`, { name });
}
export async function addMember(id: string, email: string): Promise<ApiResult<TeamMember>> {
return apiFetch('POST', `/api/v1/teams/${id}/members`, { email });
}
export async function removeMember(id: string, userId: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/teams/${id}/members/${userId}`);
}
export async function updateMemberRole(
id: string,
userId: string,
role: 'admin' | 'member'
): Promise<ApiResult<void>> {
return apiFetch('PATCH', `/api/v1/teams/${id}/members/${userId}`, { role });
}
export async function deleteTeam(id: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/teams/${id}`);
}
export async function leaveTeam(id: string): Promise<ApiResult<void>> {
return apiFetch('POST', `/api/v1/teams/${id}/leave`);
}
export async function searchUsers(email: string): Promise<ApiResult<UserSearchResult[]>> {
return apiFetch('GET', `/api/v1/users/search?email=${encodeURIComponent(email)}`);
}