diff --git a/ui/index.html b/ui/index.html
index 6f10d71..432482b 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -57,6 +57,7 @@
+
diff --git a/ui/js/os.js b/ui/js/os.js
index d02dae4..04cc59f 100644
--- a/ui/js/os.js
+++ b/ui/js/os.js
@@ -122,15 +122,28 @@ document.addEventListener('alpine:init', () => {
},
// ── Installed apps ────────────────────────────────────────────────────
- // Loaded from crimata.json manifests on the server
- installedApps: [
- { id: 'bio', name: 'Bio', icon: '🪪', url: '/apps/bio' },
- { id: 'contacts', name: 'Contacts', icon: '👥', url: '/apps/contacts' },
- { id: 'blog', name: 'Blog', icon: '📝', url: '/apps/blog' },
- ],
+ installedApps: [],
+
+ async loadApps() {
+ try {
+ const res = await fetch('/dock/apps')
+ const data = await res.json()
+ this.installedApps = data.map(app => ({
+ ...app,
+ url: `/apps/${app.id}`,
+ }))
+ } catch (e) {
+ console.error('dock unreachable:', e)
+ }
+ },
// ── Init ──────────────────────────────────────────────────────────────
- init() {
+ async init() {
+ await this.loadApps()
+
+ // Refresh running status every 10s
+ setInterval(() => this.loadApps(), 10_000)
+
// Open bio full-screen on load
const bio = this.installedApps.find(a => a.id === 'bio')
if (bio) this.openApp(bio)
diff --git a/ui/styles/dock.css b/ui/styles/dock.css
index bcb87c3..b9f0885 100644
--- a/ui/styles/dock.css
+++ b/ui/styles/dock.css
@@ -15,6 +15,7 @@
}
.dock-icon {
+ position: relative;
width: 52px;
height: 52px;
border-radius: 12px;
@@ -28,6 +29,17 @@
user-select: none;
}
+.dock-dot {
+ position: absolute;
+ bottom: -6px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: rgba(255,255,255,0.9);
+}
+
.dock-icon:hover {
transform: translateY(-8px) scale(1.1);
background: rgba(255,255,255,0.35);