Add configurable history and storage retention policies
This commit is contained in:
@@ -41,5 +41,6 @@ export async function GET({ params }: { params: { id: string } }) {
|
||||
|
||||
const events = db().prepare('SELECT id, created_at, message FROM campaign_events WHERE deployment_id = ? ORDER BY id').all(params.id);
|
||||
const watchdog = db().prepare('SELECT incident_id FROM watchdog_actions WHERE campaign_id = ? LIMIT 1').get(params.id) as { incident_id: string } | undefined;
|
||||
return json({ deployment: dep, servers, files, scripts, events, watchdogIncidentId: watchdog?.incident_id ?? null });
|
||||
const fileContentsExpired = !!db().prepare("SELECT 1 FROM retention_expirations WHERE category='file-content' AND record_id=?").get(params.id);
|
||||
return json({ fileContentsExpired, deployment: dep, servers, files, scripts, events, watchdogIncidentId: watchdog?.incident_id ?? null });
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { db } from '$lib/server/db/index.js';
|
||||
// so the DiffViewer can render a unified diff without a second query.
|
||||
|
||||
export async function GET({ params }: { params: { id: string; fileId: string } }) {
|
||||
if (db().prepare("SELECT 1 FROM retention_expirations WHERE category='file-content' AND record_id=?").get(params.id)) {
|
||||
return json({ error: 'File contents expired under the retention policy.' }, { status: 410 });
|
||||
}
|
||||
const row = db()
|
||||
.prepare(
|
||||
`SELECT verb, rel_path, sha256_before, sha256_after, content_before, content_after
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServer } from '$lib/server/fleet/servers.js';
|
||||
import { listTelemetryRequests } from '$lib/server/fleet/telemetry-requests.js';
|
||||
import { TELEMETRY_REQUEST_RETENTION } from '$lib/telemetry-diagnostics.js';
|
||||
import { listTelemetryRequests, telemetryRetention } from '$lib/server/fleet/telemetry-requests.js';
|
||||
|
||||
export async function GET({ params }: { params: { name: string } }) {
|
||||
if (!getServer(params.name)) return json({ ok: false, error: 'no such server' }, { status: 404 });
|
||||
return json({ ok: true, requests: listTelemetryRequests(params.name), retention: TELEMETRY_REQUEST_RETENTION }, {
|
||||
return json({ ok: true, requests: listTelemetryRequests(params.name), retention: telemetryRetention() }, {
|
||||
headers: { 'Cache-Control': 'no-store' }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { retentionPreview, retentionControls, sampleRetentionPreview, setRetentionControl } from '$lib/server/retention.js';
|
||||
import { gameFileRetentionPreview } from '$lib/server/game-files/index.js';
|
||||
|
||||
export function GET() {
|
||||
return json({ policies: [...retentionPreview(),...sampleRetentionPreview(),...gameFileRetentionPreview()], controls: retentionControls() }, { headers: { 'Cache-Control': 'no-store' } });
|
||||
}
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
try { setRetentionControl(await request.json()); return json({ ok: true }); }
|
||||
catch (error) { return json({ error: (error as Error).message }, { status: 400 }); }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { validateFleetSettings } from '$lib/fleet-settings.js';
|
||||
import { retentionPreview,retentionControls,sampleRetentionPreview } from '$lib/server/retention.js';
|
||||
import { gameFileRetentionPreview } from '$lib/server/game-files/index.js';
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
try {
|
||||
const settings = validateFleetSettings(await request.json());
|
||||
return json({ policies:[...retentionPreview(settings),...sampleRetentionPreview(settings),...gameFileRetentionPreview(settings)],controls:retentionControls() },{ headers:{'Cache-Control':'no-store'} });
|
||||
} catch (error) { return json({error:(error as Error).message},{status:400}); }
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
attempts: number; error: string | null;
|
||||
}
|
||||
interface Detail {
|
||||
fileContentsExpired?: boolean;
|
||||
deployment: {
|
||||
id: string; action: string; started_at: string;
|
||||
finished_at: string | null; triggered_by: string | null; beta_id?: number | null;
|
||||
@@ -96,7 +97,7 @@
|
||||
diffOpen = true; diffLoading = true; diffError = ''; openFile = null;
|
||||
try {
|
||||
const response = await fetch('/api/campaigns/' + id + '/file/' + f.id);
|
||||
if (!response.ok) throw new Error('Could not load this file comparison. Close and select it again to retry.');
|
||||
if (!response.ok) throw new Error(response.status === 410 ? 'File contents expired under the retention policy.' : 'Could not load this file comparison. Close and select it again to retry.');
|
||||
const body = await response.json();
|
||||
if (id === $page.params.id && current === diffRequest) openFile = {id:f.id, ...body};
|
||||
} catch (error) { if (id === $page.params.id && current === diffRequest) diffError = (error as Error).message; }
|
||||
@@ -139,6 +140,7 @@
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/campaigns" />
|
||||
{#if detail.fileContentsExpired}<p role="status">File contents have expired under the retention policy. File comparisons and rollback are unavailable.</p>{/if}
|
||||
|
||||
<section class="outcome" aria-label="Campaign outcome">
|
||||
<h2>{completed} / {detail.servers.length} servers activated{unresolved ? ` · ${unresolved} unresolved` : ''}{withdrawn ? ` · ${withdrawn} withdrawn` : ''}{deletedIncomplete ? ` · ${deletedIncomplete} deleted before activation` : ''}</h2>
|
||||
@@ -209,7 +211,7 @@
|
||||
<h3><HistoricalServerIdentity name={srv} deletedAt={detail.servers.find(server => server.server === srv)?.server_deleted_at} /></h3>
|
||||
<div class="file-rows">
|
||||
{#each files as f (f.id)}
|
||||
<button class="file-row" onclick={() => loadDiff(f)}>
|
||||
<button class="file-row" disabled={detail.fileContentsExpired} onclick={() => loadDiff(f)}>
|
||||
<Badge variant={verbVariant(f.verb)} size="sm" uppercase>{f.verb}</Badge>
|
||||
{#if f.apply_state !== 'applied'}<Badge variant="neutral" size="sm">{f.apply_state === 'pending' ? 'Queued' : f.apply_state === 'applying' ? 'Needs inspection' : 'Cancelled'}</Badge>{/if}
|
||||
<span class="path">{f.rel_path}</span>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import RetentionTools from '$ui/components/RetentionTools.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import { defaultFleetSettings, fleetSettingLabels, additionalConcurrencyKeys, concurrencyKeys, retentionKeys, settingHelp, type FleetSettings } from '$lib/fleet-settings.js';
|
||||
import { defaultFleetSettings, fleetSettingLabels, additionalConcurrencyKeys, concurrencyKeys, retentionKeys, settingHelp, settingMinimum, type FleetSettings } from '$lib/fleet-settings.js';
|
||||
let settings = $state<FleetSettings | null>(null);
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
@@ -52,14 +53,15 @@
|
||||
<fieldset disabled={saving}>
|
||||
{#each section.keys as key}
|
||||
<label for={key}>{fleetSettingLabels[key]}{key.endsWith('Days') ? ' (days)' : ''}</label>
|
||||
<input id={key} type="number" min={key === 'observationKeepDays' || additionalConcurrencyKeys.includes(key) ? 0 : 1}
|
||||
<input id={key} type="number" min={settingMinimum(key)}
|
||||
max={key.endsWith('Days') ? 36500 : undefined} step="1" required bind:value={settings[key]} aria-describedby={`${key}-help`} />
|
||||
<p id={`${key}-help`}>Default: {defaultFleetSettings[key]}. {settingHelp[key] ?? ''} {additionalConcurrencyKeys.includes(key) ? '0 adds no extra limit.' : ''}</p>
|
||||
<p id={`${key}-help`}>Default: {defaultFleetSettings[key]}. {settingHelp[key] ?? ''} {additionalConcurrencyKeys.includes(key) ? '0 adds no extra limit.' : key === 'versionKeepDays' ? '0 disables age expiry; the version count policy may still apply.' : key.endsWith('Days') && key !== 'gameFileGraceDays' ? '0 keeps indefinitely.' : ''}</p>
|
||||
{/each}
|
||||
{#if section.title === 'Concurrency'}<p>Lowering a limit lets active operations finish; queued work waits for capacity. Player safety checks still apply.</p>{/if}
|
||||
<Button type="submit" disabled={saving}>{saving ? 'Saving…' : `Save ${section.title.toLowerCase()}`}</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{#if section.title === 'Retention'}<RetentionTools {settings} />{/if}
|
||||
</section>
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user