Feat v0.7.3 extension shell migration (#57)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 3m2s
CI/CD / build-and-deploy (push) Successful in 27s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #57.
This commit is contained in:
2026-04-02 13:07:59 +00:00
committed by xcaliber
parent d6c7b21713
commit 32e4d8725c
19 changed files with 204 additions and 63 deletions

View File

@@ -7,9 +7,10 @@
# #
# Pipeline: # Pipeline:
# 0. Detect changes (path-based gating for all downstream jobs) # 0. Detect changes (path-based gating for all downstream jobs)
# 1a. Frontend tests — skipped if only BE/docs changed # 1a. Frontend tests — skipped if only BE/docs/packages changed
# 1b. Go unit tests — all non-DB packages + SQLite integration (race-enabled) # 1b. Go unit tests — all non-DB packages + SQLite integration (race-enabled)
# 1c. Go test (PG) — PG store + handlers against Postgres (race-enabled) # 1c. Go test (PG) — PG store + handlers against Postgres (race-enabled)
# 1d. Test runners — disabled until v0.7.5 (Playwright headless fix)
# 2. Build + Deploy — skipped if docs-only change # 2. Build + Deploy — skipped if docs-only change
# #
# Test coverage mapping (no package tested by zero jobs): # Test coverage mapping (no package tested by zero jobs):
@@ -23,7 +24,9 @@
# Path gating rules: # Path gating rules:
# src/, src/editor/ → frontend tests # src/, src/editor/ → frontend tests
# server/, scripts/db-* → backend tests (PG + SQLite) # server/, scripts/db-* → backend tests (PG + SQLite)
# packages/ → test-runners (surface/extension tests)
# Dockerfile*, k8s/, .gitea/ → all tests (infra change) # Dockerfile*, k8s/, .gitea/ → all tests (infra change)
# ci/ → infra (CI scripts)
# docs/, *.md → skip all tests + deploy # docs/, *.md → skip all tests + deploy
# VERSION, scripts/* → frontend + backend tests # VERSION, scripts/* → frontend + backend tests
# Tags (v*) → always full pipeline # Tags (v*) → always full pipeline
@@ -100,6 +103,7 @@ jobs:
outputs: outputs:
frontend: ${{ steps.filter.outputs.frontend }} frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }} backend: ${{ steps.filter.outputs.backend }}
packages: ${{ steps.filter.outputs.packages }}
infra: ${{ steps.filter.outputs.infra }} infra: ${{ steps.filter.outputs.infra }}
docs_only: ${{ steps.filter.outputs.docs_only }} docs_only: ${{ steps.filter.outputs.docs_only }}
steps: steps:
@@ -137,7 +141,7 @@ jobs:
echo "${CHANGED}" | sed 's/^/ /' echo "${CHANGED}" | sed 's/^/ /'
# Classify # Classify
FE=false; BE=false; INFRA=false; DOCS=false; OTHER=false FE=false; BE=false; PKG=false; INFRA=false; DOCS=false; OTHER=false
while IFS= read -r file; do while IFS= read -r file; do
[[ -z "$file" ]] && continue [[ -z "$file" ]] && continue
case "$file" in case "$file" in
@@ -145,19 +149,23 @@ jobs:
FE=true ;; FE=true ;;
server/*|scripts/db-*) server/*|scripts/db-*)
BE=true ;; BE=true ;;
packages/*)
PKG=true ;;
.gitea/*|k8s/*|Dockerfile*|docker-compose*|docker-entrypoint*|nginx.conf) .gitea/*|k8s/*|Dockerfile*|docker-compose*|docker-entrypoint*|nginx.conf)
INFRA=true ;; INFRA=true ;;
docs/*|*.md|CHANGELOG.md|LICENSE) docs/*|*.md|CHANGELOG.md|LICENSE)
DOCS=true ;; DOCS=true ;;
VERSION|scripts/*) VERSION|scripts/*)
FE=true; BE=true ;; FE=true; BE=true ;;
ci/*)
INFRA=true ;;
*) *)
OTHER=true ;; OTHER=true ;;
esac esac
done <<< "${CHANGED}" done <<< "${CHANGED}"
# Docs-only: only docs changed, nothing else # Docs-only: only docs changed, nothing else
if [[ "$DOCS" == "true" && "$FE" == "false" && "$BE" == "false" && "$INFRA" == "false" && "$OTHER" == "false" ]]; then if [[ "$DOCS" == "true" && "$FE" == "false" && "$BE" == "false" && "$PKG" == "false" && "$INFRA" == "false" && "$OTHER" == "false" ]]; then
DOCS_ONLY=true DOCS_ONLY=true
else else
DOCS_ONLY=false DOCS_ONLY=false
@@ -165,6 +173,7 @@ jobs:
echo "frontend=${FE}" >> "$GITHUB_OUTPUT" echo "frontend=${FE}" >> "$GITHUB_OUTPUT"
echo "backend=${BE}" >> "$GITHUB_OUTPUT" echo "backend=${BE}" >> "$GITHUB_OUTPUT"
echo "packages=${PKG}" >> "$GITHUB_OUTPUT"
echo "infra=${INFRA}" >> "$GITHUB_OUTPUT" echo "infra=${INFRA}" >> "$GITHUB_OUTPUT"
echo "docs_only=${DOCS_ONLY}" >> "$GITHUB_OUTPUT" echo "docs_only=${DOCS_ONLY}" >> "$GITHUB_OUTPUT"
@@ -172,6 +181,7 @@ jobs:
echo "━━━ Change Detection ━━━" echo "━━━ Change Detection ━━━"
echo " frontend: ${FE}" echo " frontend: ${FE}"
echo " backend: ${BE}" echo " backend: ${BE}"
echo " packages: ${PKG}"
echo " infra: ${INFRA}" echo " infra: ${INFRA}"
echo " docs_only: ${DOCS_ONLY}" echo " docs_only: ${DOCS_ONLY}"
@@ -375,12 +385,13 @@ jobs:
# test-runners surface after deploy until this is resolved. # test-runners surface after deploy until this is resolved.
# See: docker-compose.ci.yml, ci/surface-test-driver.js # See: docker-compose.ci.yml, ci/surface-test-driver.js
# #
# Runs when: backend or frontend changed (runners test both tiers). # Runs when: backend, frontend, or packages changed (runners test all tiers).
# Skipped when: only docs changed. # Skipped when: only docs changed.
test-runners: test-runners:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [detect-changes] needs: [detect-changes]
if: false # disabled — see comment above if: false # disabled — see comment above. Condition for v0.7.5:
# needs.detect-changes.outputs.backend == 'true' || needs.detect-changes.outputs.frontend == 'true' || needs.detect-changes.outputs.packages == 'true' || needs.detect-changes.outputs.infra == 'true'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -2,6 +2,27 @@
All notable changes to Armature are documented here. All notable changes to Armature are documented here.
## v0.7.3 — Extension Shell Migration
**Shell Topbar Migration**
- Migrated Chat, Notes, and Schedules from legacy `sw.shell.Topbar` component to the v0.7.0 shell topbar contract (`sw.shell.topbar.setTitle/setSlot`)
- Eliminated double topbar (shell-injected + surface-owned) on all three extension surfaces
- Chat: reactive slot updates for thread title + People button when conversation changes
- Notes: slot content for + New Note, Import .md, and Graph toggle buttons
- Schedules: reactive slot with schedule count and + New Schedule button; removed legacy fallback branch
**Runner Test Updates**
- Added `shell-topbar` test suite to chat-runner, notes-runner, and schedules-runner
- Tests fetch surface JS and assert: no legacy `sw.shell.Topbar` reference, uses `sw.shell.topbar.setTitle/setSlot` API
- 6 new tests across 3 runners
**Package Versions**
- Chat surface v0.3.0, Notes surface v0.9.0, Schedules surface v0.2.0
- Chat runner v0.2.0, Notes runner v0.2.0, Schedules runner v0.2.0
**Roadmap**
- Headless E2E automation moved to v0.7.5 (independent from shell migration)
## v0.7.1 — Surface Runner Framework ## v0.7.1 — Surface Runner Framework
**`sw.testing` SDK Module** **`sw.testing` SDK Module**

View File

@@ -1,6 +1,6 @@
# Armature — Roadmap # Armature — Roadmap
## Current: v0.7.2Package Runners + CI Gate ## Current: v0.7.3Extension Shell Migration
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox, Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
storage, realtime, and ops are kernel primitives. Everything else is an extension. storage, realtime, and ops are kernel primitives. Everything else is an extension.
@@ -152,9 +152,7 @@ Design doc: `docs/DESIGN-surface-runners.md`
| CI integration | done | `test-runners` stage in Gitea CI. Playwright driver, `wait-for-healthy.sh`. | | CI integration | done | `test-runners` stage in Gitea CI. Playwright driver, `wait-for-healthy.sh`. |
| CI DinD networking fix | done | Resolve container IP via `docker inspect` — DinD port mapping doesn't expose to runner localhost. | | CI DinD networking fix | done | Resolve container IP via `docker inspect` — DinD port mapping doesn't expose to runner localhost. |
### v0.7.3 — Extension Shell Migration + Headless E2E ### v0.7.3 — Extension Shell Migration
**Extension Shell Migration**
Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component, Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component,
producing a double topbar (shell-injected + surface-owned). Migrate all producing a double topbar (shell-injected + surface-owned). Migrate all
@@ -163,20 +161,10 @@ same pattern as the kernel surface migrations.
| Step | Status | Description | | Step | Status | Description |
|------|--------|-------------| |------|--------|-------------|
| Chat → shell topbar | | Delete `<Topbar>`, use `setTitle('Chat')` + `setSlot()` for thread title/people button. | | Chat → shell topbar | done | Delete `<Topbar>`, use `setTitle('Chat')` + `setSlot()` for thread title/people button. |
| Notes → shell topbar | | Delete `<Topbar>`, use `setTitle('Notes')` + `setSlot()` for sidebar tabs/search. | | Notes → shell topbar | done | Delete `<Topbar>`, use `setTitle('Notes')` + `setSlot()` for action buttons. |
| Schedules → shell topbar | | Delete `<Topbar>`, use `setTitle('Schedules')`. Pattern A (title only). | | Schedules → shell topbar | done | Delete `<Topbar>`, use `setTitle('Schedules')` + `setSlot()` for count/new button. |
| Update package runner tests | | Assert shell topbar present, no double topbar, on migrated surfaces. | | Update package runner tests | done | Shell-topbar test suite in each runner — assert no legacy Topbar, uses shell API. |
**Headless E2E Automation**
| Step | Status | Description |
|------|--------|-------------|
| Playwright test harness | | `ci/e2e-surface-test.sh` — docker-compose, chromium, run-all, assert. |
| Screenshot-on-failure | | Full-page screenshot + console log. CI artifacts. |
| Navigation smoke test | | Playwright visits every surface. Asserts topbar, no JS errors, home link works. |
| Visual regression baseline | | Optional screenshot diff. Not a gate — report for review. |
| CI pipeline integration | | After unit tests + API runners. Failure blocks merge. |
### v0.7.4 — Documentation + Deferred Surface Work ### v0.7.4 — Documentation + Deferred Surface Work
@@ -188,6 +176,16 @@ same pattern as the kernel surface migrations.
| Extension config section docs | | Document `config_section` manifest field for Settings/Admin extensibility. | | Extension config section docs | | Document `config_section` manifest field for Settings/Admin extensibility. |
| Team Admin Workflows evaluation | | 723-line inline designer — extract to surface or split into files. Decision doc if needed. | | Team Admin Workflows evaluation | | 723-line inline designer — extract to surface or split into files. Decision doc if needed. |
### v0.7.5 — Headless E2E + CI Gate
| Step | Status | Description |
|------|--------|-------------|
| Playwright test harness | | `ci/e2e-surface-test.sh` — docker-compose, chromium, run-all, assert. |
| Screenshot-on-failure | | Full-page screenshot + console log. CI artifacts. |
| Navigation smoke test | | Playwright visits every surface. Asserts topbar, no JS errors, home link works. |
| Visual regression baseline | | Optional screenshot diff. Not a gate — report for review. |
| CI pipeline integration | | Enable `test-runners` stage, add `e2e-smoke` stage. Failure blocks merge. |
--- ---
## Post-v0.7.x ## Post-v0.7.x

View File

@@ -1 +1 @@
0.7.2 0.7.3

View File

@@ -35,7 +35,8 @@
var modules = [ var modules = [
'conversations.js', 'conversations.js',
'messaging.js' 'messaging.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Chat Runner — chat/shell-topbar suite
*
* Validates the Chat surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('chat/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/chat/js/main.js');
t.assert.eq(resp.status, 200, 'fetched chat main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/chat/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Chat Runner", "title": "Chat Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Chat package — conversations, messaging, search." "description": "Integration tests for Chat package — conversations, messaging, search."
} }

View File

@@ -1,12 +1,12 @@
/** /**
* Chat — Surface Entry Point (v0.2.0) * Chat — Surface Entry Point (v0.3.0)
* *
* Messaging surface built on chat-core library: * Messaging surface built on chat-core library:
* sw.api.ext('chat-core') — conversation/message CRUD * sw.api.ext('chat-core') — conversation/message CRUD
* sw.api.ext('chat') — typing indicators * sw.api.ext('chat') — typing indicators
* sw.realtime — live events * sw.realtime — live events
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar * sw.shell.topbar shell topbar API
*/ */
(async function () { (async function () {
'use strict'; 'use strict';
@@ -42,7 +42,6 @@
var api = sw.api.ext('chat-core'); var api = sw.api.ext('chat-core');
var chatApi = sw.api.ext('chat'); var chatApi = sw.api.ext('chat');
var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui; var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui;
var Topbar = sw.shell.Topbar;
// Import UserPicker directly (not in sw.ui index) // Import UserPicker directly (not in sw.ui index)
var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver); var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver);
@@ -831,20 +830,33 @@
var selectedConv = conversations.find(c => c.id === selectedId); var selectedConv = conversations.find(c => c.id === selectedId);
var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : ''; var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : '';
// ── Shell topbar ───────────────────────────
useEffect(() => {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Chat');
}, []);
useEffect(() => {
if (!sw.shell?.topbar) return;
if (selectedId) {
sw.shell.topbar.setSlot(html`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>
`);
} else {
sw.shell.topbar.setSlot(null);
}
}, [selectedId, threadTitle, showParticipants]);
if (loading) { if (loading) {
return html`<div class="ext-chat-loading"><${Spinner} /></div>`; return html`<div class="ext-chat-loading"><${Spinner} /></div>`;
} }
return html` return html`
<div class="ext-chat-app"> <div class="ext-chat-app">
<${Topbar} title="Chat">
${selectedId && html`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>`}
<//>
<div class="ext-chat-body"> <div class="ext-chat-body">
<${ConversationList} <${ConversationList}
selected=${selectedId} selected=${selectedId}

View File

@@ -6,7 +6,7 @@
"route": "/s/chat", "route": "/s/chat",
"auth": "authenticated", "auth": "authenticated",
"layout": "single", "layout": "single",
"version": "0.2.0", "version": "0.3.0",
"icon": "\ud83d\udcac", "icon": "\ud83d\udcac",
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.", "description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
"author": "armature", "author": "armature",

View File

@@ -36,7 +36,8 @@
var modules = [ var modules = [
'notes-crud.js', 'notes-crud.js',
'folders.js', 'folders.js',
'tags-search.js' 'tags-search.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Notes Runner — notes/shell-topbar suite
*
* Validates the Notes surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('notes/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/notes/js/main.js');
t.assert.eq(resp.status, 200, 'fetched notes main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/notes/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Notes Runner", "title": "Notes Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search." "description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search."
} }

View File

@@ -1,10 +1,10 @@
/** /**
* Notes — Surface Entry Point (v0.8.0) * Notes — Surface Entry Point (v0.9.0)
* *
* Markdown notes surface using the SDK: * Markdown notes surface using the SDK:
* sw.api.ext('notes') — scoped API client * sw.api.ext('notes') — scoped API client
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar * sw.shell.topbar shell topbar API
* window.CM — CodeMirror 6 bundle (optional) * window.CM — CodeMirror 6 bundle (optional)
*/ */
(async function () { (async function () {
@@ -59,7 +59,6 @@
// ── SDK modules ──────────────────────────── // ── SDK modules ────────────────────────────
var api = sw.api.ext('notes'); var api = sw.api.ext('notes');
var { Button, Spinner } = sw.ui; var { Button, Spinner } = sw.ui;
var Topbar = sw.shell.Topbar;
// ── Debounce helper ──────────────────────── // ── Debounce helper ────────────────────────
var _timers = {}; var _timers = {};
@@ -1729,13 +1728,19 @@
debounce('search', function() { loadNotes(); }, 300); debounce('search', function() { loadNotes(); }, 300);
} }
return html` // ── Shell topbar ───────────────────────────
<${Topbar} title="Notes"> useEffect(() => {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Notes');
sw.shell.topbar.setSlot(html`
<${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//> <${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//>
<${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//> <${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//>
<${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm" <${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm"
onClick=${function() { setShowGraph(!showGraph); }}>Graph<//> onClick=${function() { setShowGraph(!showGraph); }}>Graph<//>
<//> `);
}, [showGraph]);
return html`
<div class="ext-notes-app"> <div class="ext-notes-app">
<div class="ext-notes-sidebar"> <div class="ext-notes-sidebar">
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} /> <${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />

View File

@@ -6,7 +6,7 @@
"route": "/s/notes", "route": "/s/notes",
"auth": "authenticated", "auth": "authenticated",
"layout": "single", "layout": "single",
"version": "0.8.0", "version": "0.9.0",
"icon": "📝", "icon": "📝",
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.", "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
"author": "armature", "author": "armature",

View File

@@ -34,7 +34,8 @@
if (window.SR.base) assetBase = window.SR.base + assetBase; if (window.SR.base) assetBase = window.SR.base + assetBase;
var modules = [ var modules = [
'schedules-crud.js' 'schedules-crud.js',
'shell-topbar.js'
]; ];
var loaded = 0; var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Schedules Runner — schedules/shell-topbar suite
*
* Validates the Schedules surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('schedules/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/schedules/js/main.js');
t.assert.eq(resp.status, 200, 'fetched schedules main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/schedules/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner", "type": "test-runner",
"title": "Schedules Runner", "title": "Schedules Runner",
"auth": "admin", "auth": "admin",
"version": "0.1.0", "version": "0.2.0",
"description": "Integration tests for Schedules package — CRUD, run, enable/disable." "description": "Integration tests for Schedules package — CRUD, run, enable/disable."
} }

View File

@@ -5,7 +5,7 @@
* this surface talks directly to the core scheduled-tasks endpoints. * this surface talks directly to the core scheduled-tasks endpoints.
* *
* SDK usage: * SDK usage:
* sw.shell.Topbar — standard navigation bar * sw.shell.topbar — shell topbar API
* sw.ui.* — primitive components * sw.ui.* — primitive components
* sw.api.* — raw REST calls to /api/v1/schedules * sw.api.* — raw REST calls to /api/v1/schedules
*/ */
@@ -39,7 +39,6 @@
var { useState, useEffect, useCallback } = hooks; var { useState, useEffect, useCallback } = hooks;
var { render } = preact; var { render } = preact;
var { Button, FormField, Dialog, Spinner, Banner } = sw.ui; var { Button, FormField, Dialog, Spinner, Banner } = sw.ui;
var Topbar = sw.shell?.Topbar;
// ── API helpers (core kernel endpoints) ──── // ── API helpers (core kernel endpoints) ────
var API = '/api/v1/schedules'; var API = '/api/v1/schedules';
@@ -365,6 +364,16 @@
useEffect(function () { loadItems(); }, []); useEffect(function () { loadItems(); }, []);
// ── Shell topbar ───────────────────────────
useEffect(function () {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Schedules');
sw.shell.topbar.setSlot(html`
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
`);
}, [items.length]);
function handleSave() { function handleSave() {
setEditing(null); setEditing(null);
loadItems(); loadItems();
@@ -372,17 +381,6 @@
return html` return html`
<div class="ext-schedules-app"> <div class="ext-schedules-app">
${Topbar ? html`
<${Topbar} title="Schedules">
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
<//>
` : html`
<div class="ext-schedules-header">
<h1>Schedules</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
</div>
`}
${error && html`<${Banner} variant="danger" text=${error} />`} ${error && html`<${Banner} variant="danger" text=${error} />`}

View File

@@ -2,7 +2,7 @@
"id": "schedules", "id": "schedules",
"title": "Schedules", "title": "Schedules",
"type": "full", "type": "full",
"version": "0.1.0", "version": "0.2.0",
"tier": "browser", "tier": "browser",
"author": "Armature", "author": "Armature",
"icon": "⏰", "icon": "⏰",