Skip to content
Open
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
59 changes: 59 additions & 0 deletions ui/src/lib/SnapshotService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export interface SnapshotConnection {
srcDev: any;
src: any;
dstDev: any;
dst: any;
Comment on lines +1 to +5
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The any type removes type safety. Consider defining more specific types for these connection properties based on the actual data structure used in the application.

Suggested change
export interface SnapshotConnection {
srcDev: any;
src: any;
dstDev: any;
dst: any;
export interface Device {
id: string;
name: string;
// Add other relevant properties as needed
}
export interface Source {
id: string;
name: string;
// Add other relevant properties as needed
}
export interface SnapshotConnection {
srcDev: Device;
src: Source;
dstDev: Device;
dst: Source;

Copilot uses AI. Check for mistakes.
}

export interface Snapshot {
id: string;
name: string;
timestamp: number;
connections: SnapshotConnection[];
}

const STORAGE_KEY = 'nmos_crosspoint_snapshots';

class _SnapshotService {
saveSnapshot(name: string, connections: SnapshotConnection[]): Snapshot {
const snapshot: Snapshot = {
id: crypto.randomUUID(),
name,
timestamp: Date.now(),
connections: structuredClone(connections)
};

const snapshots = this.getAllSnapshots();
snapshots.push(snapshot);
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshots));

return snapshot;
}

getAllSnapshots(): Snapshot[] {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (data) {
return JSON.parse(data);
}
} catch (e) {
console.error('Error loading snapshots:', e);
}
return [];
}

getSnapshot(id: string): Snapshot | null {
const snapshots = this.getAllSnapshots();
const filtered = snapshots.filter(s => s.id === id);
return filtered.length > 0 ? filtered[0] : null;
}

deleteSnapshot(id: string): void {
const snapshots = this.getAllSnapshots();
const filtered = snapshots.filter(s => s.id !== id);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
}
}

const SnapshotService = new _SnapshotService();
export default SnapshotService;
219 changes: 218 additions & 1 deletion ui/src/routes/crosspoint.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import { onDestroy, onMount } from "svelte";
import { createEventDispatcher } from 'svelte';

import { Icon, ChevronRight, VideoCamera, Microphone, CodeBracketSquare, MagnifyingGlass, SpeakerWave, Tv,Pencil, Eye, EyeSlash, Link, InformationCircle } from "svelte-hero-icons";
import { Icon, ChevronRight, VideoCamera, Microphone, CodeBracketSquare, MagnifyingGlass, SpeakerWave, Tv,Pencil, Eye, EyeSlash, Link, InformationCircle, Camera, ArrowUturnLeft } from "svelte-hero-icons";
import { getSearchTokens, tokenSearch } from "../lib/functions";
import OverlayMenuService from "../lib/OverlayMenu/OverlayMenuService";
import SnapshotService, { type Snapshot } from "../lib/SnapshotService";

interface CrosspointConnect {
source:string,
Expand Down Expand Up @@ -40,6 +41,14 @@

let sync:Subject<any> ;

// Snapshot state variables
let snapshots: Snapshot[] = [];
let snapshotName: string = "";
let showSnapshotDialog: boolean = false;
let showRecallDialog: boolean = false;
let saveSnapshotModal: any;
let recallSnapshotModal: any;

let flowTypes = ["video", "audio", "data", "mqtt", "websocket", "audiochannel", "unknown"];


Expand Down Expand Up @@ -97,6 +106,9 @@
}
}catch(e){}

// Load snapshots
loadSnapshots();

