@synopackageland/templates 0.15.1 → 0.16.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.
Files changed (46) hide show
  1. package/README.md +6 -4
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +3 -2
  4. package/templates/binary/ui/app.css +21 -4
  5. package/templates/compose/dsm-package.yaml +1 -1
  6. package/templates/native-go/README.md +15 -0
  7. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  8. package/templates/native-go/backend/cmd/app/main.go +4 -0
  9. package/templates/native-go/backend/go.mod +1 -1
  10. package/templates/native-go/backend/go.sum +2 -2
  11. package/templates/native-go/backend/internal/app/routes.go +12 -2
  12. package/templates/native-go/backend/internal/app/routes_test.go +4 -3
  13. package/templates/native-go/backend/internal/app/shared_folder.go +37 -127
  14. package/templates/native-go/backend/internal/app/shared_folder_test.go +95 -108
  15. package/templates/native-go/i18n/messages/de.json +4 -3
  16. package/templates/native-go/i18n/messages/en.json +4 -3
  17. package/templates/native-go/i18n/messages/es.json +4 -3
  18. package/templates/native-go/i18n/messages/fr.json +4 -3
  19. package/templates/native-go/i18n/messages/it.json +4 -3
  20. package/templates/native-go/i18n/messages/ja.json +4 -3
  21. package/templates/native-go/i18n/messages/ko.json +4 -3
  22. package/templates/native-go/i18n/messages/zh-CN.json +4 -3
  23. package/templates/native-go/i18n/messages/zh-TW.json +4 -3
  24. package/templates/native-go/ui/app.css +57 -25
  25. package/templates/native-go/ui/app.js +7 -4
  26. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -3
  27. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +1 -1
  28. package/templates/native-go-vue/frontend/DESIGN.md +1 -1
  29. package/templates/native-go-vue/frontend/package-lock.json +118 -193
  30. package/templates/native-go-vue/frontend/package.json +3 -3
  31. package/templates/native-go-vue/frontend/src/App.vue +13 -5
  32. package/templates/native-go-vue/frontend/src/i18n/de.json +5 -2
  33. package/templates/native-go-vue/frontend/src/i18n/en.json +5 -2
  34. package/templates/native-go-vue/frontend/src/i18n/es.json +5 -2
  35. package/templates/native-go-vue/frontend/src/i18n/fr.json +5 -2
  36. package/templates/native-go-vue/frontend/src/i18n/it.json +5 -2
  37. package/templates/native-go-vue/frontend/src/i18n/ja.json +5 -2
  38. package/templates/native-go-vue/frontend/src/i18n/ko.json +5 -2
  39. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +5 -2
  40. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +5 -2
  41. package/templates/native-go-vue/frontend/src/style.css +2 -2
  42. package/templates/native-go-vue/frontend/test/apps.test.ts +22 -2
  43. package/templates/native-node/dsm-package.yaml +1 -1
  44. package/templates/native-python/dsm-package.yaml +1 -1
  45. package/templates/native-rust/backend/Cargo.lock +2 -2
  46. package/templates/native-rust/backend/Cargo.toml +1 -1
@@ -1,10 +1,13 @@
1
1
  package app
2
2
 
3
3
  import (
4
+ "context"
4
5
  "errors"
5
6
  "os"
6
7
  "path/filepath"
7
8
  "testing"
9
+
10
+ "github.com/synopackageland/syno-pkg-land-sdk/sharedfolder"
8
11
  )
9
12
 
10
13
  func writeConfig(t *testing.T, dir, body string) {
@@ -14,145 +17,129 @@ func writeConfig(t *testing.T, dir, body string) {
14
17
  }
15
18
  }
16
19
 
17
- func TestReadSharedFoldersWithoutConfigIsEmpty(t *testing.T) {
18
- got, err := ReadSharedFolders(t.TempDir())
19
- if err != nil || len(got) != 0 {
20
- t.Fatalf("got %v %v", got, err)
20
+ // sharedFixture has /volume1/Photos with the mode, chosen for key "media".
21
+ func sharedFixture(t *testing.T, mode os.FileMode) Config {
22
+ t.Helper()
23
+ cfg := testConfig(t)
24
+ cfg.VolumeRoot = t.TempDir()
25
+ dir := filepath.Join(cfg.VolumeRoot, "volume1", "Photos")
26
+ if err := os.MkdirAll(dir, 0o755); err != nil {
27
+ t.Fatal(err)
21
28
  }
29
+ if err := os.Chmod(dir, mode); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ t.Cleanup(func() { os.Chmod(dir, 0o755) })
33
+ writeConfig(t, cfg.DataDir, `{"schema":1,"shared_folders":{"media":"Photos"}}`)
34
+ cfg.SharedFolderRequired = map[string]sharedfolder.Access{"media": sharedfolder.Write}
35
+ return cfg
22
36
  }
23
37
 
24
- func TestReadSharedFoldersReturnsNamesByKey(t *testing.T) {
25
- dir := t.TempDir()
26
- writeConfig(t, dir, `{"schema":1,"shared_folders":{"media":"Family Photos"}}`)
27
- got, err := ReadSharedFolders(dir)
28
- if err != nil || got["media"] != "Family Photos" || len(got) != 1 {
29
- t.Fatalf("got %v %v", got, err)
38
+ func skipRoot(t *testing.T) {
39
+ t.Helper()
40
+ if os.Geteuid() == 0 {
41
+ t.Skip("root ignores directory modes")
30
42
  }
31
43
  }
32
44
 
33
- func TestReadSharedFoldersRefusesUnknownSchemaAndBadNames(t *testing.T) {
34
- for _, body := range []string{
35
- `{"schema":2,"shared_folders":{"media":"x"}}`,
36
- `{"schema":1,"shared_folders":{"media":"a/b"}}`,
37
- `{"schema":1,"shared_folders":{"media":".."}}`,
38
- `{"schema":1,"shared_folders":{"media":"@appdata"}}`,
39
- `{"schema":1,"shared_folders":{"media":""}}`,
40
- `not json`,
41
- } {
42
- dir := t.TempDir()
43
- writeConfig(t, dir, body)
44
- if _, err := ReadSharedFolders(dir); err == nil {
45
- t.Errorf("%s: want an error", body)
46
- }
45
+ func TestSharedFolderStatusIsEmptyWithoutAFolder(t *testing.T) {
46
+ code, out := call(t, testConfig(t), alice, "sharedfolder.status", `{}`)
47
+ if code != "" || len(out["data"].(map[string]any)["folders"].([]any)) != 0 {
48
+ t.Fatalf("%q %v", code, out)
47
49
  }
48
50
  }
49
51
 
50
- func TestResolveSharedFolderFindsTheNameOnItsVolume(t *testing.T) {
51
- root := t.TempDir()
52
- for _, d := range []string{"volume1/other", "volume2/Photos", "volume2/@appdata"} {
53
- if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil {
54
- t.Fatal(err)
52
+ func TestSharedFolderStatusReportsGrantedAndPending(t *testing.T) {
53
+ skipRoot(t)
54
+ for _, c := range []struct {
55
+ mode os.FileMode
56
+ state string
57
+ }{{0o755, "ready"}, {0o555, "pending"}} {
58
+ cfg := sharedFixture(t, c.mode)
59
+ code, out := call(t, cfg, alice, "sharedfolder.status", `{}`)
60
+ if code != "" {
61
+ t.Fatalf("%q %v", code, out)
62
+ }
63
+ row := out["data"].(map[string]any)["folders"].([]any)[0].(map[string]any)
64
+ if row["state"] != c.state || row["required"] != "write" || row["name"] != "Photos" || row["path"] != filepath.Join(cfg.VolumeRoot, "volume1", "Photos") {
65
+ t.Fatalf("mode %o: %v", c.mode, row)
55
66
  }
56
- }
57
- got, err := ResolveSharedFolder(root, "Photos")
58
- if err != nil || got != filepath.Join(root, "volume2", "Photos") {
59
- t.Fatalf("got %q %v", got, err)
60
67
  }
61
68
  }
62
69
 
63
- func TestResolveSharedFolderFollowsAMovedShare(t *testing.T) {
64
- root := t.TempDir()
65
- os.MkdirAll(filepath.Join(root, "volume1", "Photos"), 0o755)
66
- first, _ := ResolveSharedFolder(root, "Photos")
67
- os.Rename(filepath.Join(root, "volume1", "Photos"), filepath.Join(root, "Photos.tmp"))
68
- os.MkdirAll(filepath.Join(root, "volume2"), 0o755)
69
- os.Rename(filepath.Join(root, "Photos.tmp"), filepath.Join(root, "volume2", "Photos"))
70
- second, err := ResolveSharedFolder(root, "Photos")
71
- if err != nil || first == second || second != filepath.Join(root, "volume2", "Photos") {
72
- t.Fatalf("%q -> %q %v", first, second, err)
70
+ func TestSharedFolderStatusIsAdministratorOnlyByDefault(t *testing.T) {
71
+ cfg := sharedFixture(t, 0o755)
72
+ if code, _ := call(t, cfg, bob, "sharedfolder.status", `{}`); code == "" {
73
+ t.Fatal("a non-administrator read the status")
73
74
  }
74
75
  }
75
76
 
76
- func TestResolveSharedFolderErrors(t *testing.T) {
77
- root := t.TempDir()
78
- if _, err := ResolveSharedFolder(root, "Missing"); !errors.Is(err, ErrSharedFolderNotFound) {
79
- t.Fatalf("missing: %v", err)
80
- }
81
- os.MkdirAll(filepath.Join(root, "volume1", "Dup"), 0o755)
82
- os.MkdirAll(filepath.Join(root, "volume2", "Dup"), 0o755)
83
- if _, err := ResolveSharedFolder(root, "Dup"); !errors.Is(err, ErrSharedFolderAmbiguous) {
84
- t.Fatalf("ambiguous: %v", err)
85
- }
86
- for _, bad := range []string{"", "..", "a/b", "@appdata", ".x"} {
87
- if _, err := ResolveSharedFolder(root, bad); err == nil {
88
- t.Errorf("%q: want an error", bad)
89
- }
77
+ func TestAppStatusNoLongerCarriesSharedFolders(t *testing.T) {
78
+ cfg := sharedFixture(t, 0o755)
79
+ _, out := call(t, cfg, alice, "app.status", `{}`)
80
+ data := out["data"].(map[string]any)
81
+ if _, present := data["shared_folders"]; present {
82
+ t.Fatalf("app.status: %v", data)
90
83
  }
91
84
  }
92
85
 
93
- func TestResolveSharedFolderIgnoresAFileOfThatName(t *testing.T) {
94
- root := t.TempDir()
95
- os.MkdirAll(filepath.Join(root, "volume1"), 0o755)
96
- os.WriteFile(filepath.Join(root, "volume1", "Notes"), nil, 0o644)
97
- if _, err := ResolveSharedFolder(root, "Notes"); !errors.Is(err, ErrSharedFolderNotFound) {
98
- t.Fatalf("got %v", err)
86
+ func TestSharedFolderStatusFailsOnACorruptConfig(t *testing.T) {
87
+ cfg := testConfig(t)
88
+ writeConfig(t, cfg.DataDir, `not json`)
89
+ if code, _ := call(t, cfg, alice, "sharedfolder.status", `{}`); code != "internal" {
90
+ t.Fatalf("code %q", code)
99
91
  }
100
92
  }
101
93
 
102
- func TestStatusOmitsSharedFoldersWhenNoneConfigured(t *testing.T) {
103
- _, out := call(t, testConfig(t), alice, "app.status", `{}`)
104
- if _, present := out["data"].(map[string]any)["shared_folders"]; present {
105
- t.Fatalf("unexpected shared_folders: %v", out)
94
+ func TestStorageAdapterFollowsTheChosenFolderAndAMovedShare(t *testing.T) {
95
+ skipRoot(t)
96
+ cfg := sharedFixture(t, 0o755)
97
+ var applied []string
98
+ cfg.StorageAdapter = func(_ context.Context, key, path string) error {
99
+ applied = append(applied, key+"="+path)
100
+ return nil
101
+ }
102
+ first := filepath.Join(cfg.VolumeRoot, "volume1", "Photos")
103
+ for i := 0; i < 2; i++ {
104
+ if code, _ := call(t, cfg, alice, "sharedfolder.status", `{}`); code != "" {
105
+ t.Fatal(code)
106
+ }
106
107
  }
107
- }
108
-
109
- func TestStatusReportsEachConfiguredSharedFolder(t *testing.T) {
110
- cfg := testConfig(t)
111
- cfg.VolumeRoot = t.TempDir()
112
- os.MkdirAll(filepath.Join(cfg.VolumeRoot, "volume1", "Photos"), 0o755)
113
- writeConfig(t, cfg.DataDir, `{"schema":1,"shared_folders":{"media":"Photos","gone":"Missing"}}`)
114
- _, out := call(t, cfg, alice, "app.status", `{}`)
115
- rows := out["data"].(map[string]any)["shared_folders"].([]any)
116
- if len(rows) != 2 {
117
- t.Fatalf("rows: %v", rows)
108
+ if len(applied) != 1 || applied[0] != "media="+first {
109
+ t.Fatalf("first reads apply once: %v", applied)
118
110
  }
119
- byKey := map[string]map[string]any{}
120
- for _, r := range rows {
121
- m := r.(map[string]any)
122
- byKey[m["key"].(string)] = m
111
+ second := filepath.Join(cfg.VolumeRoot, "volume2", "Photos")
112
+ if err := os.MkdirAll(filepath.Dir(second), 0o755); err != nil {
113
+ t.Fatal(err)
114
+ }
115
+ if err := os.Rename(first, second); err != nil {
116
+ t.Fatal(err)
123
117
  }
124
- media, gone := byKey["media"], byKey["gone"]
125
- if media["name"] != "Photos" || media["exists"] != true || media["readable"] != true || media["writable"] != true || media["path"] != filepath.Join(cfg.VolumeRoot, "volume1", "Photos") {
126
- t.Fatalf("media: %v", media)
118
+ if code, _ := call(t, cfg, alice, "sharedfolder.status", `{}`); code != "" {
119
+ t.Fatal(code)
127
120
  }
128
- if gone["exists"] != false || gone["readable"] != false || gone["writable"] != false || gone["error"] == nil || gone["path"] != nil {
129
- t.Fatalf("gone: %v", gone)
121
+ if len(applied) != 2 || applied[1] != "media="+second {
122
+ t.Fatalf("a moved share applies the new path: %v", applied)
130
123
  }
131
124
  }
132
125
 
133
- func TestStatusReportsAFolderThePackageAccountCannotWrite(t *testing.T) {
134
- if os.Geteuid() == 0 {
135
- t.Skip("root ignores directory modes")
136
- }
137
- cfg := testConfig(t)
138
- cfg.VolumeRoot = t.TempDir()
139
- dir := filepath.Join(cfg.VolumeRoot, "volume1", "Locked")
140
- os.MkdirAll(dir, 0o755)
141
- os.Chmod(dir, 0o555)
142
- t.Cleanup(func() { os.Chmod(dir, 0o755) })
143
- writeConfig(t, cfg.DataDir, `{"schema":1,"shared_folders":{"media":"Locked"}}`)
144
- _, out := call(t, cfg, alice, "app.status", `{}`)
145
- row := out["data"].(map[string]any)["shared_folders"].([]any)[0].(map[string]any)
146
- if row["exists"] != true || row["readable"] != true || row["writable"] != false {
147
- t.Fatalf("row: %v", row)
126
+ func TestStorageAdapterWaitsForThePermission(t *testing.T) {
127
+ skipRoot(t)
128
+ cfg := sharedFixture(t, 0o555)
129
+ called := false
130
+ cfg.StorageAdapter = func(context.Context, string, string) error { called = true; return nil }
131
+ if code, _ := call(t, cfg, alice, "sharedfolder.status", `{}`); code != "" || called {
132
+ t.Fatalf("pending folder: code %q called %v", code, called)
148
133
  }
149
134
  }
150
135
 
151
- func TestStatusReportsACorruptConfigWithoutFailing(t *testing.T) {
152
- cfg := testConfig(t)
153
- writeConfig(t, cfg.DataDir, `not json`)
154
- code, out := call(t, cfg, alice, "app.status", `{}`)
155
- if code != "" || out["data"].(map[string]any)["shared_folders_error"] == nil {
156
- t.Fatalf("%q %v", code, out)
136
+ func TestSharedFolderPathNamesWhatToGrant(t *testing.T) {
137
+ skipRoot(t)
138
+ _, err := sharedFixture(t, 0o555).SharedFolderPath("media")
139
+ if !errors.Is(err, sharedfolder.ErrPermissionPending) {
140
+ t.Fatalf("got %v", err)
141
+ }
142
+ if path, err := sharedFixture(t, 0o755).SharedFolderPath("media"); err != nil || path == "" {
143
+ t.Fatalf("%q %v", path, err)
157
144
  }
158
145
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "Überprüfen Sie den eingegebenen Wert und versuchen Sie es erneut.",
9
9
  "error.bad_response": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
10
10
  "error.cross_origin": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
11
- "error.origin_required": "Die Anfrage wurde abgelehnt: Der Browser hat nicht angegeben, dass sie von der Seite dieser App stammt. Öffnen Sie die App in einem aktuellen Browser erneut.",
11
+ "error.origin_required": "Anfrage abgelehnt. Öffnen Sie die App in einem aktuellen Browser erneut.",
12
12
  "error.internal": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
13
13
  "error.json_required": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
14
14
  "error.method_not_allowed": "Dieser Vorgang kann nicht ausgeführt werden. Bitte versuchen Sie es später erneut.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "Nur Mitglieder der Gruppe administrators können den Gruß ändern.",
29
29
  "greeting.label": "Neuer Gruß",
30
30
  "greeting.save": "Speichern",
31
- "greeting.setBy": "{text} — festgelegt von {user}",
31
+ "greeting.setBy": "Festgelegt von {user}",
32
32
  "status.backend": "Backend",
33
33
  "status.heading": "Backend-Status",
34
34
  "status.package": "Paketname",
35
35
  "status.uid": "Läuft als UID",
36
36
  "status.user": "Benutzer",
37
37
  "status.userAdmin": "{user} (Administrator)",
38
- "status.version": "Version"
38
+ "status.version": "Version",
39
+ "status.platform": "Plattform"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "Check the entered value and try again.",
9
9
  "error.bad_response": "Unable to perform this operation. Please try again later.",
10
10
  "error.cross_origin": "Unable to perform this operation. Please try again later.",
11
- "error.origin_required": "The request was refused: the browser did not show that it came from this App's page. Open the App again in a current browser.",
11
+ "error.origin_required": "Request refused. Open the App again in a current browser.",
12
12
  "error.internal": "Unable to perform this operation. Please try again later.",
13
13
  "error.json_required": "Unable to perform this operation. Please try again later.",
14
14
  "error.method_not_allowed": "Unable to perform this operation. Please try again later.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "Only members of the administrators group can change the greeting.",
29
29
  "greeting.label": "New greeting",
30
30
  "greeting.save": "Save",
31
- "greeting.setBy": "{text} — set by {user}",
31
+ "greeting.setBy": "Set by {user}",
32
32
  "status.backend": "Backend",
33
33
  "status.heading": "Backend status",
34
34
  "status.package": "Package name",
35
35
  "status.uid": "Runs as UID",
36
36
  "status.user": "User",
37
37
  "status.userAdmin": "{user} (administrator)",
38
- "status.version": "Version"
38
+ "status.version": "Version",
39
+ "status.platform": "Platform"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "Compruebe el valor introducido y vuelva a intentarlo.",
9
9
  "error.bad_response": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
10
10
  "error.cross_origin": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
11
- "error.origin_required": "Se rechazó la solicitud: el navegador no indicó que procediera de la página de esta App. Vuelva a abrir la App en un navegador actual.",
11
+ "error.origin_required": "Solicitud rechazada. Vuelva a abrir la App en un navegador actualizado.",
12
12
  "error.internal": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
13
13
  "error.json_required": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
14
14
  "error.method_not_allowed": "No se puede realizar esta operación. Vuelva a intentarlo más tarde.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "Solo los miembros del grupo administrators pueden cambiar el saludo.",
29
29
  "greeting.label": "Nuevo saludo",
30
30
  "greeting.save": "Guardar",
31
- "greeting.setBy": "{text} — establecido por {user}",
31
+ "greeting.setBy": "Establecido por {user}",
32
32
  "status.backend": "Backend",
33
33
  "status.heading": "Estado del backend",
34
34
  "status.package": "Nombre del paquete",
35
35
  "status.uid": "Se ejecuta como UID",
36
36
  "status.user": "Usuario",
37
37
  "status.userAdmin": "{user} (administrador)",
38
- "status.version": "Versión"
38
+ "status.version": "Versión",
39
+ "status.platform": "Plataforma"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "Vérifiez la valeur saisie et réessayez.",
9
9
  "error.bad_response": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
10
10
  "error.cross_origin": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
11
- "error.origin_required": "La requête a été refusée : le navigateur n’a pas indiqué qu’elle provenait de la page de cette App. Rouvrez l’App dans un navigateur récent.",
11
+ "error.origin_required": "Requête refusée. Rouvrez l’App dans un navigateur récent.",
12
12
  "error.internal": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
13
13
  "error.json_required": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
14
14
  "error.method_not_allowed": "Impossible d'effectuer cette opération. Veuillez réessayer ultérieurement.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "Seuls les membres du groupe administrators peuvent modifier le message d'accueil.",
29
29
  "greeting.label": "Nouveau message d'accueil",
30
30
  "greeting.save": "Sauvegarder",
31
- "greeting.setBy": "{text} — défini par {user}",
31
+ "greeting.setBy": "Défini par {user}",
32
32
  "status.backend": "Backend",
33
33
  "status.heading": "Statut du backend",
34
34
  "status.package": "Nom du paquet",
35
35
  "status.uid": "Exécuté avec l'UID",
36
36
  "status.user": "Utilisateur",
37
37
  "status.userAdmin": "{user} (administrateur)",
38
- "status.version": "Version"
38
+ "status.version": "Version",
39
+ "status.platform": "Plateforme"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "Controllare il valore inserito e riprovare.",
9
9
  "error.bad_response": "Impossibile eseguire questa operazione. Riprovare più tardi.",
10
10
  "error.cross_origin": "Impossibile eseguire questa operazione. Riprovare più tardi.",
11
- "error.origin_required": "La richiesta è stata rifiutata: il browser non ha indicato che proviene dalla pagina di questa App. Riaprire l’App in un browser aggiornato.",
11
+ "error.origin_required": "Richiesta rifiutata. Riaprire l’App in un browser aggiornato.",
12
12
  "error.internal": "Impossibile eseguire questa operazione. Riprovare più tardi.",
13
13
  "error.json_required": "Impossibile eseguire questa operazione. Riprovare più tardi.",
14
14
  "error.method_not_allowed": "Impossibile eseguire questa operazione. Riprovare più tardi.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "Solo i membri del gruppo administrators possono modificare il saluto.",
29
29
  "greeting.label": "Nuovo saluto",
30
30
  "greeting.save": "Salva",
31
- "greeting.setBy": "{text} — impostato da {user}",
31
+ "greeting.setBy": "Impostato da {user}",
32
32
  "status.backend": "Backend",
33
33
  "status.heading": "Stato del backend",
34
34
  "status.package": "Nome pacchetto",
35
35
  "status.uid": "Eseguito come UID",
36
36
  "status.user": "Utente",
37
37
  "status.userAdmin": "{user} (amministratore)",
38
- "status.version": "Versione"
38
+ "status.version": "Versione",
39
+ "status.platform": "Piattaforma"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "入力した値を確認して、もう一度お試しください。",
9
9
  "error.bad_response": "この操作は実行できません。後でもう一度お試しください。",
10
10
  "error.cross_origin": "この操作は実行できません。後でもう一度お試しください。",
11
- "error.origin_required": "リクエストは拒否されました。ブラウザーがこの App のページからのリクエストであることを示しませんでした。最新のブラウザーで App を開き直してください。",
11
+ "error.origin_required": "リクエストは拒否されました。最新のブラウザーで App を開き直してください。",
12
12
  "error.internal": "この操作は実行できません。後でもう一度お試しください。",
13
13
  "error.json_required": "この操作は実行できません。後でもう一度お試しください。",
14
14
  "error.method_not_allowed": "この操作は実行できません。後でもう一度お試しください。",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "あいさつを変更できるのは administrators グループのメンバーのみです。",
29
29
  "greeting.label": "新しいあいさつ",
30
30
  "greeting.save": "保存",
31
- "greeting.setBy": "{text} — {user} が設定",
31
+ "greeting.setBy": "{user} が設定",
32
32
  "status.backend": "バックエンド",
33
33
  "status.heading": "バックエンドの状態",
34
34
  "status.package": "パッケージ名",
35
35
  "status.uid": "実行 UID",
36
36
  "status.user": "ユーザー",
37
37
  "status.userAdmin": "{user}(管理者)",
38
- "status.version": "バージョン"
38
+ "status.version": "バージョン",
39
+ "status.platform": "プラットフォーム"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "입력한 값을 확인한 후 다시 시도하십시오.",
9
9
  "error.bad_response": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
10
10
  "error.cross_origin": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
11
- "error.origin_required": "요청이 거부되었습니다. 브라우저가 이 App 페이지에서 보낸 요청임을 알리지 않았습니다. 최신 브라우저에서 App을 다시 여십시오.",
11
+ "error.origin_required": "요청이 거부되었습니다. 최신 브라우저에서 App을 다시 여십시오.",
12
12
  "error.internal": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
13
13
  "error.json_required": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
14
14
  "error.method_not_allowed": "이 작업을 수행할 수 없습니다. 나중에 다시 시도하십시오.",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "administrators 그룹의 구성원만 인사말을 변경할 수 있습니다.",
29
29
  "greeting.label": "새 인사말",
30
30
  "greeting.save": "저장",
31
- "greeting.setBy": "{text} — {user}님이 설정함",
31
+ "greeting.setBy": "{user}님이 설정함",
32
32
  "status.backend": "백엔드",
33
33
  "status.heading": "백엔드 상태",
34
34
  "status.package": "패키지 이름",
35
35
  "status.uid": "실행 UID",
36
36
  "status.user": "사용자",
37
37
  "status.userAdmin": "{user}(관리자)",
38
- "status.version": "버전"
38
+ "status.version": "버전",
39
+ "status.platform": "플랫폼"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "请检查输入的值后重试。",
9
9
  "error.bad_response": "无法执行此操作。请稍后再试。",
10
10
  "error.cross_origin": "无法执行此操作。请稍后再试。",
11
- "error.origin_required": "请求已被拒绝:浏览器没有表明它来自此 App 的页面。请使用新版浏览器重新打开 App。",
11
+ "error.origin_required": "请求已被拒绝。请使用新版浏览器重新打开 App。",
12
12
  "error.internal": "无法执行此操作。请稍后再试。",
13
13
  "error.json_required": "无法执行此操作。请稍后再试。",
14
14
  "error.method_not_allowed": "无法执行此操作。请稍后再试。",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "只有 administrators 群组的成员可以更改问候语。",
29
29
  "greeting.label": "新的问候语",
30
30
  "greeting.save": "保存",
31
- "greeting.setBy": "{text} — 由 {user} 设置",
31
+ "greeting.setBy": "由 {user} 设置",
32
32
  "status.backend": "后端",
33
33
  "status.heading": "后端状态",
34
34
  "status.package": "套件名称",
35
35
  "status.uid": "运行身份 UID",
36
36
  "status.user": "用户帐号",
37
37
  "status.userAdmin": "{user}(管理员)",
38
- "status.version": "版本"
38
+ "status.version": "版本",
39
+ "status.platform": "平台"
39
40
  }
@@ -8,7 +8,7 @@
8
8
  "error.bad_request": "請檢查輸入的值後再試一次。",
9
9
  "error.bad_response": "無法執行此操作。請稍後再試。",
10
10
  "error.cross_origin": "無法執行此操作。請稍後再試。",
11
- "error.origin_required": "要求已被拒絕:瀏覽器沒有表明它來自這個 App 的頁面。請使用新版瀏覽器重新開啟 App。",
11
+ "error.origin_required": "要求已被拒絕。請使用新版瀏覽器重新開啟 App。",
12
12
  "error.internal": "無法執行此操作。請稍後再試。",
13
13
  "error.json_required": "無法執行此操作。請稍後再試。",
14
14
  "error.method_not_allowed": "無法執行此操作。請稍後再試。",
@@ -28,12 +28,13 @@
28
28
  "greeting.adminOnly": "只有 administrators 群組的成員可以變更問候語。",
29
29
  "greeting.label": "新的問候語",
30
30
  "greeting.save": "儲存",
31
- "greeting.setBy": "{text} — 由 {user} 設定",
31
+ "greeting.setBy": "由 {user} 設定",
32
32
  "status.backend": "後端",
33
33
  "status.heading": "後端狀態",
34
34
  "status.package": "套件名稱",
35
35
  "status.uid": "執行身分 UID",
36
36
  "status.user": "使用者帳號",
37
37
  "status.userAdmin": "{user}(管理員)",
38
- "status.version": "版本"
38
+ "status.version": "版本",
39
+ "status.platform": "平台"
39
40
  }
@@ -1,45 +1,77 @@
1
1
  /* Scoped to .dsmapp: the DSM desktop shares one page with every other app. */
2
2
  .dsmapp {
3
+ /* Static UI subset from @synopackageland/ui 0.12.0 dsm7-embedded theme. */
4
+ --ui-font-sans: Verdana, Arial, "Microsoft JhengHei", "Microsoft YaHei", Meiryo, sans-serif;
5
+ --ui-font-size-sm: 12px;
6
+ --ui-font-size: 13px;
7
+ --ui-font-size-lg: 14px;
8
+ --ui-font-size-title: 16px;
9
+ --ui-line-height: 20px;
10
+ --ui-hairline: 1px;
11
+ --ui-text: #414b55;
12
+ --ui-text-secondary: #414b55d1;
13
+ --ui-surface: #fff;
14
+ --ui-border: #c6d4e0b3;
15
+ --ui-primary: #057feb;
16
+ --ui-primary-hover: #0075db;
17
+ --ui-primary-active: #0062b8;
18
+ --ui-primary-foreground: #fff;
19
+ --ui-danger-text: #a81b1b;
20
+ --ui-radius-sm: 3px;
21
+ --ui-control-md: 28px;
22
+ --ui-space-1: 4px;
23
+ --ui-space-2: 8px;
24
+ --ui-space-3: 12px;
25
+ --ui-space-4: 16px;
26
+ --ui-space-5: 20px;
27
+ --ui-space-6: 24px;
3
28
  box-sizing: border-box;
4
29
  height: 100%;
5
30
  overflow: auto;
6
- padding: 20px 24px;
7
- font: 13px/1.5 "Noto Sans", "Helvetica Neue", Arial, sans-serif;
8
- color: #1d2733;
9
- background: #fff;
31
+ padding: var(--ui-space-5) var(--ui-space-6);
32
+ font: var(--ui-font-size)/var(--ui-line-height) var(--ui-font-sans);
33
+ color: var(--ui-text);
34
+ background: var(--ui-surface);
10
35
  }
11
- .dsmapp-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
12
- .dsmapp h1 { margin: 0 0 4px; font-size: 20px; font-weight: 600; }
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
+ .dsmapp h1 { margin: 0 0 var(--ui-space-1); font-size: var(--ui-font-size-title); font-weight: 600; }
13
39
  .dsmapp-secondary {
14
- height: 28px; padding: 0 12px; border: 1px solid #c8d0da; border-radius: 3px;
15
- background: #fff; color: inherit; font: inherit; cursor: pointer;
40
+ height: var(--ui-control-md); padding: 0 var(--ui-space-3); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);
41
+ background: var(--ui-surface); color: inherit; font: inherit; cursor: pointer;
16
42
  }
17
- .dsmapp h2 { margin: 24px 0 8px; font-size: 14px; font-weight: 600; }
18
- .dsmapp-greeting { margin: 0 0 12px; font-size: 15px; }
19
- .dsmapp-form { display: flex; gap: 8px; max-width: 420px; }
43
+ .dsmapp h2 { margin: var(--ui-space-6) 0 var(--ui-space-2); font-size: var(--ui-font-size-lg); font-weight: 600; }
44
+ .dsmapp-greeting { margin: 0 0 var(--ui-space-3); font-size: var(--ui-font-size-lg); }
45
+ .dsmapp-form { display: flex; gap: var(--ui-space-2); max-width: 420px; }
20
46
  .dsmapp-form[hidden] { display: none; }
21
47
  .dsmapp-form input {
22
- flex: 1; height: 28px; padding: 0 8px;
23
- border: 1px solid #c8d0da; border-radius: 3px; font: inherit;
48
+ flex: 1; min-width: 0; height: var(--ui-control-md); padding: 0 var(--ui-space-2);
49
+ border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); font: inherit;
24
50
  }
25
51
  .dsmapp-form button {
26
- height: 30px; padding: 0 16px; border: 0; border-radius: 3px;
27
- background: #0086e5; color: #fff; font: inherit; cursor: pointer;
52
+ height: var(--ui-control-md); padding: 0 var(--ui-space-4); border: 0; border-radius: var(--ui-radius-sm);
53
+ background: var(--ui-primary); color: var(--ui-primary-foreground); font: inherit; cursor: pointer;
28
54
  }
29
55
  .dsmapp-form button:disabled { opacity: 0.6; cursor: default; }
30
- .dsmapp-note { margin: 0; color: #5d6b7a; }
56
+ .dsmapp-note { margin: 0; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); }
31
57
  .dsmapp-note[hidden] { display: none; }
32
- .dsmapp-message { min-height: 1.5em; color: #c0392b; }
33
- .dsmapp-facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0; }
34
- .dsmapp-facts dt { color: #5d6b7a; }
58
+ .dsmapp-message { min-height: var(--ui-line-height); color: var(--ui-danger-text); }
59
+ .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
+ .dsmapp-facts dt { color: var(--ui-text-secondary); }
35
61
  .dsmapp-facts dd { margin: 0; }
36
62
  /* SDK portal panel (ctx.portal.mountPanel). */
37
- .dsmapp-portal-form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; max-width: 560px; }
38
- .dsmapp-portal input { height: 28px; padding: 0 8px; border: 1px solid #c8d0da; border-radius: 3px; font: inherit; }
63
+ .dsmapp-portal-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); max-width: 560px; }
64
+ .dsmapp-portal input { height: var(--ui-control-md); padding: 0 var(--ui-space-2); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); font: inherit; }
39
65
  .dsmapp-portal button {
40
- height: 30px; padding: 0 12px; border: 1px solid #c8d0da; border-radius: 3px;
41
- background: #fff; color: inherit; font: inherit; cursor: pointer; margin-top: 8px;
66
+ height: var(--ui-control-md); padding: 0 var(--ui-space-3); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);
67
+ background: var(--ui-surface); color: inherit; font: inherit; cursor: pointer; margin-top: var(--ui-space-2);
42
68
  }
43
69
  .dsmapp-portal-form button { margin-top: 0; }
44
- .dsmapp-portal-form button[type="submit"] { border-color: #0086e5; background: #0086e5; color: #fff; }
45
- .dsmapp-portal-error { color: #c0392b; }
70
+ .dsmapp-portal-form button[type="submit"] { border-color: var(--ui-primary); background: var(--ui-primary); color: var(--ui-primary-foreground); }
71
+ .dsmapp-portal-error { color: var(--ui-danger-text); }
72
+ .dsmapp-secondary:hover, .dsmapp-portal button:hover { border-color: var(--ui-primary); }
73
+ .dsmapp-form button:hover, .dsmapp-portal-form button[type="submit"]:hover { background: var(--ui-primary-hover); }
74
+ .dsmapp-form button:active, .dsmapp-portal-form button[type="submit"]:active { background: var(--ui-primary-active); }
75
+ .dsmapp input:focus-visible, .dsmapp button:focus-visible { outline: 2px solid var(--ui-primary); outline-offset: 2px; }
76
+ .dsmapp-facts dd { min-width: 0; overflow-wrap: anywhere; }
77
+ @media (resolution >= 2dppx) { .dsmapp { --ui-hairline: 0.5px; } }
@@ -37,6 +37,7 @@
37
37
  var t = ctx.t;
38
38
  var status = el("dl", { class: "dsmapp-facts" });
39
39
  var greeting = el("p", { class: "dsmapp-greeting", text: t("app.loading") });
40
+ var setBy = el("p", { class: "dsmapp-note", hidden: "" });
40
41
  var input = el("input", { type: "text", maxlength: "200", "aria-label": t("greeting.label"), placeholder: t("greeting.label") });
41
42
  var save = el("button", { type: "submit", text: t("greeting.save") });
42
43
  var refreshButton = el("button", { type: "button", class: "dsmapp-secondary", text: t("app.refresh") });
@@ -49,6 +50,7 @@
49
50
  root.replaceChildren(
50
51
  el("div", { class: "dsmapp-header" }, [el("h1", { text: t("app.title") }), refreshButton]),
51
52
  greeting,
53
+ setBy,
52
54
  form,
53
55
  adminOnly,
54
56
  message,
@@ -72,7 +74,8 @@
72
74
  [t("status.package"), data.package],
73
75
  [t("status.version"), data.version],
74
76
  // A Go backend reports go_version; a Python, Node.js or Rust App reports language.
75
- [t("status.backend"), (data.language || data.go_version) + " · " + data.platform],
77
+ [t("status.backend"), data.language || data.go_version],
78
+ [t("status.platform"), data.platform],
76
79
  [t("status.uid"), String(data.process.uid)]].forEach(function (row) {
77
80
  status.appendChild(el("dt", { text: row[0] }));
78
81
  status.appendChild(el("dd", { text: row[1] }));
@@ -81,9 +84,9 @@
81
84
  adminOnly.hidden = data.admin;
82
85
  }).catch(showError);
83
86
  ctx.call("greeting.get").then(function (data) {
84
- greeting.textContent = data.updated_by
85
- ? t("greeting.setBy", { text: data.text, user: data.updated_by })
86
- : data.text;
87
+ greeting.textContent = data.text;
88
+ setBy.textContent = data.updated_by ? t("greeting.setBy", { user: data.updated_by }) : "";
89
+ setBy.hidden = !data.updated_by;
87
90
  }).catch(showError);
88
91
  }
89
92