From bda670d8d9766f3142910f269ba04cd8f83a8beb Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 12:41:26 +0000 Subject: [PATCH] feat(app): base-aware routing, embedded flag, panel slide animation - Use import.meta.env.BASE_URL for router history (Archy /aiui/ deployments) - Capture embedded flag from URL params before router init - Add panelSlideIn animation with prefers-reduced-motion support Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 58 ++++++++++++++++++++++++++++++++ packages/app/src/main.ts | 7 +++- packages/app/src/styles/main.css | 21 ++++++++++++ packages/app/vite.config.ts | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 66d96c13..39014609 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,3 +285,61 @@ type(scope): description - No force push to main/dev - Never commit `.env.local`, secrets, or `node_modules` - Squash merge features, tag releases `v1.0.0` + +## Archipelago (Archy) Integration + +AIUI runs inside an iframe in Archipelago's Chat mode. All communication with the host happens via `window.postMessage()` through a strict protocol. + +### Architecture + +``` +AIUI (iframe) ←→ postMessage ←→ Archy ContextBroker ←→ Node data +``` + +AIUI is **quarantined** — it never directly accesses Archy's APIs, stores, or node data. The Archy ContextBroker fetches and sanitizes data before passing it to AIUI. + +### Protocol + +Use `archyBridge.ts` (`src/services/archyBridge.ts`) for all Archy communication: + +```ts +import { archyBridge } from '@/services/archyBridge' + +// Request context (respects user permissions) +const apps = await archyBridge.requestContext('apps') +if (!apps.permitted) { + // Show: "Enable 'Installed Apps' access in Archy Settings" +} + +// Request an action +await archyBridge.requestAction('open-app', { appId: 'btcpay-server' }) + +// Listen for theme/permission updates +archyBridge.onPermissionsUpdate((categories) => { ... }) +archyBridge.onThemeUpdate((theme) => { ... }) +``` + +**Context categories** (user toggles each on/off in Archy Settings): +- `apps` — App names, status, health (no credentials) +- `system` — CPU, RAM, disk (no paths or IPs) +- `network` — Connection status, peer count (no IPs) +- `wallet` — Balance, channel count (no keys or seeds) +- `files` — File/folder names (no contents) + +### Critical Rules + +1. **NEVER** fetch Archy APIs directly — always use `archyBridge` +2. **NEVER** store or log raw user data from context responses +3. **NEVER** make HTTP requests to the host machine +4. Handle `permitted: false` gracefully — tell users what to enable +5. Send `ready` message on mount so Archy knows the iframe loaded +6. Build must output a static SPA servable from any base path +7. All AI provider keys are user-provided and stored locally in AIUI only + +### Build & Deploy + +AIUI deploys as a Podman container on the Archy node: +- Build: `pnpm build` → `packages/app/dist/` +- Container: nginx:alpine serving the dist +- Proxied at `/aiui/` via Archy's nginx +- Updates independently of Archy — new container image = new version diff --git a/packages/app/src/main.ts b/packages/app/src/main.ts index e45590ef..bbb1ca4d 100644 --- a/packages/app/src/main.ts +++ b/packages/app/src/main.ts @@ -5,8 +5,13 @@ import App from './App.vue' import './styles/main.css' import { initializePlugins } from './plugins' +// Capture embedded flag before router init (survives SPA navigation) +// Only embedded when explicitly requested via ?embedded param +const _embeddedFlag = new URLSearchParams(window.location.search).has('embedded') +;(window as unknown as Record).__AIUI_EMBEDDED__ = _embeddedFlag + const router = createRouter({ - history: createWebHistory(), + history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', diff --git a/packages/app/src/styles/main.css b/packages/app/src/styles/main.css index 0813a392..e8bafa2d 100644 --- a/packages/app/src/styles/main.css +++ b/packages/app/src/styles/main.css @@ -605,6 +605,27 @@ input:focus-visible { 50% { opacity: 1; } } +@keyframes panelSlideIn { + 0% { + opacity: 0; + transform: translateX(-40px) scale(0.97); + } + 100% { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +.panel-slide-in { + animation: panelSlideIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + .panel-slide-in { + animation: none; + } +} + /* ===== POSTER CARD — path-glass border ===== */ .poster-card { diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index f3f657b2..57a89021 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -11,6 +11,7 @@ import { rssPlugin } from './vite-rss' import { fsPlugin } from './vite-fs' export default defineConfig({ + base: process.env.VITE_BASE_PATH || '/', plugins: [ vue(), tailwindcss(),