@realtimex/folio 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/middleware/auth.ts +77 -0
- package/api/src/routes/chat.ts +7 -1
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/ingestions.ts +45 -5
- package/api/src/routes/policies.ts +50 -7
- package/api/src/routes/stats.ts +9 -5
- package/api/src/routes/workspaces.ts +290 -0
- package/api/src/services/ChatService.ts +8 -2
- package/api/src/services/IngestionService.ts +38 -26
- package/api/src/services/PolicyEngine.ts +4 -1
- package/api/src/services/PolicyLearningService.ts +31 -6
- package/api/src/services/PolicyLoader.ts +44 -25
- package/api/src/services/RAGService.ts +52 -12
- package/dist/api/src/middleware/auth.js +59 -0
- package/dist/api/src/routes/chat.js +1 -1
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/ingestions.js +45 -8
- package/dist/api/src/routes/policies.js +49 -7
- package/dist/api/src/routes/stats.js +9 -5
- package/dist/api/src/routes/workspaces.js +220 -0
- package/dist/api/src/services/ChatService.js +7 -2
- package/dist/api/src/services/IngestionService.js +35 -30
- package/dist/api/src/services/PolicyEngine.js +2 -1
- package/dist/api/src/services/PolicyLearningService.js +28 -6
- package/dist/api/src/services/PolicyLoader.js +29 -25
- package/dist/api/src/services/RAGService.js +43 -11
- package/dist/assets/index-CTn5FcC4.js +113 -0
- package/dist/assets/index-Dq9sxoZK.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/supabase/functions/workspace-invite/index.ts +110 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +5 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +4 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +5 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +2 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +17 -4
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +4 -3
- package/supabase/migrations/20260226000000_add_processing_events.sql +1 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +1 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +3 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +4 -0
- package/supabase/migrations/20260302064608_add_ingestion_llm_settings_compat.sql +15 -0
- package/supabase/migrations/20260303000000_add_workspaces_phase1.sql +459 -0
- package/supabase/migrations/20260303010000_add_workspace_management_rpc.sql +310 -0
- package/supabase/migrations/20260303020000_workspace_scope_document_chunks.sql +139 -0
- package/dist/assets/index-DzN8-j-e.css +0 -1
- package/dist/assets/index-dnBz6SWG.js +0 -113
|
@@ -13,10 +13,19 @@ declare global {
|
|
|
13
13
|
interface Request {
|
|
14
14
|
user?: User;
|
|
15
15
|
supabase?: SupabaseClient;
|
|
16
|
+
workspaceId?: string;
|
|
17
|
+
workspaceRole?: string;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
type WorkspaceMembershipRow = {
|
|
23
|
+
workspace_id: string;
|
|
24
|
+
role: "owner" | "admin" | "member";
|
|
25
|
+
status: "active" | "invited" | "disabled";
|
|
26
|
+
created_at: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
20
29
|
function resolveSupabaseConfig(req: Request): { url: string; anonKey: string } | null {
|
|
21
30
|
const headerConfig = getSupabaseConfigFromHeaders(req.headers as Record<string, unknown>);
|
|
22
31
|
|
|
@@ -31,6 +40,64 @@ function resolveSupabaseConfig(req: Request): { url: string; anonKey: string } |
|
|
|
31
40
|
return headerConfig;
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
function resolvePreferredWorkspaceId(req: Request): string | null {
|
|
44
|
+
const raw = req.headers["x-workspace-id"];
|
|
45
|
+
if (typeof raw === "string") {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(raw) && typeof raw[0] === "string") {
|
|
50
|
+
const trimmed = raw[0].trim();
|
|
51
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function resolveWorkspaceContext(
|
|
57
|
+
req: Request,
|
|
58
|
+
supabase: SupabaseClient,
|
|
59
|
+
user: User
|
|
60
|
+
): Promise<{ workspaceId: string; workspaceRole: "owner" | "admin" | "member" } | null> {
|
|
61
|
+
const preferredWorkspaceId = resolvePreferredWorkspaceId(req);
|
|
62
|
+
const { data, error } = await supabase
|
|
63
|
+
.from("workspace_members")
|
|
64
|
+
.select("workspace_id,role,status,created_at")
|
|
65
|
+
.eq("user_id", user.id)
|
|
66
|
+
.eq("status", "active")
|
|
67
|
+
.order("created_at", { ascending: true });
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
const errorCode = (error as { code?: string }).code;
|
|
71
|
+
// Backward compatibility: allow projects that haven't migrated yet.
|
|
72
|
+
if (errorCode === "42P01") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
throw new AuthorizationError(`Failed to resolve workspace membership: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const memberships = (data ?? []) as WorkspaceMembershipRow[];
|
|
79
|
+
if (memberships.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (preferredWorkspaceId) {
|
|
84
|
+
const preferred = memberships.find((membership) => membership.workspace_id === preferredWorkspaceId);
|
|
85
|
+
if (preferred) {
|
|
86
|
+
return {
|
|
87
|
+
workspaceId: preferred.workspace_id,
|
|
88
|
+
workspaceRole: preferred.role,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const active = memberships[0];
|
|
94
|
+
if (!active) return null;
|
|
95
|
+
return {
|
|
96
|
+
workspaceId: active.workspace_id,
|
|
97
|
+
workspaceRole: active.role,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
34
101
|
export async function authMiddleware(req: Request, _res: Response, next: NextFunction): Promise<void> {
|
|
35
102
|
try {
|
|
36
103
|
const supabaseConfig = resolveSupabaseConfig(req);
|
|
@@ -60,6 +127,11 @@ export async function authMiddleware(req: Request, _res: Response, next: NextFun
|
|
|
60
127
|
|
|
61
128
|
req.user = user;
|
|
62
129
|
req.supabase = supabase;
|
|
130
|
+
const workspace = await resolveWorkspaceContext(req, supabase, user);
|
|
131
|
+
if (workspace) {
|
|
132
|
+
req.workspaceId = workspace.workspaceId;
|
|
133
|
+
req.workspaceRole = workspace.workspaceRole;
|
|
134
|
+
}
|
|
63
135
|
Logger.setPersistence(supabase, user.id);
|
|
64
136
|
return next();
|
|
65
137
|
}
|
|
@@ -90,6 +162,11 @@ export async function authMiddleware(req: Request, _res: Response, next: NextFun
|
|
|
90
162
|
|
|
91
163
|
req.user = user;
|
|
92
164
|
req.supabase = supabase;
|
|
165
|
+
const workspace = await resolveWorkspaceContext(req, supabase, user);
|
|
166
|
+
if (workspace) {
|
|
167
|
+
req.workspaceId = workspace.workspaceId;
|
|
168
|
+
req.workspaceRole = workspace.workspaceRole;
|
|
169
|
+
}
|
|
93
170
|
Logger.setPersistence(supabase, user.id);
|
|
94
171
|
next();
|
|
95
172
|
} catch (error) {
|
package/api/src/routes/chat.ts
CHANGED
|
@@ -142,7 +142,13 @@ router.post(
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
try {
|
|
145
|
-
const aiMessage = await ChatService.handleMessage(
|
|
145
|
+
const aiMessage = await ChatService.handleMessage(
|
|
146
|
+
normalizedSessionId,
|
|
147
|
+
req.user.id,
|
|
148
|
+
trimmedContent,
|
|
149
|
+
req.supabase,
|
|
150
|
+
req.workspaceId
|
|
151
|
+
);
|
|
146
152
|
res.json({ success: true, message: aiMessage });
|
|
147
153
|
} catch (error) {
|
|
148
154
|
const message = error instanceof Error ? error.message : "Failed to process message";
|
package/api/src/routes/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import settingsRoutes from "./settings.js";
|
|
|
14
14
|
import rulesRoutes from "./rules.js";
|
|
15
15
|
import chatRoutes from "./chat.js";
|
|
16
16
|
import statsRoutes from "./stats.js";
|
|
17
|
+
import workspaceRoutes from "./workspaces.js";
|
|
17
18
|
|
|
18
19
|
const router = Router();
|
|
19
20
|
|
|
@@ -31,5 +32,6 @@ router.use("/settings", settingsRoutes);
|
|
|
31
32
|
router.use("/rules", rulesRoutes);
|
|
32
33
|
router.use("/chat", chatRoutes);
|
|
33
34
|
router.use("/stats", statsRoutes);
|
|
35
|
+
router.use("/workspaces", workspaceRoutes);
|
|
34
36
|
|
|
35
37
|
export default router;
|
|
@@ -22,10 +22,14 @@ router.get(
|
|
|
22
22
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
+
if (!req.workspaceId) {
|
|
26
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
25
29
|
const page = Math.max(1, parseInt(req.query["page"] as string) || 1);
|
|
26
30
|
const pageSize = Math.min(100, Math.max(1, parseInt(req.query["pageSize"] as string) || 20));
|
|
27
31
|
const query = (req.query["q"] as string | undefined)?.trim() || undefined;
|
|
28
|
-
const { ingestions, total } = await IngestionService.list(req.supabase, req.
|
|
32
|
+
const { ingestions, total } = await IngestionService.list(req.supabase, req.workspaceId, { page, pageSize, query });
|
|
29
33
|
res.json({ success: true, ingestions, total, page, pageSize });
|
|
30
34
|
})
|
|
31
35
|
);
|
|
@@ -38,7 +42,11 @@ router.get(
|
|
|
38
42
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
if (!req.workspaceId) {
|
|
46
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const ingestion = await IngestionService.get(req.params["id"] as string, req.supabase, req.workspaceId);
|
|
42
50
|
if (!ingestion) {
|
|
43
51
|
res.status(404).json({ success: false, error: "Not found" });
|
|
44
52
|
return;
|
|
@@ -56,6 +64,10 @@ router.post(
|
|
|
56
64
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
67
|
+
if (!req.workspaceId) {
|
|
68
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
59
71
|
const file = req.file;
|
|
60
72
|
if (!file) {
|
|
61
73
|
res.status(400).json({ success: false, error: "No file uploaded" });
|
|
@@ -90,6 +102,7 @@ router.post(
|
|
|
90
102
|
const ingestion = await IngestionService.ingest({
|
|
91
103
|
supabase: req.supabase,
|
|
92
104
|
userId: req.user.id,
|
|
105
|
+
workspaceId: req.workspaceId,
|
|
93
106
|
filename: file.originalname,
|
|
94
107
|
mimeType: file.mimetype,
|
|
95
108
|
fileSize: file.size,
|
|
@@ -111,7 +124,11 @@ router.post(
|
|
|
111
124
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
112
125
|
return;
|
|
113
126
|
}
|
|
114
|
-
|
|
127
|
+
if (!req.workspaceId) {
|
|
128
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const matched = await IngestionService.rerun(req.params["id"] as string, req.supabase, req.user.id, req.workspaceId);
|
|
115
132
|
res.json({ success: true, matched });
|
|
116
133
|
})
|
|
117
134
|
);
|
|
@@ -124,6 +141,10 @@ router.post(
|
|
|
124
141
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
125
142
|
return;
|
|
126
143
|
}
|
|
144
|
+
if (!req.workspaceId) {
|
|
145
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
127
148
|
|
|
128
149
|
const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
|
|
129
150
|
if (!policyId) {
|
|
@@ -140,6 +161,7 @@ router.post(
|
|
|
140
161
|
policyId,
|
|
141
162
|
req.supabase,
|
|
142
163
|
req.user.id,
|
|
164
|
+
req.workspaceId,
|
|
143
165
|
{
|
|
144
166
|
learn,
|
|
145
167
|
rerun,
|
|
@@ -170,6 +192,10 @@ router.post(
|
|
|
170
192
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
171
193
|
return;
|
|
172
194
|
}
|
|
195
|
+
if (!req.workspaceId) {
|
|
196
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
173
199
|
|
|
174
200
|
const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
|
|
175
201
|
if (!policyId) {
|
|
@@ -183,6 +209,7 @@ router.post(
|
|
|
183
209
|
policyId,
|
|
184
210
|
req.supabase,
|
|
185
211
|
req.user.id,
|
|
212
|
+
req.workspaceId,
|
|
186
213
|
{
|
|
187
214
|
provider: typeof req.body?.provider === "string" ? req.body.provider : undefined,
|
|
188
215
|
model: typeof req.body?.model === "string" ? req.body.model : undefined,
|
|
@@ -212,6 +239,10 @@ router.post(
|
|
|
212
239
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
213
240
|
return;
|
|
214
241
|
}
|
|
242
|
+
if (!req.workspaceId) {
|
|
243
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
215
246
|
const { data: settingsRow } = await req.supabase
|
|
216
247
|
.from("user_settings")
|
|
217
248
|
.select("llm_provider, llm_model, ingestion_llm_provider, ingestion_llm_model")
|
|
@@ -224,6 +255,7 @@ router.post(
|
|
|
224
255
|
req.params["id"] as string,
|
|
225
256
|
req.supabase,
|
|
226
257
|
req.user.id,
|
|
258
|
+
req.workspaceId,
|
|
227
259
|
llmSettings
|
|
228
260
|
);
|
|
229
261
|
res.json({ success: true, summary });
|
|
@@ -238,6 +270,10 @@ router.patch(
|
|
|
238
270
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
239
271
|
return;
|
|
240
272
|
}
|
|
273
|
+
if (!req.workspaceId) {
|
|
274
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
241
277
|
const tags: unknown = req.body?.tags;
|
|
242
278
|
if (!Array.isArray(tags) || tags.some((t) => typeof t !== "string")) {
|
|
243
279
|
res.status(400).json({ success: false, error: "tags must be an array of strings" });
|
|
@@ -248,7 +284,7 @@ router.patch(
|
|
|
248
284
|
.from("ingestions")
|
|
249
285
|
.update({ tags: normalized })
|
|
250
286
|
.eq("id", req.params["id"] as string)
|
|
251
|
-
.eq("
|
|
287
|
+
.eq("workspace_id", req.workspaceId);
|
|
252
288
|
if (error) {
|
|
253
289
|
res.status(500).json({ success: false, error: error.message });
|
|
254
290
|
return;
|
|
@@ -265,7 +301,11 @@ router.delete(
|
|
|
265
301
|
res.status(401).json({ success: false, error: "Authentication required" });
|
|
266
302
|
return;
|
|
267
303
|
}
|
|
268
|
-
|
|
304
|
+
if (!req.workspaceId) {
|
|
305
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const deleted = await IngestionService.delete(req.params["id"] as string, req.supabase, req.workspaceId);
|
|
269
309
|
if (!deleted) {
|
|
270
310
|
res.status(404).json({ success: false, error: "Not found" });
|
|
271
311
|
return;
|
|
@@ -15,8 +15,17 @@ router.use(optionalAuth);
|
|
|
15
15
|
router.get(
|
|
16
16
|
"/",
|
|
17
17
|
asyncHandler(async (req, res) => {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (!req.supabase || !req.user) {
|
|
19
|
+
const policies = await PolicyLoader.load(false, req.supabase, req.workspaceId);
|
|
20
|
+
res.json({ success: true, policies });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!req.workspaceId) {
|
|
24
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const policies = await PolicyLoader.load(false, req.supabase, req.workspaceId);
|
|
28
|
+
if (policies.length === 0) {
|
|
20
29
|
res.json({ success: true, policies });
|
|
21
30
|
return;
|
|
22
31
|
}
|
|
@@ -24,6 +33,7 @@ router.get(
|
|
|
24
33
|
const stats = await PolicyLearningService.getPolicyLearningStats({
|
|
25
34
|
supabase: req.supabase,
|
|
26
35
|
userId: req.user.id,
|
|
36
|
+
workspaceId: req.workspaceId,
|
|
27
37
|
policyIds: policies.map((policy) => policy.metadata.id),
|
|
28
38
|
});
|
|
29
39
|
|
|
@@ -47,12 +57,20 @@ router.get(
|
|
|
47
57
|
router.post(
|
|
48
58
|
"/",
|
|
49
59
|
asyncHandler(async (req, res) => {
|
|
60
|
+
if (!req.supabase || !req.user) {
|
|
61
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!req.workspaceId) {
|
|
65
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
50
68
|
const policy = req.body;
|
|
51
69
|
if (!PolicyLoader.validate(policy)) {
|
|
52
70
|
res.status(400).json({ success: false, error: "Invalid policy schema" });
|
|
53
71
|
return;
|
|
54
72
|
}
|
|
55
|
-
const filePath = await PolicyLoader.save(policy, req.supabase, req.user
|
|
73
|
+
const filePath = await PolicyLoader.save(policy, req.supabase, req.user.id, req.workspaceId);
|
|
56
74
|
res.status(201).json({ success: true, filePath });
|
|
57
75
|
})
|
|
58
76
|
);
|
|
@@ -61,7 +79,15 @@ router.post(
|
|
|
61
79
|
router.delete(
|
|
62
80
|
"/:id",
|
|
63
81
|
asyncHandler(async (req, res) => {
|
|
64
|
-
|
|
82
|
+
if (!req.supabase || !req.user) {
|
|
83
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!req.workspaceId) {
|
|
87
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const deleted = await PolicyLoader.delete(req.params["id"] as string, req.supabase, req.user.id, req.workspaceId);
|
|
65
91
|
if (!deleted) {
|
|
66
92
|
res.status(404).json({ success: false, error: "Policy not found" });
|
|
67
93
|
return;
|
|
@@ -74,12 +100,21 @@ router.delete(
|
|
|
74
100
|
router.patch(
|
|
75
101
|
"/:id",
|
|
76
102
|
asyncHandler(async (req, res) => {
|
|
103
|
+
if (!req.supabase || !req.user) {
|
|
104
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!req.workspaceId) {
|
|
108
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
77
111
|
const { enabled, name, description, tags, priority } = req.body;
|
|
78
112
|
await PolicyLoader.patch(
|
|
79
113
|
req.params["id"] as string,
|
|
80
114
|
{ enabled, name, description, tags, priority },
|
|
81
115
|
req.supabase,
|
|
82
|
-
req.user
|
|
116
|
+
req.user.id,
|
|
117
|
+
req.workspaceId
|
|
83
118
|
);
|
|
84
119
|
res.json({ success: true });
|
|
85
120
|
})
|
|
@@ -89,8 +124,16 @@ router.patch(
|
|
|
89
124
|
router.post(
|
|
90
125
|
"/reload",
|
|
91
126
|
asyncHandler(async (req, res) => {
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
if (!req.supabase || !req.user) {
|
|
128
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!req.workspaceId) {
|
|
132
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
PolicyLoader.invalidateCache(req.workspaceId);
|
|
136
|
+
const policies = await PolicyLoader.load(true, req.supabase, req.workspaceId);
|
|
94
137
|
res.json({ success: true, count: policies.length });
|
|
95
138
|
})
|
|
96
139
|
);
|
package/api/src/routes/stats.ts
CHANGED
|
@@ -20,33 +20,37 @@ router.get("/", async (req, res) => {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
|
-
const
|
|
23
|
+
const workspaceId = req.workspaceId;
|
|
24
|
+
if (!workspaceId) {
|
|
25
|
+
res.status(403).json({ success: false, error: "Workspace membership required" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
24
28
|
const s = req.supabase; // the scoped service client
|
|
25
29
|
|
|
26
30
|
// 1. Total Documents Ingested
|
|
27
31
|
const { count: totalDocumentsCount, error: err1 } = await s
|
|
28
32
|
.from("ingestions")
|
|
29
33
|
.select("*", { count: 'exact', head: true })
|
|
30
|
-
.eq("
|
|
34
|
+
.eq("workspace_id", workspaceId);
|
|
31
35
|
|
|
32
36
|
// 2. Active Policies
|
|
33
37
|
const { count: activePoliciesCount, error: err2 } = await s
|
|
34
38
|
.from("policies")
|
|
35
39
|
.select("*", { count: 'exact', head: true })
|
|
36
|
-
.eq("
|
|
40
|
+
.eq("workspace_id", workspaceId)
|
|
37
41
|
.eq("enabled", true);
|
|
38
42
|
|
|
39
43
|
// 3. RAG Knowledge Base (Chunks)
|
|
40
44
|
const { count: ragChunksCount, error: err3 } = await s
|
|
41
45
|
.from("document_chunks")
|
|
42
46
|
.select("*", { count: 'exact', head: true })
|
|
43
|
-
.eq("
|
|
47
|
+
.eq("workspace_id", workspaceId);
|
|
44
48
|
|
|
45
49
|
// 4. Automation Runs (Sum of actions taken across ingestions)
|
|
46
50
|
const { data: ingestionsWithActions, error: err4 } = await s
|
|
47
51
|
.from("ingestions")
|
|
48
52
|
.select("actions_taken")
|
|
49
|
-
.eq("
|
|
53
|
+
.eq("workspace_id", workspaceId)
|
|
50
54
|
.not("actions_taken", "is", null);
|
|
51
55
|
|
|
52
56
|
let automationRunsCount = 0;
|