sync = ServerConnector.sync("crosspoint");
sync.subscribe((obj:any)=>{
sourceState = obj;
Expand Down Expand Up @@ -772,6 +784,122 @@
labelModal.close()
}

// Snapshot functions
function loadSnapshots() {
snapshots = SnapshotService.getAllSnapshots();
}

function openSaveSnapshotDialog() {
if (preparedConnectList.length === 0) {
ServerConnector.addFeedback({
message: "No connections to save. Prepare connections first.",
level: "warning"
});
return;
}
const now = new Date();
snapshotName = `Snapshot ${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
saveSnapshotModal.showModal();
}

function saveCurrentSnapshot() {
if (snapshotName.trim() === "") {
ServerConnector.addFeedback({
message: "Please enter a snapshot name",
level: "warning"
});
return;
}
SnapshotService.saveSnapshot(snapshotName, preparedConnectList);
loadSnapshots();
saveSnapshotModal.close();
ServerConnector.addFeedback({
message: `Snapshot "${snapshotName}" saved successfully`,
level: "success"
});
}

function openRecallDialog() {
loadSnapshots();
recallSnapshotModal.showModal();
}

function recallSnapshot(snapshotId: string) {
const snapshot = SnapshotService.getSnapshot(snapshotId);
if (!snapshot) {
ServerConnector.addFeedback({
message: "Snapshot not found",
level: "error"
});
return;
}

// Clear current prepared connections
preparedConnectList = [];

// Recall each connection from the snapshot
let recallPromises: Promise<any>[] = [];
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Promise<any> type is too broad. Consider using a more specific type to improve type safety and code maintainability.

Copilot uses AI. Check for mistakes.
snapshot.connections.forEach((conn) => {
let srcString = getDevcieNameString(conn.srcDev, conn.src);
let dstString = getDevcieNameString(conn.dstDev, conn.dst);
Comment on lines +843 to +844
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'getDevcieNameString' to 'getDeviceNameString'.

Suggested change
let srcString = getDevcieNameString(conn.srcDev, conn.src);
let dstString = getDevcieNameString(conn.dstDev, conn.dst);
let srcString = getDeviceNameString(conn.srcDev, conn.src);
let dstString = getDeviceNameString(conn.dstDev, conn.dst);

Copilot uses AI. Check for mistakes.

let promise = ServerConnector.post("makeconnection", {
prepare: true,
source: srcString,
destination: dstString
}).then((response) => {
let newList: any[] = [];
response.data.connections.forEach((c: any) => {
newList.push({
srcDev: c.srcDev,
src: c.src,
dstDev: c.dstDev,
dst: c.dst
});
});
return newList;
}).catch((e) => {
ServerConnector.addFeedback({
message: "Error recalling connection: " + e.message,
level: "error"
});
return [];
});
recallPromises.push(promise);
});

Promise.all(recallPromises).then((results) => {
results.forEach((newList) => {
cleanPreparedConnections(newList);
});
receivers = [...receivers];
updateGlobalTake();
recallSnapshotModal.close();
ServerConnector.addFeedback({
message: `Snapshot "${snapshot.name}" recalled successfully`,
level: "success"
});
});
}

function deleteSnapshot(snapshotId: string) {
const snapshot = SnapshotService.getSnapshot(snapshotId);
if (!snapshot) return;

if (confirm(`Delete snapshot "${snapshot.name}"?`)) {
SnapshotService.deleteSnapshot(snapshotId);
loadSnapshots();
ServerConnector.addFeedback({
message: `Snapshot "${snapshot.name}" deleted`,
level: "info"
});
}
}

export function openSnapshotManager() {
openRecallDialog();
}




Expand Down Expand Up @@ -813,6 +941,20 @@
<input on:input={()=>changeFilter()} bind:checked={filter.showAudioChannels} type="checkbox" class="toggle" />
</label>
</li>

<li>
<button on:click={openSaveSnapshotDialog} class="btn btn-sm btn-ghost gap-2">
<Icon src={Camera} size="20"></Icon>
<span>Save Snapshot</span>
</button>
</li>

<li>
<button on:click={openRecallDialog} class="btn btn-sm btn-ghost gap-2">
<Icon src={ArrowUturnLeft} size="20"></Icon>
<span>Recall Snapshot</span>
</button>
</li>
</ul>


Expand Down Expand Up @@ -1022,4 +1164,79 @@
</form>
</div>
</div>
</dialog>

<!-- Save Snapshot Dialog -->
<dialog bind:this={saveSnapshotModal} class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
</form>
<h3 class="font-bold text-lg">Save Snapshot</h3>
<p class="py-2">Connections to save: {preparedConnectList.length}</p>
<div class="form-control">
<label class="label">
<span class="label-text">Snapshot Name</span>
</label>
<input
bind:value={snapshotName}
type="text"
placeholder="Enter snapshot name"
class="input input-bordered w-full"
on:keypress={(e)=>{if(e.keyCode == 13) saveCurrentSnapshot()}}
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyCode property is deprecated. Use e.key === 'Enter' instead for better browser compatibility and adherence to modern standards.

Suggested change
on:keypress={(e)=>{if(e.keyCode == 13) saveCurrentSnapshot()}}
on:keypress={(e)=>{if(e.key === 'Enter') saveCurrentSnapshot()}}

Copilot uses AI. Check for mistakes.
/>
</div>
<div class="modal-action">
<form method="dialog">
<button on:click={saveCurrentSnapshot} class="btn btn-primary">Save</button>
<button class="btn">Cancel</button>
</form>
</div>
</div>
</dialog>

<!-- Recall Snapshot Dialog -->
<dialog bind:this={recallSnapshotModal} class="modal">
<div class="modal-box" style="max-width:80%;">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
</form>
<h3 class="font-bold text-lg">Recall Snapshot</h3>

{#if snapshots.length === 0}
<p class="py-4">No snapshots saved yet.</p>
{:else}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Connections</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each snapshots as snapshot}
<tr>
<td>{snapshot.name}</td>
<td>{new Date(snapshot.timestamp).toLocaleString()}</td>
<td>{snapshot.connections.length}</td>
<td>
<button on:click={() => recallSnapshot(snapshot.id)} class="btn btn-sm btn-primary">Recall</button>
<button on:click={() => deleteSnapshot(snapshot.id)} class="btn btn-sm btn-error">Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}

<div class="modal-action">
<form method="dialog">
<button class="btn">Close</button>
</form>
</div>
</div>
</dialog>