@synopackageland/templates 0.42.0 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +6 -0
  4. package/templates/binary/dsm-package.yaml +4 -1
  5. package/templates/native-go/README.md +4 -1
  6. package/templates/native-go/dsm-package.yaml +4 -1
  7. package/templates/native-go/i18n/glossary.json +4 -4
  8. package/templates/native-go/i18n/messages/de.json +1 -1
  9. package/templates/native-go/i18n/messages/en.json +1 -1
  10. package/templates/native-go/i18n/messages/es.json +1 -1
  11. package/templates/native-go/i18n/messages/fr.json +1 -1
  12. package/templates/native-go/i18n/messages/it.json +1 -1
  13. package/templates/native-go/i18n/messages/ja.json +1 -1
  14. package/templates/native-go/i18n/messages/ko.json +1 -1
  15. package/templates/native-go/i18n/messages/zh-CN.json +1 -1
  16. package/templates/native-go/i18n/messages/zh-TW.json +1 -1
  17. package/templates/native-go/ui/app.css +2 -1
  18. package/templates/native-go/ui/app.js +8 -4
  19. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +6 -3
  20. package/templates/native-go-jobs/frontend/test/background-shell.test.ts +18 -0
  21. package/templates/native-go-vue/README.md +12 -5
  22. package/templates/native-go-vue/frontend/DESIGN.md +7 -3
  23. package/templates/native-go-vue/frontend/src/App.vue +7 -4
  24. package/templates/native-go-vue/frontend/src/i18n/de.json +4 -7
  25. package/templates/native-go-vue/frontend/src/i18n/en.json +4 -7
  26. package/templates/native-go-vue/frontend/src/i18n/es.json +4 -7
  27. package/templates/native-go-vue/frontend/src/i18n/fr.json +4 -7
  28. package/templates/native-go-vue/frontend/src/i18n/it.json +4 -7
  29. package/templates/native-go-vue/frontend/src/i18n/ja.json +4 -7
  30. package/templates/native-go-vue/frontend/src/i18n/ko.json +4 -7
  31. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +4 -7
  32. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +4 -7
  33. package/templates/native-go-vue/frontend/test/apps.test.ts +49 -7
  34. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +6 -4
  35. package/templates/native-node/dsm-package.yaml +4 -1
  36. package/templates/native-node-vue/README.md +11 -3
  37. package/templates/native-python/dsm-package.yaml +4 -1
  38. package/templates/native-python-vue/README.md +11 -3
  39. package/templates/native-rust/dsm-package.yaml +4 -1
  40. package/templates/native-rust-vue/README.md +11 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.42.2
4
+
5
+ - New Apps get no header Refresh. The Vue templates leave `AppShell`'s `#actions` empty (Sign out only with standalone login), so the shell draws no bar above the page; the plain UI's title row drops its Refresh button
6
+ - Data stays fresh without it: reopening the window reads the status, panels and portal again, the Vue status page reads again each time it is shown, and a failed read offers Retry beside its error
7
+ - Delegate templates keep their Retry inside the capability's own error Callout
8
+ - READMEs, `DESIGN.md` and `AGENTS.md` say `#actions` is for real page actions (such as Add), never a Refresh
9
+ - Drop the unused `refresh`, `greeting`, `save` and `saved` messages; error texts no longer ask the user to refresh
10
+
11
+ ## 0.42.1
12
+
13
+ - The commented `backup` example in the native and binary `dsm-package.yaml` says online backup reads SQLite in place, and shows the optional `exclude` list of data-folder globs left out of the archive (SDK 0.53.35, CLI 0.53.13)
14
+
3
15
  ## 0.42.0
4
16
 
5
17
  - Scaffold pnpm projects with pinned package managers and reproducible lockfiles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.42.0",
3
+ "version": "0.42.2",
4
4
  "description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,6 +17,12 @@ Only a confirmed refusal (signed out, expired session, not an administrator)
17
17
  shows the sign-in page or a `@synopackageland/ui` Dialog; loading text inside a
18
18
  section is fine.
19
19
 
20
+ Do not add a header Refresh. Re-read data when the window is shown again
21
+ (`ctx.reopen`) and when a page is shown; a failed read offers Retry beside its
22
+ error. `AppShell`'s `#actions` is for real page actions (such as Add); with
23
+ nothing in it the shell draws no bar. A section that needs a manual re-read
24
+ gets its own action in that section.
25
+
20
26
  Everything below holds for every native backend language; the backend
21
27
  sections at the end name each language's files, types and test commands.
22
28
 
@@ -44,9 +44,12 @@ ui: {dir: ui, launch: new-page}
44
44
  # generated scripts/backup/ as root: a fixed script that only switches to the
45
45
  # package account. See `synopkgland skill show backup`.
46
46
  # backup:
47
- # online: false # true keeps the App running; SQLite is snapshotted
47
+ # online: false # true keeps the App running; SQLite is read in
48
+ # # place. Other files are read as they are: a file
49
+ # # being written meanwhile may be inconsistent
48
50
  # data_version: "1.0" # "major.minor"; a restore refuses newer data
49
51
  # hook: bin/backup-hook # optional packaged freeze/thaw executable
52
+ # exclude: [tmp] # optional data-folder globs left out (scratch data)
50
53
  # To opt out, use only:
51
54
  # backup:
52
55
  # hyper_backup: false
@@ -205,7 +205,10 @@ the new version; `synopkgland check` refuses mismatched versions.
205
205
  `ui/app.js` must register `window.DsmPackageApps[<package>].mount(root, ctx)`
206
206
  (the template shows how, using `document.currentScript.dataset.package`).
207
207
  `ctx.call(method, params)` is the only transport. `ctx.reopen(fn)` runs `fn`
208
- whenever DSM shows the window again and returns an idempotent unsubscribe function. Scope your CSS under your own root class:
208
+ whenever DSM shows the window again and returns an idempotent unsubscribe function.
209
+ The template re-reads its status there instead of showing a Refresh button; a
210
+ failed read shows Retry beside its error. Keep header buttons for real page
211
+ actions, such as Add. Scope your CSS under your own root class:
209
212
  every DSM app shares one page.
210
213
 
211
214
  Plain JS and Vue receive the same `ctx.host` adapter. `ctx.host.platform()`
@@ -78,9 +78,12 @@ capabilities: {}
78
78
  # generated scripts/backup/ as root: a fixed script that only switches to the
79
79
  # package account. See `synopkgland skill show backup`.
