Change theme
This commit is contained in:
@@ -8,7 +8,6 @@
|
|||||||
let connectionError = null;
|
let connectionError = null;
|
||||||
let processingTargets = {};
|
let processingTargets = {};
|
||||||
|
|
||||||
// Selection router state tracker
|
|
||||||
let activeFocusedInstance = null;
|
let activeFocusedInstance = null;
|
||||||
|
|
||||||
async function syncTelemetry() {
|
async function syncTelemetry() {
|
||||||
@@ -16,7 +15,6 @@
|
|||||||
instances = await fetchInstances();
|
instances = await fetchInstances();
|
||||||
connectionError = null;
|
connectionError = null;
|
||||||
|
|
||||||
// Keep our detailed view object synced with fresh status polling adjustments
|
|
||||||
if (activeFocusedInstance) {
|
if (activeFocusedInstance) {
|
||||||
const freshSnapshot = instances.find(i => i.name === activeFocusedInstance.name);
|
const freshSnapshot = instances.find(i => i.name === activeFocusedInstance.name);
|
||||||
if (freshSnapshot) activeFocusedInstance = freshSnapshot;
|
if (freshSnapshot) activeFocusedInstance = freshSnapshot;
|
||||||
@@ -47,24 +45,25 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="min-h-screen bg-neutral-950 text-neutral-100 font-mono p-6 select-none">
|
<main class="min-h-screen bg-stone-900 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-emerald-950 to-stone-950 text-stone-100 p-6 select-none">
|
||||||
<header class="border-b border-neutral-800 pb-4 mb-6 flex justify-between items-center">
|
|
||||||
|
<header class="border-b border-stone-700/60 pb-4 mb-6 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-lg font-bold tracking-tight text-amber-500">-- VSSM DASHBOARD --</h1>
|
<h1 class="text-xl font-serif font-bold tracking-wide text-amber-100/90">VSSM Dashboard</h1>
|
||||||
<p class="text-xs text-neutral-400 mt-1">VintageStory Server Manager</p>
|
<p class="text-xs text-stone-400 mt-1">VintageStory Server Manager</p>
|
||||||
</div>
|
</div>
|
||||||
{#if !activeFocusedInstance}
|
{#if !activeFocusedInstance}
|
||||||
<button
|
<button
|
||||||
on:click={syncTelemetry}
|
on:click={syncTelemetry}
|
||||||
class="border border-neutral-700 hover:border-neutral-500 hover:text-amber-400 px-3 py-1.5 text-xs uppercase tracking-wider transition-colors cursor-pointer">
|
class="border border-stone-600 bg-stone-800/60 hover:bg-stone-700 hover:text-amber-100 px-4 py-1.5 text-xs uppercase tracking-wider transition-colors cursor-pointer rounded shadow-sm">
|
||||||
Force Sync
|
Refresh Status
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if connectionError}
|
{#if connectionError}
|
||||||
<div class="bg-red-950/40 border border-red-900/60 text-red-400 p-3 text-xs mb-6 rounded flex items-center gap-2">
|
<div class="bg-red-900/20 border border-red-700/40 text-red-200 p-3 text-xs mb-6 rounded flex items-center gap-2">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span>
|
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
||||||
{connectionError}
|
{connectionError}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
let isSending = false;
|
let isSending = false;
|
||||||
let terminalContainer;
|
let terminalContainer;
|
||||||
|
|
||||||
|
|
||||||
function logsAreEqual(arr1, arr2) {
|
function logsAreEqual(arr1, arr2) {
|
||||||
if (arr1.length !== arr2.length) return false;
|
if (arr1.length !== arr2.length) return false;
|
||||||
for (let i = 0; i < arr1.length; i++) {
|
for (let i = 0; i < arr1.length; i++) {
|
||||||
@@ -19,11 +18,9 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function syncLogs() {
|
async function syncLogs() {
|
||||||
try {
|
try {
|
||||||
const logs = await fetchServerLogs(instance.name);
|
const logs = await fetchServerLogs(instance.name);
|
||||||
|
|
||||||
if (!logsAreEqual(logs, terminalLogs)) {
|
if (!logsAreEqual(logs, terminalLogs)) {
|
||||||
terminalLogs = logs;
|
terminalLogs = logs;
|
||||||
autoScrollToBottom();
|
autoScrollToBottom();
|
||||||
@@ -33,7 +30,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function autoScrollToBottom() {
|
function autoScrollToBottom() {
|
||||||
if (terminalContainer) {
|
if (terminalContainer) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -62,9 +58,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
syncLogs();
|
syncLogs();
|
||||||
// Check for new logs every second
|
|
||||||
const logInterval = setInterval(syncLogs, 1000);
|
const logInterval = setInterval(syncLogs, 1000);
|
||||||
|
|
||||||
return () => clearInterval(logInterval);
|
return () => clearInterval(logInterval);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -73,70 +67,70 @@
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
on:click={onBack}
|
on:click={onBack}
|
||||||
class="border border-neutral-700 hover:border-neutral-500 text-neutral-300 px-3 py-1 text-xs uppercase transition-colors cursor-pointer">
|
class="border border-stone-600 bg-stone-800/60 hover:bg-stone-700 text-stone-200 px-3 py-1 text-xs uppercase transition-colors cursor-pointer rounded shadow-sm">
|
||||||
← Back to Server List
|
← Back to Server List
|
||||||
</button>
|
</button>
|
||||||
<div class="h-4 w-px bg-neutral-800"></div>
|
<div class="h-4 w-px bg-stone-700"></div>
|
||||||
<div class="text-sm">
|
<div class="text-sm text-stone-300">
|
||||||
Server: <span class="text-amber-500 font-bold">{instance.name}</span>
|
Active Server: <span class="text-amber-200 font-bold">{instance.name}</span>
|
||||||
<span class="text-xs text-neutral-500">({instance.version})</span>
|
<span class="text-xs text-stone-400">({instance.version})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
<div class="lg:col-span-2 flex flex-col h-[450px] border border-neutral-800 bg-neutral-900 rounded overflow-hidden">
|
<div class="lg:col-span-2 flex flex-col h-[450px] border border-stone-700/60 bg-stone-900/60 rounded shadow-md overflow-hidden">
|
||||||
<div class="bg-neutral-950 border-b border-neutral-800 px-4 py-2 text-xs flex justify-between items-center select-none">
|
<div class="bg-stone-800/80 border-b border-stone-700/60 px-4 py-2 text-xs flex justify-between items-center select-none text-stone-300">
|
||||||
<span class="text-neutral-400 font-bold">Console Output</span>
|
<span class="font-medium">Live Server Console</span>
|
||||||
<span class="text-[10px] {instance.status === 'RUNNING' ? 'text-emerald-400' : 'text-neutral-500'}">
|
<span class="text-xs font-medium px-1.5 py-0.5 rounded {instance.status === 'RUNNING' ? 'text-emerald-300 bg-emerald-900/30' : 'text-stone-400 bg-stone-800'}">
|
||||||
● {instance.status}
|
{instance.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={terminalContainer}
|
bind:this={terminalContainer}
|
||||||
class="flex-1 p-4 overflow-y-auto text-xs space-y-1 text-neutral-300 font-mono select-text selection:bg-neutral-800">
|
class="flex-1 p-4 overflow-y-auto text-xs space-y-1 bg-stone-950/40 text-stone-200 font-mono select-text selection:bg-stone-700">
|
||||||
{#if terminalLogs.length === 0}
|
{#if terminalLogs.length === 0}
|
||||||
<div class="text-neutral-600 italic">[No console logs available.]</div>
|
<div class="text-stone-500 italic">[Waiting for log output...]</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each terminalLogs as line}
|
{#each terminalLogs as line}
|
||||||
<div class="whitespace-pre-wrap break-all leading-relaxed">{line}</div>
|
<div class="whitespace-pre-wrap break-all leading-relaxed opacity-90">{line}</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
on:submit|preventDefault={executeConsoleSubmit}
|
on:submit|preventDefault={executeConsoleSubmit}
|
||||||
class="border-t border-neutral-800 bg-neutral-950 flex items-center">
|
class="border-t border-stone-700/60 bg-stone-800/40 flex items-center">
|
||||||
<span class="pl-4 text-neutral-600 text-xs select-none">></span>
|
<span class="pl-4 text-stone-400 text-xs select-none">></span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={textCommandBuffer}
|
bind:value={textCommandBuffer}
|
||||||
disabled={instance.status !== 'RUNNING' || isSending}
|
disabled={instance.status !== 'RUNNING' || isSending}
|
||||||
placeholder={instance.status === 'RUNNING' ? "Type a server command..." : "Server is offline."}
|
placeholder={instance.status === 'RUNNING' ? "Type a command and press Enter..." : "Console is read-only while server is offline."}
|
||||||
class="w-full bg-transparent px-2 py-3 text-xs text-neutral-200 placeholder-neutral-600 focus:outline-none disabled:cursor-not-allowed" />
|
class="w-full bg-transparent px-3 py-3 text-xs text-stone-100 placeholder-stone-500 focus:outline-none disabled:cursor-not-allowed" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border border-neutral-800 bg-neutral-900/10 rounded p-4 space-y-4">
|
<div class="border border-stone-700/60 bg-stone-900/40 rounded p-4 space-y-4 shadow-md backdrop-blur-sm">
|
||||||
<h3 class="text-xs font-bold uppercase tracking-wider text-neutral-400 border-b border-neutral-800 pb-2">
|
<h3 class="text-xs font-bold uppercase tracking-wider text-stone-300 border-b border-stone-700/60 pb-2">
|
||||||
Server Operations
|
Operations
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="opacity-50 space-y-3 pointer-events-none">
|
<div class="space-y-3">
|
||||||
<button class="w-full text-left border border-neutral-800 bg-neutral-900/50 text-xs p-3 hover:border-neutral-700 transition-all rounded">
|
<button class="w-full text-left border border-stone-600 bg-stone-800/50 text-xs p-3 hover:bg-stone-800 transition-all rounded shadow-sm group">
|
||||||
<div class="font-bold text-neutral-200">Configure</div>
|
<div class="font-bold text-stone-200 group-hover:text-amber-200">Configure Server</div>
|
||||||
<div class="text-[10px] text-neutral-400 mt-0.5">Edit instance configuration.</div>
|
<div class="text-[10px] text-stone-400 mt-0.5">Edit operational settings and ports.</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="w-full text-left border border-neutral-800 bg-neutral-900/50 text-xs p-3 hover:border-neutral-700 transition-all rounded">
|
<button class="w-full text-left border border-stone-600 bg-stone-800/50 text-xs p-3 hover:bg-stone-800 transition-all rounded shadow-sm group">
|
||||||
<div class="font-bold text-neutral-200">Trigger Backup</div>
|
<div class="font-bold text-stone-200 group-hover:text-amber-200">Trigger Backup</div>
|
||||||
<div class="text-[10px] text-neutral-400 mt-0.5">Create a backup of the instance.</div>
|
<div class="text-[10px] text-stone-400 mt-0.5">Save a zip archive of current world data.</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="w-full text-left border border-red-950 bg-red-950/10 text-xs p-3 rounded">
|
<button class="w-full text-left border border-amber-900/40 bg-amber-950/10 text-xs p-3 rounded shadow-sm hover:bg-amber-950/20 group">
|
||||||
<div class="font-bold text-red-400">Delete Instance</div>
|
<div class="font-bold text-amber-300">Delete Instance</div>
|
||||||
<div class="text-[10px] text-red-500/70 mt-0.5">Remove server instance and optionally delete its files.</div>
|
<div class="text-[10px] text-amber-400/70 mt-0.5">Remove files and configs permanently from disk.</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
<script>
|
<script>
|
||||||
// Props received from parent context
|
|
||||||
export let instances = [];
|
export let instances = [];
|
||||||
export let processingTargets = {};
|
export let processingTargets = {};
|
||||||
export let onPowerToggle;
|
export let onPowerToggle;
|
||||||
export let onSelectInstance;
|
export let onSelectInstance;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border border-neutral-800 bg-neutral-900/20 rounded overflow-hidden">
|
<div class="border border-stone-700/60 bg-stone-900/60 rounded backdrop-blur-sm shadow-md overflow-hidden">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-neutral-800 bg-neutral-900/80 text-xs text-neutral-400 uppercase">
|
<tr class="border-b border-stone-700/60 bg-stone-800/50 text-xs text-stone-300 uppercase tracking-wider">
|
||||||
<th class="p-4 font-medium">Instance ID</th>
|
<th class="p-4 font-medium">Instance ID</th>
|
||||||
<th class="p-4 font-medium">Engine Version</th>
|
<th class="p-4 font-medium">Engine Version</th>
|
||||||
<th class="p-4 font-medium">Network Port</th>
|
<th class="p-4 font-medium">Network Port</th>
|
||||||
<th class="p-4 font-medium">Runtime Status</th>
|
<th class="p-4 font-medium">Runtime Status</th>
|
||||||
<th class="p-4 font-medium text-right">Quick Controls</th>
|
<th class="p-4 font-medium text-right">Controls</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-neutral-800/40 text-sm">
|
<tbody class="divide-y divide-stone-800/60 text-sm">
|
||||||
{#if instances.length === 0}
|
{#if instances.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="p-8 text-center text-neutral-500 text-xs">
|
<td colspan="5" class="p-8 text-center text-stone-400 text-xs italic">
|
||||||
No managed target nodes registered with daemon configuration profile.
|
No server instances found in your configuration.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
{#each instances as inst}
|
{#each instances as inst}
|
||||||
<tr
|
<tr
|
||||||
on:click={() => onSelectInstance(inst)}
|
on:click={() => onSelectInstance(inst)}
|
||||||
class="hover:bg-neutral-900/40 transition-colors cursor-pointer">
|
class="hover:bg-stone-800/40 transition-colors cursor-pointer">
|
||||||
<td class="p-4 font-bold tracking-wide text-amber-500 hover:underline">{inst.name}</td>
|
<td class="p-4 font-bold text-amber-200 hover:underline">{inst.name}</td>
|
||||||
<td class="p-4 text-neutral-400">{inst.version}</td>
|
<td class="p-4 text-stone-300">{inst.version}</td>
|
||||||
<td class="p-4 text-neutral-400 font-mono text-xs">{inst.port}</td>
|
<td class="p-4 text-stone-300 font-mono text-xs">{inst.port}</td>
|
||||||
<td class="p-4">
|
<td class="p-4">
|
||||||
<span class="inline-flex items-center gap-1.5 text-xs font-semibold px-2 py-0.5 rounded-sm
|
<span class="inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-0.5 rounded
|
||||||
{inst.status === 'RUNNING' ? 'bg-emerald-950/40 text-emerald-400 border border-emerald-900/30' : 'bg-neutral-800 text-neutral-400'}">
|
{inst.status === 'RUNNING' ? 'bg-emerald-900/40 text-emerald-200 border border-emerald-800/40' : 'bg-stone-800 text-stone-400 border border-stone-700/40'}">
|
||||||
<span class="w-1.5 h-1.5 rounded-full {inst.status === 'RUNNING' ? 'bg-emerald-400 animate-pulse' : 'bg-neutral-500'}"></span>
|
<span class="w-1.5 h-1.5 rounded-full {inst.status === 'RUNNING' ? 'bg-emerald-400' : 'bg-stone-500'}"></span>
|
||||||
{inst.status}
|
{inst.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -43,11 +42,11 @@
|
|||||||
<button
|
<button
|
||||||
disabled={processingTargets[inst.name]}
|
disabled={processingTargets[inst.name]}
|
||||||
on:click={() => onPowerToggle(inst.name, inst.status)}
|
on:click={() => onPowerToggle(inst.name, inst.status)}
|
||||||
class="w-24 text-center border text-xs uppercase font-bold py-1 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed
|
class="w-24 text-center border text-xs font-bold py-1 rounded transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed shadow-sm
|
||||||
{inst.status === 'RUNNING'
|
{inst.status === 'RUNNING'
|
||||||
? 'border-red-900/60 bg-red-950/20 text-red-400 hover:bg-red-900/40'
|
? 'border-amber-900/50 bg-amber-950/20 text-amber-300 hover:bg-amber-900/30'
|
||||||
: 'border-emerald-900/60 bg-emerald-950/20 text-emerald-400 hover:bg-emerald-900/40'}">
|
: 'border-emerald-800/60 bg-emerald-900/20 text-emerald-200 hover:bg-emerald-800/40'}">
|
||||||
{processingTargets[inst.name] ? 'PENDING...' : inst.status === 'RUNNING' ? 'STOP' : 'START'}
|
{processingTargets[inst.name] ? '...' : inst.status === 'RUNNING' ? 'STOP' : 'START'}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user