Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 83 additions & 0 deletions app/src/lib/api/logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const API_BASE = '/api';

export interface LogEntry {
id: string;
service: string;
level: string;
classification: string;
message: string;
agent_id: string | null;
workload_id: string | null;
organization_id: string | null;
created_at: string;
}

export interface LogsResponse {
entries: LogEntry[];
total: number;
has_more: boolean;
}

export interface LogsFilter {
service?: string;
level?: string;
classification?: string;
agent_id?: string;
workload_id?: string;
organization_id?: string;
from?: string;
to?: string;
q?: string;
limit?: number;
offset?: number;
}

export interface LogsRetention {
retention_days: number;
}

function buildQuery(filter: LogsFilter): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(filter)) {
if (value !== undefined && value !== null && value !== '') {
params.set(key, String(value));
}
}
const query = params.toString();
return query ? `?${query}` : '';
}

export async function listLogs(token: string, filter: LogsFilter): Promise<LogsResponse> {
const res = await fetch(`${API_BASE}/logs${buildQuery(filter)}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`Failed to list logs: ${res.status}`);
return res.json();
}

export async function getLogsRetention(token: string): Promise<LogsRetention> {
const res = await fetch(`${API_BASE}/admin/settings/logs-retention`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`Failed to get logs retention: ${res.status}`);
return res.json();
}

export async function updateLogsRetention(
token: string,
retentionDays: number,
): Promise<LogsRetention> {
const res = await fetch(`${API_BASE}/admin/settings/logs-retention`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ retention_days: retentionDays }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.status }));
throw new Error(err.error ?? `Failed to update logs retention: ${res.status}`);
}
return res.json();
}
98 changes: 98 additions & 0 deletions app/src/lib/components/settings/logs-settings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script lang="ts">
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { auth } from "$lib/auth/store.svelte";
import { getLogsRetention, updateLogsRetention } from "$lib/api/logs";

const MIN_RETENTION_DAYS = 1;
const MAX_RETENTION_DAYS = 365;

let retentionDays = $state<number | null>(null);
let inputValue = $state("");
let loading = $state(true);
let saving = $state(false);
let error = $state<string | null>(null);
let saved = $state(false);

async function load() {
if (!auth.token) return;
loading = true;
try {
const response = await getLogsRetention(auth.token);
retentionDays = response.retention_days;
inputValue = String(response.retention_days);
error = null;
} catch (e) {
error = e instanceof Error ? e.message : "failed to load retention setting";
} finally {
loading = false;
}
}

async function save() {
if (!auth.token) return;
const days = Number(inputValue);
if (!Number.isInteger(days) || days < MIN_RETENTION_DAYS || days > MAX_RETENTION_DAYS) {
error = `retention days must be between ${MIN_RETENTION_DAYS} and ${MAX_RETENTION_DAYS}`;
return;
}
saving = true;
saved = false;
try {
const response = await updateLogsRetention(auth.token, days);
retentionDays = response.retention_days;
error = null;
saved = true;
} catch (e) {
error = e instanceof Error ? e.message : "failed to save retention setting";
} finally {
saving = false;
}
}

onMount(load);
</script>

{#if error}
<div class="mb-4 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-sm text-destructive">
{error}
</div>
{/if}

<div class="flex flex-col gap-6 max-w-2xl">
<Card.Root>
<Card.Header>
<Card.Title>Log retention</Card.Title>
<Card.Description>
Logs older than this many days are deleted automatically every hour.
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="h-9 w-32 rounded bg-muted animate-pulse"></div>
{:else}
<div class="flex items-end gap-3">
<div class="flex flex-col gap-1">
<label class="text-xs text-muted-foreground" for="retention-input">Retention (days)</label>
<Input
id="retention-input"
type="number"
min={MIN_RETENTION_DAYS}
max={MAX_RETENTION_DAYS}
bind:value={inputValue}
class="w-32"
/>
</div>
<Button onclick={save} disabled={saving || inputValue === String(retentionDays)} size="sm">
{saving ? "Saving..." : "Save"}
</Button>
{#if saved && !saving}
<span class="text-xs text-green-500">Saved</span>
{/if}
</div>
{/if}
</Card.Content>
</Card.Root>
</div>
11 changes: 9 additions & 2 deletions app/src/lib/components/sidebar/app-sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
import ChartPieIcon from "@lucide/svelte/icons/chart-pie";
import MapIcon from "@lucide/svelte/icons/map";
import BookOpenIcon from "@lucide/svelte/icons/book-open";
import { Server, Layers, ShipWheel, Container, LaptopMinimal, Settings } from "@lucide/svelte";
import {
Server,
Layers,
ShipWheel,
Container,
LaptopMinimal,
Settings,
} from "@lucide/svelte";
import IconBucket from "./icon-bucket.svelte";
import IconDashboard from "./icon-dashboard.svelte";
import IconActivity from "./icon-activity.svelte";
Expand All @@ -25,7 +32,7 @@
},
{
title: "Logs",
url: "#",
url: "/logs",
icon: IconLogs,
},
],
Expand Down
13 changes: 12 additions & 1 deletion app/src/routes/(app)/admin/settings/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import UpdateSettings from "$lib/components/settings/update-settings.svelte";
import LogsSettings from "$lib/components/settings/logs-settings.svelte";

type Tab = "general" | "update";
type Tab = "general" | "update" | "logs";
let activeTab = $state<Tab>("general");
</script>

Expand Down Expand Up @@ -31,13 +32,23 @@
>
Update
</button>
<button
onclick={() => (activeTab = "logs")}
class="px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px {activeTab === 'logs'
? 'border-foreground text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
>
Logs
</button>
</div>

<div class="mt-2">
{#if activeTab === "general"}
<p class="text-sm text-muted-foreground">No general settings configured.</p>
{:else if activeTab === "update"}
<UpdateSettings />
{:else if activeTab === "logs"}
<LogsSettings />
{/if}
</div>
</div>
Loading
Loading