@synopackageland/templates 0.9.1 → 0.9.3

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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ An overlay may replace `internal/app/*_none.go` (the CLI deletes the matching
21
21
  The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
22
22
  `__BACKGROUND_ENABLED__` are substituted by `init`.
23
23
 
24
- `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.1`; that line is
24
+ `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.4`; that line is
25
25
  the single source of the SDK version and `frontend/package.json` pins
26
26
  `@synopackageland/sdk` to the same version.
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Project templates for synopkgland init: native-go (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
5
5
  "license": "MIT",
6
6
  "repository": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
@@ -11,6 +11,12 @@ For UI discovery, run `npx --no syno-pkg-land-ui catalog --json` and
11
11
  Read its prompt and exported Vue docs/types before choosing component APIs.
12
12
  Use `syno-pkg-land-ui` directly in npm scripts; keep AI guidance version-independent.
13
13
 
14
+ Open the App at once: render its shell immediately and run session, permission
15
+ and status checks in the background, never a full-window "Checking…" placeholder.
16
+ Only a confirmed refusal (signed out, expired session, not an administrator)
17
+ shows the sign-in page or a `@synopackageland/ui` Dialog; loading text inside a
18
+ section is fine.
19
+
14
20
  From the project root, run `synopkgland check --json`,
15
21
  `synopkgland build --json`, then `synopkgland inspect <spk> --json` on the emitted SPK.
16
22
  For Vue, first run `npm ci`, `npm test` and `npm run build` in frontend.
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.9.1 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.9.4 (MIT)
4
4
 
5
5
  Copyright (c) 2026 synopkgland
6
6
 
@@ -4,6 +4,6 @@ go 1.25.0
4
4
 
5
5
  toolchain go1.26.9
6
6
 
7
- require github.com/synopackageland/syno-pkg-land-sdk v0.9.1
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.9.4
8
8
 
9
9
  require golang.org/x/sys v0.44.0 // indirect
@@ -1,4 +1,4 @@
1
- github.com/synopackageland/syno-pkg-land-sdk v0.9.1 h1:Mni+769M/fldZTlv0IOZKeaK3xGu6hTSkTeWIALPqBg=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.9.1/go.mod h1:JNoqISvR4biA9IuLi0CFe4c272tsKzCN9Nyj71nrXsQ=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.9.4 h1:ZfM5aZ/zjbIqpRda4fdeMYnibLh6UjGtxJudFlk/Dno=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.9.4/go.mod h1:JNoqISvR4biA9IuLi0CFe4c272tsKzCN9Nyj71nrXsQ=
3
3
  golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
4
4
  golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -44,7 +44,8 @@ test('the default surface binds the real routes and the session-only picker',asy
44
44
  })
45
45
  const root=render({call,locale:'en'});await settle()
46
46
  button(root,'Choose save destination (save)').click();await settle()
47
- const select=[...root.querySelectorAll('button')].find(n=>n.getAttribute('aria-label')==='Select home')!;expect(select).toBeTruthy();select.click();await settle()
47
+ // The SDK picker selects a folder with a row click.
48
+ const row=root.querySelector<HTMLElement>('tr[data-row-key="/home"]')!;expect(row).toBeTruthy();row.dispatchEvent(new MouseEvent('click',{bubbles:true}));await settle()
48
49
  button(root,'Confirm selection').click();await settle()
49
50
  expect(call).toHaveBeenCalledWith('delegate.select_destination',{key:'save',path:'/home'})
50
51
  })
@@ -56,6 +56,7 @@ Evidence: `.herdr-project/syno-package-land-sdk-t-0129/library/ui-desktop.png` a
56
56
 
57
57
  - Do use existing public UI components, embedded theme tokens and state/recovery copy.
58
58
  - Do retain permission gating, readable loading/empty/error states and per-instance cleanup.
59
+ - Do open the App at once: render `AppShell` immediately and run session, permission and status checks in the background, never a full-window "Checking…" placeholder. Only a confirmed refusal (signed out, expired session, not an administrator) shows the sign-in page or a library `Dialog`; loading text inside a section is fine.
59
60
  - Do use the library's `Select`, `Checkbox`, `Input`, `Textarea`, `Form`, `FormItem`, `FilePicker` (`variant="compact"`, in the app locale) and `Link` for controls; there is no native `<input>` or `<a>`. Leave `Form` on its automatic direction (labels stack below 520px of container width) and let a `FormItem` bind its control, with no `direction="vertical"`, `role="group"` or `field.controlId`.
60
61
  - Do not infer authority from frontend observations or scheduler history.
61
62
  - Don't introduce a separate palette, component system or modal workaround around the library contract.
@@ -7,8 +7,8 @@
7
7
  "name": "native-dsm-vue-app",
8
8
  "dependencies": {
9
9
  "@lucide/vue": "1.48.0",
10
- "@synopackageland/sdk": "0.9.1",
11
- "@synopackageland/ui": "0.9.0",
10
+ "@synopackageland/sdk": "0.9.4",
11
+ "@synopackageland/ui": "0.9.1",
12
12
  "vue": "3.5.43"
13
13
  },
