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 { saveProtocol } from './db';
|
||||||
import type { Device, Measurement, Protocol } from './types';
|
import type { Device, Measurement, Protocol, SavedScan } from './types';
|
||||||
|
|
||||||
/** Eindeutige ID erzeugen */
|
/** Eindeutige ID erzeugen */
|
||||||
export function uid(): string {
|
export function uid(): string {
|
||||||
|
|
@ -73,6 +73,30 @@ export function renameDevice(protocol: Protocol, clientId: string, name: string)
|
||||||
if (d) d.customName = name.trim() || undefined;
|
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 */
|
/** Messung zum Protokoll hinzufügen */
|
||||||
export function addMeasurement(
|
export function addMeasurement(
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,14 @@
|
||||||
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import { getProtocol, saveProtocol, deleteProtocol } from '$lib/db';
|
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 { sync } from '$lib/sync.svelte';
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { pushOverlay } from '$lib/overlay.svelte';
|
import { pushOverlay } from '$lib/overlay.svelte';
|
||||||
|
|
@ -26,6 +33,9 @@
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let renameTarget = $state<Device | null>(null);
|
let renameTarget = $state<Device | null>(null);
|
||||||
let confirmDelete = $state(false);
|
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;
|
let appStateListener: PluginListenerHandle | null = null;
|
||||||
|
|
||||||
|
|
@ -162,6 +172,39 @@
|
||||||
goto('/auftraege/');
|
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) {
|
function measurementsFor(deviceClientId: string) {
|
||||||
return protocol?.measurements.filter((m) => m.deviceClientId === deviceClientId) ?? [];
|
return protocol?.measurements.filter((m) => m.deviceClientId === deviceClientId) ?? [];
|
||||||
}
|
}
|
||||||
|
|
@ -246,9 +289,16 @@
|
||||||
|
|
||||||
<!-- Geräte -->
|
<!-- Geräte -->
|
||||||
<section class="px-3 pb-3">
|
<section class="px-3 pb-3">
|
||||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
Geräte ({protocol.devices.length})
|
<h2 class="text-sm font-semibold text-zinc-300">
|
||||||
</h2>
|
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}
|
{#if protocol.devices.length === 0}
|
||||||
<p class="text-xs text-zinc-500">
|
<p class="text-xs text-zinc-500">
|
||||||
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
||||||
|
|
@ -266,6 +316,45 @@
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</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">
|
<div class="px-3">
|
||||||
<button class="text-xs text-red-400 underline" onclick={() => (confirmDelete = true)}>
|
<button class="text-xs text-red-400 underline" onclick={() => (confirmDelete = true)}>
|
||||||
Protokoll löschen
|
Protokoll löschen
|
||||||
|
|
@ -315,6 +404,28 @@
|
||||||
oncancel={() => (confirmDelete = false)}
|
oncancel={() => (confirmDelete = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/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}
|
{:else}
|
||||||
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue