From 1712c6fa1f1e595e2b1973ec9a49e20b6febb023 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:16:48 -0400 Subject: [PATCH] Docs: document cosmetic widget fields, option shape and the fetch guard scope --- src/content/docs/docs/create-a-widget/data.md | 4 +++- src/content/docs/docs/create-a-widget/index.mdx | 4 ++-- src/content/docs/docs/create-a-widget/manifest.md | 4 +++- src/content/docs/docs/security.md | 2 +- src/content/docs/docs/troubleshooting.md | 2 +- src/content/docs/docs/widgets/index.mdx | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/content/docs/docs/create-a-widget/data.md b/src/content/docs/docs/create-a-widget/data.md index 9223aca..0c436aa 100644 --- a/src/content/docs/docs/create-a-widget/data.md +++ b/src/content/docs/docs/create-a-widget/data.md @@ -38,7 +38,9 @@ A widget that renders entirely in the browser, like [Clock](/docs/widgets/clock/ | `ctx.KIND` | `AUTH`, `INVALID`, `UPSTREAM`, `NETWORK`, `TIMEOUT`, `BLOCKED`, `INTERNAL`. See [API errors](/docs/contributing/api-errors/). | | `ctx.log` | The structured logger. | -Keep every upstream call behind `ctx.fetchJSON`. It applies the SSRF guard, IP pinning, the size limit and the TLS setting. +Keep every upstream call behind `ctx.fetchJSON`. It applies the size limit and the TLS setting. An `optionsFrom` fetch also applies the SSRF guard and IP pinning, because its config arrives with the request. A refresh reads the saved config and is not guarded. Never build a URL from `ctx.params`. + +Await every call. A rejected promise that nothing awaits stops the API process, and every widget fails until it restarts. ### Metrics and XML bodies diff --git a/src/content/docs/docs/create-a-widget/index.mdx b/src/content/docs/docs/create-a-widget/index.mdx index 59622f6..5443f17 100644 --- a/src/content/docs/docs/create-a-widget/index.mdx +++ b/src/content/docs/docs/create-a-widget/index.mdx @@ -31,12 +31,12 @@ cp -r docs/widget-template ui/widgets/mywidget "fields": [ { "key": "url", "type": "text", "label": "Service URL", "placeholder": "http://host:port" }, { "key": "apiKey", "type": "secret", "label": "API key", "optional": true }, - { "key": "showTotal", "type": "toggle", "label": "Show total", "default": true } + { "key": "showTotal", "type": "toggle", "label": "Show total", "default": true, "cosmetic": true } ] } ``` -`name` must match the folder. Each field becomes a row in the widget's settings. A `secret` never returns to the browser. A `toggle` that was never saved is absent from `ctx.config`, so test for `false`. See [Manifest](/docs/create-a-widget/manifest/). +`name` must match the folder. Each field becomes a row in the widget's settings. A `secret` never returns to the browser. `cosmetic` marks a field that can change without asking for the secret again. A `toggle` that was never saved is absent from `ctx.config`, so test for `false`. See [Manifest](/docs/create-a-widget/manifest/). ## 3. Data diff --git a/src/content/docs/docs/create-a-widget/manifest.md b/src/content/docs/docs/create-a-widget/manifest.md index 1058adb..7c0efa1 100644 --- a/src/content/docs/docs/create-a-widget/manifest.md +++ b/src/content/docs/docs/create-a-widget/manifest.md @@ -107,7 +107,7 @@ A widget with no light design declares it, and keeps its dark card and dark inte | --- | --- | | `text` | An inline-edit row. | | `number` | An inline-edit row that stores a number. | -| `secret` | An inline-edit row for a masked value. Shows `Configured` once set. The value stays on the server. Leaving it blank keeps the stored value. | +| `secret` | An inline-edit row for a masked value. Shows `Configured` once set. The value stays on the server. Leaving it blank keeps the stored value, unless a field that is not `cosmetic` changed. | | `toggle` | An on and off switch, stored as a boolean. | | `color` | The swatch and colour control used elsewhere in the admin. Saves `#rrggbb`. | | `select` | A dropdown. `"variant": "pills"` renders a radio group. With `optionsFrom` it adds a Fetch button. | @@ -125,9 +125,11 @@ A widget with no light design declares it, and keeps its dark card and dark inte | `default` | The value used when none is saved. | | `hint` | Short help under the field. On a `group`, it shows at the bottom of the section. | | `optional` | When `true`, the field is not required to save. A required `secret` counts as missing only when nothing is stored. | +| `cosmetic` | When `true`, changing the field keeps a stored `secret`. Use it only on a field that cannot change the server a secret is sent to, the account it is paired with, or a path on a user-entered server. Links, labels, colours and units qualify. A repeated key needs it on every declaration. The keys a cosmetic picker `carries` count as cosmetic too. | | `transient` | When `true`, the field is sent to an `optionsFrom` fetch but not saved. Use it for a search box. Top-level fields only. | | `carries` | For a `select` with `optionsFrom`: extra config keys this picker writes, from the chosen option's `set` block. | | `showIf` | Shows the field only when a sibling matches: `{ "field": "provider", "equals": "adguard" }`, or `{ "field": "provider", "in": ["adguard", "pihole"] }`. Anything else is rejected. | +| `options` | For a `select`, `multiselect` or `picklist`: an array of `{ "value", "label" }` objects. Any other entry is rejected. | | `optionsFrom` | For a `select`: the data endpoint that returns the options at config time. | | `variant` | For a `select`: `"pills"` renders a radio group. | | `min`, `max` | For a `group`: the fewest and most entries. | diff --git a/src/content/docs/docs/security.md b/src/content/docs/docs/security.md index 4cb25c4..120b830 100644 --- a/src/content/docs/docs/security.md +++ b/src/content/docs/docs/security.md @@ -40,7 +40,7 @@ The first page load after install or upgrade sets the list. A host name is trust Stored secrets are stripped from the config before it reaches the browser. A populated field reports as set without returning its value, in config responses and in exports alike. -A secret is restored only for the request it was stored for. If a save changes where the credential would be sent, by editing a badge URL, its non-secret headers, or any non-secret field of a widget's config, the stored value is not restored and Stackyard names what must be re-entered. Matching on an item's id alone would let an imported config point an existing credential somewhere new. Turning **Allow self-signed certificate** on or off for an app, and changing the port map (`settings.server.portMap`), count as such a change. +A secret is restored only for the request it was stored for. If a save changes where the credential would be sent, by editing a badge URL, its non-secret headers, or a widget field that decides where it goes, the stored value is not restored and Stackyard names what must be re-entered. Matching on an item's id alone would let an imported config point an existing credential somewhere new. Turning **Allow self-signed certificate** on or off for an app, and changing the port map (`settings.server.portMap`), count as such a change. Secrets are stored in plain text in `apps.json` on the data volume. Protect that volume with filesystem permissions and backups. diff --git a/src/content/docs/docs/troubleshooting.md b/src/content/docs/docs/troubleshooting.md index 6036165..bd0ad14 100644 --- a/src/content/docs/docs/troubleshooting.md +++ b/src/content/docs/docs/troubleshooting.md @@ -99,7 +99,7 @@ Check that the service is reachable and the credential is still valid. ### A credential stopped working after an edit -Changing where or how a credential would be sent clears the stored value. Editing a badge URL, its non-secret headers, or a non-secret field of a widget's config all count. So do turning **Allow self-signed certificate** on or off for an app, and changing the port map (`settings.server.portMap`) in the config. Unticking **Secret** on a header row clears it on the next save. +Changing where or how a credential would be sent clears the stored value. Editing a badge URL, its non-secret headers, or a widget field that decides where the credential goes all count. Display settings such as units, colours and links do not. So do turning **Allow self-signed certificate** on or off for an app, and changing the port map (`settings.server.portMap`) in the config. Unticking **Secret** on a header row clears it on the next save. Stackyard names the items to re-enter. Enter the credential again and save. diff --git a/src/content/docs/docs/widgets/index.mdx b/src/content/docs/docs/widgets/index.mdx index 2b7595c..3b3bf3f 100644 --- a/src/content/docs/docs/widgets/index.mdx +++ b/src/content/docs/docs/widgets/index.mdx @@ -40,7 +40,7 @@ Widgets reorder in the same list as apps and folders. See [Apps and folders](/do A field marked Secret is stored on the server and never sent back to the browser. It shows as set without revealing its value. -Changing where a credential would be sent, by editing a URL or a non-secret field, clears the stored value, and Stackyard names what to re-enter. This stops an imported config from taking a credential out of an install. +Changing where a credential would be sent, by editing a URL, an account or another field that decides where it goes, clears the stored value, and Stackyard names what to re-enter. This stops an imported config from taking a credential out of an install. ## Writing a widget