@synopackageland/templates 0.16.0 → 0.17.0

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
@@ -38,12 +38,12 @@ calls each one from the generated script of the same name (`synopkgland skill sh
38
38
  The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
39
39
  `__BACKGROUND_ENABLED__` are substituted by `init`.
40
40
 
41
- `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.16.0`; that line is
41
+ `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.17.0`; that line is
42
42
  the single source of the SDK version and `frontend/package.json` pins
43
43
  `@synopackageland/sdk` to the same version. Python and Node.js name it in
44
44
  `backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
45
- (and `Cargo.lock`); all of them pin 0.16.0. The Vue frontend pins
46
- `@synopackageland/ui` 0.12.0. SDK 0.16.0 accepts UI `>=0.10.1 <0.13.0`,
45
+ (and `Cargo.lock`); all of them pin 0.17.0. The Vue frontend pins
46
+ `@synopackageland/ui` 0.12.0. SDK 0.17.0 accepts UI `>=0.10.1 <0.13.0`,
47
47
  so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
48
48
 
49
49
  ## Notice writer
@@ -149,3 +149,9 @@ The CLI preserves upstream bytes and the SDK planner generates an override and o
149
149
  ## AI commands
150
150
 
151
151
  All four native templates explicitly declare status and greeting commands in `ctl.commands`. Build packages a private `synopkgland-api.json`; only the authenticated SDK description endpoint returns it. Internal routes are not automatically commands. Read `synopkgland skill show ctl` before adding command schemas or workflow bindings.
152
+
153
+ The Vue scaffold batches independent startup status/greeting reads when the
154
+ window already identifies an administrator and supplies `ctx.batch`. Rebuild
155
+ with the current CLI and SDK to get that client. Older generated contexts keep
156
+ the single-call path. Each batch result is handled separately; the shell stays
157
+ visible while reads run. The backend permissions remain authoritative.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
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": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
@@ -62,26 +62,30 @@ call `greeting.set`.
62
62
 
63
63
  ## DSM Application Portal (`portal`)
64
64
 
65
- Only an App with the root executor (`rootOps` or `delegate`) may declare a
66
- portal: its uninstall then reloads nginx so the alias stops answering, which a
67
- no-privilege App cannot do without root. `synopkgland add portal` (with
68
- `root-ops` or `delegate` in the same call or already declared; or `portal: {}` /
69
- `portal: {alias: my-app}` in dsm-package.yaml) registers
70
- `https://<nas>/<alias>/`, which opens the app after DSM sign-in. Compose Apps
71
- cannot declare a portal. Read `synopkgland skill show portal` before changing it.
72
- The install wizard only proposes the alias; an administrator activates it from
73
- the app's portal panel (`ctx.portal`, DSM's own `SYNO.Core.AppPortal` in the
74
- admin's session) or Control Panel > Login Portal > Applications. Ordinary users
75
- get the link from the backend's read-only `portal.status`, else "ask an
76
- administrator". Never write `AppPortal.json` or `@appconf/<package>`, never add
77
- a backend route that sets the alias, and never relay QuickConnect responses or
78
- build QuickConnect links. A portal needs os_min_ver 7.1-42661 or later, and
79
- `add portal` raises a lower one. Below DSM 7.2 the executor registers the
80
- entry: there is no alias wizard step, and an administrator enables the entry
81
- from the portal panel (`ctx.portal.register()`), which the executor's approved
82
- `portal.register` operation carries out. From 7.2 the SPK registers it as a
83
- DSM resource. Beyond that, support is detected (SYNO.API.Info), not
84
- version-gated.
65
+ From DSM 7.2-64570, Apps can declare `portal` without an executor. DSM's
66
+ web-config resource registers the entry, and INFO
67
+ `instuninst_restart_services="nginx.service"` asks DSM to reload nginx after
68
+ install, upgrade and uninstall. The reload is asynchronous; package hooks stay
69
+ unprivileged and never reload nginx themselves. Schema-2 Compose/gateway Apps
70
+ may also use a top-level `portal`; legacy `integration.web.portal` is unsupported.
71
+
72
+ `synopkgland add portal` adds `portal: {}` (or set
73
+ `portal: {alias: my-app}` in dsm-package.yaml). Without an executor, set
74
+ `os_min_ver` to 7.2-64570 or later before adding Portal; the command refuses a
75
+ lower target and never raises it silently. With an executor, a portal needs
76
+ `os_min_ver` 7.1-42661 or later. Below DSM 7.2 the executor registers the entry:
77
+ DSM 7.1 rejects the unsigned web-config worker, there is no alias wizard step,
78
+ and an administrator enables it through `ctx.portal.register()` after executor
79
+ approval. From DSM 7.2, the SPK registers it as a DSM resource.
80
+
81
+ Read `synopkgland skill show portal` before changing it. The install wizard only
82
+ proposes the alias; an administrator activates it in the App's portal panel
83
+ (`ctx.portal`, DSM's own `SYNO.Core.AppPortal` in the admin session) or Control
84
+ Panel > Login Portal > Applications. Other signed-in users get the link from
85
+ read-only `portal.status`, else "ask an administrator". Never write
86
+ `AppPortal.json` or `@appconf/<package>`, never add a backend route that sets
87
+ the alias, and never relay QuickConnect responses or build QuickConnect links.
88
+ Beyond the packaging floor, support is detected with SYNO.API.Info.
85
89
 
86
90
  ### Login page look (`login_style`)
87
91
 
@@ -14,7 +14,7 @@ compose:
14
14
  web: {service: web, port: 8080, health: {path: /, status: 200}}
15
15
  network: {egress: isolated}
16
16
  identity_gateway:
17
- sdk: 0.16.0
17
+ sdk: 0.17.0
18
18
  mode: none
19
19
  listen: {http: 28000}
20
20
  # Web Apps default to new-page through the shared app-launch resolver.
@@ -297,23 +297,21 @@ refusal for unknown methods.
297
297
 
298
298
  ## DSM Application Portal link
299
299
 
300
- Only an App with the root executor (`rootOps` or `delegate`) can declare a
301
- portal; `synopkgland add root-ops portal` declares both. Uninstall then
302
- removes the App's Application Privileges rules and reloads nginx once DSM has
303
- removed the alias, so it stops answering. `synopkgland add portal` adds
304
- `portal: {}` to dsm-package.yaml so
305
- `https://<nas>/<alias>/` opens this app right after DSM sign-in. The install
306
- wizard proposes the alias (the package name in lowercase by default); an
307
- administrator turns it on from the app's portal panel or Control Panel >
308
- Login Portal > Applications. Ordinary users then see the link; until then
309
- they see "ask an administrator". DSM owns the setting: the backend's
310
- `portal.status` only reads it, and nothing in the package may write it. See
311
- `synopkgland skill show portal`.
312
-
313
- A portal needs DSM 7.3.2-86009 or later, so `add portal` raises a lower
314
- `os_min_ver` and build refuses one below it. DSM 7.1.1 rejects an unsigned
315
- package that registers a portal (Package Center error 320); 7.3.2 and 7.4
316
- install it; 7.2 is untested. To keep supporting older DSM, remove `portal:`.
300
+ From DSM 7.2-64570, an App can declare `portal` without an executor. Set
301
+ `os_min_ver` to that floor or later before `synopkgland add portal`; the command
302
+ refuses a lower no-privilege target instead of silently raising it. The command
303
+ adds `portal: {}` so `https://<nas>/<alias>/` opens the App after DSM sign-in.
304
+ The install wizard proposes the alias; an administrator applies it in the App
305
+ panel or Control Panel > Login Portal > Applications. Other users see the link
306
+ or "ask an administrator". DSM owns the setting; `portal.status` only reads it.
307
+
308
+ Every Portal SPK asks DSM to reload nginx through INFO
309
+ `instuninst_restart_services="nginx.service"` after install, upgrade and
310
+ uninstall. The reload is asynchronous; package hooks stay unprivileged.
311
+ DSM 7.1-42661 still needs an approved executor to register the entry, since its
312
+ web-config worker rejects unsigned packages. For that route, declare an
313
+ executor capability, for example `synopkgland add root-ops portal`. See
314
+ `synopkgland skill show portal` before changing Portal settings.
317
315
 
318
316
  The build also gives the portal a login page look: a neutral default
319
317
  wallpaper (its thumbnail is what Control Panel > Login Portal > Applications
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.16.0 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.17.0 (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.16.0
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.17.0
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.16.0 h1:9MYFliraGfCE+1TXyFQD6s/YPMBMhGbj4bY+HHh9Y08=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.16.0/go.mod h1:vixf5Lo4PssQDGU5zam0Mb5sFu9wPUP4Ck36BMAVbNc=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.17.0 h1:DaYDHCDlukkXtTm+3m26p7pU86r298MTasKDrV/tRl4=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.17.0/go.mod h1:vixf5Lo4PssQDGU5zam0Mb5sFu9wPUP4Ck36BMAVbNc=
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=
@@ -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.16.0",
11
- "@synopackageland/ui": "0.12.0",
10
+ "@synopackageland/sdk": "0.17.0",
11
+ "@synopackageland/ui": "0.12.1",
12
12
  "vue": "3.5.43"
13
13
  },
14
14
  "devDependencies": {
@@ -1152,8 +1152,8 @@
1152
1152
  }
1153
1153
  },
1154
1154
  "node_modules/@synopackageland/sdk": {
1155
- "version": "0.16.0",
1156
- "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.16.0.tgz",
1155
+ "version": "0.17.0",
1156
+ "resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.17.0.tgz",
1157
1157
  "integrity": "sha512-3h7PjF55kJjKDJHZE9tbR0UxOgmxfGEnY9eKkpKPStdy8OiTOGjOlB298+YvJtkRBLiOZ13En10sa/ADbU9DnA==",
1158
1158
  "license": "MIT",
1159
1159
  "peerDependencies": {
@@ -1171,9 +1171,9 @@
1171
1171
  }
1172
1172
  },
1173
1173
  "node_modules/@synopackageland/ui": {
1174
- "version": "0.12.0",
1175
- "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.12.0.tgz",
1176
- "integrity": "sha512-LGdkmceKcybJj4kvbwBZk5vewKepDi4vyGohkittpucnfOM9EOlCWkaWfWoZZ5MD0LQlwZf2ueydcrEOLq3R/A==",
1174
+ "version": "0.12.1",
1175
+ "resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.12.1.tgz",
1176
+ "integrity": "sha512-nyHKefcyKWiaXpjsjFfxrgsH4RSDHgO71bOV8uGRf89Ch2kDapNTfv42RtR2idIfNTt7A3/gKw8eUlyRn9VJDQ==",
1177
1177
  "license": "MIT",
1178
1178
  "dependencies": {
1179
1179
  "@internationalized/date": "3.12.4",
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@lucide/vue": "1.48.0",
11
- "@synopackageland/sdk": "0.16.0",
12
- "@synopackageland/ui": "0.12.0",
11
+ "@synopackageland/sdk": "0.17.0",
12
+ "@synopackageland/ui": "0.12.1",
13
13
  "vue": "3.5.43"
14
14
  },
15
15
  "devDependencies": {
@@ -43,10 +43,24 @@ async function refresh() {
43
43
  const early = !login.ready.value
44
44
  const call: typeof login.call = early ? (method, params) => props.ctx.call(method, params) : login.call
45
45
  busy.value = true; error.value = ''
46
+ // An administrator DSM names up front has the greeting read beside the status.
47
+ const startupBatch = props.ctx.user?.isAdmin === true ? props.ctx.batch : undefined
48
+ const read = !startupBatch && props.ctx.user?.isAdmin ? call<{text:string}>('greeting.get') : undefined
49
+ read?.catch(() => {})
46
50
  try {
47
- const a = await call<AppStatus>('app.status')
48
- if(!disposed) app.value = a
49
- if(a.admin) {const greeting = await call<{text:string}>('greeting.get'); if(!disposed) text.value = greeting.text}
51
+ if(startupBatch) {
52
+ const batch = early ? startupBatch : login.batch
53
+ const results = await batch([{method:'app.status'},{method:'greeting.get'}])
54
+ const status = results[0]!, greeting = results[1]!
55
+ if(!status.success) throw status.error
56
+ if(!disposed) app.value = status.data as AppStatus
57
+ if(!greeting.success) throw greeting.error
58
+ if(!disposed) text.value = (greeting.data as {text:string}).text
59
+ } else {
60
+ const a = await call<AppStatus>('app.status')
61
+ if(!disposed) app.value = a
62
+ if(a.admin) {const greeting = await (read ?? call<{text:string}>('greeting.get')); if(!disposed) text.value = greeting.text}
63
+ }
50
64
  } catch(e) {if(!disposed && !(early && login.enabled.value)) error.value = message(e)}
51
65
  finally {if(!disposed) {busy.value=false; if(again) {again=false; void refresh()}}}
52
66
  }
@@ -88,6 +88,19 @@ test('the app renders and loads status while the session check is still pending'
88
88
  expect([...root.querySelectorAll('[data-app-status] [data-app-value]')].map(node=>node.textContent)).toEqual(['Eager','admin','1'])
89
89
  }finally{app.unmount();root.remove()}
90
90
  })
91
+ test('an administrator DSM names up front has the greeting read beside the status',async()=>{
92
+ const call=vi.fn(async(method:string)=>{
93
+ if(method==='app.status')return new Promise(()=>{})
94
+ if(method==='greeting.get')return {text:'Hello Early'}
95
+ throw {code:'unknown_method'}
96
+ })
97
+ const root=document.createElement('div');document.body.append(root)
98
+ const app=mount(root,dsmWindow({call,user:{name:'admin',isAdmin:true}}))
99
+ try{
100
+ await settle()
101
+ expect(call.mock.calls.map(([method])=>method)).toContain('greeting.get')
102
+ }finally{app.unmount();root.remove()}
103
+ })
91
104
  test('a status request that raced the session check is retried once the session is confirmed',async()=>{
92
105
  let session=false,rejectEarly:(e:unknown)=>void=()=>{}
93
106
  const call=vi.fn(async(method:string)=>{
@@ -239,3 +252,16 @@ test('an administrator sees a pending Shared Folder with the grant step; a plain
239
252
  expect(root.textContent).toContain('Ask an administrator')
240
253
  }finally{app.unmount();root.remove();plain.close()}
241
254
  })
255
+
256
+ test('batches startup reads for a known administrator', async () => {
257
+ const calls: string[]=[]
258
+ const batch=vi.fn(async () => [{success:true as const,data:{package:'Batch',version:'1',user:'admin',admin:true}},{success:true as const,data:{text:'Hello Batch'}}])
259
+ const ctx=dsmWindow({user:{name:'admin',isAdmin:true},batch:batch as NonNullable<Context['batch']>,call:async <T,>(method:string)=>{calls.push(method);if(method==='identity.status')return {enabled:false} as T;throw {code:'unknown_method'}}})
260
+ const root=document.createElement('div');document.body.append(root)
261
+ const app=mount(root,ctx)
262
+ await settle()
263
+ expect(batch).toHaveBeenCalledWith([{method:'app.status'},{method:'greeting.get'}])
264
+ expect(calls).not.toContain('app.status');expect(calls).not.toContain('greeting.get')
265
+ expect((root.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Hello Batch')
266
+ app.unmount();root.remove()
267
+ })
@@ -49,7 +49,7 @@ backend:
49
49
  runtime: node
50
50
  dir: backend/app
51
51
  entry: main.js
52
- sdk: 0.16.0
52
+ sdk: 0.17.0
53
53
  node: { package: Node.js_v20 }
54
54
 
55
55
  ui:
@@ -47,7 +47,7 @@ backend:
47
47
  runtime: python
48
48
  dir: backend/app
49
49
  entry: main.py
50
- sdk: 0.16.0
50
+ sdk: 0.17.0
51
51
 
52
52
  ui:
53
53
  # Non-UI-SDK frontends open a full page; Portal is a separate opt-in.
@@ -104,8 +104,8 @@ dependencies = [
104
104
 
105
105
  [[package]]
106
106
  name = "syno-pkg-land-app"
107
- version = "0.16.0"
108
- source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.16.0#b49f8a178c7fd0e8a2b402edb8ed829104125bfd"
107
+ version = "0.17.0"
108
+ source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.17.0#91fa899beec935af9668a04c24ccd64b0d03b00b"
109
109
  dependencies = [
110
110
  "libc",
111
111
  "serde",
@@ -9,7 +9,7 @@ libc = "0.2"
9
9
  serde = { version = "1", features = ["derive"] }
10
10
  serde_json = "1"
11
11
  # The SDK version is the tag; `synopkgland check` reads it from Cargo.lock.
12
- syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.16.0" }
12
+ syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.17.0" }
13
13
 
14
14
  [profile.release]
15
15
  # A handler panic becomes an "internal" reply; abort would end the App without one.