Changeset 0.37.7 (#219)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 18:37:39 +00:00
committed by xcaliber
parent 5f1c733002
commit b6152fbf5e
50 changed files with 2105 additions and 8989 deletions

View File

@@ -119,11 +119,17 @@ type SettingsPageData struct {
// ── Loader registration ──────────────────────
// TeamAdminPageData is what the team-admin surface receives.
type TeamAdminPageData struct {
Section string `json:"section"`
}
func (e *Engine) registerLoaders() {
e.RegisterLoader("admin", e.adminLoader)
e.RegisterLoader("chat", e.chatLoader)
e.RegisterLoader("notes", e.notesLoader)
e.RegisterLoader("notes", e.notesLoader)
e.RegisterLoader("settings", e.settingsLoader)
e.RegisterLoader("team-admin", e.teamAdminLoader)
}
// ListDataProviders returns the keys of all registered data providers.
@@ -396,6 +402,14 @@ func (e *Engine) notesLoader(c *gin.Context, s store.Stores) (any, error) {
// v0.22.7: Reads feature gates from GlobalConfig to control
// which nav links/tabs are visible (BYOK, User Personas).
func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
section := c.Param("section")
if section == "" {
section = "members"
}
return &TeamAdminPageData{Section: section}, nil
}
func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
section := c.Param("section")
if section == "" {

View File

@@ -217,6 +217,12 @@ func (e *Engine) registerCoreSurfaces() {
DataRequires: []string{"settings"},
Layout: "single", Source: "core",
},
{
ID: "team-admin", Route: "/team-admin/:section", AltRoutes: []string{"/team-admin"},
Title: "Team Admin", Template: "surface-team-admin", Auth: "authenticated",
DataRequires: []string{"team-admin"},
Layout: "single", Source: "core",
},
{
ID: "workflow", Route: "/w/:id",
Title: "Workflow", Template: "workflow", Auth: "session",
@@ -337,6 +343,9 @@ func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc {
if section == "" && surfaceID == "settings" {
section = "general"
}
if section == "" && surfaceID == "team-admin" {
section = "members"
}
e.Render(c, "base.html", PageData{
Surface: surfaceID,

View File

@@ -64,6 +64,7 @@
<div class="surface-inner" id="surfaceInner">
{{if eq .Surface "chat"}}{{template "surface-chat" .}}
{{else if eq .Surface "admin"}}{{template "surface-admin" .}}
{{else if eq .Surface "team-admin"}}{{template "surface-team-admin" .}}
{{else if eq .Surface "notes"}}{{template "surface-notes" .}}
{{else if eq .Surface "settings"}}{{template "surface-settings" .}}
{{else if and .Manifest (eq .Manifest.Source "extension")}}{{template "surface-extension" .}}
@@ -140,6 +141,7 @@
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
{{if eq .Surface "admin"}}{{template "scripts-admin" .}}{{end}}
{{if eq .Surface "team-admin"}}{{template "scripts-team-admin" .}}{{end}}
{{if eq .Surface "notes"}}{{template "scripts-notes" .}}{{end}}
{{if eq .Surface "settings"}}{{template "scripts-settings" .}}{{end}}
{{/* v0.27.0: Extension surface JS — loaded from /surfaces/{id}/js/main.js */}}

View File

@@ -10,135 +10,7 @@
<div class="settings-placeholder" style="padding:40px;text-align:center;">Loading admin&hellip;</div>
</div>
{{/* Modals — kept outside mount for legacy compat during transition. */}}
{{/* These will be removed once all modal usage is confirmed replaced by Dialog primitives. */}}
{{/* Create User modal */}}
<div id="createUserModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create User</h2>
<button class="modal-close" onclick="sb.call('closeModal','createUserModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Username</label><input type="text" id="adminNewUsername" placeholder="username" autocomplete="off"></div>
<div class="form-group"><label>Email</label><input type="text" id="adminNewEmail" placeholder="email@example.com" autocomplete="off"></div>
<div class="form-group"><label>Password</label><input type="password" id="adminNewPassword" placeholder="min 8 chars" autocomplete="new-password"></div>
<div class="form-group"><label>Role</label><select id="adminNewRole"><option value="user">User</option><option value="admin">Admin</option></select></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createUserModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateUserBtn">Create User</button>
</div>
</div>
</div>
{{/* Reset Password modal */}}
<div id="resetPwModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Reset Password</h2>
<button class="modal-close" onclick="sb.call('closeModal','resetPwModal')">&#10005;</button>
</div>
<div class="modal-body">
<p id="resetPwUser" style="font-weight:600;margin-bottom:8px;"></p>
<div class="admin-warning" style="background:var(--danger-dim);border:1px solid var(--danger);border-radius:var(--radius);padding:10px 12px;font-size:12px;margin-bottom:12px;color:var(--danger);">
<strong>Warning:</strong> This will destroy the user's personal vault. All personal API keys (BYOK) will be permanently deleted.
</div>
<div class="form-group"><label>New Password</label><input type="password" id="resetPwInput" placeholder="min 8 chars" autocomplete="new-password"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','resetPwModal')">Cancel</button>
<button class="btn-md btn-danger" id="resetPwConfirmBtn">Reset Password & Destroy Vault</button>
</div>
</div>
</div>
{{/* Approve User modal */}}
<div id="approveUserModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2 id="approveUserTitle">Approve User</h2>
<button class="modal-close" onclick="sb.call('closeModal','approveUserModal')">&#10005;</button>
</div>
<div class="modal-body">
<input type="hidden" id="approveUserId">
<div id="approveTeamsList"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','approveUserModal')">Cancel</button>
<button class="btn-md btn-primary" id="approveUserSubmitBtn">Activate User</button>
</div>
</div>
</div>
{{/* Create Team modal */}}
<div id="createTeamModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create Team</h2>
<button class="modal-close" onclick="sb.call('closeModal','createTeamModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Name</label><input type="text" id="adminTeamName" placeholder="Team name"></div>
<div class="form-group"><label>Description</label><input type="text" id="adminTeamDesc" placeholder="Optional description"></div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createTeamModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateTeamBtn">Create Team</button>
</div>
</div>
</div>
{{/* Create Group modal */}}
<div id="createGroupModal" class="modal-overlay">
<div class="modal" style="max-width:440px;">
<div class="modal-header">
<h2>Create Group</h2>
<button class="modal-close" onclick="sb.call('closeModal','createGroupModal')">&#10005;</button>
</div>
<div class="modal-body">
<div class="form-group"><label>Name</label><input type="text" id="adminGroupName" placeholder="Group name"></div>
<div class="form-group"><label>Description</label><input type="text" id="adminGroupDesc" placeholder="Optional description"></div>
<div style="display:flex;gap:12px;">
<div class="form-group" style="flex:1"><label>Scope</label><select id="adminGroupScope"><option value="global">Global</option><option value="team">Team</option></select></div>
<div class="form-group" id="adminGroupTeamRow" style="flex:1;display:none"><label>Team</label><select id="adminGroupTeam"></select></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','createGroupModal')">Cancel</button>
<button class="btn-md btn-primary" id="adminCreateGroupBtn">Create Group</button>
</div>
</div>
</div>
{{/* Provider form modal */}}
<div id="providerFormModal" class="modal-overlay">
<div class="modal" style="max-width:520px;">
<div class="modal-header">
<h2 id="providerFormTitle">Add Provider</h2>
<button class="modal-close" onclick="sb.call('closeModal','providerFormModal')">&#10005;</button>
</div>
<div class="modal-body" id="adminAddProviderForm"></div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','providerFormModal')">Cancel</button>
</div>
</div>
</div>
{{/* Persona form modal */}}
<div id="personaFormModal" class="modal-overlay">
<div class="modal" style="max-width:520px;">
<div class="modal-header">
<h2 id="personaFormTitle">Create Persona</h2>
<button class="modal-close" onclick="sb.call('closeModal','personaFormModal')">&#10005;</button>
</div>
<div class="modal-body" id="adminAddPersonaForm"></div>
<div class="modal-footer">
<button class="btn-md btn-ghost" onclick="sb.call('closeModal','personaFormModal')">Cancel</button>
</div>
</div>
</div>
{{/* Legacy modals removed in v0.37.7 — all handled by Dialog primitives in Preact components */}}
{{end}}
{{/* CSS: loaded via base.html (variables, layout, primitives, modals, chat, panels, surfaces, splash) */}}

View File

@@ -354,112 +354,7 @@ window.addEventListener('unhandledrejection', function(e) {
{{/* ── Hidden File Input ─────── */}}
<input type="file" id="fileInput" multiple style="display:none;">
{{/* ── Team Admin Modal ────────────────────── */}}
<div id="teamAdminModal" class="modal-overlay">
<div class="modal" style="max-width:900px;height:80vh;display:flex;flex-direction:column;">
<div class="modal-header" style="flex-shrink:0;">
<h2 id="teamAdminTitle">Team Admin</h2>
<button id="teamAdminCloseBtn" class="modal-close"></button>
</div>
<div class="modal-body" style="display:flex;flex:1;min-height:0;padding:0;">
{{/* Team picker (multi-team admins) */}}
<div id="teamAdminPicker" style="padding:20px;flex:1;overflow-y:auto;">
<div id="teamAdminPickerList"></div>
</div>
{{/* Tabbed team management */}}
<div id="teamAdminContent" class="team-admin-content" style="display:none;">
{{/* Tab bar */}}
<div id="teamAdminTabs" style="display:flex;gap:2px;padding:8px 16px;border-bottom:1px solid var(--border);flex-shrink:0;overflow-x:auto;">
<button class="admin-tab active" data-ttab="members" onclick="sb.call('UI.switchTeamTab','members')">Members</button>
<button class="admin-tab" data-ttab="providers" onclick="sb.call('UI.switchTeamTab','providers')">Providers</button>
<button class="admin-tab" data-ttab="personas" onclick="sb.call('UI.switchTeamTab','personas')">Personas</button>
<button class="admin-tab" data-ttab="knowledge" onclick="sb.call('UI.switchTeamTab','knowledge')">Knowledge</button>
<button class="admin-tab" data-ttab="groups" onclick="sb.call('UI.switchTeamTab','groups')">Groups</button>
<button class="admin-tab" data-ttab="settings" onclick="sb.call('UI.switchTeamTab','settings')">Settings</button>
<button class="admin-tab" data-ttab="usage" onclick="sb.call('UI.switchTeamTab','usage')">Usage</button>
<button class="admin-tab" data-ttab="activity" onclick="sb.call('UI.switchTeamTab','activity')">Activity</button>
</div>
{{/* Tab content panels */}}
<div style="flex:1;overflow-y:auto;padding:16px;">
{{/* Members */}}
<div id="teamTabMembers" class="team-tab-content">
<div id="settingsTeamMembers"></div>
<div id="settingsTeamAddMember" class="admin-inline-form" style="display:none;margin-top:12px;">
<div style="display:flex;gap:8px;align-items:flex-end;">
<div class="form-group" style="flex:1"><label>User</label><select id="settingsTeamMemberUser"><option value="">Select user...</option></select></div>
<div class="form-group"><label>Role</label><select id="settingsTeamMemberRole"><option value="member">Member</option><option value="admin">Admin</option></select></div>
<button class="btn-small btn-primary" id="settingsTeamAddMemberSubmit">Add</button>
<button class="btn-small" onclick="document.getElementById('settingsTeamAddMember').style.display='none'">Cancel</button>
</div>
</div>
<button class="btn-small" id="settingsTeamAddMemberBtn" style="margin-top:8px">+ Add Member</button>
</div>
{{/* Providers */}}
<div id="teamTabProviders" class="team-tab-content" style="display:none;">
<div id="settingsTeamProviders"></div>
<div id="settingsTeamProviderForm" style="display:none;margin-top:12px;"></div>
<button class="btn-small" id="settingsTeamAddProviderBtn" style="margin-top:8px">+ Add Provider</button>
</div>
{{/* Personas */}}
<div id="teamTabPersonas" class="team-tab-content" style="display:none;">
<div id="teamPersonaList"></div>
<div id="settingsTeamAddPersona" style="display:none;margin-top:12px;"></div>
<div id="settingsTeamPersonas" style="display:none;"></div>
<div style="display:none;"><select id="teamPersona_model"></select></div>
<button class="btn-small" id="settingsTeamAddPersonaBtn" style="margin-top:8px">+ New Persona</button>
</div>
{{/* Knowledge */}}
<div id="teamTabKnowledge" class="team-tab-content" style="display:none;">
<div id="teamKBContent"></div>
</div>
{{/* Groups */}}
<div id="teamTabGroups" class="team-tab-content" style="display:none;">
<div id="settingsTeamGroups"></div>
</div>
{{/* Settings */}}
<div id="teamTabSettings" class="team-tab-content" style="display:none;">
<div style="max-width:500px;">
<div class="form-group" style="margin-bottom:12px;">
<label class="toggle-label"><input type="checkbox" id="teamSettingPrivatePolicy"><span class="toggle-track"></span><span>Require private providers (BYOK)</span></label>
<div class="toggle-hint">Team members must use their own API keys</div>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label class="toggle-label"><input type="checkbox" id="teamSettingAllowProviders" checked><span class="toggle-track"></span><span>Allow team providers</span></label>
<div class="toggle-hint">Team admins can add shared provider configs</div>
</div>
<button class="btn-small btn-primary" id="teamSettingSaveBtn">Save Settings</button>
</div>
</div>
{{/* Usage */}}
<div id="teamTabUsage" class="team-tab-content" style="display:none;">
<div id="settingsTeamUsageTotals"></div>
</div>
{{/* Activity (Audit) */}}
<div id="teamTabActivity" class="team-tab-content" style="display:none;">
<div style="margin-bottom:8px;">
<select id="teamAuditFilterAction" style="font-size:12px;padding:4px 8px;"><option value="">All actions</option></select>
</div>
<div id="settingsTeamAudit"></div>
<div id="teamAuditPagination" style="display:flex;gap:8px;align-items:center;margin-top:8px;">
<button id="teamAuditPrevBtn" class="btn-small" disabled>← Prev</button>
<span id="teamAuditPageInfo" class="text-muted" style="font-size:12px;"></span>
<button id="teamAuditNextBtn" class="btn-small">Next →</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{/* Team Admin Modal — removed in v0.37.7. Team admin is now at /team-admin. */}}
{{/* ── Debug Modal moved to base.html (available on all surfaces) ── */}}
@@ -506,24 +401,20 @@ window.addEventListener('unhandledrejection', function(e) {
{{/* App JS — order matches index.html (minus api/events/ui-primitives/ui-format already in base) */}}
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/extensions.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/panels.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-settings.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-admin.js?v={{$.Version}}"></script>
{{/* ui-settings.js and ui-admin.js removed in v0.37.7 — team admin is now at /team-admin */}}
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/tokens.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notes.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/files.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/tools-toggle.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/knowledge-ui.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/memory-ui.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/persona-kb.js?v={{$.Version}}"></script>
{{/* memory-ui.js and persona-kb.js removed in v0.37.7 — no chat surface callers */}}
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/projects-ui.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/channel-models.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notification-prefs.js?v={{$.Version}}"></script>
{{/* notification-prefs.js removed in v0.37.7 — now native Preact in settings */}}
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/notifications.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/chat.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/settings-handlers.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/admin-handlers.js?v={{$.Version}}"></script>
{{/* settings-handlers.js, admin-handlers.js, workflow-admin.js removed in v0.37.7 */}}
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-api.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-admin.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-queue.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/task-sidebar.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/app.js?v={{$.Version}}"></script>

View File

@@ -1,7 +1,6 @@
{{/*
Settings surface — v0.37.5: Preact component tree.
Replaces server-rendered DOM with a mount point.
Bridge sections (tasks, gitkeys, workflows, data) still load old JS.
Settings surface — v0.37.7: Full Preact component tree.
All sections are native Preact components. No bridges.
*/}}
{{define "surface-settings"}}
@@ -21,16 +20,7 @@
window.__SECTION__ = '{{.Section}}';
</script>
{{/* Bridge section JS — deferred sections still use old imperative code */}}
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/task-settings.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/git-credentials-ui.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/persona-kb.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/memory-ui.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notifications.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notification-prefs.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/channel-models.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/data-portability.js?v={{.Version}}"></script>
{{/* Bridge section scripts removed in v0.37.7 — all sections are native Preact */}}
{{/* Vendor: Preact + htm → globals, then SDK boot, then settings surface */}}
<script type="module" nonce="{{.CSPNonce}}">

View File

@@ -0,0 +1,34 @@
{{/*
Team Admin surface — v0.37.7: Native Preact component tree.
10 sections: members, providers, personas, knowledge, groups,
workflows, tasks, settings, usage, activity.
*/}}
{{define "surface-team-admin"}}
<div id="team-admin-mount" class="surface-team-admin" style="flex-direction:column;">
<div class="settings-placeholder" style="padding:40px;text-align:center;">Loading team admin&hellip;</div>
</div>
{{end}}
{{define "css-team-admin"}}{{end}}
{{define "scripts-team-admin"}}
<script nonce="{{.CSPNonce}}">
window.__SECTION__ = '{{.Section}}';
</script>
<script type="module" nonce="{{.CSPNonce}}">
const { h, render } = await import('{{.BasePath}}/js/sw/vendor/preact.module.js');
const hooksModule = await import('{{.BasePath}}/js/sw/vendor/hooks.module.js');
const { default: htm } = await import('{{.BasePath}}/js/sw/vendor/htm.module.js');
const html = htm.bind(h);
window.preact = window.preact || { h, render };
window.hooks = window.hooks || hooksModule;
window.html = window.html || html;
const { boot } = await import('{{.BasePath}}/js/sw/sdk/index.js?v={{.Version}}');
await boot();
await import('{{.BasePath}}/js/sw/surfaces/team-admin/index.js?v={{.Version}}');
</script>
{{end}}