IP-Scans als benannte Snapshots speichern
- "Scan speichern" friert den aktuellen Geräte-Stand als benannten Snapshot ein (saveScan in protocols.ts, tiefe Kopie) - Abschnitt "Gespeicherte Scans" auf der Protokollseite: aufklappbare Liste mit Name, Subnetz, Datum und Gerätezahl; Snapshot zeigt die damals gefundenen Geräte (DeviceCard read-only) - Scan löschen per ConfirmDialog - Snapshots bleiben am Auftrag erhalten, auch über spätere Re-Scans hinweg Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
484b5f96fa
commit
fd75748cb9
2 changed files with 140 additions and 5 deletions
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
import { saveProtocol } from './db';
|
||||
import type { Device, Measurement, Protocol } from './types';
|
||||
import type { Device, Measurement, Protocol, SavedScan } from './types';
|
||||
|
||||
/** Eindeutige ID erzeugen */
|
||||
export function uid(): string {
|
||||
|
|
@ -73,6 +73,30 @@ export function renameDevice(protocol: Protocol, clientId: string, name: string)
|
|||
if (d) d.customName = name.trim() || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Den aktuellen Geräte-Stand als benannten Scan-Snapshot einfrieren.
|
||||
* Der Snapshot ist eine tiefe Kopie — spätere Re-Scans verändern ihn nicht.
|
||||
*/
|
||||
export function saveScan(protocol: Protocol, name: string): SavedScan {
|
||||
const scan: SavedScan = {
|
||||
id: uid(),
|
||||
name: name.trim() || new Date().toLocaleString('de-DE'),
|
||||
createdAt: Date.now(),
|
||||
subnet: protocol.subnet,
|
||||
// JSON-Roundtrip löst den Svelte-State-Proxy und friert die Daten ein
|
||||
devices: JSON.parse(JSON.stringify(protocol.devices)) as Device[],
|
||||
};
|
||||
(protocol.savedScans ??= []).push(scan);
|
||||
return scan;
|
||||
}
|
||||
|
||||
/** Gespeicherten Scan-Snapshot löschen */
|
||||
export function deleteScan(protocol: Protocol, scanId: string): void {
|
||||
if (protocol.savedScans) {
|
||||
protocol.savedScans = protocol.savedScans.filter((s) => s.id !== scanId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Messung zum Protokoll hinzufügen */
|
||||
export function addMeasurement(
|
||||
protocol: Protocol,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,14 @@
|
|||
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { getProtocol, saveProtocol, deleteProtocol } from '$lib/db';
|
||||
import { addMeasurement, upsertDevice, toggleFavorite, renameDevice } from '$lib/protocols';
|
||||
import {
|
||||
addMeasurement,
|
||||
upsertDevice,
|
||||
toggleFavorite,
|
||||
renameDevice,
|
||||
saveScan,
|
||||
deleteScan,
|
||||
} from '$lib/protocols';
|
||||
import { sync } from '$lib/sync.svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
|
|
@ -26,6 +33,9 @@
|
|||
let saving = $state(false);
|
||||
let renameTarget = $state<Device | null>(null);
|
||||
let confirmDelete = $state(false);
|
||||
let saveScanOpen = $state(false);
|
||||
let expandedScan = $state<string | null>(null);
|
||||
let deleteScanId = $state<string | null>(null);
|
||||
|
||||
let appStateListener: PluginListenerHandle | null = null;
|
||||
|
||||
|
|
@ -162,6 +172,39 @@
|
|||
goto('/auftraege/');
|
||||
}
|
||||
|
||||
/** Datum + Uhrzeit kurz */
|
||||
function fmtDateTime(ts: number): string {
|
||||
return new Date(ts).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/** Vorschlag für den Scan-Namen */
|
||||
function defaultScanName(): string {
|
||||
return 'Scan ' + fmtDateTime(Date.now());
|
||||
}
|
||||
|
||||
/** Aktuellen Geräte-Stand als Snapshot speichern */
|
||||
function doSaveScan(name: string) {
|
||||
if (protocol) {
|
||||
saveScan(protocol, name);
|
||||
void persist();
|
||||
toast.show('Scan gespeichert', 'success');
|
||||
}
|
||||
saveScanOpen = false;
|
||||
}
|
||||
|
||||
function doDeleteScan() {
|
||||
if (protocol && deleteScanId) {
|
||||
deleteScan(protocol, deleteScanId);
|
||||
void persist();
|
||||
}
|
||||
deleteScanId = null;
|
||||
}
|
||||
|
||||
function measurementsFor(deviceClientId: string) {
|
||||
return protocol?.measurements.filter((m) => m.deviceClientId === deviceClientId) ?? [];
|
||||
}
|
||||
|
|
@ -246,9 +289,16 @@
|
|||
|
||||
<!-- Geräte -->
|
||||
<section class="px-3 pb-3">
|
||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">
|
||||
Geräte ({protocol.devices.length})
|
||||
</h2>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-zinc-300">
|
||||
Geräte ({protocol.devices.length})
|
||||
</h2>
|
||||
{#if protocol.devices.length > 0}
|
||||
<button class="text-xs text-sky-300 active:text-sky-200" onclick={() => (saveScanOpen = true)}>
|
||||
Scan speichern
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if protocol.devices.length === 0}
|
||||
<p class="text-xs text-zinc-500">
|
||||
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
||||
|
|
@ -266,6 +316,45 @@
|
|||
{/each}
|
||||
</section>
|
||||
|
||||
<!-- Gespeicherte Scans -->
|
||||
{#if protocol.savedScans && protocol.savedScans.length > 0}
|
||||
<section class="px-3 pb-3">
|
||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Gespeicherte Scans</h2>
|
||||
{#each protocol.savedScans as scan (scan.id)}
|
||||
<div class="mb-1.5 rounded-lg bg-zinc-900">
|
||||
<button
|
||||
class="flex w-full items-center justify-between gap-2 p-2.5 text-left"
|
||||
onclick={() => (expandedScan = expandedScan === scan.id ? null : scan.id)}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-medium">{scan.name}</div>
|
||||
<div class="text-[11px] text-zinc-500">
|
||||
{scan.devices.length} Geräte · {scan.subnet || '—'} · {fmtDateTime(scan.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<Icons.ChevronDown
|
||||
size={16}
|
||||
class="shrink-0 text-zinc-500 {expandedScan === scan.id ? 'rotate-180' : ''}"
|
||||
/>
|
||||
</button>
|
||||
{#if expandedScan === scan.id}
|
||||
<div class="border-t border-zinc-800 p-2.5">
|
||||
{#each scan.devices as d (d.clientId)}
|
||||
<DeviceCard device={d} />
|
||||
{/each}
|
||||
<button
|
||||
class="mt-1 text-xs text-red-400 underline"
|
||||
onclick={() => (deleteScanId = scan.id)}
|
||||
>
|
||||
Scan löschen
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="px-3">
|
||||
<button class="text-xs text-red-400 underline" onclick={() => (confirmDelete = true)}>
|
||||
Protokoll löschen
|
||||
|
|
@ -315,6 +404,28 @@
|
|||
oncancel={() => (confirmDelete = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if saveScanOpen}
|
||||
<TextPromptDialog
|
||||
title="Scan speichern"
|
||||
label="Name des Scans"
|
||||
value={defaultScanName()}
|
||||
placeholder="z.B. Erdgeschoss"
|
||||
onsubmit={doSaveScan}
|
||||
oncancel={() => (saveScanOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if deleteScanId}
|
||||
<ConfirmDialog
|
||||
title="Scan löschen?"
|
||||
message="Der gespeicherte Scan-Snapshot wird entfernt."
|
||||
confirmLabel="Löschen"
|
||||
danger
|
||||
onconfirm={doDeleteScan}
|
||||
oncancel={() => (deleteScanId = null)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in a new issue