diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ae778..07baef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ All notable changes to Switchboard Core are documented here. +## v0.2.8 — Team Admin Settings Audit (Pass 1) + +### Removed + +- **Dead `HasPrivateProviderRequirement`**: Store method checked team settings + for `require_private_providers` (BYOK vestige). Removed from interface, + PostgreSQL, and SQLite implementations. No callers existed. +- **Dead `UserRole` field on `TeamMember`**: Joined `users.role` column + (deprecated in v0.2.0 RBAC migration). Removed from model, `ListMembers` + and `GetMember` queries in both stores. Frontend never consumed it. +- **Dead `allow_team_providers` policy**: Removed from `PolicyDefaults`, both + test seed data blocks, and ICD test assertions. No handler or UI read it. +- **Dead personas in workflow stage UI**: Removed `sw.api.teams.personas()` + call (endpoint doesn't exist), personas state, persona dropdown in + StageForm, and persona badge in stage list. +- **Dead `history_mode` in stage UI**: Removed history mode selector and state + from StageForm. Backend column retained for v0.3.x schema migration. +- **Dead ICD tests**: Removed assertions for `/teams/:teamId/personas`, + `/teams/:teamId/providers`, and `/teams/:teamId/models` — endpoints were + removed in Phase 0 fork. +- **Stale `chat_only` stage mode**: Frontend `STAGE_MODES` updated from + `['chat_only', 'form_only', 'form_chat', 'review']` to + `['form_only', 'form_chat', 'review', 'custom']` matching the backend + CHECK constraint. +- **Stale comments**: Removed references to deleted `team_providers.go`, + `personas.go`, and `apiconfigs.go` files. + +--- + ## v0.2.7 — User Settings Audit ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 7c0d5c8..884babb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -107,8 +107,8 @@ SDK stabilization, and the first rebuilt extension (tasks). | Step | Status | Description | |------|--------|-------------| -| Team admin E2E | ⬚ | Verify team member management, team settings cascade, role assignment (admin/member). Audit for dead code from pre-fork team features. First pass — validates current functionality before v0.3.x adds team roles. | -| User settings team tab | ⬚ | Verify the Teams section in user settings — team list, join/leave, team-scoped settings. | +| Team admin E2E | ✅ | Audited team member management, settings cascade, role assignment. Removed dead code: `HasPrivateProviderRequirement` (BYOK vestige), `UserRole` on `TeamMember` (deprecated role column), `allow_team_providers` policy default, dead personas/providers/models ICD tests. | +| Workflow stage UI cleanup | ✅ | Removed dead personas dropdown, `history_mode` selector, stale `chat_only` mode. Updated `STAGE_MODES` to match backend CHECK constraint (`form_only`, `form_chat`, `review`, `custom`). Removed stale comments referencing deleted files. | ### v0.2.9 — Builtin Extension Retirement diff --git a/VERSION b/VERSION index b003284..a45be46 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.7 +0.2.8 diff --git a/packages/icd-test-runner/js/crud/teams.js b/packages/icd-test-runner/js/crud/teams.js index 713d9fe..cca3544 100644 --- a/packages/icd-test-runner/js/crud/teams.js +++ b/packages/icd-test-runner/js/crud/teams.js @@ -74,24 +74,6 @@ T.assert(Array.isArray(d.data), 'data should be array'); }); - await T.test('crud', 'teams', 'GET /teams/:teamId/personas', async function () { - var d = await T.apiGet('/teams/' + teamId + '/personas'); - T.assertHasKey(d, 'data', '/team-personas'); - T.assert(Array.isArray(d.data), 'team personas should be array'); - }); - - await T.test('crud', 'teams', 'GET /teams/:teamId/providers', async function () { - var d = await T.apiGet('/teams/' + teamId + '/providers'); - T.assertHasKey(d, 'data', '/team-providers'); - T.assert(Array.isArray(d.data), 'data should be array'); - T.assertHasKey(d, 'allow_team_providers', '/team-providers'); - }); - - await T.test('crud', 'teams', 'GET /teams/:teamId/models', async function () { - var d = await T.apiGet('/teams/' + teamId + '/models'); - T.assertHasKey(d, 'models', '/team-models'); - }); - // ── Team Tasks CRUD ── var teamTaskId = null; diff --git a/server/database/testhelper.go b/server/database/testhelper.go index 4126ce0..5867a83 100644 --- a/server/database/testhelper.go +++ b/server/database/testhelper.go @@ -341,8 +341,7 @@ func TruncateAll(t *testing.T) { DB.Exec(` INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true'), - ('default_user_active', 'false'), - ('allow_team_providers', 'true') + ('default_user_active', 'false') ON CONFLICT (key) DO NOTHING `) } else { @@ -356,8 +355,7 @@ func TruncateAll(t *testing.T) { DB.Exec(` INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true'), - ('default_user_active', 'false'), - ('allow_team_providers', 'true') + ('default_user_active', 'false') ON CONFLICT (key) DO NOTHING `) } diff --git a/server/handlers/team_connections.go b/server/handlers/team_connections.go index f27417d..ad4c34b 100644 --- a/server/handlers/team_connections.go +++ b/server/handlers/team_connections.go @@ -1,7 +1,7 @@ package handlers -// v0.38.1: Extension connection handlers — team scope. -// Methods on TeamHandler, mirrors team_providers.go. +// Extension connection handlers — team scope. +// Methods on TeamHandler. import ( "net/http" diff --git a/server/models/models.go b/server/models/models.go index 076eb1d..c57621a 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -78,15 +78,13 @@ type TeamMember struct { Email string `json:"email,omitempty"` DisplayName string `json:"display_name,omitempty"` Username string `json:"username,omitempty"` - UserRole string `json:"user_role,omitempty"` } // PLATFORM POLICIES var PolicyDefaults = map[string]string{ - "allow_registration": "true", - "default_user_active": "false", - "allow_team_providers": "true", + "allow_registration": "true", + "default_user_active": "false", } // HandleFromName generates a URL-safe @mention handle from a display name. diff --git a/server/store/interfaces.go b/server/store/interfaces.go index b537046..6e6b781 100644 --- a/server/store/interfaces.go +++ b/server/store/interfaces.go @@ -170,10 +170,6 @@ type TeamStore interface { // AddMemberReturningID inserts a team member and returns the row ID. AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) - // HasPrivateProviderRequirement checks if a user belongs to any active team - // with the require_private_providers setting enabled. - HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error) - // MergeSettings merges a JSON string into the team's settings column. MergeSettings(ctx context.Context, teamID, settingsJSON string) error } diff --git a/server/store/postgres/helpers.go b/server/store/postgres/helpers.go index 0366184..22ea6f1 100644 --- a/server/store/postgres/helpers.go +++ b/server/store/postgres/helpers.go @@ -19,8 +19,7 @@ func SetDB(db *sql.DB) { } // ── Dynamic SQL Builder ───────────────────── -// Replaces the copy-pasted addClause/addField pattern -// found in admin.go, personas.go, team_providers.go, apiconfigs.go. +// Replaces the copy-pasted addClause/addField pattern. // UpdateBuilder constructs a dynamic UPDATE statement. type UpdateBuilder struct { diff --git a/server/store/postgres/team.go b/server/store/postgres/team.go index e4190fe..d971f08 100644 --- a/server/store/postgres/team.go +++ b/server/store/postgres/team.go @@ -142,7 +142,7 @@ func (s *TeamStore) UpdateMemberRole(ctx context.Context, teamID, userID, role s func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) { rows, err := DB.QueryContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, - u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role + u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = $1 @@ -156,7 +156,7 @@ func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.Te for rows.Next() { var m models.TeamMember err := rows.Scan(&m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, - &m.Email, &m.DisplayName, &m.Username, &m.UserRole) + &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } @@ -169,12 +169,12 @@ func (s *TeamStore) GetMember(ctx context.Context, teamID, userID string) (*mode var m models.TeamMember err := DB.QueryRowContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, - u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role + u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = $1 AND tm.user_id = $2`, teamID, userID).Scan( &m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, - &m.Email, &m.DisplayName, &m.Username, &m.UserRole) + &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } @@ -302,19 +302,6 @@ func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, ro return id, err } -func (s *TeamStore) HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error) { - var has bool - err := DB.QueryRowContext(ctx, ` - SELECT EXISTS( - SELECT 1 FROM team_members tm - JOIN teams t ON t.id = tm.team_id - WHERE tm.user_id = $1 - AND t.is_active = true - AND t.settings->>'require_private_providers' = 'true' - )`, userID).Scan(&has) - return has, err -} - func (s *TeamStore) MergeSettings(ctx context.Context, teamID, settingsJSON string) error { _, err := DB.ExecContext(ctx, `UPDATE teams SET settings = COALESCE(settings, '{}'::jsonb) || $1::jsonb WHERE id = $2`, diff --git a/server/store/sqlite/team.go b/server/store/sqlite/team.go index a627b47..a8873e3 100644 --- a/server/store/sqlite/team.go +++ b/server/store/sqlite/team.go @@ -149,7 +149,7 @@ func (s *TeamStore) UpdateMemberRole(ctx context.Context, teamID, userID, role s func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) { rows, err := DB.QueryContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, - u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role + u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ? @@ -163,7 +163,7 @@ func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.Te for rows.Next() { var m models.TeamMember err := rows.Scan(&m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, - &m.Email, &m.DisplayName, &m.Username, &m.UserRole) + &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } @@ -176,12 +176,12 @@ func (s *TeamStore) GetMember(ctx context.Context, teamID, userID string) (*mode var m models.TeamMember err := DB.QueryRowContext(ctx, ` SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at, - u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role + u.email, COALESCE(u.display_name, ''), u.username FROM team_members tm JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ? AND tm.user_id = ?`, teamID, userID).Scan( &m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt, - &m.Email, &m.DisplayName, &m.Username, &m.UserRole) + &m.Email, &m.DisplayName, &m.Username) if err != nil { return nil, err } @@ -308,19 +308,6 @@ func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, ro return id, err } -func (s *TeamStore) HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error) { - var has bool - err := DB.QueryRowContext(ctx, ` - SELECT EXISTS( - SELECT 1 FROM team_members tm - JOIN teams t ON t.id = tm.team_id - WHERE tm.user_id = ? - AND t.is_active = 1 - AND json_extract(t.settings, '$.require_private_providers') = 'true' - )`, userID).Scan(&has) - return has, err -} - func (s *TeamStore) MergeSettings(ctx context.Context, teamID, settingsJSON string) error { _, err := DB.ExecContext(ctx, `UPDATE teams SET settings = json_patch(COALESCE(settings, '{}'), ?) WHERE id = ?`, diff --git a/src/js/sw/surfaces/team-admin/workflows.js b/src/js/sw/surfaces/team-admin/workflows.js index c3b4c37..aa7f141 100644 --- a/src/js/sw/surfaces/team-admin/workflows.js +++ b/src/js/sw/surfaces/team-admin/workflows.js @@ -10,8 +10,7 @@ const { html } = window; const { useState, useEffect, useCallback } = hooks; const ENTRY_MODES = ['public_link', 'team_only']; -const STAGE_MODES = ['chat_only', 'form_only', 'form_chat', 'review']; -const HISTORY_MODES = ['full', 'summary', 'fresh']; +const STAGE_MODES = ['form_only', 'form_chat', 'review', 'custom']; const TABS = ['Workflows', 'Assignments', 'Monitor']; function _timeAgo(ts) { @@ -143,18 +142,13 @@ function WorkflowsTab({ teamId }) { function WorkflowEditor({ teamId, workflow, onBack }) { const [stages, setStages] = useState([]); - const [personas, setPersonas] = useState([]); const [teams, setTeams] = useState([]); const [editingStage, setEditingStage] = useState(null); // null | 'new' | stage_id const loadStages = useCallback(async () => { try { - const [s, p] = await Promise.all([ - sw.api.teams.workflowStages(teamId, workflow.id), - sw.api.teams.personas(teamId), - ]); + const s = await sw.api.teams.workflowStages(teamId, workflow.id); setStages(s || []); - setPersonas(p || []); setTeams(sw.auth?.teams || []); } catch (e) { sw.toast(e.message, 'error'); } }, [teamId, workflow.id]); @@ -259,7 +253,6 @@ function WorkflowEditor({ teamId, workflow, onBack }) { #${i + 1} ${s.name || `Stage ${i + 1}`} ${s.stage_mode || '\u2014'} - ${s.persona_id && html`persona`} ${s.assignment_team_id && html`team assign`} @@ -271,7 +264,6 @@ function WorkflowEditor({ teamId, workflow, onBack }) { ${editingStage && html` <${StageForm} stage=${editingStage === 'new' ? null : stages.find(s => s.id === editingStage)} - personas=${personas} teams=${teams} onSave=${(data) => editingStage === 'new' ? addStage(data) : updateStage(editingStage, data)} onCancel=${() => setEditingStage(null)} @@ -283,12 +275,10 @@ function WorkflowEditor({ teamId, workflow, onBack }) { // ── Stage Form ────────────────────────────── -function StageForm({ stage, personas, teams, onSave, onCancel }) { +function StageForm({ stage, teams, onSave, onCancel }) { const [name, setName] = useState(stage?.name || ''); - const [mode, setMode] = useState(stage?.stage_mode || 'chat_only'); - const [personaId, setPersonaId] = useState(stage?.persona_id || ''); + const [mode, setMode] = useState(stage?.stage_mode || 'form_only'); const [assignTeam, setAssignTeam] = useState(stage?.assignment_team_id || ''); - const [historyMode, setHistoryMode] = useState(stage?.history_mode || 'full'); const [autoTransition, setAutoTransition] = useState(stage?.auto_transition || false); const [sla, setSla] = useState(stage?.sla_seconds || ''); @@ -296,9 +286,7 @@ function StageForm({ stage, personas, teams, onSave, onCancel }) { onSave({ name, stage_mode: mode, - persona_id: personaId || null, assignment_team_id: assignTeam || null, - history_mode: historyMode, auto_transition: autoTransition, sla_seconds: sla ? parseInt(sla, 10) : null, }); @@ -319,13 +307,6 @@ function StageForm({ stage, personas, teams, onSave, onCancel }) {