80
80
  # backup:
81
- # online: false # true keeps the App running; SQLite is snapshotted
81
+ # online: false # true keeps the App running; SQLite is read in
82
+ # # place. Other files are read as they are: a file
83
+ # # being written meanwhile may be inconsistent
82
84
  # data_version: "1.0" # "major.minor"; a restore refuses newer data
83
85
  # hook: bin/backup-hook # optional packaged freeze/thaw executable
86
+ # exclude: [tmp] # optional data-folder globs left out (scratch data)
84
87
  # To opt out, use only:
85
88
  # backup:
86
89
  # hyper_backup: false
@@ -163,9 +163,7 @@
163
163
  "es": "Actualizar",
164
164
  "it": "Aggiorna"
165
165
  },
166
- "keys": [
167
- "app.refresh"
168
- ]
166
+ "keys": []
169
167
  },
170
168
  {
171
169
  "id": "retry",
@@ -181,7 +179,9 @@
181
179
  "es": "Volver a intentar",
182
180
  "it": "Riprova"
183
181
  },
184
- "keys": []
182
+ "keys": [
183
+ "app.retry"
184
+ ]
185
185
  },
186
186
  {
187
187
  "id": "back",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "Aktualisieren",
2
+ "app.retry": "Neuer Versuch",
3
3
  "app.title": "Es funktioniert",
4
4
  "error.admin_required": "Dieses Benutzerkonto verfügt nicht über die erforderlichen Berechtigungen, um diese Aktion durchzuführen",
5
5
  "error.app_privilege_denied": "Dieses Benutzerkonto verfügt nicht über die erforderlichen Berechtigungen, um diese Aktion durchzuführen",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "Refresh",
2
+ "app.retry": "Retry",
3
3
  "app.title": "It works",
4
4
  "error.admin_required": "Sorry, this user account has not been assigned proper privileges to perform this action",
5
5
  "error.app_privilege_denied": "Sorry, this user account has not been assigned proper privileges to perform this action",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "Actualizar",
2
+ "app.retry": "Volver a intentar",
3
3
  "app.title": "Funciona",
4
4
  "error.admin_required": "A esta cuenta de usuario no se le han asignado los privilegios adecuados para realizar esta acción",
5
5
  "error.app_privilege_denied": "A esta cuenta de usuario no se le han asignado los privilegios adecuados para realizar esta acción",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "Actualiser",
2
+ "app.retry": "Réessayer",
3
3
  "app.title": "Ça fonctionne",
4
4
  "error.admin_required": "Désolé, ce compte utilisateur ne dispose pas des droits nécessaires pour accomplir cette action",
5
5
  "error.app_privilege_denied": "Désolé, ce compte utilisateur ne dispose pas des droits nécessaires pour accomplir cette action",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "Aggiorna",
2
+ "app.retry": "Riprova",
3
3
  "app.title": "Funziona",
4
4
  "error.admin_required": "Siamo spiacenti, a questo account utente non sono stati assegnati privilegi appropriati per eseguire questa azione",
5
5
  "error.app_privilege_denied": "Siamo spiacenti, a questo account utente non sono stati assegnati privilegi appropriati per eseguire questa azione",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "最新の情報に更新",
2
+ "app.retry": "再試行",
3
3
  "app.title": "動作しています",
4
4
  "error.admin_required": "申し訳ありませんが、このユーザー アカウントにはこの操作を行うための権限が与えられていません",
5
5
  "error.app_privilege_denied": "申し訳ありませんが、このユーザー アカウントにはこの操作を行うための権限が与えられていません",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "새로 고침",
2
+ "app.retry": "재시도",
3
3
  "app.title": "정상적으로 작동합니다",
4
4
  "error.admin_required": "죄송합니다. 이 사용자 계정에는 이 작업을 수행하기에 적절한 권한이 할당되지 않았습니다",
5
5
  "error.app_privilege_denied": "죄송합니다. 이 사용자 계정에는 이 작업을 수행하기에 적절한 권한이 할당되지 않았습니다",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "刷新",
2
+ "app.retry": "重试",
3
3
  "app.title": "运行成功",
4
4
  "error.admin_required": "抱歉,此用户帐户权限不足,无法执行此操作",
5
5
  "error.app_privilege_denied": "抱歉,此用户帐户权限不足,无法执行此操作",
@@ -1,5 +1,5 @@
1
1
  {
2
- "app.refresh": "重新整理",
2
+ "app.retry": "重試",
3
3
  "app.title": "成功運作了",
4
4
  "error.admin_required": "抱歉,此使用者帳號權限不足,無法執行此動作",
5
5
  "error.app_privilege_denied": "抱歉,此使用者帳號權限不足,無法執行此動作",
@@ -34,7 +34,6 @@
34
34
  background: var(--ui-surface);
35
35
  }
36
36
  .dsmapp *, .dsmapp *::before, .dsmapp *::after { box-sizing: inherit; }
37
- .dsmapp-header { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-space-3); }
38
37
  .dsmapp h1 { margin: 0 0 var(--ui-space-1); font-size: var(--ui-font-size-title); font-weight: 600; }
39
38
  .dsmapp-secondary {
40
39
  height: var(--ui-control-md); padding: 0 var(--ui-space-3); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);
@@ -55,6 +54,8 @@
55
54
  .dsmapp-form button:disabled { opacity: 0.6; cursor: default; }
56
55
  .dsmapp-note { margin: 0; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); }
57
56
  .dsmapp-note[hidden] { display: none; }
57
+ .dsmapp-message-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-3); }
58
+ .dsmapp-message-row .dsmapp-message { flex: 1 1 auto; }
58
59
  .dsmapp-message { min-height: var(--ui-line-height); color: var(--ui-danger-text); }
59
60
  .dsmapp-facts { display: grid; grid-template-columns: fit-content(40%) minmax(0, 1fr); gap: var(--ui-space-1) var(--ui-space-4); margin: 0; }
60
61
  .dsmapp-facts dt { color: var(--ui-text-secondary); }
