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:
Kelorastus
2026-09-15 15:23:17 +02:00
commit b7f3061be3
102 changed files with 47611 additions and 0 deletions
+260
View File
@@ -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>