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).
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>
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>
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>