14
14
  "devDependencies": {
@@ -1148,13 +1148,13 @@
1148
1148
  }
1149
1149
  },
1150
1150
  "node_modules/@synopackageland/sdk": {
1151
- "version": "0.9.1",
1152
- "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.1.tgz",
1153
- "integrity": "sha512-V7V2Ad7KTcQGo3fgMl2PofqhwDvHzCxYJG4q3DqVRuJgNNhxeflZUSYTWHtO/xTB9OZ5KPGoN8hqHIrNDyzAXg==",
1151
+ "version": "0.9.4",
1152
+ "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.4.tgz",
1153
+ "integrity": "sha512-GvPDd8hoU5mzkXD4s/RShY1dBiu/zRoPtd0zFb/yjciZxlzdgopn4VnOjhq7xScbrmK9RuVZRbXLGT16NdobUg==",
1154
1154
  "license": "MIT",
1155
1155
  "peerDependencies": {
1156
1156
  "@lucide/vue": "^1.48.0",
1157
- "@synopackageland/ui": "^0.9.0",
1157
+ "@synopackageland/ui": "^0.9.1",
1158
1158
  "vue": "^3.5.0"
1159
1159
  },
1160
1160
  "peerDependenciesMeta": {
@@ -1167,9 +1167,9 @@
1167
1167
  }
1168
1168
  },
