feat(desktop): implement workspace foundation + desktop-client skeleton
Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant, thread workspace_id through tenant monitoring quota. Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/ preload/renderer, shared Vue component package (packages/ui-shared), and server surface — desktop client registration + token rotation + heartbeat, SSE task event stream, desktop accounts/tasks/content handlers, publish job endpoint, and supporting repositories, services, sqlc queries, and migrations. Hard cutover per plan: remove browser-extension monitoring callback endpoints, stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
type seedState struct {
|
||||
TenantID int64
|
||||
UserID int64
|
||||
WorkspaceID int64
|
||||
TestTenantID int64
|
||||
TestUserID int64
|
||||
PlanID int64
|
||||
@@ -180,6 +181,13 @@ func seedBusinessData(ctx context.Context, pool *pgxpool.Pool, membershipCfg con
|
||||
if err := ensureMembership(ctx, tx, state.TenantID, state.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.WorkspaceID, err = ensureDefaultWorkspace(ctx, tx, state.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensurePrimaryWorkspaceMembership(ctx, tx, state.WorkspaceID, state.TenantID, state.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.PlanID, err = ensurePlan(ctx, tx, defaultPlan.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -355,6 +363,43 @@ func ensureMembershipWithRole(ctx context.Context, tx pgx.Tx, tenantID, userID i
|
||||
return wrapSeedErr("insert membership", err)
|
||||
}
|
||||
|
||||
func ensureDefaultWorkspace(ctx context.Context, tx pgx.Tx, tenantID int64) (int64, error) {
|
||||
var workspaceID int64
|
||||
err := tx.QueryRow(ctx, `
|
||||
INSERT INTO workspaces (tenant_id, name, slug, is_default)
|
||||
VALUES ($1, 'Default', 'default', TRUE)
|
||||
ON CONFLICT (tenant_id, slug) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
is_default = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING id
|
||||
`, tenantID).Scan(&workspaceID)
|
||||
return workspaceID, wrapSeedErr("insert workspace", err)
|
||||
}
|
||||
|
||||
func ensurePrimaryWorkspaceMembership(ctx context.Context, tx pgx.Tx, workspaceID, tenantID, userID int64) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE workspace_memberships
|
||||
SET is_primary = FALSE
|
||||
WHERE user_id = $1
|
||||
AND tenant_id = $2
|
||||
AND is_primary = TRUE
|
||||
AND workspace_id <> $3
|
||||
`, userID, tenantID, workspaceID); err != nil {
|
||||
return wrapSeedErr("clear primary workspace", err)
|
||||
}
|
||||
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO workspace_memberships (workspace_id, user_id, tenant_id, role, is_primary)
|
||||
VALUES ($1, $2, $3, 'member', TRUE)
|
||||
ON CONFLICT (workspace_id, user_id) DO UPDATE
|
||||
SET tenant_id = EXCLUDED.tenant_id,
|
||||
role = EXCLUDED.role,
|
||||
is_primary = TRUE
|
||||
`, workspaceID, userID, tenantID)
|
||||
return wrapSeedErr("insert workspace membership", err)
|
||||
}
|
||||
|
||||
func ensureSecondaryTenantUser(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
@@ -379,6 +424,13 @@ func ensureSecondaryTenantUser(
|
||||
if err := ensureMembershipWithRole(ctx, tx, tenantID, userID, "tenant_admin"); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
workspaceID, err := ensureDefaultWorkspace(ctx, tx, tenantID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if err := ensurePrimaryWorkspaceMembership(ctx, tx, workspaceID, tenantID, userID); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if err := ensureSubscription(ctx, tx, tenantID, planID, defaultPlan.SubscriptionDuration); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -636,12 +688,13 @@ func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) err
|
||||
enabledPlatforms, _ := json.Marshal([]string{"deepseek", "qwen", "doubao"})
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO tenant_monitoring_quotas (
|
||||
tenant_id, max_brands, collection_mode, question_selection_mode, collect_frequency,
|
||||
tenant_id, workspace_id, max_brands, collection_mode, question_selection_mode, collect_frequency,
|
||||
enabled_platforms, primary_installation_id, task_daily_hard_cap, plan_tier
|
||||
)
|
||||
VALUES ($1, 1, 'plugin', 'best_effort_all', 'daily', $2::jsonb, $3, 36, 'free')
|
||||
VALUES ($1, $2, 1, 'plugin', 'best_effort_all', 'daily', $3::jsonb, $4, 36, 'free')
|
||||
ON CONFLICT (tenant_id) DO UPDATE
|
||||
SET collection_mode = EXCLUDED.collection_mode,
|
||||
SET workspace_id = COALESCE(tenant_monitoring_quotas.workspace_id, EXCLUDED.workspace_id),
|
||||
collection_mode = EXCLUDED.collection_mode,
|
||||
question_selection_mode = EXCLUDED.question_selection_mode,
|
||||
collect_frequency = EXCLUDED.collect_frequency,
|
||||
enabled_platforms = EXCLUDED.enabled_platforms,
|
||||
@@ -649,7 +702,7 @@ func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) err
|
||||
task_daily_hard_cap = EXCLUDED.task_daily_hard_cap,
|
||||
plan_tier = EXCLUDED.plan_tier,
|
||||
updated_at = NOW()
|
||||
`, state.TenantID, string(enabledPlatforms), state.PluginInstallationID)
|
||||
`, state.TenantID, state.WorkspaceID, string(enabledPlatforms), state.PluginInstallationID)
|
||||
return wrapSeedErr("upsert monitoring quota", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ func main() {
|
||||
defer workerCancel()
|
||||
|
||||
app.GenerationStreams.Run(workerCtx)
|
||||
app.DesktopTaskStreams.Run(workerCtx)
|
||||
|
||||
monitoringCallbackService := tenantapp.NewMonitoringCallbackService(app.DB, app.MonitoringDB, app.RabbitMQ, app.LLM, app.Logger)
|
||||
tenantapp.NewMonitoringResultIngestWorker(monitoringCallbackService, app.RabbitMQ, app.Logger, app.Config.MonitoringWorkers).Start(workerCtx)
|
||||
|
||||
Reference in New Issue
Block a user