@@ -36,14 +36,16 @@
36
36
  function mount(root, ctx) {
37
37
  var t = ctx.t;
38
38
  var status = el("dl", { class: "dsmapp-facts" });
39
- var refreshButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.refresh") });
40
39
  var message = el("p", { class: "dsmapp-message", role: "status" });
40
+ // Shown only after a failed read; the App re-reads on reopen, so no header Refresh.
41
+ var retryButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.retry") });
42
+ retryButton.hidden = true;
41
43
  var portal = ctx.portal ? el("section", { class: "dsmapp-portal" }) : null;
42
44
 
43
45
  root.classList.add("dsmapp");
44
46
  root.replaceChildren(
45
- el("div", { class: "dsmapp-header" }, [el("h1", { text: t("app.title") }), refreshButton]),
46
- message,
47
+ el("h1", { text: t("app.title") }),
48
+ el("div", { class: "dsmapp-message-row" }, [message, retryButton]),
47
49
  el("h2", { text: t("status.heading") }),
48
50
  status
49
51
  );
@@ -54,10 +56,12 @@
54
56
  var code = (error && error.code) || "internal";
55
57
  var text = t("error." + code);
56
58
  message.textContent = text === "error." + code ? (error && error.message) || t("error.internal") : text;
59
+ retryButton.hidden = false;
57
60
  }
58
61
 
59
62
  function refresh() {
60
63
  message.textContent = "";
64
+ retryButton.hidden = true;
61
65
  ctx.call("app.status").then(function (data) {
62
66
  status.replaceChildren();
63
67
  [[t("status.user"), data.admin ? t("status.userAdmin", { user: data.user }) : data.user],
@@ -73,7 +77,7 @@
73
77
  }).catch(showError);
74
78
  }
75
79
 
76
- refreshButton.addEventListener("click", refresh);
80
+ retryButton.addEventListener("click", refresh);
77
81
 
78
82
  ctx.reopen(refresh);
79
83
  refresh();
@@ -102,7 +102,8 @@ test('the app mount forwards ctx.executor to administrator setup only',async()=>
102
102
  }
103
103
  }
104
104
  })