1169
1169
  "node_modules/@synopackageland/ui": {
1170
- "version": "0.9.0",
1171
- "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.0.tgz",
1172
- "integrity": "sha512-0OnItM4pJ8zYHafpWkAdbVb4x8alqkz9FKwOR1Y3DWdelIxR6DTiTrCOLmqat1GwjtuyqLuebhoPX5rejZjWUA==",
1170
+ "version": "0.9.1",
1171
+ "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.1.tgz",
1172
+ "integrity": "sha512-L2k0eeN9+TLPMh5+kMbq6pV8n0qqUq6Ynme/KSp3mCqDb1MXBdr8MbAGtxdbNYMj9QepbA9mccgygwI9fFoG5A==",
1173
1173
  "license": "MIT",
1174
1174
  "dependencies": {
1175
1175
  "@internationalized/date": "3.12.4",
@@ -2538,9 +2538,9 @@
2538
2538
  "license": "MIT"
2539
2539
  },
2540
2540
  "node_modules/zod": {
2541
- "version": "4.6.5",
2542
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
2543
- "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
2541
+ "version": "4.6.6",
2542
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz",
2543
+ "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==",
2544
2544
  "license": "MIT",
2545
2545
  "funding": {
2546
2546
  "url": "https://github.com/sponsors/colinhacks"
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@lucide/vue": "1.48.0",
11
- "@synopackageland/sdk": "0.9.1",
12
- "@synopackageland/ui": "0.9.0",
11
+ "@synopackageland/sdk": "0.9.4",
12
+ "@synopackageland/ui": "0.9.1",
13
13
  "vue": "3.5.43"
14
14
  },
15
15
  "devDependencies": {
@@ -25,14 +25,21 @@ function message(e: unknown) {
25
25
  const errors = copy.value.errors as Record<string, string>
26
26
  return (code !== 'request_failed' && errors[code]) || errors.request_failed!.replace('{code}', code)
27
27
  }
28
+ let again = false
28
29
  async function refresh() {
29
- if(disposed || busy.value || (login.enabled.value && !login.subject.value)) return
30
+ if(disposed || (login.enabled.value && !login.subject.value)) return
31
+ if(busy.value) {again = true; return}
32
+ // Sent before the session check settles: a session failure must not sign the user
33
+ // out, and the confirmed session refreshes again.
34
+ const early = !login.ready.value
35
+ const call: typeof login.call = early ? (method, params) => props.ctx.call(method, params) : login.call
30
36
  busy.value = true; error.value = ''
31
37
  try {
32
- const a = await login.call<AppStatus>('app.status')
38
+ const a = await call<AppStatus>('app.status')
33
39
  if(!disposed) app.value = a
34
- if(a.admin) {const greeting = await login.call<{text:string}>('greeting.get'); if(!disposed) text.value = greeting.text}
35
- } catch(e) {if(!disposed) error.value = message(e)} finally {if(!disposed) busy.value=false}
40
+ if(a.admin) {const greeting = await call<{text:string}>('greeting.get'); if(!disposed) text.value = greeting.text}
41
+ } catch(e) {if(!disposed && !(early && login.enabled.value)) error.value = message(e)}
42
+ finally {if(!disposed) {busy.value=false; if(again) {again=false; void refresh()}}}
36
43
  }
37
44
  async function save() {
38
45
  if(disposed || busy.value) return
@@ -47,10 +54,11 @@ watch(login.subject, (subject, previous) => {
47
54
  async function signOut() {
48
55
  try {await login.logout()} catch(e) {if(!disposed) error.value=message(e)}
49
56
  }
50
- onMounted(async()=>{
57
+ onMounted(()=>{
51
58
  unsubscribe = props.ctx.reopen(()=>{if(!disposed) void refresh()})
52
- await login.start()
53
- if((!login.enabled.value || login.subject.value)) await refresh()
59
+ // The session check runs in the background; only a confirmed sign-in need replaces the App.
60
+ void login.start()
61
+ void refresh()
54
62
  })
55
63
  onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
56
64
  </script>
@@ -58,8 +66,7 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
58
66
  <template>
59
67
  <AppRoot modal-scope="app" style="height: 100%">
60
68
  <ConfigProvider :locale="uiLocale(ctx.locale)">
61
- <p v-if="!login.ready.value" role="status">{{ copy.checking }}</p>
62
- <LoginPage v-else-if="login.enabled.value && !login.subject.value" :session="login" :locale="ctx.locale" />
69
+ <LoginPage v-if="login.ready.value && login.enabled.value && !login.subject.value" :session="login" :locale="ctx.locale" />
63
70
  <AppShell v-else v-model="page" :locale="ctx.locale" :pages="pages" :description="subtitle">
64
71
  <template #actions><Button variant="secondary" :disabled="busy" @click="refresh">{{ copy.refresh }}</Button><Button v-if="login.enabled.value" variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
65
72
  <div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "Sitzung wird geprüft…",
3
2
  "refresh": "Aktualisieren",
4
3
  "signOut": "Abmelden",
5
4
  "settings": "Paketeinstellungen",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "Checking session…",
3
2
  "refresh": "Refresh",
4
3
  "signOut": "Sign out",
5
4
  "settings": "Package settings",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "Comprobando la sesión…",
3
2
  "refresh": "Actualizar",
4
3
  "signOut": "Cerrar sesión",
5
4
  "settings": "Configuración del paquete",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "Vérification de la session…",
3
2
  "refresh": "Actualiser",
4
3
  "signOut": "Se déconnecter",
5
4
  "settings": "Paramètres du paquet",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "Verifica della sessione…",
3
2
  "refresh": "Aggiorna",
4
3
  "signOut": "Disconnetti",
5
4
  "settings": "Impostazioni del pacchetto",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "セッションを確認しています…",
3
2
  "refresh": "最新の情報に更新",
4
3
  "signOut": "サインアウト",
5
4
  "settings": "パッケージ設定",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "세션 확인 중…",
3
2
  "refresh": "새로 고침",
4
3
  "signOut": "로그아웃",
5
4
  "settings": "패키지 설정",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "正在检查登录状态…",
3
2
  "refresh": "刷新",
4
3
  "signOut": "注销",
5
4
  "settings": "套件设置",
@@ -1,5 +1,4 @@
1
1
  {
2
- "checking": "正在檢查登入狀態…",
3
2
  "refresh": "重新整理",
4
3
  "signOut": "登出",
5
4
  "settings": "套件設定",
@@ -55,3 +55,40 @@ test('a portal app shows the Application Portal section on the status page',asyn
55
55
  app.unmount()
56
56
  }finally{without.close();f.root.remove()}
57
57
  })
58
+ test('the app renders and loads status while the session check is still pending',async()=>{
59
+ const call=vi.fn(async(method:string)=>{
60
+ if(method==='identity.status')return new Promise(()=>{})
61
+ if(method==='app.status')return {package:'Eager',version:'1',user:'admin',admin:true}
62
+ if(method==='greeting.get')return {text:'Hello Eager'}
63
+ throw {code:'unknown_method'}
64
+ })
65
+ const ctx={locale:'en',call,reopen(){return ()=>{}}} as unknown as Context
66
+ const root=document.createElement('div');document.body.append(root)
67
+ const app=mount(root,ctx)
68
+ try{
69
+ await settle()
70
+ expect(root.textContent).not.toContain('Checking session')
71
+ expect(root.textContent).toContain('Eager · admin · 1')
72
+ }finally{app.unmount();root.remove()}
73
+ })
74
+ test('a status request that raced the session check is retried once the session is confirmed',async()=>{
75
+ let session=false,rejectEarly:(e:unknown)=>void=()=>{}
76
+ const call=vi.fn(async(method:string)=>{
77
+ if(method==='identity.status')return {enabled:true}
78
+ if(method==='identity.session'){session=true;return {user:'admin',uid:'1024',expiresAt:new Date(Date.now()+3600000).toISOString()}}
79
+ if(method==='app.status'){
80
+ if(!session)return new Promise((_,reject)=>{rejectEarly=reject})
81
+ return {package:'Raced',version:'1',user:'admin',admin:true}
82
+ }
83
+ if(method==='greeting.get')return {text:'Hello Raced'}
84
+ throw {code:'unknown_method'}
85
+ })
86
+ const ctx={locale:'en',call,reopen(){return ()=>{}}} as unknown as Context
87
+ const root=document.createElement('div');document.body.append(root)
88
+ const app=mount(root,ctx)
89
+ try{
90
+ await settle();rejectEarly({code:'session_required'});await settle()
91
+ expect(root.textContent).not.toContain('Your DSM session expired')
92
+ expect(root.textContent).toContain('Raced · admin · 1')
93
+ }finally{app.unmount();root.remove()}
94
+ })