Skip to content

Add profile management API and CLI (list/get/create/edit/remove/rename)#38

Open
struanb wants to merge 9 commits intomainfrom
claude/plan-vue-admin-interface-9V19s
Open

Add profile management API and CLI (list/get/create/edit/remove/rename)#38
struanb wants to merge 9 commits intomainfrom
claude/plan-vue-admin-interface-9V19s

Conversation

@struanb
Copy link
Copy Markdown
Contributor

@struanb struanb commented Mar 27, 2026

Introduces a new Profile::Manage module (mirroring User::Manage) that
exposes full CRUD plus rename operations for profile files. Key points:

  • New manageProfiles permission gates all operations
  • Profile ID is the filename stem; profile JSON 'name' field is the
    human-readable display name (decoupled from the ID)
  • All operations work on every file in profiles/*.json, including
    inactive (active: false) ones
  • Active profiles are validated via Profile->validate() before write;
    inactive profiles are accepted as-is (drafts)
  • rename() uses OS rename(2) for atomicity; cache is explicitly
    invalidated so Data::load picks up renames and deletes where mtime
    of remaining files would otherwise be unchanged
  • Data::invalidate_profile_cache() exported for this purpose

Backend changes:
app/server/lib/Profile/Manage.pm (new)
app/server/lib/Data.pm invalidate_profile_cache export
app/server/lib/User.pm manageProfiles permission + import
app/server/lib/App.pm 6 new /profiles/* route handlers

CLI changes:
cli/dockside_cli.py api_profile_, cmd_profile_,
_add_profile_fields, profile subcommand

https://claude.ai/code/session_01H8i2RbXH3JTLacSF2Y8dja

claude and others added 9 commits March 22, 2026 00:14
Introduces a new Profile::Manage module (mirroring User::Manage) that
exposes full CRUD plus rename operations for profile files.  Key points:

- New manageProfiles permission gates all operations
- Profile ID is the filename stem; profile JSON 'name' field is the
  human-readable display name (decoupled from the ID)
- All operations work on every file in profiles/*.json, including
  inactive (active: false) ones
- Active profiles are validated via Profile->validate() before write;
  inactive profiles are accepted as-is (drafts)
- rename() uses OS rename(2) for atomicity; cache is explicitly
  invalidated so Data::load picks up renames and deletes where mtime
  of remaining files would otherwise be unchanged
- Data::invalidate_profile_cache() exported for this purpose

Backend changes:
  app/server/lib/Profile/Manage.pm  (new)
  app/server/lib/Data.pm            invalidate_profile_cache export
  app/server/lib/User.pm            manageProfiles permission + import
  app/server/lib/App.pm             6 new /profiles/* route handlers

CLI changes:
  cli/dockside_cli.py               api_profile_*, cmd_profile_*,
                                    _add_profile_fields, profile subcommand

https://claude.ai/code/session_01H8i2RbXH3JTLacSF2Y8dja
Adds a full admin UI at /admin (users, roles, profiles) and a self-service
account editor at /account, accessible from the top navbar.

Frontend:
- AdminSidebar: sectioned vertical nav (USERS / ROLES / PROFILES) mirroring
  the devtainer sidebar pattern; collapses per section; active/inactive dot
  for profiles
- AdminMain: renders UserDetail / RoleDetail / ProfileDetail based on route
- UserDetail: name, email, role select, password, permissions (tri-state
  ValueTag), resources (allow/deny tag rows), gh_token reveal, SshEditor;
  selfEdit=true mode restricted to personal fields only
- RoleDetail: name, permissions (on/off ValueTag), resources; delete disabled
  if role assigned to any user
- ProfileDetail: structured fields (id, name, description, active toggle,
  version) + json-editor-vue for the full profile body; inline rename (only
  when no unsaved edits)
- PermissionsEditor: data-driven from schemas/admin.js; groups permissions;
  shows inherited-from-role hint for users; tri-state (inherit/grant/deny)
  for users, bi-state (on/off) for roles
- ResourcesEditor: per-resource-type tag rows with allow/deny ValueTag;
  add-new inline input; serialises to array or object form
- SshEditor: publicKeys textarea + keypairs mini-UI (add modal, delete);
  private key never shown after entry
- ValueTag: new tri-state chip (grey=inherited, green=granted, red=denied)
- JsonEditor: thin wrapper around json-editor-vue (same package for Vue 2+3;
  Vue 3 migration = 2-line change in this file only)
- ConfirmModal: thin b-modal wrapper for delete confirmations
- Vuex admin module (namespaced) with users/roles/profiles state and full
  CRUD actions
- schemas/admin.js: data-only permission/resource definitions; adding a new
  permission = one line here, no template changes needed
- services/admin.js: axios API service; POST for user/profile create+update
- Routes: /admin, /admin/:type, /admin/:type/:id, /account
- Header: Admin link (manageUsers|manageProfiles only) + username link
- webpack.common.js: mainFields + babel transpile for json-editor-vue deps

Backend (App.pm, User/Manage.pm, User.pm):
- POST body reading support via has_request_body() callback dispatch;
  get_args() merges JSON body with query string (query string wins)
- /users/me and /users/me/update routes for self-service (no manageUsers
  permission required; whitelist: name, email, gh_token, ssh only)
- SPA HTML now served for /admin/* and /account routes
- updateSelf() in User::Manage whitelists personal fields only

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
…ltips

- Use allowInherit=true on resource ValueTags so clicking cycles
  green (allowed) → red (denied) → removed, instead of skipping denied
- Remove @blur="cancelAdd" so typing a new resource value is not lost
  when the user clicks elsewhere; Enter commits, Escape cancels
- Add nullLabel prop to ValueTag for context-sensitive absent tooltips;
  use "remove" for resources and improve all three state tooltip texts
- Update legend text to accurately describe the three-state cycling

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
…on; profile JSON view

- New ResourceTagsInput.vue: vue-tags-input-based component replacing the
  ValueTag+plain-input approach. Features:
  - Autocomplete from server-supplied runtimes, networks, IDEs, profiles, authModes
  - 'value:disabled' convention in autocomplete to deny a specific resource value
  - Green tags = allowed, red tags = denied, grey = plain (images)
  - Enter key intercepted at wrapper level to prevent accidental form submission
  - Value prop drives display (controlled); local tags updated immediately on change

- ResourcesEditor.vue rewritten to use ResourceTagsInput per resource row.
  Reads hostResources and profiles from Vuex store for autosuggestion lists.

- schemas/admin.js: added allowDeny flag to RESOURCES (false for images).

- store/admin.js: added hostResources state, setHostResources mutation, and
  fetchResources action (non-fatal); included in fetchAll.

- services/admin.js: added getResources() calling GET /resources.

- App.pm: added GET /resources endpoint returning runtimes, networks, IDEs,
  and static authModes from HOSTINFO and Containers; added $HOSTINFO/$HOSTNAME
  to Data imports and added use Containers.

- JsonEditor.vue: added readonly prop; passes readOnly to json-editor-vue and
  hides the mode switcher when readonly.

- ProfileDetail.vue: show profile JSON as a read-only tree when in view mode
  (was previously just a hint saying "switch to Edit mode").

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
ResourceTagsInput.vue:
- Autocomplete opens on focus (autocomplete-min-length=0)
- Click a tag to toggle allowed↔denied (green✓↔red✗); event delegation
  on the wrapper captures tag-text clicks, excluding the × close button
- ✓/✗ indicators on allowed/denied tags via CSS ::after on tag text div
- Updated placeholder: 'Type to add · value:disabled to deny · * to allow all'

ResourcesEditor.vue:
- * prepended to every resource type's suggestion list so "allow all"
  is always the first autocomplete option

UserDetail.vue:
- gh_token is now write-once: when the server returns '<redacted>' the
  field shows a locked "Token set" badge and is not editable
- In save payload, gh_token is omitted when blank (not being changed),
  preserving the existing server-side token without sending '<redacted>'

ProfileDetail.vue:
- New profiles are pre-populated with PROFILE_TEMPLATE_BODY (version 4),
  listing every top-level property with empty defaults so users can see
  what the schema supports

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
- admin.js: switch createRole/updateRole from GET+params to POST+JSON to
  avoid bracket-notation serialisation of nested permission objects
- App.pm: use get_args() for role create/update handlers so POST JSON body
  is read; add /resources endpoint returning runtimes, networks, IDEs,
  authModes from host introspection
- User/Manage.pm: guard _decode_value against Perl refs (prevents
  'length called on a reference' warnings when POST JSON decodes nested
  data structures as native hashrefs/arrayrefs)
- UserDetail.vue: default new-user resources to ["*"] for all resource
  types; block save when role field is empty with a clear error message

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
- store/index.js + App.pm: add reactive profiles state to main store
  (init from window.dockside.profiles); add fetchProfiles action calling
  new /profiles/mine endpoint which returns the calling user's accessible
  profiles in the same dict format as the page bootstrap
- Container.vue: use store profiles (reactive) instead of static
  window.dockside.profiles; dispatch fetchProfiles on prelaunch so
  navigating to /container/new always shows current profile list
- store/admin.js: after createProfile/updateProfile/removeProfile/
  renameProfile dispatch root fetchProfiles to keep the launch page
  in sync immediately after admin changes
- Header.vue: show Launch and Docs navbar items in /admin routes
  (removed !isAdminRoute guard)
- ResourceTagsInput.vue: fix double ✓/✗ indicators (> div::after hit
  both .ti-content and .ti-actions divs); switch to
  .ti-tag-center > span::before for single left-side indicator;
  add subtle border-left + tinted background on .ti-actions to
  visually separate the dismiss × from the tag text; update
  handleTagAreaClick to read text from .ti-tag-center > span
- UserDetail.vue: gh_token is now editable in edit/new mode even when
  a token is already set; placeholder changes to "Enter new token to
  replace existing, or leave blank to keep"; locked badge only shown
  in view mode when token is set

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
…v link

- App.pm: include name and email in window.dockside.user bootstrap via
  $User->details() so they're available client-side without an extra request
- Header.vue: add displayName computed — returns first word of name (first
  name for multi-word, or the only name as surname fallback), then
  obfuscated email (first 1-3 chars + … + @Domain), then username as final
  fallback; tooltip still shows full username

https://claude.ai/code/session_011NEhVStYQAP6JzmTKWUCV7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants