@voxgig/build 4.4.0 → 4.12.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 (100) hide show
  1. package/README.md +51 -2
  2. package/build.ts +20 -0
  3. package/dist/api/api_gen.d.ts +7 -0
  4. package/dist/api/api_gen.js +370 -0
  5. package/dist/api/api_gen.js.map +1 -0
  6. package/dist/build.d.ts +16 -1
  7. package/dist/build.js +17 -1
  8. package/dist/build.js.map +1 -1
  9. package/dist/doc/doc_gen.d.ts +8 -0
  10. package/dist/doc/doc_gen.js +397 -0
  11. package/dist/doc/doc_gen.js.map +1 -0
  12. package/dist/env/web/web_gen.js +197 -11
  13. package/dist/env/web/web_gen.js.map +1 -1
  14. package/dist/shape/ent.js +4 -2
  15. package/dist/shape/ent.js.map +1 -1
  16. package/env/web/web_gen.ts +219 -13
  17. package/package.json +6 -9
  18. package/tm/web/backend/env/shared/seed.ts.frag +23 -0
  19. package/tm/web/backend/env/web/api.ts.frag +157 -0
  20. package/tm/web/backend/env/web/web.ts.frag +43 -8
  21. package/tm/web/backend/srv/api/api-srv.ts.frag +4 -0
  22. package/tm/web/backend/srv/api/expose.ts.frag +33 -0
  23. package/tm/web/backend/srv/api/get_info.ts.frag +6 -0
  24. package/tm/web/backend/srv/api/on_ent.ts.frag +124 -0
  25. package/tm/web/backend/srv/auth/apikey_util.ts.frag +10 -0
  26. package/tm/web/backend/srv/auth/auth-srv.ts.frag +1 -0
  27. package/tm/web/backend/srv/auth/change_pass.ts.frag +12 -0
  28. package/tm/web/backend/srv/auth/create_apikey.ts.frag +33 -0
  29. package/tm/web/backend/srv/auth/get_info.ts.frag +1 -0
  30. package/tm/web/backend/srv/auth/list_apikey.ts.frag +15 -0
  31. package/tm/web/backend/srv/auth/load_auth.ts.frag +10 -1
  32. package/tm/web/backend/srv/auth/remind_pass.ts.frag +35 -0
  33. package/tm/web/backend/srv/auth/revoke_apikey.ts.frag +23 -0
  34. package/tm/web/backend/srv/auth/signin_user.ts.frag +12 -2
  35. package/tm/web/backend/srv/auth/signout_user.ts.frag +11 -1
  36. package/tm/web/backend/srv/auth/update_user.ts.frag +15 -0
  37. package/tm/web/backend/srv/auth/user_util.ts.frag +21 -0
  38. package/tm/web/backend/srv/auth/web_change_pass.ts.frag +16 -0
  39. package/tm/web/backend/srv/auth/web_create_apikey.ts.frag +15 -0
  40. package/tm/web/backend/srv/auth/web_list_apikey.ts.frag +11 -0
  41. package/tm/web/backend/srv/auth/web_load_auth.ts.frag +1 -0
  42. package/tm/web/backend/srv/auth/web_remind_pass.ts.frag +7 -0
  43. package/tm/web/backend/srv/auth/web_revoke_apikey.ts.frag +14 -0
  44. package/tm/web/backend/srv/auth/web_signin_user.ts.frag +1 -0
  45. package/tm/web/backend/srv/auth/web_signout_user.ts.frag +11 -3
  46. package/tm/web/backend/srv/auth/web_update_user.ts.frag +11 -0
  47. package/tm/web/backend/srv/ent/access.ts.frag +179 -0
  48. package/tm/web/backend/srv/ent/cmd_list.ts.frag +86 -0
  49. package/tm/web/backend/srv/ent/cmd_load.ts.frag +56 -0
  50. package/tm/web/backend/srv/ent/cmd_remove.ts.frag +60 -0
  51. package/tm/web/backend/srv/ent/cmd_save.ts.frag +141 -0
  52. package/tm/web/backend/srv/ent/ent-srv.ts.frag +4 -0
  53. package/tm/web/backend/srv/ent/get_info.ts.frag +6 -0
  54. package/tm/web/backend/srv/ent/web_cmd_list.ts.frag +17 -0
  55. package/tm/web/backend/srv/ent/web_cmd_load.ts.frag +17 -0
  56. package/tm/web/backend/srv/ent/web_cmd_remove.ts.frag +17 -0
  57. package/tm/web/backend/srv/ent/web_cmd_save.ts.frag +17 -0
  58. package/tm/web/backend/test/unit/env/web/api-router.test.ts.frag +315 -0
  59. package/tm/web/backend/test/unit/env/web/surface.test.ts.frag +109 -0
  60. package/tm/web/backend/test/unit/srv/api/api.setup.ts.frag +162 -0
  61. package/tm/web/backend/test/unit/srv/api/api.test.ts.frag +225 -0
  62. package/tm/web/backend/test/unit/srv/auth/apikey.test.ts.frag +152 -0
  63. package/tm/web/backend/test/unit/srv/auth/auth.setup.ts.frag +40 -0
  64. package/tm/web/backend/test/unit/srv/auth/session.test.ts.frag +189 -0
  65. package/tm/web/backend/test/unit/srv/ent/ent.setup.ts.frag +53 -0
  66. package/tm/web/backend/test/unit/srv/ent/proxy.test.ts.frag +109 -0
  67. package/tm/web/docs/explanation/web-architecture.md.frag +50 -0
  68. package/tm/web/docs/how-to/add-a-custom-entity-view.md.frag +53 -0
  69. package/tm/web/docs/how-to/change-the-theme.md.frag +63 -0
  70. package/tm/web/docs/how-to/customise-the-web-app.md.frag +68 -0
  71. package/tm/web/docs/how-to/use-the-api.md.frag +53 -0
  72. package/tm/web/docs/reference/web-app.md.frag +98 -0
  73. package/tm/web/web/AGENTS.md.frag +52 -0
  74. package/tm/web/web/e2e/smoke.spec.js.frag +13 -11
  75. package/tm/web/web/index.html.frag +1 -0
  76. package/tm/web/web/package.json.frag +1 -0
  77. package/tm/web/web/playwright.config.js.frag +2 -1
  78. package/tm/web/web/src/api.js.frag +92 -0
  79. package/tm/web/web/src/bus.js.frag +14 -8
  80. package/tm/web/web/src/cmp/admin.js.frag +293 -212
  81. package/tm/web/web/src/cmp/admin.md.frag +51 -0
  82. package/tm/web/web/src/cmp/app.js.frag +12 -15
  83. package/tm/web/web/src/cmp/app.md.frag +28 -0
  84. package/tm/web/web/src/cmp/auth.js.frag +69 -41
  85. package/tm/web/web/src/cmp/auth.md.frag +36 -0
  86. package/tm/web/web/src/cmp/public.js.frag +58 -0
  87. package/tm/web/web/src/cmp/public.md.frag +26 -0
  88. package/tm/web/web/src/cmp/settings.js.frag +178 -0
  89. package/tm/web/web/src/cmp/settings.md.frag +30 -0
  90. package/tm/web/web/src/cmp/shell.js.frag +231 -0
  91. package/tm/web/web/src/cmp/shell.md.frag +49 -0
  92. package/tm/web/web/src/cmp/view/custom-view.js.frag +47 -0
  93. package/tm/web/web/src/cmp/view/custom-view.md.frag +31 -0
  94. package/tm/web/web/src/custom.css.frag +8 -0
  95. package/tm/web/web/src/customise.js.frag +30 -0
  96. package/tm/web/web/src/hooks.js.frag +79 -0
  97. package/tm/web/web/src/main.js.frag +17 -4
  98. package/tm/web/web/src/model.js.frag +162 -0
  99. package/tm/web/web/src/style.css.frag +135 -21
  100. package/tm/web/web/src/theme.js.frag +82 -0
@@ -1,30 +1,27 @@
1
- // App shell: shows the auth component always; the entity admin only when
2
- // signed in. Reacts to bus auth events.
1
+ // Top-level router: signed-out visitors see the public content site; signed-in
2
+ // users see the enterprise app shell. Driven by bus auth events.
3
3
 
4
4
  import { bus, onEvent } from '../bus.js'
5
5
 
6
6
 
7
7
  class VgApp extends HTMLElement {
8
8
  connectedCallback() {
9
- this.innerHTML = `
10
- <header><h1>$$Name$$</h1><vg-auth></vg-auth></header>
11
- <main id="vg-main"></main>`
12
-
13
- onEvent('auth', ({ user }) => this.setMain(user))
14
-
9
+ this.signedIn = undefined
10
+ onEvent('auth', ({ user }) => this.route(user))
15
11
  // Resolve the cookie session on load.
16
12
  bus.post('cmp:auth,load:state')
17
13
  }
18
14
 
19
- setMain(user) {
20
- const main = this.querySelector('#vg-main')
21
- if (user) {
22
- main.innerHTML = '<vg-entity-admin></vg-entity-admin>'
23
- }
24
- else {
25
- main.innerHTML = '<p class="vg-hint">Sign in to manage your data.</p>'
15
+ route(user) {
16
+ // Avoid needless re-mounts when the auth state hasn't actually changed.
17
+ const signedIn = !!user
18
+ if (this.signedIn === signedIn) {
19
+ return
26
20
  }
21
+ this.signedIn = signedIn
22
+ this.innerHTML = signedIn ? '<vg-shell></vg-shell>' : '<vg-public></vg-public>'
27
23
  }
28
24
  }
29
25
 
30
26
  customElements.define('vg-app', VgApp)
27
+
@@ -0,0 +1,28 @@
1
+ # Component: vg-app (`cmp/app.js`)
2
+
3
+ The root router component: mounts the public site or the authenticated
4
+ shell depending on auth state, and re-routes when that state changes.
5
+
6
+ ## Structure
7
+
8
+ ```mermaid
9
+ flowchart TB
10
+ app[vg-app] -->|signed out| pub[vg-public]
11
+ app -->|signed in| shell[vg-shell]
12
+ pub --> auth[vg-auth]
13
+ ```
14
+
15
+ ## Messages
16
+
17
+ | Message | Direction | Purpose |
18
+ |---|---|---|
19
+ | `cmp:auth,load:state` | post | Trigger initial auth load on mount |
20
+ | `cmp:evt,name:auth` (event `auth`) | sub via `onEvent` | Re-route on sign-in / sign-out |
21
+
22
+ ## Notes
23
+
24
+ - Mounted once from `index.html`; everything else hangs off it.
25
+ - Re-routing replaces `innerHTML`, tearing down the previous subtree —
26
+ long-lived components must tolerate re-mounts (see the `isConnected`
27
+ guard convention in shell/admin).
28
+
@@ -1,8 +1,10 @@
1
- // Auth: a seneca plugin holding the signed-in principal, and the
2
- // <vg-auth> web component (sign-in form / signed-in bar) driving it
3
- // purely through bus messages.
1
+ // Auth: a seneca plugin holding the signed-in principal (state), and the
2
+ // <vg-auth> login form used on the public site. The signed-in UI lives in
3
+ // the app shell's user menu, not here.
4
4
 
5
5
  import { bus, emit } from '../bus.js'
6
+ import { remindPass } from '../api.js'
7
+ import * as Hooks from '../hooks.js'
6
8
 
7
9
 
8
10
  bus.use(function auth_cmp() {
@@ -16,7 +18,7 @@ bus.use(function auth_cmp() {
16
18
 
17
19
  // Resolve current auth from the backend (cookie session).
18
20
  seneca.add('cmp:auth,load:state', function (msg, reply) {
19
- this.act('aim:req,on:auth,load:auth', function (err, out) {
21
+ this.act('aim:web,on:auth,load:auth', function (err, out) {
20
22
  user = (!err && out.ok && 'signedin' === out.state) ? out.user : null
21
23
  emit('auth', { user })
22
24
  reply({ ok: true, user })
@@ -24,7 +26,7 @@ bus.use(function auth_cmp() {
24
26
  })
25
27
 
26
28
  seneca.add('cmp:auth,signin:user', function (msg, reply) {
27
- this.act('aim:req,on:auth,signin:user', {
29
+ this.act('aim:web,on:auth,signin:user', {
28
30
  email: msg.email,
29
31
  password: msg.password,
30
32
  }, function (err, out) {
@@ -38,7 +40,7 @@ bus.use(function auth_cmp() {
38
40
  })
39
41
 
40
42
  seneca.add('cmp:auth,signout:user', function (msg, reply) {
41
- this.act('aim:req,on:auth,signout:user', function () {
43
+ this.act('aim:web,on:auth,signout:user', function () {
42
44
  user = null
43
45
  emit('auth', { user })
44
46
  reply({ ok: true })
@@ -47,51 +49,77 @@ bus.use(function auth_cmp() {
47
49
  })
48
50
 
49
51
 
52
+ // The public login form (+ a forgot-password flow that never actually
53
+ // emails — the backend logs the reset code).
50
54
  class VgAuth extends HTMLElement {
51
55
  connectedCallback() {
56
+ this.mode = 'signin'
52
57
  this.render()
53
58
  }
54
59
 
55
- async render() {
56
- const state = await bus.post('cmp:auth,get:state')
57
-
58
- if (state.user) {
59
- this.innerHTML = `
60
- <div class="vg-auth-bar">
61
- <span>Signed in: <b>${state.user.email}</b></span>
62
- <button id="vg-signout">Sign out</button>
63
- </div>`
64
- this.querySelector('#vg-signout').onclick = async () => {
65
- await bus.post('cmp:auth,signout:user')
66
- this.render()
67
- }
60
+ render() {
61
+ if ('forgot' === this.mode) {
62
+ this.renderForgot()
68
63
  }
69
64
  else {
70
- this.innerHTML = `
71
- <form class="vg-auth-form">
72
- <h2>Sign in</h2>
73
- <label>Email <input name="email" type="email" required /></label>
74
- <label>Password <input name="password" type="password" required /></label>
75
- <button type="submit">Sign in</button>
76
- <div class="vg-auth-err" id="vg-auth-err"></div>
77
- </form>`
78
- this.querySelector('form').onsubmit = async (ev) => {
79
- ev.preventDefault()
80
- const fd = new FormData(ev.target)
81
- const res = await bus.post('cmp:auth,signin:user', {
82
- email: fd.get('email'),
83
- password: fd.get('password'),
84
- })
85
- if (!res.ok) {
86
- this.querySelector('#vg-auth-err').textContent =
87
- 'Sign in failed: ' + res.why
88
- }
89
- else {
90
- this.render()
91
- }
65
+ this.renderSignin()
66
+ }
67
+ }
68
+
69
+ renderSignin() {
70
+ this.innerHTML = `
71
+ <form class="vg-auth-form">
72
+ <h2>Sign in</h2>
73
+ <label>Email <input name="email" type="email" required /></label>
74
+ <label>Password <input name="password" type="password" required /></label>
75
+ <button type="submit">Sign in</button>
76
+ <a href="#" class="vg-link" id="vg-forgot">Forgot password?</a>
77
+ ${Hooks.html('auth:form:footer', {})}
78
+ <div class="vg-auth-err" id="vg-auth-err"></div>
79
+ </form>`
80
+ this.querySelector('#vg-forgot').onclick = (ev) => {
81
+ ev.preventDefault()
82
+ this.mode = 'forgot'
83
+ this.render()
84
+ }
85
+ this.querySelector('form').onsubmit = async (ev) => {
86
+ ev.preventDefault()
87
+ const fd = new FormData(ev.target)
88
+ const res = await bus.post('cmp:auth,signin:user', {
89
+ email: fd.get('email'),
90
+ password: fd.get('password'),
91
+ })
92
+ if (!res.ok) {
93
+ this.querySelector('#vg-auth-err').textContent = 'Sign in failed: ' + res.why
92
94
  }
93
95
  }
94
96
  }
97
+
98
+ renderForgot() {
99
+ this.innerHTML = `
100
+ <form class="vg-auth-form">
101
+ <h2>Reset password</h2>
102
+ <p class="vg-hint">Enter your email and we'll send a reset link.</p>
103
+ <label>Email <input name="email" type="email" required /></label>
104
+ <button type="submit">Send reset link</button>
105
+ <a href="#" class="vg-link" id="vg-back">Back to sign in</a>
106
+ <div class="vg-auth-msg" id="vg-auth-msg"></div>
107
+ </form>`
108
+ this.querySelector('#vg-back').onclick = (ev) => {
109
+ ev.preventDefault()
110
+ this.mode = 'signin'
111
+ this.render()
112
+ }
113
+ this.querySelector('form').onsubmit = async (ev) => {
114
+ ev.preventDefault()
115
+ const fd = new FormData(ev.target)
116
+ await remindPass(fd.get('email'))
117
+ // Always the same message (no user enumeration; nothing is actually sent).
118
+ this.querySelector('#vg-auth-msg').textContent =
119
+ 'If that email is registered, a reset link has been sent.'
120
+ }
121
+ }
95
122
  }
96
123
 
97
124
  customElements.define('vg-auth', VgAuth)
125
+
@@ -0,0 +1,36 @@
1
+ # Component: vg-auth (`cmp/auth.js`)
2
+
3
+ The sign-in form (with forgot-password), and the client-side `cmp:auth`
4
+ state service other components query.
5
+
6
+ ## Message flow
7
+
8
+ ```mermaid
9
+ sequenceDiagram
10
+ participant U as User
11
+ participant A as vg-auth
12
+ participant G as gateway (backend)
13
+ U->>A: submit email + password
14
+ A->>G: aim:req,on:auth,signin:user
15
+ G-->>A: { ok, user }
16
+ A->>A: emit 'auth' event (cmp:evt,name:auth)
17
+ Note over A: vg-app re-routes to the shell
18
+ ```
19
+
20
+ ## Messages
21
+
22
+ | Message | Direction | Purpose |
23
+ |---|---|---|
24
+ | `cmp:auth,load:state` | answers | Load the session (`aim:req,on:auth,load:auth`) |
25
+ | `cmp:auth,get:state` | answers | Current `{ user }` for other components |
26
+ | `cmp:auth,signin:user` | answers | Sign in, then emit `auth` |
27
+ | `cmp:auth,signout:user` | answers | Sign out, then emit `auth` |
28
+ | `aim:req,on:auth,remind:pass` | post | Forgot-password (server stub; no email sent) |
29
+ | event `auth` | emit | Auth state changed (the store cache also clears on this) |
30
+
31
+ ## Customisation
32
+
33
+ | Hook point | Kind | Effect |
34
+ |---|---|---|
35
+ | `auth:form:footer` | html | Extra links/markup under the form |
36
+
@@ -0,0 +1,58 @@
1
+ // The public content site (marketing pages) shown to signed-out visitors,
2
+ // with the login form. Static content — the app itself is behind auth.
3
+
4
+ import * as Hooks from '../hooks.js'
5
+
6
+ class VgPublic extends HTMLElement {
7
+ connectedCallback() {
8
+ this.innerHTML = `
9
+ <div class="vg-public">
10
+ <header class="vg-public-nav">
11
+ <span class="vg-brand">📋 Todo</span>
12
+ <nav>
13
+ <a href="#features">Features</a>
14
+ <a href="#about">About</a>
15
+ </nav>
16
+ </header>
17
+
18
+ <section class="vg-hero">
19
+ <div class="vg-hero-copy">
20
+ <h1>Plan together. Ship faster.</h1>
21
+ <p>Organise work into projects, invite your team, and track every
22
+ todo — a simple, collaborative workspace.</p>
23
+ </div>
24
+ <div class="vg-hero-auth">
25
+ <vg-auth></vg-auth>
26
+ </div>
27
+ </section>
28
+
29
+ <section id="features" class="vg-features">
30
+ <h2>Features</h2>
31
+ <div class="vg-feature-grid">
32
+ <div class="vg-feature"><h3>Projects</h3>
33
+ <p>Group work into projects and assign collaborators.</p></div>
34
+ <div class="vg-feature"><h3>Todo lists</h3>
35
+ <p>Break projects into lists and items, done when done.</p></div>
36
+ <div class="vg-feature"><h3>Collaboration</h3>
37
+ <p>Everyone assigned to a project sees and edits its work.</p></div>
38
+ </div>
39
+ </section>
40
+
41
+ <section id="about" class="vg-about">
42
+ <h2>About</h2>
43
+ <p>A demonstration of a model-driven enterprise app: the entire UI
44
+ — navigation, forms and entity relationships — is generated from
45
+ the data model.</p>
46
+ </section>
47
+
48
+ ${Hooks.html('public:sections', {})}
49
+
50
+ <footer class="vg-public-footer">
51
+ <span>&copy; Todo. A Voxgig demo.</span>
52
+ </footer>
53
+ </div>`
54
+ }
55
+ }
56
+
57
+ customElements.define('vg-public', VgPublic)
58
+
@@ -0,0 +1,26 @@
1
+ # Component: vg-public (`cmp/public.js`)
2
+
3
+ The public content site shown to signed-out visitors: marketing sections
4
+ (hero, features, about) plus the sign-in form.
5
+
6
+ ## Structure
7
+
8
+ ```mermaid
9
+ flowchart TB
10
+ pub[vg-public] --> hero[hero / features / about sections]
11
+ pub --> auth[vg-auth - the login form]
12
+ ```
13
+
14
+ ## Customisation
15
+
16
+ | Hook point | Kind | Effect |
17
+ |---|---|---|
18
+ | `public:sections` | html | Append extra marketing sections |
19
+
20
+ Static content is meant to be edited directly — this component is
21
+ create-once and project-owned; replace the copy with your product's.
22
+
23
+ ## Messages
24
+
25
+ None of its own — sign-in flows through the embedded `vg-auth`.
26
+
@@ -0,0 +1,178 @@
1
+ // User settings & security: update profile and change password. Backed by
2
+ // the auth service wrappers (aim:req,on:auth,{update:user,change:pass}).
3
+
4
+ import { bus } from '../bus.js'
5
+ import * as Api from '../api.js'
6
+ import * as Hooks from '../hooks.js'
7
+
8
+
9
+ class VgSettings extends HTMLElement {
10
+ async connectedCallback() {
11
+ const state = await bus.post('cmp:auth,get:state')
12
+ this.user = state.user || {}
13
+ this.apikeys = []
14
+ this.newKey = null
15
+ this.render()
16
+ this.loadKeys()
17
+ }
18
+
19
+ async loadKeys() {
20
+ const res = await Api.listApikeys()
21
+ this.apikeys = (res.ok && res.items) || []
22
+ if (this.isConnected) {
23
+ this.renderKeys()
24
+ }
25
+ }
26
+
27
+ render() {
28
+ this.innerHTML = `
29
+ <div class="vg-entity vg-settings">
30
+ <h2>Settings &amp; security</h2>
31
+
32
+ <section class="vg-card">
33
+ <h3>Profile</h3>
34
+ <form class="vg-entity-form" id="vg-profile">
35
+ <label>Name <input name="name" type="text" value="${esc(this.user.name)}" /></label>
36
+ <label>Email <input type="email" value="${esc(this.user.email)}" disabled /></label>
37
+ <div class="vg-form-actions"><button type="submit" class="vg-primary">Save profile</button></div>
38
+ <div class="vg-form-msg" id="vg-profile-msg"></div>
39
+ </form>
40
+ </section>
41
+
42
+ <section class="vg-card">
43
+ <h3>Change password</h3>
44
+ <form class="vg-entity-form" id="vg-pass">
45
+ <label>New password <input name="password" type="password" required minlength="8" /></label>
46
+ <label>Confirm <input name="confirm" type="password" required minlength="8" /></label>
47
+ <div class="vg-form-actions"><button type="submit" class="vg-primary">Change password</button></div>
48
+ <div class="vg-form-msg" id="vg-pass-msg"></div>
49
+ </form>
50
+ </section>
51
+
52
+ <section class="vg-card">
53
+ <h3>API keys</h3>
54
+ <p class="vg-muted">Access the REST API (<code>/api/v1/…</code>) with
55
+ <code>Authorization: Bearer &lt;key&gt;</code>. A key is shown
56
+ only once, at creation.</p>
57
+ <form class="vg-entity-form vg-apikey-form" id="vg-apikey-create">
58
+ <label>Name <input name="name" type="text" required placeholder="e.g. ci-deploy" /></label>
59
+ <div class="vg-form-actions"><button type="submit" class="vg-primary">Create key</button></div>
60
+ <div class="vg-form-msg" id="vg-apikey-msg"></div>
61
+ </form>
62
+ <div id="vg-apikey-new"></div>
63
+ <div id="vg-apikey-list"></div>
64
+ </section>
65
+
66
+ ${Hooks.html('settings:sections', { user: this.user })}
67
+ </div>`
68
+
69
+ this.querySelector('#vg-profile').onsubmit = async (ev) => {
70
+ ev.preventDefault()
71
+ const name = ev.target.querySelector('[name=name]').value
72
+ const res = await Api.updateUser({ name })
73
+ const msg = this.querySelector('#vg-profile-msg')
74
+ if (res.ok) {
75
+ this.user.name = res.user ? res.user.name : name
76
+ msg.textContent = 'Profile saved.'
77
+ msg.className = 'vg-form-msg vg-ok'
78
+ }
79
+ else {
80
+ msg.textContent = 'Save failed: ' + (res.why || '')
81
+ msg.className = 'vg-form-msg vg-err'
82
+ }
83
+ }
84
+
85
+ this.querySelector('#vg-apikey-create').onsubmit = async (ev) => {
86
+ ev.preventDefault()
87
+ const name = ev.target.querySelector('[name=name]').value.trim()
88
+ const msg = this.querySelector('#vg-apikey-msg')
89
+ const res = await Api.createApikey(name)
90
+ if (res.ok) {
91
+ ev.target.reset()
92
+ this.newKey = res.key
93
+ msg.textContent = ''
94
+ await this.loadKeys()
95
+ }
96
+ else {
97
+ msg.textContent = 'Create failed: ' + (res.why || '')
98
+ msg.className = 'vg-form-msg vg-err'
99
+ }
100
+ }
101
+ this.renderKeys()
102
+
103
+ this.querySelector('#vg-pass').onsubmit = async (ev) => {
104
+ ev.preventDefault()
105
+ const pw = ev.target.querySelector('[name=password]').value
106
+ const confirm = ev.target.querySelector('[name=confirm]').value
107
+ const msg = this.querySelector('#vg-pass-msg')
108
+ if (pw !== confirm) {
109
+ msg.textContent = 'Passwords do not match.'
110
+ msg.className = 'vg-form-msg vg-err'
111
+ return
112
+ }
113
+ const res = await Api.changePass(pw)
114
+ if (res.ok) {
115
+ ev.target.reset()
116
+ msg.textContent = 'Password changed.'
117
+ msg.className = 'vg-form-msg vg-ok'
118
+ }
119
+ else {
120
+ msg.textContent = 'Change failed: ' + (res.why || '')
121
+ msg.className = 'vg-form-msg vg-err'
122
+ }
123
+ }
124
+ }
125
+
126
+ // The API-keys list (re-rendered after create/revoke). The freshly
127
+ // created key is shown ONCE (it is never retrievable again).
128
+ renderKeys() {
129
+ const fresh = this.querySelector('#vg-apikey-new')
130
+ if (fresh) {
131
+ fresh.innerHTML = this.newKey ? `
132
+ <div class="vg-apikey-once">
133
+ <strong>Copy your new key now — it will not be shown again:</strong>
134
+ <code class="vg-apikey-value">${esc(this.newKey)}</code>
135
+ </div>` : ''
136
+ }
137
+
138
+ const list = this.querySelector('#vg-apikey-list')
139
+ if (!list) {
140
+ return
141
+ }
142
+ list.innerHTML = this.apikeys.length ? `
143
+ <table class="vg-table vg-apikey-table">
144
+ <thead><tr><th>Name</th><th>Key</th><th>Created</th><th>Status</th><th></th></tr></thead>
145
+ <tbody>
146
+ ${this.apikeys.map((k) => `
147
+ <tr data-id="${esc(k.id)}">
148
+ <td>${esc(k.name)}</td>
149
+ <td><code>${esc(k.prefix)}…</code></td>
150
+ <td>${k.t_c ? new Date(k.t_c).toISOString().slice(0, 10) : ''}</td>
151
+ <td>${k.revoked ? 'revoked' : 'active'}</td>
152
+ <td>${k.revoked ? '' :
153
+ `<button type="button" class="vg-apikey-revoke" data-id="${esc(k.id)}">Revoke</button>`}</td>
154
+ </tr>`).join('')}
155
+ </tbody>
156
+ </table>` : '<p class="vg-muted">No API keys yet.</p>'
157
+
158
+ for (const btn of list.querySelectorAll('.vg-apikey-revoke')) {
159
+ btn.onclick = async () => {
160
+ const res = await Api.revokeApikey(btn.dataset.id)
161
+ if (res.ok) {
162
+ this.newKey = null
163
+ await this.loadKeys()
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+
171
+ function esc(s) {
172
+ return String(null == s ? '' : s).replace(/[&<>"]/g, (c) =>
173
+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]))
174
+ }
175
+
176
+
177
+ customElements.define('vg-settings', VgSettings)
178
+
@@ -0,0 +1,30 @@
1
+ # Component: vg-settings (`cmp/settings.js`)
2
+
3
+ User settings & security: profile (name/email) and change-password.
4
+ Mounted in the shell's main area from the user menu.
5
+
6
+ ## Message flow
7
+
8
+ ```mermaid
9
+ sequenceDiagram
10
+ participant S as vg-settings
11
+ participant G as gateway (backend)
12
+ S->>G: aim:req,on:auth,update:user (profile save)
13
+ S->>G: aim:req,on:auth,change:pass (old + new password)
14
+ G-->>S: { ok } | { ok:false, why }
15
+ ```
16
+
17
+ ## Messages
18
+
19
+ | Message | Direction | Purpose |
20
+ |---|---|---|
21
+ | `cmp:auth,get:state` | post | Current user to populate the form |
22
+ | `aim:req,on:auth,update:user` | post (via `api.js`) | Save profile |
23
+ | `aim:req,on:auth,change:pass` | post (via `api.js`) | Change password (min length 8) |
24
+
25
+ ## Customisation
26
+
27
+ | Hook point | Kind | Effect |
28
+ |---|---|---|
29
+ | `settings:sections` | html | Extra settings sections |
30
+