* feat: complete the group model (god_admin, site groups, aggregates), enforce naming, fix docs 500s + status dots (v1.26.0)
- seed god_admin + nest into app_super_admin; auto-provision site groups (S_super_admin, S_hosts_*/S_apps_* aggregates, S_everyone) on site create + self-heal on Directory load
- map service resources to the app kind (site_local_app_<slug>_*); nest per-resource groups into site aggregates (physical inheritance lattice)
- enforce the group naming convention server-side on POST /groups; surface god_admin + site groups on the site resource modal
- fix in-app /docs/<slug> 500s (Dockerfile never copied docs/); serve doc images at /docs/images
- fix Directory status dots (neutral grey when agent endpoint unreachable); align Profile/API cards full-width
- group resolver: keep the site slug verbatim (site_local not re-slugified)
- bump to 1.26.0
* fix: use verbatim resource slugs in group names (matches access-request tests + live convention)
The group naming inserts a kind segment (resourceGroupCns(site, kind, slug, level)),
but the access-request tests + the live directory convention are verbatim
({site}_{slug}_{level} -- the kind is carried in the resource slug, e.g. host_theta-env).
For bare test slugs this produced site_x_host_artest-host_x_access instead of the
expected site_x_artest-host_x_access, so the requester was never removed from the
auto-provisioned access group and every request 409'd. resourceGroupCns is now
(site, slug, level) with the verbatim slug; the kind is used only to pick the
aggregate the group nests into.
- Add utils/groups.js: the group schema + inheritance resolver (god_admin,
{site}_super_admin, {site}_hosts_*/{site}_apps_* aggregates, per-resource
admin/access/<capability>, meta everyone/{site}_everyone). admin implies
access; capabilities explicit; hosts/apps orthogonal; cross-site isolated.
- permission.js: recognize god_admin (legacy app_super_admin aliased) and add
onResource/requireResource for resource-level checks + everyone meta grants.
- user.js isAdmin: recognize god_admin + site-scoped super/app-admin groups.
- Remove the standalone Groups page (nav + route + view); groups are managed on
adopted Directory resources. Add a /docs/groups help link in the Directory
toolbar (GROUPS.md copied into the SSO docs).
- tests/groups.test.js: full resolver coverage (15 tests).
Co-Authored-By: Claude <noreply@anthropic.com>
- Merge theta-agent into Directory: remove the Agents page; add green/yellow/red
status dots to host rows and a Metrics tab (telemetry + discovery) to the
resource modal, joined to hosts by hostname, live via socket.io + 30s refresh.
- Discovery Plugins New-plugin modal: slug derived from name (field removed),
cron dropdown (hourly/daily/weekly/custom), configSchema-driven settings
(Proxmox url/tokenId/tokenSecret) sent as a populated config.
- Directory resource slug now read-only + derived from name.
- Vault page restyled to match the site.
- Navbar: username no longer underlined; only the active link is bold+underlined.
- docs/agents.md: document the Directory status/metrics + NAT troubleshooting.
Co-Authored-By: Claude <noreply@anthropic.com>
- /api/vault proxy now injects X-Vault-Token: the proxy declared its request
hook with http-proxy-middleware v3 syntax (on: { proxyReq }), which the
installed HPM v2 silently ignores — so every vault call reached OpenBao
unauthenticated (the recurring 403). Rewritten as v2 onProxyReq.
- Header injection ordered before fixRequestBody (the body write flushes
headers; setting X-Vault-Token after it failed on every POST/PUT).
- initORM add-only schema heal: sequelize.sync() never ALTERs, so newer columns
(PluginInstance.lastLog) are now added via describeTable + addColumn.
- Long-lived external-app tokens via sso-app role (768h periodic); VaultAppToken
stores each app token's accessor and renews it at boot + every 6h; re-minting
revokes the previous token via its accessor.
- Wire-level tests for the vault proxy + app-token accessor lifecycle.
- package.json + lockfile bumped to 1.23.0.
Co-Authored-By: Claude <noreply@anthropic.com>
- New admin Agents page (nav + /agents route + views/agents.ejs): live list of
connected theta-agent hosts with CPU/RAM/disk/ZFS/GPU telemetry and online
status, updated live over socket.io ('agent.telemetry'/'agent.discovery').
- Auth + admin-gate the /api/agent REST router (it was mounted without
middleware.auth — anyone could list nodes / send commands). The agent
WebSocket (/api/agent/ws) is unaffected (handled by the raw wss upgrade with
its own token auth).
- package.json + lockfile bumped to 1.22.0 to match the tag.
Co-Authored-By: Claude <noreply@anthropic.com>
The no_native_dialogs regression test forbids native alert/confirm/prompt in
views (they block browser events). Replace the native confirm() in deleteShared
with app.messages.confirm().
- vault_broker: always reconcile policy content before serving a cached
token (compare-and-skip), so stale stored policies can't cause a recurring
403 'permission denied'; policy content is parsed live by OpenBao, so edits
apply to existing tokens immediately.
- Shared secrets: publish to secret/shared/<owner>/<slug>; grant read to users
and apps by editing the grantee's policy content (live-applied). New
SharedSecret/SharedSecretGrant ORM models, /api/shared-secrets router, and a
Shared tab in the vault UI.
- package.json + lockfile bumped to 1.21.0 to match the tag.
Co-Authored-By: Claude <noreply@anthropic.com>
### Fixed
- **Navbar shows Catalog/Vault for unauthenticated users** — Changed nav
gating from `groups: []` (always visible) to `groups: ['login']` and
added synthetic 'login' group handling in app-base.js.
- **500 ENOENT: no such file or directory, open '/docs/directory.md'** —
Created the missing documentation file.
### Changed
- **SMTP configuration UI-only** — Removed SMTP from static config files
(conf/base.js, sso-secrets.js, setup.env.example). SMTP is now only
configurable via the runtime UI at /conf.
### Added
- **Test email/SMS capability** — Added POST /api/conf/test-email and
POST /api/conf/test-sms endpoints with UI buttons in the Configuration
page. Saves config first, then sends test message to verify settings.
### theta-env setup.sh
- **Non-interactive theta-agent configuration** — Added CFG_THETA_AGENT_ENABLE,
CFG_THETA_AGENT_LDAP_AUTH, and CFG_THETA_AGENT_FULL_CONTROL variables to
setup.env (all default to 1/enabled).
Co-Authored-By: Claude <noreply@anthropic.com>
GET /api/conf no longer returns smtp.pass / oauth.jwtSecret in cleartext
(masked to ********). POST treats a blank or ******** secret submission as
"keep the stored value," so editing the From address or token lifetimes no
longer requires re-entering or leaks the SMTP password / JWT secret. The /conf
form fields carry a leave-unchanged hint. Storage stays in OpenBao at
secret/sso-manager/conf (unchanged); no theta-suite policy change needed.
Co-Authored-By: Claude <noreply@anthropic.com>
Generalize the half-built discovery plugins into a real plugin system: plugin
TYPES (the plugins/<category>/<type>.js modules with manifests) and loadable,
configurable, multi-copy plugin INSTANCES (PluginInstance ORM model) managed
from a dedicated /plugins page and /api/plugins API, with per-instance secrets
in OpenBao at secret/plugins/<id>/conf.
- plugin_registry.js: getTypes/getModule/splitConfig/mask + required-field helpers
- PluginInstance model (Sequelize): id/pluginType/category/name/slug(unique)/
enabled/cron/config(json, non-secret)/lastRun*; registered in models/index.js
- plugin_secrets.js: read/write/remove/mergeForRun over @simpleworkjs/bao-conf
- scheduler.js: schedules from the DB registry; per-instance stable BullMQ
JobScheduler ids (plugin:<id>) for load/unload; legacy migration from
conf.discovery.plugins on first boot (idempotent, empty-table-guarded)
- api_plugins.js (replaces routes/plugins.js): types/list/get/create/update/
secrets/test/load/unload/run/delete/runs; admin-gated; secrets always masked
- /plugins page (plugins.ejs) + nav; Agents & Scheduler tab removed from
/directory; /docs/agents aliased to /docs/plugins
- proxmox/unifi/nmap gained manifests (configSchema/validate/run alias)
- tests/plugins.test.js: registry unit + plugin_secrets (mocked bao-conf) +
PluginInstance model round-trip/unique-slug
- docs (plugins.md, vault.md, _config.yml, API.md) + 1.16.1 -> 1.17.0
Requires theta-suite >= v1.30.1 for the sso-broker secret/plugins/* grant;
fails-soft with a clear error if absent.
Co-Authored-By: Claude <noreply@anthropic.com>
Both view routes did server-side auth via req.user, but this app's auth-token is
a header set by client JS (localStorage), not a cookie — so req.user is
undefined on a browser navigation. permission.byGroup(undefined,...) throws
status 401, and the middleware.auth gate on /vault threw Auth.errors.login()
(401) for the same reason.
Both routes now render the shell unconditionally (like /users, /directory) and
gate client-side. conf.ejs already called app.auth.forceLogin; vault.ejs now
derives isAdmin + personal namespace from /api/user/me after forceLogin
instead of server-rendering them. /api/conf and /api/vault still enforce
app_sso_admin + OpenBao scope server-side — only the view-route gating moved
client-side where the session lives. Also removed a dead duplicate /conf route.
Co-authored-by: Claude <noreply@anthropic.com>
Closes the end-user half of the directory and adds nested LDAP groups.
The directory could describe the lab but could not tell anyone what they had
or how to reach it, and several of the paths meant to do so were silently
returning nothing:
- GET /api/discovery/me resolved groups from req.user.groups, which does not
exist (req.user carries memberOf), so it returned only isPublic resources
for every human caller -- "My Services" was blank for everyone. The same
read made isDirectoryAdmin() false for real admins.
- The portal's "Discover More Services" called the admin-gated endpoint and
swallowed the 403, so it never rendered for non-admins at all.
- Services reported no address, because /me had reimplemented getMyAccess
without its parent-walking resolution.
Adds the catalog at /, self-service access requests, and admin access
visibility (per-resource counts, and the reverse "what can this user reach").
Nested groups come in two halves. groupOfNames.member already accepts a group
DN, so nesting needs no schema -- what it needs is resolution, which no
released OpenLDAP performs. The all-in-one image therefore builds slapd from a
pinned master commit for the nestgroup overlay, and the app computes the
closure itself when pointed at a server without it. Both paths are covered.
member-values is deliberately left out of nestgroup-flags: it expands `member`
when reading a group, which destroys the distinction between "listed here" and
"reachable through a nested group" and is not recoverable afterwards.
Full suite green in both resolution modes: 215 passed, 2 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- app_super_admin is a new cross-app LDAP group (also recognized by proxy
and jump-host) that grants full admin here regardless of app_sso_admin
membership: bypassed centrally in utils/permission.js's byGroup, folded
into GET /api/user/me's isAdmin flag, and added to nav/forceLogin gates
alongside app_sso_admin.
- Renamed the Executive page to Overview (route, view, API path
/api/metrics/overview, nav label, docs), keeping /executive as a 301
redirect alongside the existing /admin, /notifications, /dashboard
legacy redirects.
- Directory modal's Associated LDAP Groups tab now lets you view/add/remove
members and owners of each associated group directly, reusing the same
PUT/DELETE group/:group/:uid routes and member-mapping pattern already
used on the Groups page -- no backend change needed.
- Groups page: the search/sort bar is now sticky, staying visible while
scrolling through a long group list. Introduces --sw-content-offset (set
in top.ejs alongside #spa-shell's margin-top) so an in-page sticky
element can offset itself below the fixed navbar/update-banner instead
of being hidden behind them at the viewport's true top:0.
- Directory table: Kind/Name/Env/Host merged into a single "Resource"
column, matching the same information more compactly.
- app-base.js (byte-identical across the 3 apps): added app.util.revealItem(),
which scrolls a just-added/-edited element into view and flashes its
background -- wired into the Directory table, the Groups tab's member
list, and the Groups page's create-group flow.
- Bumped @simpleworkjs/frontend to ^0.2.7 (published with the same
revealItem() addition for any future consumer of its app.js, even though
none of the 3 apps currently load that file directly -- they use the
legacy app-base.js instead).
Retires the static #secretModal/#editModal elements in favor of the
shared app.modal singleton, matching the pattern already shipped in
directory.ejs, proxy, and jump-host this round. Converts the
always-visible create-form card into a "+ New Token" button + modal,
switches badge classes from bg-* to text-bg-*, and replaces the
checkmark-flash copy feedback (broken by FontAwesome's <i>-to-<svg>
replacement) with toast-based copyFieldValue.
saveResource() called app.modal.close() then, after an intervening
await loadResources(), conditionally app.modal.open() to show a newly
created OAuth client's secret. app.modal is a singleton -- close()
immediately followed by open() in the same tick collides with Bootstrap's
hide-transition guard (show() silently no-ops while _isTransitioning is
still true from the just-started hide()). The await made this race
unlikely to lose in practice, but not guaranteed to -- found while fixing
the same bug (with no such await, so guaranteed to lose) in jump-host and
proxy's API-token create flows.
Now the resource-edit modal is only closed when we're NOT about to
immediately show the OAuth secret; app.modal.open() alone already
overwrites the (already-visible) modal's content in place, no close()
needed first.
loadLdapGroups()'s cache guard (if (ldapGroupsCache) return;) also skipped
the DOM-repopulation step on every call after the first, but
#ldap-groups-datalist is rebuilt fresh and empty on every app.modal.open()
-- so the "Associated LDAP Groups" tab's group-name autocomplete silently
lost all its suggestions starting on the second Add/Edit. Now the fetch
stays cached, but the datalist is always repopulated.
Verified live: opened the resource modal on Proxy twice in a row, confirmed
the datalist has all 17 options both times (would have been 0 on the
second open with the old code).
* Add Resource audit fields (created/updated by/on) and site-slug group prefixing
Resource had no created_by/created_on/updated_by/updated_on fields at all,
unlike proxy's Host and jump-host's ApiToken which already track this --
needed for the upcoming resource-modal footer. @simpleworkjs/orm has no
auto-timestamp hook, so these are set explicitly in the directory-admin
route handlers on every create/update.
Also: when a host/service resource is created, its two auto-created LDAP
groups (<slug>_access/_admin) now get prefixed with the nearest ancestor
site's slug (via a new Resource.findAncestorSiteSlug walk), so groups from
different sites don't collide/look identical. Falls back to today's
unprefixed naming when a resource has no site ancestor.
Included the checked-in dev inventory.sqlite's ALTER TABLE for the new
columns, since @simpleworkjs/orm's sync() only creates missing tables, never
alters existing ones -- the raw model change alone would have broken every
Resource read/write against this file with "no such column: created_by".
* Migrate Resource modal onto app.modal's tabs/footer/URL, add Children tab
The Directory's resource modal was a separate, hand-rolled, always-in-DOM
Bootstrap modal, independent of the shared app.modal singleton -- migrating
it onto app.modal (now published with tabs/footer/url support in
@simpleworkjs/frontend 0.2.6) is the pilot for standardizing entity modals
across the stack.
- General/Details/Associated LDAP Groups/Children tabs, replacing the old
single long form (Details keeps every kind-conditional container
unchanged; toggleFormFields() didn't need to change at all).
- Footer shows created/updated by/on (via the new Resource audit fields)
and the Save button; Groups/Children tabs are hidden in add-mode since
they need an existing resource id.
- New Children tab lists a resource's existing children (reusing the
already-loaded edges/resourcesById data, no new endpoint) and an "Add
Child Resource" button that reuses openAddModal's existing preset-parent
support. Folded the pre-existing generic "Relationships (Graph Edges)"
section in underneath, under an "advanced" subheading, rather than
dropping it or giving it a 5th tab of its own.
- GET /directory/:slug (mirroring the existing /users/:uid precedent) plus
a client-side app.modal.deepLinkSlug() check makes a resource's modal
linkable and directly loadable.
- Converted the groups/edges lists from jq-repeat to plain manual DOM
rendering: jq-repeat's MutationObserver-based scope (re)registration for
an element that's destroyed and recreated on every modal open runs
asynchronously, so populating synchronously right after open() (as
refreshGroupsUI/refreshEdgesUI must) raced it -- on the second and later
opens, the old scope's destroy() ran after the new data was pushed onto
it, silently discarding it. Manual rendering (matching the new Children
tab) sidesteps the race entirely.
- The #res-name/#res-kind auto-slug handler is now bound via
app.modal.on() (delegated) instead of directly -- a direct bind would
have silently stopped firing after the first Add/Edit, since the modal
body is rebuilt from scratch on every open().
Verified live against the running dev stack: tabs/footer/groups/children
all render and populate correctly (including on a second open, confirming
the jq-repeat race fix), the address bar updates to /directory/{slug} and
reverts on close, browser Back closes the modal via popstate without a
page reload, and a resource created under a Site gets correctly
site-slug-prefixed LDAP groups.
- Removed the list/tree view toggle -- tree (with indentation/parent
arrows) is always used. Simplifies renderTable() back down to one
code path instead of branching on a view mode nobody was toggling
away from in practice.
- Clicking a resource's name now opens the same modal the pencil/edit
button does, rather than requiring the small icon click. The edit
modal already surfaces full detail (parent, addresses, OAuth config,
groups, edges) for every resource kind, so this reuses it rather than
building a second, read-only view that would drift from the real one.
Verified live: tree view renders correctly with no toggle present, and
clicking a name (tested on the theta-proxy OAuth resource) opens the
detail modal with the correct parent already selected -- also
confirming the earlier "OAuth client has no parent" fix end-to-end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Landing here with ?redirect= and no explanation is exactly what happens
when another app's "Log in with SSO" sends an unauthenticated user
through /oauth/authorize, which bounces them here. Shows a contextual
banner: a specific message when the redirect target is an OAuth
authorize URL, a generic "you'll be sent back" message otherwise.
Verified live for both cases (OAuth-authorize redirect and a plain
redirect) against a local stack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
app_sso_service_account is a marker group: membership hides an account
from the Users page's People tab entirely (users.ejs filters it out),
which is exactly right for a non-person account but has no guardrail
against adding a real person by mistake -- which just happened in
production (see #113) and looked exactly like the account had vanished.
Adding a member to any other group via this dropdown is unchanged
(fires immediately, no confirmation); only app_sso_service_account now
asks first, via app.messages.confirm.
Verified live against a local stack: confirmation shows the right
warning, Cancel leaves the group untouched, Confirm adds the member
normally, and every other group's add-member flow is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Native confirm() dialogs block browser automation entirely (discovered
via a frozen tab while browser-testing the app.messages/app.modal
adoption), and native alert()/confirm() are visually inconsistent with
the rest of the UI. Replaced every call site with
app.messages.action/confirm/toast:
- directory.ejs: rotateSecret/deleteResource confirms and all inline
save/add/remove-group/edge error alerts now target #resourceModal's
actionMessage (or, for deleteResource — called from the outer table
row, not the modal — the page's own card).
- impersonate_modal.ejs, onboarding.ejs: no local .actionMessage target
exists on these pages, so their alerts became page-wide toasts.
- executive.ejs: two alerts in sendNotification's validation now use the
existing $compose target; saveTos's alert now reuses the function's
own msgEl inline-message element instead of introducing a second
mechanism.
- users.ejs, profile.ejs, proxy's profile.ejs: toggleActive's alert
(no row context available at the call site) became a toast;
revokeInvite/revokeToken/rotateToken use the row/card element already
in scope.
- app.js: removed app.user.remove and app.oauthClient.remove, which
contained native confirm() guards and had zero callers anywhere in the
app — dead code, deleted rather than converted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the vendored app.util.actionMessage/actionConfirm/alert (the
latter added ad hoc to fix "app.util.alert is not a function") with the
published @simpleworkjs/frontend package: app.messages.action/confirm,
app.modal.open, and app.validate.js (which also replaces the identical
vendored val.js). Gains real HTML-escaping on message content and a toast
fallback when there's no inline .actionMessage target, neither of which
the vendored code had.
app.api/app.auth/app.pubsub/app.socket in app-base.js are untouched —
they're app-specific (dual-mode callback/promise API, auth-token header
injection) and not something the generic frontend package's app.js
provides, so it isn't loaded here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- User.update/addSSHkey now ensure the ldapPublicKey objectClass is present
before writing sshPublicKey, so accounts predating that objectClass
(e.g. the bootstrap admin) no longer 500 on PUT /api/user/:uid.
- populateHostDropdown in directory.ejs was missing an `oauth` branch,
leaving the parent-Service picker blank when adding an OAuth Integration.
- Bump to 1.5.1.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).
Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
for each of the current user user/me groups. sso-manager-node sends LDAP
DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
undefined `callback2` reference that threw on a non-function callback.
Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.
sso-manager-node specifics:
- val.js adopts the shared superset (adds the target/hostname rules and
the password policy, and fixes the let-shadowed `message` that stopped
custom rule messages from reaching validateMessage).
- GET /api/user/me now also reports isAdmin (membership in app_sso_admin).
- public/js/app.js: $.isFunction -> typeof (removed in jQuery 4).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/directory.md existed but was orphaned: not in the /docs registry,
not linked anywhere. Now:
- registered as /docs/directory ("Directory & Inventory")
- help icon on the Directory page header links to it (same pattern as
users/groups/profile pages)
- linked from the docs site index feature list
- extended with the shared slug conventions (site_<name>, host_<hostname>),
the automatic registration story (theta-env stack seeding, ldap-client
Linux host enrollment), and the admin + read-only API surface (the
read-only graph routes live at /api/discovery, not /api/directory).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sites & Replication page (added in the prior multi-master LDAP
release) 500'd on every load: views/sites.ejs included nonexistent
partials 'header'/'footer' instead of this app's actual 'top'/'bottom'.
Fixed to match every other view.
Also refreshed all README screenshots against the current UI and added
a new Sites & Replication screenshot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add conf.ldap.ldapsHost / conf.ldap.ldapsPort so the /integrations page
can advertise an internal-only LDAPS hostname separate from the public
OAuth issuer. This avoids forcing admins to port-forward 636 publicly.
- routes/index.js derives LDAPS URL from ldapsHost/ldapsPort with issuer fallback
- integrations.ejs adds a contextual help panel explaining TLS hostname
validation, the public-issuer default, and recommended internal-DNS /
Docker-internal alternatives
- conf/base.js, secrets.js.example, DEPLOYMENT.md, docs/configuration.md,
and docs/ldap.md document and expose the new options
- Add tests/integrations.test.js for default and custom ldapsHost behavior
- Bump version to 1.1.17
Co-authored-by: Claude <noreply@anthropic.com>
- New docs/concepts-{accounts,oauth-apps,api-tokens}.md -- plain-language
guides aimed at less technical readers, each linking onward to the
existing schema/protocol-level doc for anyone who wants that detail.
Card help links (Users, Groups, OAuth cards, My groups, Members of
<uid>'s group) now point here instead of straight at the technical
docs; the LDAP-protocol-wiring cards (raw connection details for
connecting a 3rd-party app) stay pointed at the technical ldap.md,
since that's genuinely the right depth for that task.
- The "New API Token" card had no help link at all -- added, pointing to
the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
garbled heading + stray <hr> at the top: Jekyll front matter (meant
only for the GitHub Pages build) was never stripped before being
handed to the markdown renderer. Also fixed: cross-doc links
(ldap.html, index.html, etc.) never resolved in-app, since this
viewer serves docs at /docs/<slug> with no .html suffix -- rewritten
to the correct in-app URL, same idea as the existing image-path fix.
Bumps to v1.1.12.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The single header-wide help icon (added last release) pointed at a
per-page doc guess, but a page can have several cards covering different
topics (e.g. Integrations has both OAuth and LDAP cards). Removed it and
added a small help icon directly to each card that has real corresponding
doc content, linking straight to that doc -- Invite User/Add new
user/User List/Service Accounts (users.ejs), group cards (groups.ejs),
OAuth Apps + LDAP connection cards (integrations.ejs), My groups/Members
of <uid>'s group/New API Token (profile.ejs).
Bumps to v1.1.11.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C