105
- test('the header Refresh reloads the save destinations',async()=>{
105
+ // No header Refresh: reopening the App reloads the save destinations.
106
+ test('reopening the App reloads the save destinations',async()=>{
106
107
  const call=vi.fn(async(method:string)=>{
107
108
  if(method==='app.status')return {package:'DelegateApp',version:'1',user:'alice',admin:false}
108
109
  if(method==='delegate.status')return {state:'ready',disabled:false,lifetime:'until-revoked',destinations:['save']}
@@ -112,11 +113,13 @@ test('the header Refresh reloads the save destinations',async()=>{
112
113
  throw {code:'unknown_method'}
113
114
  })
114
115
  const root=document.createElement('div');document.body.append(root)
115
- const app=mount(root,dsmWindow({call}))
116
+ const reopen:Array<()=>void>=[]
117
+ const app=mount(root,dsmWindow({call,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}}))
116
118
  cleanup.push(()=>{app.unmount();root.remove()});await settle()
117
119
  const reads=()=>call.mock.calls.filter(([method])=>method==='delegate.destination').length
118
120
  const before=reads();expect(before).toBeGreaterThan(0)
119
- button(root,'Refresh').click();await settle()
121
+ expect([...root.querySelectorAll('button')].some(n=>n.textContent?.trim()==='Refresh')).toBe(false)
122
+ reopen.forEach(fn=>fn());await settle()
120
123
  expect(reads()).toBeGreaterThan(before)
121
124
  })
122
125
  test('the panels show a skeleton while the delegate status loads',async()=>{
@@ -17,3 +17,21 @@ test('a jobs project opens its shell without starting a job',async()=>{
17
17
  expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(false)
18
18
  }finally{app.unmount();root.remove()}
19
19
  })
20
+ // No header Refresh: showing the status page again reads its status again.
21
+ test('returning to the status page reads its status again',async()=>{
22
+ const call=vi.fn(async(method:string)=>{
23
+ if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
24
+ throw {code:'unknown_method'}
25
+ })
26
+ const root=document.createElement('div');document.body.append(root)
27
+ const app=mount(root,dsmWindow({call}))
28
+ try{
29
+ await settle()
30
+ const reads=()=>call.mock.calls.filter(([method])=>method==='app.status').length
31
+ const item=(name:string)=>[...root.querySelectorAll<HTMLElement>('button')].find(n=>n.textContent?.trim()===name)!
32
+ item('Background jobs').click();await settle()
33
+ const before=reads()
34
+ item('Status').click();await settle()
35
+ expect(reads()).toBeGreaterThan(before)
36
+ }finally{app.unmount();root.remove()}
37
+ })
@@ -42,8 +42,7 @@ pnpm test
42
42
  pnpm run build
43
43
  ```
44
44
 
45
- The base app reads package status and edits the package greeting through
46
- `ctx.call`. Each window has its own Vue mount, request state and host
45
+ The base app reads package status through `ctx.call`. Each window has its own Vue mount, request state and host
47
46
  subscriptions; unmount disposes subscriptions and ignores late responses.
48
47
 
49
48
  The build emits `app.module.js`, `app.css` and any lazy chunks. The entry
@@ -94,7 +93,7 @@ SDK's light default in place.
94
93
 
95
94
  Use the installed `@synopackageland/ui` and its declared compatible patch range.
96
95
  `App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
97
- `pages` from `shellPages`, the page's buttons in `#actions` and the pages in its
96
+ `pages` from `shellPages`, real page actions in `#actions` and the pages in its
98
97
  default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
99
98
  `#actions` in a bar above the page and scrolls only the content; a settings
100
99
  page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
@@ -109,6 +108,14 @@ device-width viewport. Its full-page sidebar names the App with
109
108
  shows the signed-in DSM account and its role from `ctx.user`. See the ui's
110
109
  `docs/adaptive-layout.md`.
111
110
 
111
+ The template puts no Refresh in `#actions`, and with nothing there `AppShell`
112
+ draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
113
+ the status, the panels and the portal again when the window is shown again,
114
+ and showing the status page again reads it again. A failed read offers Retry
115
+ beside its error. Sign out appears in `#actions` only when standalone login is
116
+ on. Keep `#actions` for real page actions, such as Add; a section that needs a
117
+ manual re-read gets its own action in that section, not a header Refresh.
118
+
112
119
  The frontend SDK and UI use patch ranges; the lockfile records the installed
113
120
  versions. Update the lockfile deliberately when taking a newer patch.
114
121
 
@@ -120,8 +127,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
120
127
  even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
121
128
  with Back keep their title bar. `header-actions` share one "…" menu; only an
122
129
  entry marked `primary: true`, or a sole entry with an icon, becomes a separate
123
- bar button. The template's `#actions` stays page actions; no title or empty-bar
124
- workaround is needed.
130
+ bar button. The template's `#actions` holds only real page actions; no title or
131
+ empty-bar workaround is needed.
125
132
 
126
133
  `AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
127
134
  a full page as well, bounded at `height: 100%`) with the UI locale from
@@ -30,8 +30,8 @@ Use `syno-pkg-land-ui` directly in package scripts; keep AI guidance version-ind
30
30
  ## Overview
31
31
 
32
32
  The template runs inside a DSM package window. `AppShell` provides status and
33
- optional background navigation. The base page shows package status and a greeting
34
- editor; the root overlay adds its existing fixed-operation authorization gate.
33
+ optional background navigation. The base page shows package status; the root
34
+ overlay adds its existing fixed-operation authorization gate.
35
35
 
36
36
  Use the installed `@synopackageland/ui` controls and `themes/dsm7-embedded.css`.
37
37
  The theme owns colors, typography, spacing and dialogs. Keep per-app modal scope,
@@ -39,7 +39,11 @@ a unique Vue ID prefix and vertical scrolling at the DSM minimum window size.
39
39
  In a DSM window, Dialogs may leave the window (`dialog-bounds="viewport"`) and a
40
40
  press on one brings the window to the front; below 600px they are bottom sheets.
41
41
 
42
- Refresh and save expose busy state and localized errors. Reopen refreshes status.
42
+ There is no header Refresh: reopening the window, or showing the status page
43
+ again, reads it again, and a failed read offers Retry beside its error. The
44
+ shell's `#actions` holds only real page actions (Sign out with standalone
45
+ login); empty, the shell draws no bar. Reads expose busy state and localized
46
+ errors.
43
47
  Each mount owns its state; unmount unsubscribes from host events and ignores late
44
48
  responses. Backend route policies decide access.
45
49
 
@@ -46,13 +46,15 @@ async function refresh() {
46
46
  } catch(e) {if(!disposed && !(early && login.enabled.value)) error.value = message(e)}
47
47
  finally {if(!disposed) {busy.value=false; if(again) {again=false; void refresh()}}}
48
48
  }
49
- // The header Refresh also reloads the panels below and reads the portal again.
49
+ // No header Refresh: reopening the App, or showing its status page again, reads
50
+ // the status, the panels below and the portal again.
50
51
  const panels = ref<{refresh(): unknown}>()
51
52
  const portalReads = ref(0)
52
53
  const portalKey = computed(() => `${app.value?.user}:${portalReads.value}`)
53
54
  function refreshAll() {
54
55
  void refresh(); void panels.value?.refresh(); portalReads.value++
55
56
  }
57
+ watch(page, shown => {if(shown==='status') refreshAll()})
56
58
  watch(login.subject, (subject, previous) => {
57
59
  if(!subject) {app.value=undefined;error.value=''}
58
60
  else if((!previous || previous.user!==subject.user)) void refresh()
@@ -61,7 +63,7 @@ async function signOut() {
61
63
  try {await login.logout()} catch(e) {if(!disposed) error.value=message(e)}
62
64
  }
63
65
  onMounted(()=>{
64
- unsubscribe = props.ctx.reopen(()=>{if(!disposed) void refresh()})
66
+ unsubscribe = props.ctx.reopen(()=>{if(!disposed) refreshAll()})
65
67
  // The session check runs in the background; only a confirmed sign-in need replaces the App.
66
68
  void login.start()
67
69
  void refresh()
@@ -72,10 +74,11 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose()})
72
74
  <template>
73
75
  <AppShell app-root v-model="page" :ctx="shellCtx" :locale="ctx.locale" :pages="pages" :denied="signIn" sidebar-storage-key="__PACKAGE__">
74
76
  <template #denied><LoginPage :session="login" :locale="ctx.locale" /></template>
75
- <template #actions><Button variant="secondary" :disabled="busy" @click="refreshAll">{{ copy.refresh }}</Button><Button v-if="login.enabled.value" variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
77
+ <!-- #actions holds real page actions (such as Add), never a Refresh. Without one the shell draws no bar. -->
78
+ <template v-if="login.enabled.value" #actions><Button variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
76
79
  <ExecutorGate :ctx="ctx" :admin="ctx.user?.isAdmin ?? !!app?.admin">
77
80
  <div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
78
- <Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span></Callout>
81
+ <Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span><template #actions><Button variant="secondary" :disabled="busy" @click="refreshAll">{{ copy.retry }}</Button></template></Callout>
79
82
  <div data-page="status" v-show="page==='status'">
80
83
  <SettingsSection :title="copy.statusTitle" :collapsible="false" :loading="busy && !app">
81
84
  <div v-if="app" data-app-status>
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "Aktualisieren",
2
+ "retry": "Neuer Versuch",
3
3
  "signOut": "Abmelden",
4
4
  "settings": "Paketeinstellungen",
5
5
  "actionRequired": "Aktion erforderlich",
6
6
  "statusTitle": "Paketstatus",
7
- "statusError": "Status nicht verfügbar. Aktualisieren Sie die Ansicht",
8
- "greeting": "Begrüßung",
9
- "save": "Speichern",
10
- "saved": "Begrüßung gespeichert",
7
+ "statusError": "Status nicht verfügbar",
11
8
  "errors": {
12
9
  "session_required": "Ihre DSM-Sitzung ist abgelaufen. Melden Sie sich erneut an",
13
10
  "admin_required": "Diese Aktion muss ein DSM-Administrator ausführen",
14
11
  "app_privilege_denied": "Die DSM-Anwendungsberechtigungen erlauben diesem Konto nicht, diese App zu verwenden",
15
- "auth_unavailable": "Die DSM-Authentifizierung ist nicht verfügbar. Aktualisieren Sie die Ansicht und versuchen Sie es erneut",
16
- "request_failed": "Anfrage fehlgeschlagen ({code}). Aktualisieren Sie die Ansicht und versuchen Sie es erneut"
12
+ "auth_unavailable": "Die DSM-Authentifizierung ist nicht verfügbar. Versuchen Sie es später erneut",
13
+ "request_failed": "Anfrage fehlgeschlagen ({code}). Versuchen Sie es erneut"
17
14
  },
18
15
  "packageName": "Paketname",
19
16
  "user": "Benutzer",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "Refresh",
2
+ "retry": "Retry",
3
3
  "signOut": "Sign out",
4
4
  "settings": "Package settings",
5
5
  "actionRequired": "Action required",
6
6
  "statusTitle": "Package status",
7
- "statusError": "Unable to load status. Refresh to try again",
8
- "greeting": "Greeting",
9
- "save": "Save",
10
- "saved": "Greeting saved",
7
+ "statusError": "Unable to load status",
11
8
  "errors": {
12
9
  "session_required": "Your DSM session expired. Sign in again",
13
10
  "admin_required": "A DSM administrator must perform this action",
14
11
  "app_privilege_denied": "DSM Application Privileges do not allow this account to use this app",
15
- "auth_unavailable": "DSM authentication is unavailable. Refresh to try again",
16
- "request_failed": "Request failed ({code}). Refresh and try again"
12
+ "auth_unavailable": "DSM authentication is unavailable. Try again later",
13
+ "request_failed": "Request failed ({code}). Try again"
17
14
  },
18
15
  "packageName": "Package name",
19
16
  "user": "User",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "Actualizar",
2
+ "retry": "Volver a intentar",
3
3
  "signOut": "Cerrar sesión",
4
4
  "settings": "Configuración del paquete",
5
5
  "actionRequired": "Acción necesaria",
6
6
  "statusTitle": "Estado del paquete",
7
- "statusError": "Estado no disponible. Actualice la página",
8
- "greeting": "Saludo",
9
- "save": "Guardar",
10
- "saved": "Saludo guardado",
7
+ "statusError": "Estado no disponible",
11
8
  "errors": {
12
9
  "session_required": "Su sesión de DSM ha caducado. Vuelva a iniciar sesión",
13
10
  "admin_required": "Un administrador de DSM debe realizar esta acción",
14
11
  "app_privilege_denied": "Los privilegios de aplicación de DSM no permiten que esta cuenta use esta aplicación",
15
- "auth_unavailable": "La autenticación de DSM no está disponible. Actualice e inténtelo de nuevo",
16
- "request_failed": "La solicitud falló ({code}). Actualice e inténtelo de nuevo"
12
+ "auth_unavailable": "La autenticación de DSM no está disponible. Inténtelo de nuevo más tarde",
13
+ "request_failed": "La solicitud falló ({code}). Inténtelo de nuevo"
17
14
  },
18
15
  "packageName": "Nombre del paquete",
19
16
  "user": "Usuario",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "Actualiser",
2
+ "retry": "Réessayer",
3
3
  "signOut": "Se déconnecter",
4
4
  "settings": "Paramètres du paquet",
5
5
  "actionRequired": "Action requise",
6
6
  "statusTitle": "État du paquet",
7
- "statusError": "État indisponible. Actualisez la page",
8
- "greeting": "Message d’accueil",
9
- "save": "Sauvegarder",
10
- "saved": "Message d’accueil enregistré",
7
+ "statusError": "État indisponible",
11
8
  "errors": {
12
9
  "session_required": "Votre session DSM a expiré. Reconnectez-vous",
13
10
  "admin_required": "Un administrateur DSM doit effectuer cette action",
14
11
  "app_privilege_denied": "Les privilèges d’application DSM n’autorisent pas ce compte à utiliser cette application",
15
- "auth_unavailable": "L’authentification DSM n’est pas disponible. Actualisez et réessayez",
16
- "request_failed": "La requête a échoué ({code}). Actualisez et réessayez"
12
+ "auth_unavailable": "L’authentification DSM n’est pas disponible. Réessayez plus tard",
13
+ "request_failed": "La requête a échoué ({code}). Réessayez"
17
14
  },
18
15
  "packageName": "Nom du paquet",
19
16
  "user": "Utilisateur",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "Aggiorna",
2
+ "retry": "Riprova",
3
3
  "signOut": "Disconnetti",
4
4
  "settings": "Impostazioni del pacchetto",
5
5
  "actionRequired": "Azione richiesta",
6
6
  "statusTitle": "Stato del pacchetto",
7
- "statusError": "Stato non disponibile. Aggiornare la pagina",
8
- "greeting": "Saluto",
9
- "save": "Salva",
10
- "saved": "Saluto salvato",
7
+ "statusError": "Stato non disponibile",
11
8
  "errors": {
12
9
  "session_required": "La sessione DSM è scaduta. Accedere di nuovo",
13
10
  "admin_required": "Questa azione deve essere eseguita da un amministratore DSM",
14
11
  "app_privilege_denied": "I privilegi delle applicazioni DSM non consentono a questo account di usare questa app",
15
- "auth_unavailable": "L’autenticazione DSM non è disponibile. Aggiornare e riprovare",
16
- "request_failed": "Richiesta non riuscita ({code}). Aggiornare e riprovare"
12
+ "auth_unavailable": "L’autenticazione DSM non è disponibile. Riprovare più tardi",
13
+ "request_failed": "Richiesta non riuscita ({code}). Riprovare"
17
14
  },
18
15
  "packageName": "Nome pacchetto",
19
16
  "user": "Utente",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "最新の情報に更新",
2
+ "retry": "再試行",
3
3
  "signOut": "サインアウト",
4
4
  "settings": "パッケージ設定",
5
5
  "actionRequired": "対応が必要です",
6
6
  "statusTitle": "パッケージの状態",
7
- "statusError": "ステータスを読み込めません。更新してください",
8
- "greeting": "あいさつ",
9
- "save": "保存",
10
- "saved": "あいさつを保存しました",
7
+ "statusError": "ステータスを読み込めません",
11
8
  "errors": {
12
9
  "session_required": "DSM セッションの有効期限が切れました。もう一度サインインしてください",
13
10
  "admin_required": "この操作は DSM 管理者が行う必要があります",
14
11
  "app_privilege_denied": "DSM のアプリケーション権限により、このアカウントはこのアプリを使用できません",
15
- "auth_unavailable": "DSM 認証を利用できません。最新の情報に更新してから、もう一度お試しください",
16
- "request_failed": "リクエストに失敗しました({code})。最新の情報に更新してから、もう一度お試しください"
12
+ "auth_unavailable": "DSM 認証を利用できません。しばらくしてから、もう一度お試しください",
13
+ "request_failed": "リクエストに失敗しました({code})。もう一度お試しください"
17
14
  },
18
15
  "packageName": "パッケージ名",
19
16
  "user": "ユーザー",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "새로 고침",
2
+ "retry": "재시도",
3
3
  "signOut": "로그아웃",
4
4
  "settings": "패키지 설정",
5
5
  "actionRequired": "조치 필요",
6
6
  "statusTitle": "패키지 상태",
7
- "statusError": "상태를 불러올 수 없습니다. 새로 고침하십시오",
8
- "greeting": "인사말",
9
- "save": "저장",
10
- "saved": "인사말이 저장되었습니다",
7
+ "statusError": "상태를 불러올 수 없습니다",
11
8
  "errors": {
12
9
  "session_required": "DSM 세션이 만료되었습니다. 다시 로그인하십시오",
13
10
  "admin_required": "이 작업은 DSM 관리자가 수행해야 합니다",
14
11
  "app_privilege_denied": "DSM 응용 프로그램 권한에서 이 계정의 앱 사용이 허용되지 않습니다",
15
- "auth_unavailable": "DSM 인증을 사용할 수 없습니다. 새로 고친 후 다시 시도하십시오",
16
- "request_failed": "요청이 실패했습니다({code}). 새로 고친 후 다시 시도하십시오"
12
+ "auth_unavailable": "DSM 인증을 사용할 수 없습니다. 잠시 후 다시 시도하십시오",
13
+ "request_failed": "요청이 실패했습니다({code}). 다시 시도하십시오"
17
14
  },
18
15
  "packageName": "패키지 이름",
19
16
  "user": "사용자",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "刷新",
2
+ "retry": "重试",
3
3
  "signOut": "注销",
4
4
  "settings": "套件设置",
5
5
  "actionRequired": "需要处理",
6
6
  "statusTitle": "套件状态",
7
- "statusError": "无法加载状态,请刷新",
8
- "greeting": "问候语",
9
- "save": "保存",
10
- "saved": "问候语已保存",
7
+ "statusError": "无法加载状态",
11
8
  "errors": {
12
9
  "session_required": "DSM 登录已过期,请重新登录",
13
10
  "admin_required": "此操作必须由 DSM 管理员执行",
14
11
  "app_privilege_denied": "DSM 应用程序权限不允许此账户使用此 App",
15
- "auth_unavailable": "DSM 验证暂时不可用,请刷新后重试",
16
- "request_failed": "请求失败({code}),请刷新后重试"
12
+ "auth_unavailable": "DSM 验证暂时不可用,请稍后重试",
13
+ "request_failed": "请求失败({code}),请重试"
17
14
  },
18
15
  "packageName": "套件名称",
19
16
  "user": "用户帐号",
@@ -1,19 +1,16 @@
1
1
  {
2
- "refresh": "重新整理",
2
+ "retry": "重試",
3
3
  "signOut": "登出",
4
4
  "settings": "套件設定",
5
5
  "actionRequired": "需要處理",
6
6
  "statusTitle": "套件狀態",
7
- "statusError": "無法載入狀態,請重新整理",
8
- "greeting": "問候語",
9
- "save": "儲存",
10
- "saved": "問候語已儲存",
7
+ "statusError": "無法載入狀態",
11
8
  "errors": {
12
9
  "session_required": "DSM 登入已逾時,請重新登入",
13
10
  "admin_required": "此操作須由 DSM 管理員執行",
14
11
  "app_privilege_denied": "DSM 應用程式權限不允許此帳號使用這個 App",
15
- "auth_unavailable": "DSM 驗證目前無法使用,請重新整理後再試",
16
- "request_failed": "要求失敗({code}),請重新整理後再試"
12
+ "auth_unavailable": "DSM 驗證目前無法使用,請稍後再試",
13
+ "request_failed": "要求失敗({code}),請再試一次"
17
14
  },
18
15
  "packageName": "套件名稱",
19
16
  "user": "使用者帳號",
@@ -47,13 +47,26 @@ test('a DSM window stays light when the system prefers dark',async()=>{
47
47
  expect(f.root.textContent).toContain('Light')
48
48
  }finally{f.close();Object.defineProperty(window,'matchMedia',{configurable:true,writable:true,value:query})}
49
49
  })
50
- test('failed status loading offers a retry and releases the mounting class',async()=>{
50
+ test('failed status loading offers a Retry beside the error and releases the mounting class',async()=>{
51
51
  const f=fixture('Failure')
52
52
  try{
53
- await settle();f.call.mockRejectedValue({code:'auth_unavailable'});f.reopen.forEach(fn=>fn());await settle()
53
+ await settle();f.app.unmount()
54
+ let fail=true
55
+ f.call.mockImplementation(async(method:string)=>{
56
+ if(method!=='app.status')throw {code:'unknown_method'}
57
+ if(fail){fail=false;throw {code:'auth_unavailable'}}
58
+ return {package:'Failure',version:'1',user:'admin',admin:true}
59
+ })
60
+ const app=mount(f.root,f.ctx);await settle()
54
61
  expect(f.root.textContent).toContain('DSM authentication is unavailable')
55
- expect(f.root.querySelector('button')?.disabled).toBe(false)
56
- f.app.unmount();expect(f.root.classList.contains('dsmapp-vue-root')).toBe(false)
62
+ const retry=[...f.root.querySelectorAll<HTMLButtonElement>('[data-ui-callout-actions] button')].find(n=>n.textContent==='Retry')!
63
+ expect(retry.disabled).toBe(false)
64
+ const reads=f.call.mock.calls.filter(([method])=>method==='app.status').length
65
+ retry.click();await settle()
66
+ expect(f.call.mock.calls.filter(([method])=>method==='app.status').length).toBeGreaterThan(reads)
67
+ expect(f.root.textContent).not.toContain('DSM authentication is unavailable')
68
+ expect(f.root.querySelector('[data-app-status]')).not.toBeNull()
69
+ app.unmount();expect(f.root.classList.contains('dsmapp-vue-root')).toBe(false)
57
70
  }finally{f.close()}
58
71
  })
59
72
  test('a portal app shows the Application Portal section on the status page',async()=>{
@@ -70,7 +83,7 @@ test('a portal app shows the Application Portal section on the status page',asyn
70
83
  app.unmount()
71
84
  }finally{without.close();f.root.remove()}
72
85
  })
73
- test('the header Refresh reads the portal and executor status again',async()=>{
86
+ test('reopening the App reads the status, portal and executor status again',async()=>{
74
87
  const f=fixture('Portal')
75
88
  try{
76
89
  await settle();f.app.unmount()
@@ -79,13 +92,42 @@ test('the header Refresh reads the portal and executor status again',async()=>{
79
92
  f.ctx.portal={messages:{title:'Application Portal',link:'Direct link:'},status:portal,apply:vi.fn(),disable:vi.fn(),openControlPanel:vi.fn(),mountPanel:vi.fn()}
80
93
  f.ctx.executor={status:executor,authorize:vi.fn(),control:vi.fn()} as unknown as Context['executor']
81
94
  const app=mount(f.root,f.ctx);await settle()
82
- const [portalReads,executorReads]=[portal.mock.calls.length,executor.mock.calls.length]
83
- ;[...f.root.querySelectorAll('button')].find(n=>n.textContent==='Refresh')!.click();await settle()
95
+ const reads=()=>f.call.mock.calls.filter(([method])=>method==='app.status').length
96
+ const [statusReads,portalReads,executorReads]=[reads(),portal.mock.calls.length,executor.mock.calls.length]
97
+ f.reopen.forEach(fn=>fn());await settle()
98
+ expect(reads()).toBeGreaterThan(statusReads)
84
99
  expect(portal.mock.calls.length).toBeGreaterThan(portalReads)
85
100
  expect(executor.mock.calls.length).toBeGreaterThan(executorReads)
86
101
  app.unmount()
87
102
  }finally{f.root.remove()}
88
103
  })
104
+ // The template puts no Refresh in the shell's #actions: with nothing there the
105
+ // shell draws no bar above the page. Sign out shows only with standalone login.
106
+ test('the status page has no header bar and no Refresh button',async()=>{
107
+ const f=fixture('Bare')
108
+ try{
109
+ await settle()
110
+ expect([...f.root.querySelectorAll('button')].some(n=>n.textContent?.trim()==='Refresh')).toBe(false)
111
+ const bar=f.root.querySelector<HTMLElement>('[data-app-page-bar]')
112
+ expect(bar===null||bar.hidden).toBe(true)
113
+ }finally{f.close()}
114
+ })
115
+ test('with standalone login the bar holds only Sign out',async()=>{
116
+ const call=vi.fn(async(method:string)=>{
117
+ if(method==='identity.status')return {enabled:true}
118
+ if(method==='identity.session')return {user:'alice',uid:'1',expiresAt:new Date(Date.now()+3600_000).toISOString()}
119
+ if(method==='app.status')return {package:'Login',version:'1',user:'alice',admin:false}
120
+ throw {code:'unknown_method'}
121
+ })
122
+ const root=document.createElement('div');document.body.append(root)
123
+ const app=mount(root,dsmWindow({call}))
124
+ try{
125
+ await settle()
126
+ const bar=root.querySelector<HTMLElement>('[data-app-page-bar]')!
127
+ expect(bar.hidden).toBe(false)
128
+ expect([...bar.querySelectorAll('button')].map(n=>n.textContent?.trim())).toEqual(['Sign out'])
129
+ }finally{app.unmount();root.remove()}
130
+ })
89
131
  test('the app renders and loads status while the session check is still pending',async()=>{
90
132
  const call=vi.fn(async(method:string)=>{
91
133
  if(method==='identity.status')return new Promise(()=>{})
@@ -8,8 +8,10 @@ import {appCopy} from '../src/i18n'
8
8
  import {identityCopy} from '@synopackageland/sdk/vue'
9
9
  async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
10
10
  const buttons=(root:HTMLElement)=>[...root.querySelectorAll('button')].map(b=>b.textContent?.trim())
11
+ const reopen:Array<()=>void>=[]
11
12
  function start(locale:string,call:(method:string)=>Promise<unknown>){
12
- const ctx=dsmWindow({locale,call:vi.fn(call) as never})
13
+ reopen.length=0
14
+ const ctx=dsmWindow({locale,call:vi.fn(call) as never,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}})
13
15
  const root=document.createElement('div');document.body.append(root)
14
16
  const app=mount(root,ctx)
15
17
  return {root,close(){app.unmount();root.remove()}}
@@ -23,12 +25,12 @@ test('the status page and its errors follow the DSM language',async()=>{
23
25
  })
24
26
  try{
25
27
  await settle()
26
- expect(buttons(root)).toContain(copy.refresh)
28
+ expect(buttons(root)).not.toContain('Aktualisieren')
27
29
  expect(root.textContent).toContain(copy.statusTitle)
28
30
  expect(root.textContent).toContain(copy.packageName)
29
- expect(root.textContent).not.toContain(copy.greeting)
30
- denied=true;[...root.querySelectorAll('button')].find(button=>button.textContent?.trim()===copy.refresh)!.click();await settle()
31
+ denied=true;reopen.forEach(fn=>fn());await settle()
31
32
  expect(root.textContent).toContain(copy.errors.admin_required)
33
+ expect(buttons(root)).toContain(copy.retry)
32
34
  expect(root.querySelector('[aria-label]')?.getAttribute('aria-label')).not.toBe('Package settings')
33
35
  }finally{close()}
34
36
  })
@@ -87,9 +87,12 @@ capabilities: {}
87
87
  # generated scripts/backup/ as root: a fixed script that only switches to the
88
88
  # package account. See `synopkgland skill show backup`.
89
89
  # backup:
90
- # online: false # true keeps the App running; SQLite is snapshotted
90
+ # online: false # true keeps the App running; SQLite is read in
91
+ # # place. Other files are read as they are: a file
92
+ # # being written meanwhile may be inconsistent
91
93
  # data_version: "1.0" # "major.minor"; a restore refuses newer data
92
94
  # hook: bin/backup-hook # optional packaged freeze/thaw executable
95
+ # exclude: [tmp] # optional data-folder globs left out (scratch data)
93
96
  # To opt out, use only:
94
97
  # backup:
95
98
  # hyper_backup: false
@@ -45,7 +45,7 @@ bridge App's runtime serves.
45
45
 
46
46
  Use the installed `@synopackageland/ui` and its declared compatible patch range.
47
47
  `App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
48
- `pages` from `shellPages`, the page's buttons in `#actions` and the pages in its
48
+ `pages` from `shellPages`, real page actions in `#actions` and the pages in its
49
49
  default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
50
50
  `#actions` in a bar above the page and scrolls only the content; a settings
51
51
  page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
60
60
  shows the signed-in DSM account and its role from `ctx.user`. See the ui's
61
61
  `docs/adaptive-layout.md`.
62
62
 
63
+ The template puts no Refresh in `#actions`, and with nothing there `AppShell`
64
+ draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
65
+ the status, the panels and the portal again when the window is shown again,
66
+ and showing the status page again reads it again. A failed read offers Retry
67
+ beside its error. Sign out appears in `#actions` only when standalone login is
68
+ on. Keep `#actions` for real page actions, such as Add; a section that needs a
69
+ manual re-read gets its own action in that section, not a header Refresh.
70
+
63
71
  The frontend SDK and UI use patch ranges; the lockfile records the installed
64
72
  versions. Update the lockfile deliberately when taking a newer patch.
65
73
 
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
71
79
  even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
72
80
  with Back keep their title bar. `header-actions` share one "…" menu; only an
73
81
  entry marked `primary: true`, or a sole entry with an icon, becomes a separate
74
- bar button. The template's `#actions` stays page actions; no title or empty-bar
75
- workaround is needed.
82
+ bar button. The template's `#actions` holds only real page actions; no title or
83
+ empty-bar workaround is needed.
76
84
 
77
85
  `AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
78
86
  a full page as well, bounded at `height: 100%`) with the UI locale from
@@ -84,9 +84,12 @@ capabilities: {}
84
84
  # generated scripts/backup/ as root: a fixed script that only switches to the
85
85
  # package account. See `synopkgland skill show backup`.
86
86
  # backup:
87
- # online: false # true keeps the App running; SQLite is snapshotted
87
+ # online: false # true keeps the App running; SQLite is read in
88
+ # # place. Other files are read as they are: a file
89
+ # # being written meanwhile may be inconsistent
88
90
  # data_version: "1.0" # "major.minor"; a restore refuses newer data
89
91
  # hook: bin/backup-hook # optional packaged freeze/thaw executable
92
+ # exclude: [tmp] # optional data-folder globs left out (scratch data)
90
93
  # To opt out, use only:
91
94
  # backup:
92
95
  # hyper_backup: false
@@ -45,7 +45,7 @@ bridge App's runtime serves.
45
45
 
46
46
  Use the installed `@synopackageland/ui` and its declared compatible patch range.
47
47
  `App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
48
- `pages` from `shellPages`, the page's buttons in `#actions` and the pages in its
48
+ `pages` from `shellPages`, real page actions in `#actions` and the pages in its
49
49
  default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
50
50
  `#actions` in a bar above the page and scrolls only the content; a settings
51
51
  page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
60
60
  shows the signed-in DSM account and its role from `ctx.user`. See the ui's
61
61
  `docs/adaptive-layout.md`.
62
62
 
63
+ The template puts no Refresh in `#actions`, and with nothing there `AppShell`
64
+ draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
65
+ the status, the panels and the portal again when the window is shown again,
66
+ and showing the status page again reads it again. A failed read offers Retry
67
+ beside its error. Sign out appears in `#actions` only when standalone login is
68
+ on. Keep `#actions` for real page actions, such as Add; a section that needs a
69
+ manual re-read gets its own action in that section, not a header Refresh.
70
+
63
71
  The frontend SDK and UI use patch ranges; the lockfile records the installed
64
72
  versions. Update the lockfile deliberately when taking a newer patch.
65
73
 
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
71
79
  even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
72
80
  with Back keep their title bar. `header-actions` share one "…" menu; only an
73
81
  entry marked `primary: true`, or a sole entry with an icon, becomes a separate
74
- bar button. The template's `#actions` stays page actions; no title or empty-bar
75
- workaround is needed.
82
+ bar button. The template's `#actions` holds only real page actions; no title or
83
+ empty-bar workaround is needed.
76
84
 
77
85
  `AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
78
86
  a full page as well, bounded at `height: 100%`) with the UI locale from
@@ -85,9 +85,12 @@ capabilities: {}
85
85
  # generated scripts/backup/ as root: a fixed script that only switches to the
86
86
  # package account. See `synopkgland skill show backup`.
87
87
  # backup:
88
- # online: false # true keeps the App running; SQLite is snapshotted
88
+ # online: false # true keeps the App running; SQLite is read in
89
+ # # place. Other files are read as they are: a file
90
+ # # being written meanwhile may be inconsistent
89
91
  # data_version: "1.0" # "major.minor"; a restore refuses newer data
90
92
  # hook: bin/backup-hook # optional packaged freeze/thaw executable
93
+ # exclude: [tmp] # optional data-folder globs left out (scratch data)
91
94
  # To opt out, use only:
92
95
  # backup:
93
96
  # hyper_backup: false
@@ -45,7 +45,7 @@ bridge App's runtime serves.
45
45
 
46
46
  Use the installed `@synopackageland/ui` and its declared compatible patch range.
47
47
  `App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
48
- `pages` from `shellPages`, the page's buttons in `#actions` and the pages in its
48
+ `pages` from `shellPages`, real page actions in `#actions` and the pages in its
49
49
  default slot. On desktop, the sidebar names the page: the shell shows no page title, puts
50
50
  `#actions` in a bar above the page and scrolls only the content; a settings
51
51
  page (`SectionTabs fill` or `SettingsPage`) keeps its tabs and footer in place
@@ -60,6 +60,14 @@ device-width viewport. Its full-page sidebar names the App with
60
60
  shows the signed-in DSM account and its role from `ctx.user`. See the ui's
61
61
  `docs/adaptive-layout.md`.
62
62
 
63
+ The template puts no Refresh in `#actions`, and with nothing there `AppShell`
64
+ draws no bar above the page. Data stays fresh without one: `ctx.reopen` reads
65
+ the status, the panels and the portal again when the window is shown again,
66
+ and showing the status page again reads it again. A failed read offers Retry
67
+ beside its error. Sign out appears in `#actions` only when standalone login is
68
+ on. Keep `#actions` for real page actions, such as Add; a section that needs a
69
+ manual re-read gets its own action in that section, not a header Refresh.
70
+
63
71
  The frontend SDK and UI use patch ranges; the lockfile records the installed
64
72
  versions. Update the lockfile deliberately when taking a newer patch.
65
73
 
@@ -71,8 +79,8 @@ keeps that branding, and the UI hides a bar with nothing in it. When using
71
79
  even with `phone-bar-title="none"`. Pages outside the tab bar and detail pages
72
80
  with Back keep their title bar. `header-actions` share one "…" menu; only an
73
81
  entry marked `primary: true`, or a sole entry with an icon, becomes a separate
74
- bar button. The template's `#actions` stays page actions; no title or empty-bar
75
- workaround is needed.
82
+ bar button. The template's `#actions` holds only real page actions; no title or
83
+ empty-bar workaround is needed.
76
84
 
77
85
  `AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
78
86
  a full page as well, bounded at `height: 100%`) with the UI locale from