ODIAN Launcher: hub, installer, local stack and remembered sign-in (M2)
- Tauri 2 + Svelte 5 hub in ODIAN dark: patch notes and roadmap, install and update progress, local stack status, settings with DHP and MCP addresses. - crates/odian-packs: Rust installer for pack format v1 (signed manifests, resumable verified downloads from a DHP or an ODIAN drive), byte-compatible with ODIAN_MCP_Server's Python odian_packs. - Local stack on 127.0.0.1: Postgres -> Engine API -> tile server -> Odian.exe, one Job Object, clean shutdown; database snapshot restore on a fresh PC only. - Keycloak sign-in through the DHP's MCP, remembered until Sign out with a launcher session that renews the Engine token silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import { launcher } from "../lib/launcher.svelte";
|
||||
|
||||
interface Props {
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
let { onOpenSettings }: Props = $props();
|
||||
|
||||
let maximized = $state(false);
|
||||
|
||||
const status = $derived(launcher.status);
|
||||
const checking = $derived(launcher.refreshing || status?.phase === "checking");
|
||||
|
||||
function windowAction(task: () => Promise<void>) {
|
||||
return () => {
|
||||
task().catch((e) => console.error("window control failed", e));
|
||||
};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let disposed = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
const sync = () => {
|
||||
launcher.win
|
||||
.isMaximized()
|
||||
.then((value) => (maximized = value))
|
||||
.catch(() => {});
|
||||
};
|
||||
sync();
|
||||
launcher.win
|
||||
.onResized(sync)
|
||||
.then((u) => (disposed ? u() : (unlisten = u)))
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten?.();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="titlebar" data-tauri-drag-region>
|
||||
<div class="brand" data-tauri-drag-region>
|
||||
<svg class="mark" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true" data-tauri-drag-region>
|
||||
<rect x="0.5" y="0.5" width="17" height="17" fill="none" stroke="currentColor" stroke-opacity="0.45" />
|
||||
<path d="M9 3.5L14.5 9 9 14.5 3.5 9z" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<rect x="8" y="8" width="2" height="2" fill="currentColor" />
|
||||
</svg>
|
||||
<span class="wordmark" data-tauri-drag-region>ODIAN</span>
|
||||
<span class="product" data-tauri-drag-region>Launcher</span>
|
||||
</div>
|
||||
|
||||
{#if status}
|
||||
<span class="divider" aria-hidden="true" data-tauri-drag-region></span>
|
||||
<div class="site" data-tauri-drag-region>
|
||||
<span class="site-name" title={status.site.url} data-tauri-drag-region>{launcher.siteName}</span>
|
||||
<span
|
||||
class="badge"
|
||||
class:online={status.online}
|
||||
class:checking
|
||||
role="status"
|
||||
aria-busy={checking}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<span class="badge-dot" aria-hidden="true"></span>
|
||||
{status.online ? "Online" : "Offline"}
|
||||
</span>
|
||||
<button
|
||||
class="icon-btn refresh"
|
||||
aria-label="Check the connection to your DHP"
|
||||
title="Check connection"
|
||||
disabled={checking}
|
||||
onclick={() => launcher.refresh()}
|
||||
>
|
||||
<span class="refresh-icon" class:spinning={checking}><Icon name="refresh" size={14} /></span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="drag" data-tauri-drag-region></div>
|
||||
|
||||
<button class="icon-btn settings" aria-label="Settings" title="Settings" aria-haspopup="dialog" onclick={onOpenSettings}>
|
||||
<Icon name="settings" />
|
||||
</button>
|
||||
|
||||
<div class="window-controls">
|
||||
<button class="win" aria-label="Minimize" title="Minimize" onclick={windowAction(() => launcher.win.minimize())}>
|
||||
<Icon name="minimize" size={14} strokeWidth={1.25} />
|
||||
</button>
|
||||
<button
|
||||
class="win"
|
||||
aria-label={maximized ? "Restore" : "Maximize"}
|
||||
title={maximized ? "Restore" : "Maximize"}
|
||||
onclick={windowAction(() => launcher.win.toggleMaximize())}
|
||||
>
|
||||
<Icon name={maximized ? "restore" : "maximize"} size={14} strokeWidth={1.25} />
|
||||
</button>
|
||||
<button class="win close" aria-label="Close" title="Close" onclick={windowAction(() => launcher.win.close())}>
|
||||
<Icon name="close" size={14} strokeWidth={1.25} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.titlebar {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--titlebar-h);
|
||||
padding-left: var(--space-4);
|
||||
border-bottom: 1px solid rgba(35, 35, 38, 0.8);
|
||||
background: linear-gradient(180deg, rgba(9, 9, 11, 0.9), rgba(9, 9, 11, 0.6));
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mark {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
margin-right: calc(var(--wordmark-tracking) * -1);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--wordmark-tracking);
|
||||
}
|
||||
|
||||
.product {
|
||||
margin-left: 6px;
|
||||
color: var(--muted);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 var(--space-4);
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
.site {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.site-name {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
color: var(--ink-2);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid rgba(245, 180, 84, 0.3);
|
||||
border-radius: var(--radius);
|
||||
background: var(--warn-soft);
|
||||
color: var(--warn);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
transition:
|
||||
color var(--dur) var(--ease),
|
||||
background-color var(--dur) var(--ease),
|
||||
border-color var(--dur) var(--ease);
|
||||
}
|
||||
.badge.online {
|
||||
border-color: rgba(134, 239, 172, 0.28);
|
||||
background: var(--ok-soft);
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.badge.checking .badge-dot {
|
||||
animation: pulse 1.1s var(--ease) infinite;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.refresh:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
display: grid;
|
||||
}
|
||||
.spinning {
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.drag {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.settings {
|
||||
margin-right: var(--space-2);
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.win {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
transition:
|
||||
background-color var(--dur-fast) var(--ease),
|
||||
color var(--dur-fast) var(--ease);
|
||||
}
|
||||
.win:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
.win.close:hover {
|
||||
background: var(--danger);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
.win:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user