forked from wrenn/wrenn
v0.0.1 (#8)
Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com> Reviewed-on: wrenn/sandbox#8
This commit is contained in:
38
frontend/src/lib/api/audit.ts
Normal file
38
frontend/src/lib/api/audit.ts
Normal 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 : ''}`);
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
76
frontend/src/lib/api/builds.ts
Normal file
76
frontend/src/lib/api/builds.ts
Normal 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`);
|
||||
}
|
||||
@ -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>> {
|
||||
|
||||
72
frontend/src/lib/api/channels.ts
Normal file
72
frontend/src/lib/api/channels.ts
Normal 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 });
|
||||
}
|
||||
84
frontend/src/lib/api/hosts.ts
Normal file
84
frontend/src/lib/api/hosts.ts
Normal 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(' · ') || '—';
|
||||
}
|
||||
25
frontend/src/lib/api/metrics.ts
Normal file
25
frontend/src/lib/api/metrics.ts
Normal 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;
|
||||
44
frontend/src/lib/api/stats.ts
Normal file
44
frontend/src/lib/api/stats.ts
Normal 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',
|
||||
};
|
||||
85
frontend/src/lib/api/team.ts
Normal file
85
frontend/src/lib/api/team.ts
Normal 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)}`);
|
||||
}
|
||||
Reference in New Issue
Block a user