chore: P0 skeleton

Bootstrap the Dashward repo per arch/UBUNTU-DASHBOARD-SPACE.md:

- pnpm-workspaces monorepo (sdk, extension, container, widgets-builtin/*)
- GNOME extension stub (metadata.json, src/*.ts placeholders for warden,
  guard, supervisor, entry UX, DBus service)
- WebKit container stub (GJS main + page-side runtime + dashboard.html)
- TypeScript widget SDK (defineWidget + types)
- Builtin clock widget as the first SDK consumer example
- DBus interface XML (proto/shell.iface.xml) and shared types
- esbuild configs for extension and container; tsc for SDK
- Design doc copied in at repo root for discoverability

No functional logic yet -- all components are placeholders that compose
in extension.ts so the build chain can be exercised. P1 (workspace
warden) starts next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-22 23:00:02 +01:00
commit 3bf3aa1989
41 changed files with 1361 additions and 0 deletions

683
UBUNTU-DASHBOARD-SPACE.md Normal file
View File

@@ -0,0 +1,683 @@
# Ubuntu Dashboard Space — Design
Status: design. Goal: a macOS-Dashboard-style **dedicated rightmost workspace**
on GNOME/Ubuntu that hosts user-defined widgets. Widgets are written against
a TypeScript SDK and rendered as web components inside an embedded WebKit
view; the GNOME Shell extension is the workspace warden and the IPC bridge,
nothing heavier.
Codename: **Dashward** (decided 2026-05-22; see §19).
---
## 1. Goals & non-goals
### Goals
- A workspace that is **created by the extension** at enable time, lives at
the rightmost position, and is **destroyed** at disable time. It is not
one of the user's existing workspaces, even temporarily.
- **Regular windows cannot enter it** — neither by overview drag, by
`super+shift+arrow`, by `wmctrl`/`xdotool`, nor by being newly mapped
there. Attempts get bounced to the prior workspace immediately.
- Widgets are **web components in TS**. A buggy widget cannot take down
GNOME Shell.
- Hot-reload during widget development.
- Light/dark theme follows the system.
- Zero hand-edits to user dotfiles outside `~/.local/share/dashward/` and
the extension install dir.
### Non-goals (v1)
- Cross-distro support beyond Ubuntu 25.04 / GNOME 48. KDE, XFCE, Cinnamon
are out of scope until v1 lands and stabilizes.
- A widget marketplace, signing, or sandboxing beyond what WebKit's process
model already provides. Widgets are trusted code at v1.
- macOS-style "ripple in" widget add animation. Edit mode is utilitarian.
- Multi-monitor span: dashboard appears on the **primary** monitor only;
other monitors get the regular last-workspace view.
- Replacing the regular workspace switcher UX. The dashboard is reached by a
dedicated gesture/shortcut, not by being workspace N+1 in the existing
switcher.
---
## 2. Hard constraints (carried from chat)
| C# | Constraint | Resolution |
|---|---|---|
| C1 | Dashboard does **not** reuse any existing workspace | §5: append a new workspace at enable, mark it, remove at disable |
| C2 | Dashboard is **always rightmost** | §5: position invariant maintained on every workspace mutation |
| C3 | Regular windows cannot be **dragged into** dashboard | §6: layered defense on `window-added`, overview drop target, gesture handlers |
| C4 | Widgets in **TypeScript** | §9: SDK is a TS package; widgets are TS modules bundled to ESM |
| C5 | Avoid the GridSpaces lockout failure mode | §17: VM-only iteration loop; extension never touches `gnome-extensions enable` on host until VM-green |
---
## 3. Reality check (probed 2026-05-22 on ubuntu2504-test)
| Component | Version | Note |
|---|---|---|
| Ubuntu | 25.04 (Plucky) | |
| GNOME Shell | 48.0-1ubuntu1 | ESM extension format |
| Mutter | 48.1 (`libmutter-16-0`) | API gen 16, `Meta.WorkspaceManager` stable |
| gjs | 1.82.1 | SpiderMonkey 128 era — ESM, top-level await OK |
| WebKit2GTK 4.1 (GTK3) | 2.50.4 + `gir1.2-webkit2-4.1` pre-installed | **Chosen** for the container — zero new GIR deps |
| WebKitGTK 6.0 (GTK4) | 2.50.4 lib present, GIR not installed | Migration target post-v1 |
| Session | Wayland (gdm `Ubuntu on Wayland` default) | |
| Pre-existing ext | `ding`, `tiling-assistant`, `ubuntu-appindicators`, `ubuntu-dock` | tiling-assistant is the relevant precedent for window manipulation |
Implication: we can build v1 with only what's already on disk — no new apt
packages required. (Build chain is in dev only; not shipped onto the VM at
runtime.)
---
## 4. Architecture
Three processes, two IPC channels:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ gnome-shell (GJS, in-process) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Dashward extension │ │
│ │ • WorkspaceWarden (create/destroy + position invariant) │ │
│ │ • WindowGuard (block window-added on dashboard ws) │ │
│ │ • EntryUX (gesture, shortcut, panel indicator) │ │
│ │ • ContainerSupervisor (spawn/respawn web container, pin window) │ │
│ │ • DBusService "top.hangmanlab.Dashward.Shell" │ │
│ └─────────┬────────────────────────────────────────────────────────┘ │
└────────────┼────────────────────────────────────────────────────────────┘
│ DBus session bus
┌─────────────────────────────────────────────────────────────────────────┐
│ dashward-container (GJS + WebKit2 4.1, separate process) │
│ • One GtkWindow, fullscreen, undecorated, app_id = container │
│ • WebKitWebView loads file://…/dashboard.html │
│ • DBus client: pushes shell events into the page via │
│ UserContentManager.evaluate_javascript() │
│ • UserContentManager script-message handlers receive page→shell calls │
└─────────────────────────────────────────────────────────────────────────┘
│ WebKit user-content-manager bridge
┌─────────────────────────────────────────────────────────────────────────┐
│ dashboard.html (WebKit content process, sandboxed by WebKit) │
│ • Runtime: dashboard-runtime.js (host shell, layout grid, edit mode) │
│ • SDK: @dashward/widget-sdk (TS, bundled in) │
│ • Widgets: each loaded into its own <iframe> for crash isolation │
└─────────────────────────────────────────────────────────────────────────┘
```
**Why the container is its own process**: GJS code crashing inside
`gnome-shell` kills the session. Container is forked under `Gio.Subprocess`;
when it crashes, supervisor sees the exit, hides the dashboard slot's
content, and respawns with backoff. Shell stays up.
**Why widgets are in `<iframe>`s and not just `<div>`s**: a widget that
loops, throws unhandled, or leaks memory only affects its iframe — the host
page and other widgets keep running. Iframes also give us a clean origin
boundary for widget asset paths.
---
## 5. Workspace lifecycle
### Enable
1. Snapshot user state:
- `org.gnome.mutter dynamic-workspaces` → save under
`~/.local/state/dashward/saved-settings.json`.
- `org.gnome.desktop.wm.preferences num-workspaces` → save.
- Current workspace count `N` (from `Meta.WorkspaceManager`).
2. Set `dynamic-workspaces = false` (we need a fixed slot at the end that
doesn't get GC'd when empty).
3. Set `num-workspaces = N + 1`.
4. Take the new last workspace (index `N`) as **the dashboard slot**. Tag
it: stash its `Meta.Workspace` reference under `wm._dashSpaceWs` and
record the workspace's index in `WorkspaceWarden.index`.
5. Spawn the container; pin its window to `wm._dashSpaceWs` (see §7).
6. Install signal handlers (§6).
### Steady state — invariants
After **every** workspace mutation (workspace appended/removed/reordered)
the warden re-checks:
- `WorkspaceWarden.ws` still exists in `WorkspaceManager.get_workspaces()`.
If not (something forcibly removed it), recreate at the end. Log.
- Its index equals `WorkspaceManager.get_n_workspaces() - 1`. If not, move
it to the end via `reorder_workspace(ws, N-1)`.
### Disable
1. Kill container.
2. Remove the dashboard workspace via
`WorkspaceManager.remove_workspace(ws, current_time)`.
3. Restore `num-workspaces` and `dynamic-workspaces` to snapshotted values.
4. Disconnect all signals; clear `wm._dashSpaceWs`.
### Edge cases
- **User changes `num-workspaces` externally** while extension is enabled:
warden treats this as the new "N", appends dashboard at N (effectively
`num-workspaces = newN + 1`). Saved setting is updated.
- **User logs out / shell restart (Wayland: only logout; X11: alt-F2 `r`)**:
`disable()` is called by Shell. If we somehow miss it (crash), a
best-effort cleanup runs on next `enable()`: detect workspace tagged with
our marker (`Meta.Workspace.get_work_area_all_monitors` is not enough;
we tag via a sticky window — see §7) and remove.
- **Other extensions also manage workspaces** (e.g. tiling-assistant
adjusts but doesn't add/remove; should be compatible. WorkspaceMatrix
would conflict — flag in README as incompatible).
---
## 6. Window-defense rules
### Layer 1: `window-added` bounce-back
```js
ws.connectObject('window-added', (_, window) => {
if (window === containerWindow) return; // whitelist
if (window.is_override_redirect()) return; // tooltip, menu, etc.
const fallback = priorWorkspaceFor(window);
window.change_workspace(fallback);
}, this);
```
`priorWorkspaceFor` heuristic: the window's `get_user_time()` workspace if
known, else workspace `N-1` (dashboard index minus 1), else workspace 0.
This single hook catches:
- new windows mapped to dashboard (`window-created` race: even if the
window initially picks dashboard via `_NET_WM_DESKTOP`, `window-added`
fires immediately when it joins);
- programmatic moves via `wmctrl -r ... -t N`, `xdotool windowmove`,
`Meta.Window.change_workspace`;
- keyboard `super+shift+→` on the rightmost workspace.
### Layer 2: overview drop refusal
In the activities overview, workspace thumbnails are drop targets via
`WorkspaceThumbnail` + `_acceptDrop`. We monkey-patch the dashboard
thumbnail's `acceptDrop` to return `false` (and the visual to show "no
drop" cursor). Cleaner than letting Layer 1 bounce it back after the visual
drop completed.
### Layer 3: hide from regular switcher
The dashboard workspace renders specially in the overview: instead of the
normal thumbnail, we draw a placeholder card with the Dashward icon and
"Dashboard" label. This is purely cosmetic (Layers 1+2 already enforce);
the goal is **user clarity** — they don't try to drag, because the slot
visibly isn't a regular workspace.
Implementation: override `WorkspaceThumbnail` for the specific workspace
via `WorkspacesView._workspaces[N-1]` patching, or simpler: hide it from
the thumbnail strip entirely. The dashboard is accessed via gesture /
shortcut (§14), not by clicking its thumbnail. **Default: hide from strip.**
### Layer 4: workspace switcher (Super+Page_Up/Down)
By default this cycles through `0..N`. We want Super+Page_Down on the last
real workspace to **not** land on dashboard. Implementation: hook
`Meta.KeyBindingAction.WORKSPACE_DOWN` and clamp target to `N-1`. (User
still gets to dashboard via dedicated gesture/shortcut.)
---
## 7. Web container process
### Window properties
- Wayland app_id: `top.hangmanlab.dashward.container` (set via
`GLib.set_prgname` + `Gtk.Window.set_default_icon_name` — for Wayland
app_id detection on the Shell side, we identify by
`Meta.Window.get_gtk_application_id()`).
- Title: `Dashward`.
- Decorations: none (`set_decorated(false)`).
- Fullscreen on the primary monitor.
- Type hint: `NORMAL` (not `DOCK`, since dock windows skip workspaces).
- Stickiness: **NOT sticky**; it must belong to the dashboard workspace
only.
### Lifetime managed by `ContainerSupervisor`
```
spawn() → wait for window-created signal → match by app_id →
move to dashboardWs (Layer 1 whitelist needed!) → record windowId →
WebKit ready → first eval: bootstrap dashboard-runtime.js
```
Crash recovery: restart with exponential backoff (1s, 2s, 5s, 10s, then
give up and surface error in panel indicator). The container's stderr
streams to `~/.local/state/dashward/container.log`.
### Visibility model
The container window **always exists** on the dashboard workspace; user
"opens dashboard" = switching to that workspace; "closes" = switching
away. No need to map/unmap the window per visit — Mutter culls it from
rendering when its workspace isn't active. This is the same model the
overview uses for inactive workspace windows.
**Exception**: on disable / shell-shutdown, container is gracefully asked
to quit via DBus, then SIGTERM after 2s.
---
## 8. IPC layers
### 8.1 Shell ↔ Container (DBus)
Bus name: `top.hangmanlab.Dashward.Shell` (owned by the extension).
Object path: `/top/hangmanlab/Dashward`. Interface methods:
| Method | Direction | Purpose |
|---|---|---|
| `Ready()` | container → shell | container finished bootstrap; safe to enter edit mode |
| `RequestEnterDashboard()` | container → shell | "switch to dashboard workspace" (used by `<a href>` clicks etc.) |
| `RequestExitDashboard()` | container → shell | "switch to previous workspace" |
| `GetSystemInfo() → a{sv}` | container → shell | brightness, battery, network, theme |
| `WidgetIPC(widgetId, method, args)` | container → shell | namespaced widget-to-shell calls; routed by SDK |
Signals (shell → container):
| Signal | Payload | Purpose |
|---|---|---|
| `ThemeChanged` | `s` (`"light"`/`"dark"`) | system theme toggle |
| `EditModeRequested` | `b` (enter/exit) | extension's panel button or keybind |
| `SystemInfoChanged` | `a{sv}` (partial) | battery/network/etc updates |
### 8.2 Container GJS ↔ WebKit page (UserContentManager)
GJS-side container exposes script-message handlers:
```js
ucm.register_script_message_handler('shellCall'); // page → GJS
ucm.register_script_message_handler('log'); // widget logs to GJS stderr
```
Page-side bridge:
```js
window.__dashShell__ = {
call(method, args) {
return new Promise((resolve, reject) => {
const id = nextId();
pending.set(id, { resolve, reject });
webkit.messageHandlers.shellCall.postMessage({ id, method, args });
});
}
};
```
Reverse (GJS → page): `webView.evaluate_javascript(...)`.
### 8.3 Page ↔ widget iframe
`window.postMessage` with the SDK's wrapper. SDK injects into each iframe a
preload script that exposes:
```ts
const dashboard = {
config: { get(): T, set(T): Promise<void>, onChange(cb) },
system: { onThemeChange(cb), getTheme(): 'light'|'dark', /* … */ },
lifecycle: { onMount(cb), onUnmount(cb), onVisibilityChange(cb) },
shell: { call(method, args): Promise<any> }, // gated by manifest permissions
};
```
---
## 9. Widget SDK
Package: `@dashward/widget-sdk` (TypeScript, ESM, distributed as both
source-for-bundle and as a CDN-style local file the runtime can load).
### Authoring API
```ts
import { defineWidget } from '@dashward/widget-sdk';
interface ClockConfig { hour12: boolean; showSeconds: boolean }
export default defineWidget<ClockConfig>({
id: 'clock',
defaultConfig: { hour12: false, showSeconds: true },
mount(host, { config, system, lifecycle }) {
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `<div class="clock"></div>`;
const tick = () => { /* render */ };
const timer = setInterval(tick, 1000);
lifecycle.onUnmount(() => clearInterval(timer));
config.onChange(() => tick());
system.onThemeChange(t => root.host.dataset.theme = t);
},
configUI(host, { config }) {
// optional: render a settings panel for edit mode
},
});
```
Surfaces:
| API | Capability |
|---|---|
| `config.get/set/onChange` | persistent JSON config; auto-saved to `~/.local/share/dashward/widgets/<id>/config.json` |
| `system.getTheme/onThemeChange` | follow Shell theme |
| `system.getBattery/getNetwork/getBrightness` | shell DBus relay (requires manifest permission) |
| `lifecycle.onMount/onUnmount/onVisibilityChange` | called by host when widget appears/disappears |
| `shell.call(method, args)` | escape hatch to namespaced shell DBus calls (requires permission) |
What's **not** exposed: file system, network sockets, arbitrary
`Gio`/`Meta` access. Widgets that need that file an upstream issue.
---
## 10. Widget manifest
`widget.json` at the widget root:
```json
{
"id": "clock",
"name": "Clock",
"version": "0.1.0",
"entry": "dist/index.js",
"icon": "icon.svg",
"size": { "w": 2, "h": 2, "minW": 1, "minH": 1, "maxW": 4, "maxH": 4 },
"permissions": ["timer"],
"configSchema": { "hour12": "boolean", "showSeconds": "boolean" }
}
```
Permissions vocabulary v1: `timer`, `battery`, `network`, `brightness`,
`notifications`. Permission grant is currently implicit (manifest declares
it, runtime allows it); a user-visible grant UI is v2.
Widget directory layout:
```
widgets/clock/
├── widget.json
├── src/
│ └── index.ts
├── dist/
│ └── index.js # esbuild-bundled, ESM, references SDK as bare import
└── icon.svg
```
Install location at runtime:
```
~/.local/share/dashward/widgets/
├── clock/ # user's widgets (drop a dir here, runtime sees it)
├── weather/
└── …
```
Plus a built-in pack shipped with the extension at
`extension/widgets-builtin/` (clock, calendar, system-stats, notes).
---
## 11. Layout & edit mode
### Grid
- 12 columns wide; rows grow vertically as needed.
- Cell aspect ratio: 1:1 at the canonical size of `floor(viewportWidth /
12)` px. So a widget with `size: { w: 3, h: 2 }` occupies 3 cols × 2
rows = 3w × 2h grid cells, naturally responsive.
- Widgets snap to grid; no free positioning in v1.
### Storage
User layout in `~/.local/share/dashward/layout.json`:
```json
{
"widgets": [
{ "instanceId": "clock-1", "widgetId": "clock", "x": 0, "y": 0, "w": 2, "h": 2 },
{ "instanceId": "weather-1", "widgetId": "weather", "x": 2, "y": 0, "w": 3, "h": 2 }
]
}
```
Multiple instances of the same widget are allowed (hence `instanceId` vs
`widgetId`); each has its own config file scoped by `instanceId`.
### Edit mode
Triggered by:
- Top-edge floating button (visible only when dashboard is active).
- Keyboard `e` while on dashboard.
- Long-press on any widget.
In edit mode: widgets show resize handles + delete X; a "+" tray slides
in from the right with available widgets; drag to add. Exit on `Esc` or
clicking outside.
Out of edit mode: widgets are passive; no drag/resize.
---
## 12. Theming
- Container reads `org.gnome.desktop.interface color-scheme` at startup.
- Shell subscribes to GSettings changes; pushes `ThemeChanged` DBus
signal to container; container does `document.documentElement.dataset.
theme = 'dark' | 'light'`.
- SDK exposes `system.onThemeChange(cb)`; widgets style via CSS
`[data-theme="dark"]` attribute selectors (no JS theming, CSS does it).
- Background is a configurable color + optional wallpaper image (defaults
to a desaturated copy of the user's desktop wallpaper).
---
## 13. Entry / exit UX
Three ways in:
| Path | Default binding | Implementation |
|---|---|---|
| Gesture | 4-finger swipe right past last workspace | hook `Shell.WorkspacesView._swipeTracker`; on `end` with target > N-1, jump to dashboard |
| Keyboard | `Super+Grave` | `Meta.KeyBinding` via Shell.ActionMode.NORMAL |
| Panel button | left-click Dashward indicator | `PanelMenu.Button` with custom icon |
All three call the same `EntryUX.openDashboard()` which animates the
workspace switch via existing Shell APIs.
Exit: same gesture/shortcut/button toggles. Plus `Esc` while on dashboard
(but only if no widget has focus capturing it).
---
## 14. Persistence & permissions
Filesystem layout (all under `XDG_*` standards):
```
~/.local/share/dashward/
├── layout.json # grid layout
├── widgets/<instanceId>/config.json # per-instance config
└── widgets-installed/<widgetId>/ # user-installed widget bundles
~/.local/state/dashward/
├── saved-settings.json # gsettings snapshot for restore
├── container.log
└── extension.log
~/.cache/dashward/ # WebKit cache, regenerable
```
Nothing else outside the extension's install directory is touched.
Permissions today: declared in manifest, allowed by runtime. v2: a
first-run prompt per widget; revocable from edit mode.
---
## 15. Directory layout (the repo)
```
Dashward/
├── extension/ # GNOME Shell extension
│ ├── metadata.json # GNOME-mandated, shell-version: ["48"]
│ ├── src/
│ │ ├── extension.ts # entry, exports default class
│ │ ├── workspace-warden.ts
│ │ ├── window-guard.ts
│ │ ├── container-supervisor.ts
│ │ ├── entry-ux.ts
│ │ ├── dbus-service.ts
│ │ └── prefs.ts # GNOME Extension prefs window
│ ├── dist/ # esbuild output, single extension.js
│ ├── widgets-builtin/ # ships with extension
│ │ ├── clock/
│ │ ├── calendar/
│ │ ├── system-stats/
│ │ └── notes/
│ └── tsconfig.json
├── container/ # WebKit kiosk process
│ ├── src/
│ │ ├── main.ts # GJS bootstrap
│ │ ├── window.ts # GtkWindow + WebKitWebView
│ │ ├── dbus-client.ts
│ │ └── bridge.ts # UserContentManager wiring
│ ├── runtime/ # injected into the page
│ │ ├── runtime.ts # grid host, edit mode, widget loader
│ │ ├── dashboard.html
│ │ └── styles.css
│ └── dist/
├── sdk/ # @dashward/widget-sdk
│ ├── src/
│ │ ├── index.ts
│ │ ├── define-widget.ts
│ │ ├── config.ts
│ │ ├── system.ts
│ │ └── types.ts
│ ├── package.json
│ └── tsconfig.json
├── widgets/ # example/3rd-party widgets dev tree
│ └── … # each is a self-contained TS project
├── proto/ # DBus xml + shared TS types
│ ├── shell.iface.xml
│ └── types.ts
├── scripts/
│ ├── dev-vm.sh # rsync to VM + reload extension
│ ├── package.sh # produces .shell-extension.zip
│ └── lint.sh
└── README.md
```
---
## 16. Build chain
Single root `package.json` + pnpm workspaces (`sdk`, `extension`,
`container`, `widgets/*`).
| Subproject | Bundler | Output |
|---|---|---|
| `sdk` | tsc | ESM, types, published as workspace dep |
| `extension` | esbuild | single `dist/extension.js` (GNOME loads one file) |
| `container` | esbuild | single `dist/main.js` (GJS-friendly ESM) |
| each widget | esbuild | `dist/index.js`, SDK marked external (loaded by runtime) |
GJS specifics (carried from your [[project_fabric_esm]] habit):
- `"module": "NodeNext"` in tsconfig; explicit `.js` import suffixes; default-import any CJS dep.
- `Gio` / `Meta` etc. imported via `gi://` URLs.
- Type stubs: `@girs/gnome-shell`, `@girs/meta-16`, `@girs/gtk-3.0`,
`@girs/webkit2-4.1`.
Dev cycle:
```bash
pnpm dev # watches src/, rebuilds extension+container+widgets
pnpm vm:sync # rsync dist → VM, restart shell (or just kill container)
```
Inside the VM, a small helper script (`~/dashward-dev/reload.sh`):
- Wayland: re-enable extension via `gnome-extensions disable && enable`
(a full Shell restart on Wayland requires logout — see §17).
- Container-only changes: `pkill dashward-container`; supervisor
respawns.
---
## 17. Implementation phases
| Phase | Deliverable | Acceptance |
|---|---|---|
| P0 | Repo skeleton, build chain, hello-world extension that logs on enable | extension shows in `gnome-extensions list` on VM |
| P1 | WorkspaceWarden in isolation: appends/removes ws, position invariant | manual: enable → see N+1 workspaces; disable → back to N; mutations preserved |
| P2 | WindowGuard | manual: try `wmctrl -r foo -t <dashboard>`, `super+shift+→`, overview drag — all bounce |
| P3 | Container spawn + WebKit window + pin to dashboard | container window appears on dashboard ws only |
| P4 | DBus + WebKit bridge round-trip | `dashboard.system.getTheme()` returns shell-side value |
| P5 | Runtime: grid host, layout.json load/save, one builtin widget (clock) | clock renders, persists across enable cycles |
| P6 | SDK + 3 more builtins (calendar, system-stats, notes) | each widget works in isolation; iframe crash containment verified |
| P7 | Edit mode (add/remove/resize/rearrange) | layout edits persist; instances scoped configs |
| P8 | Entry UX (gesture, shortcut, panel button) | all three paths open dashboard |
| P9 | Polish: theming, animations, error surfaces | passes a manual checklist |
| P10 | Stretch: WebKit 6.0 migration, multi-monitor, widget marketplace stub | post-v1 |
**Critical**: P1, P2, P3 all run **only on the VM** ([[project_gridspaces_test_vm]]). The host's GNOME Shell never sees the extension until P9 and only via `gnome-extensions install` from a packaged zip — never via a `~/.local/share/gnome-shell/extensions/` symlink, which would survive logout and lock us out as in [[project_gridspaces_lockout]].
---
## 18. Risks & open questions
### Risks
| R# | Risk | Mitigation |
|---|---|---|
| R1 | Wayland shell restart requires logout — slow iteration | Container hot-reload covers 80%; for extension changes, accept the logout cost; consider a nested Wayland session (`mutter --nested`) for ultra-fast iteration in P5+ |
| R2 | `dynamic-workspaces = false` is intrusive — user may rely on dynamic behavior | Restore on disable; document in README; consider an "advanced mode" later that keeps dynamic mode and races the GC, but **not v1** |
| R3 | Overview shows the dashboard slot oddly when hidden from strip | Spend a P9 day on the cosmetic; if upstream `WorkspacesView` proves fragile across GNOME minor versions, fall back to "show the slot with a placeholder thumbnail" |
| R4 | WebKitGTK 4.1 (GTK3) is older — long-term deprecation risk | Migration path: swap container to GTK4 + WebKitGTK 6.0 (install `gir1.2-webkit-6.0`). API surface is similar; isolated to `container/`, won't touch SDK or widgets |
| R5 | Widget IPC permissions are honor-system in v1 | Acceptable for personal-use v1; v2 adds first-run prompt |
| R6 | tiling-assistant or other extensions interact with workspace count | Document incompatibility list in README; test against default Ubuntu 25.04 extension set on VM during P1 |
| R7 | A misbehaving extension load could crash shell on enable | Wrap entire `enable()` in try/catch; on first-throw, write error to `~/.local/state/dashward/extension.log` and bail rather than partial-init |
### Open questions
| Q# | Question | Status |
|---|---|---|
| Q1 | Project name | **Decided 2026-05-22: `Dashward`** |
| Q2 | Where does the repo live? | **Decided 2026-05-22: local `~/dev/Dashward/`; remote = `https://git.hangman-lab.top/hzhang/Dashward.git` (Gitea on server.t0)** |
| Q3 | Should widgets be able to span the entire dashboard (full-bleed)? | Open — no in v1; revisit |
| Q4 | Multi-monitor: dashboard on primary only, or per-monitor? | Open — primary only in v1 |
| Q5 | Should `Super+Grave` clash check happen at install or runtime? | Open — runtime check on `enable()`; if taken, fall back to no shortcut and warn |
| Q6 | Widget config schema validation (Zod? JSON Schema? none?) | Open — none in v1; SDK trusts the widget's TS types |
| Q7 | Do we ship a widget devtools page (live reload of widget under dev)? | Open — P10 stretch |
---
## 19. Naming, hosting & next actions
**Codename:** Dashward (decided 2026-05-22).
**Local working tree:** `~/dev/Dashward/`. Not under HangmanLab — Dashward is a personal tool, orthogonal to the lab estate.
**Remote:** Gitea on `server.t0` at `https://git.hangman-lab.top/hzhang/Dashward.git`. Created when the P0 skeleton is ready to push. Per [[project-server-topology]] t0 hosts Git + auth; per [[project-local-sim-env]] the local-sim hijack of `*.hangman-lab.top` is **opt-in** (set up by `setup-sim.sh`) — when the hijack is off (default state), `git.hangman-lab.top` resolves to prod, which is what we want for pushing Dashward.
Next concrete step:
1. ✅ Q1, Q2 decided.
2. Create the repo skeleton on disk (§15) — empty files, working `pnpm install`, no logic.
3. Create the Gitea repo (via Gitea API or web UI) and `git push -u origin main`.
4. P0 hello-world extension on the VM, prove the dev loop end-to-end.
5. Begin P1.
See [[project-gridspaces-lockout]] and [[project-gridspaces-test-vm]] for the testing discipline this project inherits.