web-agent-bridge 3.2.0 → 3.3.0
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/LICENSE +72 -72
- package/README.ar.md +1286 -1152
- package/README.md +1764 -1635
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab.js +80 -80
- package/examples/bidi-agent.js +119 -119
- package/examples/cross-site-agent.js +91 -91
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/puppeteer-agent.js +108 -108
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +16 -3
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -0
- package/public/agent-workspace.html +349 -349
- package/public/ai.html +198 -198
- package/public/api.html +413 -412
- package/public/browser.html +486 -486
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +706 -706
- package/public/dns.html +507 -0
- package/public/docs.html +587 -587
- package/public/feed.xml +89 -89
- package/public/growth.html +463 -463
- package/public/index.html +1070 -982
- package/public/integrations.html +556 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +580 -580
- package/public/phone-shield.html +281 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/register.html +105 -105
- package/public/robots.txt +87 -87
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/terms.html +256 -256
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/commander.js +262 -262
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +12 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +1 -1
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +531 -427
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +681 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-premium.js +671 -671
- package/server/routes/admin.js +261 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +45 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +417 -417
- package/server/routes/gateway.js +173 -157
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -185
- package/server/routes/wab-api.js +850 -501
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -261
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +204 -204
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/plugins.js +771 -771
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/stripe.js +192 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/public/score.html +0 -263
- package/server/migrations/006_growth_suite.sql +0 -138
- package/server/routes/growth.js +0 -962
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
|
@@ -1,540 +1,540 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WAB Agent Workspace — Server-side Route & API
|
|
3
|
-
* ════════════════════════════════════════════════════════════════════════
|
|
4
|
-
* Premium workspace endpoints for the 4-panel agent workspace.
|
|
5
|
-
* Handles subscriptions, workspace sessions, agent execution, and admin stats.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const express = require('express');
|
|
9
|
-
const router = express.Router();
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const { authenticateToken } = require('../middleware/auth');
|
|
12
|
-
const { authenticateAdmin } = require('../middleware/adminAuth');
|
|
13
|
-
const { db } = require('../models/db');
|
|
14
|
-
|
|
15
|
-
// ─── Schema ──────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
db.exec(`
|
|
18
|
-
CREATE TABLE IF NOT EXISTS workspace_subscriptions (
|
|
19
|
-
id TEXT PRIMARY KEY,
|
|
20
|
-
user_id TEXT NOT NULL,
|
|
21
|
-
plan TEXT NOT NULL DEFAULT 'free' CHECK(plan IN ('free','starter','pro','enterprise')),
|
|
22
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','suspended')),
|
|
23
|
-
tasks_today INTEGER DEFAULT 0,
|
|
24
|
-
tasks_total INTEGER DEFAULT 0,
|
|
25
|
-
deals_completed INTEGER DEFAULT 0,
|
|
26
|
-
total_savings REAL DEFAULT 0,
|
|
27
|
-
last_task_date TEXT,
|
|
28
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
29
|
-
expires_at TEXT,
|
|
30
|
-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
CREATE TABLE IF NOT EXISTS workspace_sessions (
|
|
34
|
-
id TEXT PRIMARY KEY,
|
|
35
|
-
user_id TEXT NOT NULL,
|
|
36
|
-
session_token TEXT NOT NULL,
|
|
37
|
-
active INTEGER DEFAULT 1,
|
|
38
|
-
panels_state TEXT DEFAULT '{}',
|
|
39
|
-
last_activity TEXT DEFAULT (datetime('now')),
|
|
40
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
41
|
-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
CREATE TABLE IF NOT EXISTS workspace_deals (
|
|
45
|
-
id TEXT PRIMARY KEY,
|
|
46
|
-
user_id TEXT NOT NULL,
|
|
47
|
-
task_id TEXT,
|
|
48
|
-
offer_source TEXT,
|
|
49
|
-
offer_title TEXT,
|
|
50
|
-
original_price REAL,
|
|
51
|
-
final_price REAL,
|
|
52
|
-
savings REAL DEFAULT 0,
|
|
53
|
-
status TEXT DEFAULT 'presented' CHECK(status IN ('presented','clicked','agent_executing','login_required','completed','failed')),
|
|
54
|
-
deal_url TEXT,
|
|
55
|
-
requires_login INTEGER DEFAULT 0,
|
|
56
|
-
login_method TEXT,
|
|
57
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
58
|
-
completed_at TEXT,
|
|
59
|
-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
CREATE TABLE IF NOT EXISTS workspace_analytics (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
user_id TEXT,
|
|
65
|
-
event_type TEXT NOT NULL,
|
|
66
|
-
event_data TEXT DEFAULT '{}',
|
|
67
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
CREATE TABLE IF NOT EXISTS workspace_favorites (
|
|
71
|
-
id TEXT PRIMARY KEY,
|
|
72
|
-
user_id TEXT NOT NULL,
|
|
73
|
-
url TEXT NOT NULL,
|
|
74
|
-
title TEXT DEFAULT '',
|
|
75
|
-
description TEXT DEFAULT '',
|
|
76
|
-
favicon TEXT DEFAULT '',
|
|
77
|
-
folder TEXT DEFAULT 'default',
|
|
78
|
-
position INTEGER DEFAULT 0,
|
|
79
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
80
|
-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
CREATE TABLE IF NOT EXISTS workspace_history (
|
|
84
|
-
id TEXT PRIMARY KEY,
|
|
85
|
-
user_id TEXT NOT NULL,
|
|
86
|
-
url TEXT NOT NULL,
|
|
87
|
-
title TEXT DEFAULT '',
|
|
88
|
-
visit_count INTEGER DEFAULT 1,
|
|
89
|
-
last_visited TEXT DEFAULT (datetime('now')),
|
|
90
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
91
|
-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
CREATE INDEX IF NOT EXISTS idx_ws_subs_user ON workspace_subscriptions(user_id);
|
|
95
|
-
CREATE INDEX IF NOT EXISTS idx_ws_sessions_user ON workspace_sessions(user_id);
|
|
96
|
-
CREATE INDEX IF NOT EXISTS idx_ws_deals_user ON workspace_deals(user_id);
|
|
97
|
-
CREATE INDEX IF NOT EXISTS idx_ws_analytics_type ON workspace_analytics(event_type);
|
|
98
|
-
CREATE INDEX IF NOT EXISTS idx_ws_analytics_date ON workspace_analytics(created_at);
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_ws_favorites_user ON workspace_favorites(user_id);
|
|
100
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_ws_favorites_user_url ON workspace_favorites(user_id, url);
|
|
101
|
-
CREATE INDEX IF NOT EXISTS idx_ws_history_user ON workspace_history(user_id);
|
|
102
|
-
CREATE INDEX IF NOT EXISTS idx_ws_history_visited ON workspace_history(last_visited);
|
|
103
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_ws_history_user_url ON workspace_history(user_id, url);
|
|
104
|
-
`);
|
|
105
|
-
|
|
106
|
-
// ─── Plan Limits ─────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
const PLAN_LIMITS = {
|
|
109
|
-
free: { dailyTasks: 5, maxResults: 3, negotiation: false, agentExecute: false },
|
|
110
|
-
starter: { dailyTasks: 30, maxResults: 10, negotiation: true, agentExecute: false },
|
|
111
|
-
pro: { dailyTasks: -1, maxResults: 20, negotiation: true, agentExecute: true },
|
|
112
|
-
enterprise: { dailyTasks: -1, maxResults: 50, negotiation: true, agentExecute: true },
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// ─── Prepared Statements ─────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
const stmts = {
|
|
118
|
-
getSub: db.prepare('SELECT * FROM workspace_subscriptions WHERE user_id = ? AND status = ?'),
|
|
119
|
-
insertSub: db.prepare(`INSERT INTO workspace_subscriptions (id, user_id, plan, status) VALUES (?, ?, ?, 'active')`),
|
|
120
|
-
updateSubPlan: db.prepare('UPDATE workspace_subscriptions SET plan = ?, status = ? WHERE user_id = ? AND status = ?'),
|
|
121
|
-
incrementTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = tasks_today + 1, tasks_total = tasks_total + 1, last_task_date = date('now') WHERE user_id = ? AND status = 'active'`),
|
|
122
|
-
resetDailyTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = 0 WHERE last_task_date < date('now')`),
|
|
123
|
-
addDealSavings: db.prepare(`UPDATE workspace_subscriptions SET deals_completed = deals_completed + 1, total_savings = total_savings + ? WHERE user_id = ? AND status = 'active'`),
|
|
124
|
-
|
|
125
|
-
insertSession: db.prepare('INSERT INTO workspace_sessions (id, user_id, session_token) VALUES (?, ?, ?)'),
|
|
126
|
-
getSession: db.prepare('SELECT * FROM workspace_sessions WHERE session_token = ? AND active = 1'),
|
|
127
|
-
endSession: db.prepare("UPDATE workspace_sessions SET active = 0 WHERE session_token = ?"),
|
|
128
|
-
|
|
129
|
-
insertDeal: db.prepare('INSERT INTO workspace_deals (id, user_id, task_id, offer_source, offer_title, original_price, final_price, savings, deal_url, requires_login) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
130
|
-
updateDealStatus: db.prepare('UPDATE workspace_deals SET status = ?, completed_at = datetime(\'now\') WHERE id = ?'),
|
|
131
|
-
getUserDeals: db.prepare('SELECT * FROM workspace_deals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'),
|
|
132
|
-
|
|
133
|
-
// Favorites
|
|
134
|
-
insertFavorite: db.prepare('INSERT INTO workspace_favorites (id, user_id, url, title, description, favicon, folder, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
135
|
-
getFavorites: db.prepare('SELECT * FROM workspace_favorites WHERE user_id = ? ORDER BY folder, position, created_at DESC'),
|
|
136
|
-
getFavoritesByFolder: db.prepare('SELECT * FROM workspace_favorites WHERE user_id = ? AND folder = ? ORDER BY position, created_at DESC'),
|
|
137
|
-
deleteFavorite: db.prepare('DELETE FROM workspace_favorites WHERE id = ? AND user_id = ?'),
|
|
138
|
-
deleteFavoriteByUrl: db.prepare('DELETE FROM workspace_favorites WHERE user_id = ? AND url = ?'),
|
|
139
|
-
updateFavorite: db.prepare('UPDATE workspace_favorites SET title = ?, folder = ?, position = ? WHERE id = ? AND user_id = ?'),
|
|
140
|
-
|
|
141
|
-
// History
|
|
142
|
-
upsertHistory: db.prepare(`INSERT INTO workspace_history (id, user_id, url, title, visit_count, last_visited)
|
|
143
|
-
VALUES (?, ?, ?, ?, 1, datetime('now'))
|
|
144
|
-
ON CONFLICT(user_id, url) DO UPDATE SET
|
|
145
|
-
title = excluded.title,
|
|
146
|
-
visit_count = visit_count + 1,
|
|
147
|
-
last_visited = datetime('now')`),
|
|
148
|
-
getHistory: db.prepare('SELECT * FROM workspace_history WHERE user_id = ? ORDER BY last_visited DESC LIMIT ? OFFSET ?'),
|
|
149
|
-
searchHistory: db.prepare("SELECT * FROM workspace_history WHERE user_id = ? AND (url LIKE ? OR title LIKE ?) ORDER BY last_visited DESC LIMIT ?"),
|
|
150
|
-
deleteHistoryItem: db.prepare('DELETE FROM workspace_history WHERE id = ? AND user_id = ?'),
|
|
151
|
-
clearHistory: db.prepare('DELETE FROM workspace_history WHERE user_id = ?'),
|
|
152
|
-
|
|
153
|
-
logEvent: db.prepare('INSERT INTO workspace_analytics (id, user_id, event_type, event_data) VALUES (?, ?, ?, ?)'),
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Reset daily task counts
|
|
157
|
-
try { stmts.resetDailyTasks.run(); } catch (_) {}
|
|
158
|
-
|
|
159
|
-
// ─── User Routes ─────────────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* GET /api/workspace/subscription — Get user's workspace subscription
|
|
163
|
-
*/
|
|
164
|
-
router.get('/subscription', authenticateToken, (req, res) => {
|
|
165
|
-
let sub = stmts.getSub.get(req.user.id, 'active');
|
|
166
|
-
|
|
167
|
-
if (!sub) {
|
|
168
|
-
// Auto-create free subscription
|
|
169
|
-
const id = crypto.randomUUID();
|
|
170
|
-
stmts.insertSub.run(id, req.user.id, 'free');
|
|
171
|
-
sub = stmts.getSub.get(req.user.id, 'active');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
|
|
175
|
-
const remaining = limits.dailyTasks < 0 ? -1 : Math.max(0, limits.dailyTasks - (sub.tasks_today || 0));
|
|
176
|
-
|
|
177
|
-
res.json({
|
|
178
|
-
plan: sub.plan,
|
|
179
|
-
status: sub.status,
|
|
180
|
-
tasksToday: sub.tasks_today,
|
|
181
|
-
tasksTotal: sub.tasks_total,
|
|
182
|
-
dealsCompleted: sub.deals_completed,
|
|
183
|
-
totalSavings: sub.total_savings,
|
|
184
|
-
limits,
|
|
185
|
-
remainingTasks: remaining,
|
|
186
|
-
createdAt: sub.created_at,
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* POST /api/workspace/subscription — Upgrade/change plan
|
|
192
|
-
*/
|
|
193
|
-
router.post('/subscription', authenticateToken, (req, res) => {
|
|
194
|
-
const { plan } = req.body;
|
|
195
|
-
if (!PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
|
|
196
|
-
|
|
197
|
-
let sub = stmts.getSub.get(req.user.id, 'active');
|
|
198
|
-
if (sub) {
|
|
199
|
-
stmts.updateSubPlan.run(plan, 'active', req.user.id, 'active');
|
|
200
|
-
} else {
|
|
201
|
-
const id = crypto.randomUUID();
|
|
202
|
-
stmts.insertSub.run(id, req.user.id, plan);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
logEvent(req.user.id, 'plan_changed', { plan });
|
|
206
|
-
res.json({ success: true, plan });
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* POST /api/workspace/check-limits — Check if user can execute a task
|
|
211
|
-
*/
|
|
212
|
-
router.post('/check-limits', authenticateToken, (req, res) => {
|
|
213
|
-
const sub = stmts.getSub.get(req.user.id, 'active');
|
|
214
|
-
if (!sub) return res.json({ allowed: true, plan: 'free' }); // New user
|
|
215
|
-
|
|
216
|
-
const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
|
|
217
|
-
if (limits.dailyTasks >= 0 && sub.tasks_today >= limits.dailyTasks) {
|
|
218
|
-
return res.json({
|
|
219
|
-
allowed: false,
|
|
220
|
-
reason: 'daily_limit',
|
|
221
|
-
plan: sub.plan,
|
|
222
|
-
limit: limits.dailyTasks,
|
|
223
|
-
used: sub.tasks_today,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
stmts.incrementTasks.run(req.user.id);
|
|
228
|
-
res.json({ allowed: true, plan: sub.plan, remaining: limits.dailyTasks < 0 ? -1 : limits.dailyTasks - sub.tasks_today - 1 });
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* POST /api/workspace/deal — Record a deal action
|
|
233
|
-
*/
|
|
234
|
-
router.post('/deal', authenticateToken, (req, res) => {
|
|
235
|
-
const { taskId, source, title, originalPrice, finalPrice, url, requiresLogin } = req.body;
|
|
236
|
-
const savings = originalPrice && finalPrice ? originalPrice - finalPrice : 0;
|
|
237
|
-
const id = crypto.randomUUID();
|
|
238
|
-
|
|
239
|
-
stmts.insertDeal.run(id, req.user.id, taskId || null, source || '', title || '', originalPrice || 0, finalPrice || 0, savings, url || '', requiresLogin ? 1 : 0);
|
|
240
|
-
logEvent(req.user.id, 'deal_created', { dealId: id, source, savings });
|
|
241
|
-
|
|
242
|
-
res.json({ dealId: id, savings });
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* POST /api/workspace/deal/:id/status — Update deal status
|
|
247
|
-
*/
|
|
248
|
-
router.post('/deal/:id/status', authenticateToken, (req, res) => {
|
|
249
|
-
const { status } = req.body;
|
|
250
|
-
const valid = ['clicked', 'agent_executing', 'login_required', 'completed', 'failed'];
|
|
251
|
-
if (!valid.includes(status)) return res.status(400).json({ error: 'Invalid status' });
|
|
252
|
-
|
|
253
|
-
stmts.updateDealStatus.run(status, req.params.id);
|
|
254
|
-
|
|
255
|
-
if (status === 'completed') {
|
|
256
|
-
// Update savings in subscription
|
|
257
|
-
const deal = db.prepare('SELECT savings FROM workspace_deals WHERE id = ?').get(req.params.id);
|
|
258
|
-
if (deal) {
|
|
259
|
-
stmts.addDealSavings.run(deal.savings || 0, req.user.id);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
logEvent(req.user.id, 'deal_status', { dealId: req.params.id, status });
|
|
264
|
-
res.json({ success: true });
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* GET /api/workspace/deals — Get user's deal history
|
|
269
|
-
*/
|
|
270
|
-
router.get('/deals', authenticateToken, (req, res) => {
|
|
271
|
-
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
272
|
-
const deals = stmts.getUserDeals.all(req.user.id, limit);
|
|
273
|
-
res.json({ deals });
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* POST /api/workspace/event — Log workspace analytics event
|
|
278
|
-
*/
|
|
279
|
-
router.post('/event', authenticateToken, (req, res) => {
|
|
280
|
-
const { type, data } = req.body;
|
|
281
|
-
if (!type) return res.status(400).json({ error: 'Event type required' });
|
|
282
|
-
logEvent(req.user.id, type, data || {});
|
|
283
|
-
res.json({ ok: true });
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ─── Favorites API ───────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* GET /api/workspace/favorites — Get user's bookmarks
|
|
290
|
-
*/
|
|
291
|
-
router.get('/favorites', authenticateToken, (req, res) => {
|
|
292
|
-
const folder = req.query.folder;
|
|
293
|
-
const favorites = folder
|
|
294
|
-
? stmts.getFavoritesByFolder.all(req.user.id, folder)
|
|
295
|
-
: stmts.getFavorites.all(req.user.id);
|
|
296
|
-
res.json({ favorites });
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* POST /api/workspace/favorites — Add a bookmark
|
|
301
|
-
*/
|
|
302
|
-
router.post('/favorites', authenticateToken, (req, res) => {
|
|
303
|
-
const { url, title, description, favicon, folder } = req.body;
|
|
304
|
-
if (!url) return res.status(400).json({ error: 'URL is required' });
|
|
305
|
-
const id = crypto.randomUUID();
|
|
306
|
-
const maxPos = db.prepare('SELECT MAX(position) as m FROM workspace_favorites WHERE user_id = ? AND folder = ?')
|
|
307
|
-
.get(req.user.id, folder || 'default');
|
|
308
|
-
const position = (maxPos?.m || 0) + 1;
|
|
309
|
-
try {
|
|
310
|
-
stmts.insertFavorite.run(id, req.user.id, url, title || '', description || '', favicon || '', folder || 'default', position);
|
|
311
|
-
} catch (e) {
|
|
312
|
-
if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: 'Already bookmarked' });
|
|
313
|
-
throw e;
|
|
314
|
-
}
|
|
315
|
-
logEvent(req.user.id, 'favorite_added', { url });
|
|
316
|
-
res.json({ id, url, title, folder: folder || 'default', position });
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* PUT /api/workspace/favorites/:id — Update a bookmark
|
|
321
|
-
*/
|
|
322
|
-
router.put('/favorites/:id', authenticateToken, (req, res) => {
|
|
323
|
-
const { title, folder, position } = req.body;
|
|
324
|
-
const changes = stmts.updateFavorite.run(title ?? '', folder ?? 'default', position ?? 0, req.params.id, req.user.id);
|
|
325
|
-
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
326
|
-
res.json({ success: true });
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* DELETE /api/workspace/favorites/:id — Remove a bookmark by ID
|
|
331
|
-
*/
|
|
332
|
-
router.delete('/favorites/:id', authenticateToken, (req, res) => {
|
|
333
|
-
const changes = stmts.deleteFavorite.run(req.params.id, req.user.id);
|
|
334
|
-
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
335
|
-
logEvent(req.user.id, 'favorite_removed', { id: req.params.id });
|
|
336
|
-
res.json({ success: true });
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* DELETE /api/workspace/favorites?url=... — Remove a bookmark by URL
|
|
341
|
-
*/
|
|
342
|
-
router.delete('/favorites', authenticateToken, (req, res) => {
|
|
343
|
-
const { url } = req.query;
|
|
344
|
-
if (!url) return res.status(400).json({ error: 'URL query parameter required' });
|
|
345
|
-
const changes = stmts.deleteFavoriteByUrl.run(req.user.id, url);
|
|
346
|
-
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
347
|
-
logEvent(req.user.id, 'favorite_removed', { url });
|
|
348
|
-
res.json({ success: true });
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// ─── History API ─────────────────────────────────────────────────────
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* GET /api/workspace/history — Get browsing history
|
|
355
|
-
*/
|
|
356
|
-
router.get('/history', authenticateToken, (req, res) => {
|
|
357
|
-
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
358
|
-
const offset = parseInt(req.query.offset) || 0;
|
|
359
|
-
const q = req.query.q;
|
|
360
|
-
let items;
|
|
361
|
-
if (q) {
|
|
362
|
-
const pattern = `%${q}%`;
|
|
363
|
-
items = stmts.searchHistory.all(req.user.id, pattern, pattern, limit);
|
|
364
|
-
} else {
|
|
365
|
-
items = stmts.getHistory.all(req.user.id, limit, offset);
|
|
366
|
-
}
|
|
367
|
-
res.json({ history: items });
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* POST /api/workspace/history — Record a history entry
|
|
372
|
-
*/
|
|
373
|
-
router.post('/history', authenticateToken, (req, res) => {
|
|
374
|
-
const { url, title } = req.body;
|
|
375
|
-
if (!url) return res.status(400).json({ error: 'URL is required' });
|
|
376
|
-
const id = crypto.randomUUID();
|
|
377
|
-
stmts.upsertHistory.run(id, req.user.id, url, title || '');
|
|
378
|
-
res.json({ success: true });
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* DELETE /api/workspace/history/:id — Delete a single history entry
|
|
383
|
-
*/
|
|
384
|
-
router.delete('/history/:id', authenticateToken, (req, res) => {
|
|
385
|
-
const changes = stmts.deleteHistoryItem.run(req.params.id, req.user.id);
|
|
386
|
-
if (!changes.changes) return res.status(404).json({ error: 'History entry not found' });
|
|
387
|
-
res.json({ success: true });
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* DELETE /api/workspace/history — Clear all history
|
|
392
|
-
*/
|
|
393
|
-
router.delete('/history', authenticateToken, (req, res) => {
|
|
394
|
-
stmts.clearHistory.run(req.user.id);
|
|
395
|
-
logEvent(req.user.id, 'history_cleared', {});
|
|
396
|
-
res.json({ success: true });
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// ─── Admin Routes ────────────────────────────────────────────────────
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* GET /api/workspace/admin/stats — Global workspace stats
|
|
403
|
-
*/
|
|
404
|
-
router.get('/admin/stats', authenticateAdmin, (req, res) => {
|
|
405
|
-
const totalUsers = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
406
|
-
const activeToday = db.prepare("SELECT COUNT(*) as c FROM workspace_subscriptions WHERE last_task_date = date('now')").get()?.c || 0;
|
|
407
|
-
const totalTasks = db.prepare('SELECT SUM(tasks_total) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
408
|
-
const totalDeals = db.prepare('SELECT SUM(deals_completed) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
409
|
-
const totalSavings = db.prepare('SELECT SUM(total_savings) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
410
|
-
|
|
411
|
-
const planBreakdown = db.prepare(`
|
|
412
|
-
SELECT plan, COUNT(*) as count, SUM(tasks_total) as tasks, SUM(total_savings) as savings
|
|
413
|
-
FROM workspace_subscriptions WHERE status = 'active' GROUP BY plan
|
|
414
|
-
`).all();
|
|
415
|
-
|
|
416
|
-
const recentEvents = db.prepare(`
|
|
417
|
-
SELECT event_type, COUNT(*) as count
|
|
418
|
-
FROM workspace_analytics
|
|
419
|
-
WHERE created_at > datetime('now', '-24 hours')
|
|
420
|
-
GROUP BY event_type ORDER BY count DESC LIMIT 10
|
|
421
|
-
`).all();
|
|
422
|
-
|
|
423
|
-
const dailyTasks = db.prepare(`
|
|
424
|
-
SELECT date(created_at) as day, COUNT(*) as count
|
|
425
|
-
FROM workspace_analytics
|
|
426
|
-
WHERE event_type IN ('task_started','deal_created') AND created_at > datetime('now', '-30 days')
|
|
427
|
-
GROUP BY day ORDER BY day
|
|
428
|
-
`).all();
|
|
429
|
-
|
|
430
|
-
res.json({
|
|
431
|
-
totalUsers,
|
|
432
|
-
activeToday,
|
|
433
|
-
totalTasks,
|
|
434
|
-
totalDeals,
|
|
435
|
-
totalSavings,
|
|
436
|
-
planBreakdown,
|
|
437
|
-
recentEvents,
|
|
438
|
-
dailyTasks,
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* GET /api/workspace/admin/subscriptions — List all subscriptions
|
|
444
|
-
*/
|
|
445
|
-
router.get('/admin/subscriptions', authenticateAdmin, (req, res) => {
|
|
446
|
-
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
447
|
-
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
448
|
-
const offset = (page - 1) * limit;
|
|
449
|
-
|
|
450
|
-
const subs = db.prepare(`
|
|
451
|
-
SELECT ws.*, u.email, u.name
|
|
452
|
-
FROM workspace_subscriptions ws
|
|
453
|
-
LEFT JOIN users u ON ws.user_id = u.id
|
|
454
|
-
ORDER BY ws.created_at DESC LIMIT ? OFFSET ?
|
|
455
|
-
`).all(limit, offset);
|
|
456
|
-
|
|
457
|
-
const total = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
458
|
-
|
|
459
|
-
res.json({ subscriptions: subs, total, page, pages: Math.ceil(total / limit) });
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* PUT /api/workspace/admin/subscription/:userId — Admin update subscription
|
|
464
|
-
*/
|
|
465
|
-
router.put('/admin/subscription/:userId', authenticateAdmin, (req, res) => {
|
|
466
|
-
const { plan, status } = req.body;
|
|
467
|
-
if (plan && !PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
|
|
468
|
-
|
|
469
|
-
if (plan) {
|
|
470
|
-
stmts.updateSubPlan.run(plan, status || 'active', req.params.userId, 'active');
|
|
471
|
-
}
|
|
472
|
-
if (status && !plan) {
|
|
473
|
-
db.prepare('UPDATE workspace_subscriptions SET status = ? WHERE user_id = ? AND status = ?')
|
|
474
|
-
.run(status, req.params.userId, 'active');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
res.json({ success: true });
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* GET /api/workspace/admin/deals — List all deals
|
|
482
|
-
*/
|
|
483
|
-
router.get('/admin/deals', authenticateAdmin, (req, res) => {
|
|
484
|
-
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
485
|
-
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
486
|
-
const offset = (page - 1) * limit;
|
|
487
|
-
|
|
488
|
-
const deals = db.prepare(`
|
|
489
|
-
SELECT wd.*, u.email, u.name as user_name
|
|
490
|
-
FROM workspace_deals wd
|
|
491
|
-
LEFT JOIN users u ON wd.user_id = u.id
|
|
492
|
-
ORDER BY wd.created_at DESC LIMIT ? OFFSET ?
|
|
493
|
-
`).all(limit, offset);
|
|
494
|
-
|
|
495
|
-
const total = db.prepare('SELECT COUNT(*) as c FROM workspace_deals').get()?.c || 0;
|
|
496
|
-
const totalSavings = db.prepare('SELECT SUM(savings) as s FROM workspace_deals WHERE status = ?').get('completed')?.s || 0;
|
|
497
|
-
|
|
498
|
-
res.json({ deals, total, totalSavings, page, pages: Math.ceil(total / limit) });
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* GET /api/workspace/admin/analytics — Workspace analytics dashboard
|
|
503
|
-
*/
|
|
504
|
-
router.get('/admin/analytics', authenticateAdmin, (req, res) => {
|
|
505
|
-
const days = Math.min(parseInt(req.query.days) || 30, 90);
|
|
506
|
-
|
|
507
|
-
const eventsByType = db.prepare(`
|
|
508
|
-
SELECT event_type, COUNT(*) as count
|
|
509
|
-
FROM workspace_analytics
|
|
510
|
-
WHERE created_at > datetime('now', '-${days} days')
|
|
511
|
-
GROUP BY event_type ORDER BY count DESC
|
|
512
|
-
`).all();
|
|
513
|
-
|
|
514
|
-
const dailyActivity = db.prepare(`
|
|
515
|
-
SELECT date(created_at) as day, COUNT(*) as events,
|
|
516
|
-
COUNT(DISTINCT user_id) as unique_users
|
|
517
|
-
FROM workspace_analytics
|
|
518
|
-
WHERE created_at > datetime('now', '-${days} days')
|
|
519
|
-
GROUP BY day ORDER BY day
|
|
520
|
-
`).all();
|
|
521
|
-
|
|
522
|
-
const topUsers = db.prepare(`
|
|
523
|
-
SELECT ws.user_id, u.email, u.name, ws.plan, ws.tasks_total, ws.deals_completed, ws.total_savings
|
|
524
|
-
FROM workspace_subscriptions ws
|
|
525
|
-
LEFT JOIN users u ON ws.user_id = u.id
|
|
526
|
-
ORDER BY ws.tasks_total DESC LIMIT 20
|
|
527
|
-
`).all();
|
|
528
|
-
|
|
529
|
-
res.json({ eventsByType, dailyActivity, topUsers });
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
533
|
-
|
|
534
|
-
function logEvent(userId, type, data) {
|
|
535
|
-
try {
|
|
536
|
-
stmts.logEvent.run(crypto.randomUUID(), userId || null, type, JSON.stringify(data));
|
|
537
|
-
} catch (_) {}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
module.exports = router;
|
|
1
|
+
/**
|
|
2
|
+
* WAB Agent Workspace — Server-side Route & API
|
|
3
|
+
* ════════════════════════════════════════════════════════════════════════
|
|
4
|
+
* Premium workspace endpoints for the 4-panel agent workspace.
|
|
5
|
+
* Handles subscriptions, workspace sessions, agent execution, and admin stats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const express = require('express');
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { authenticateToken } = require('../middleware/auth');
|
|
12
|
+
const { authenticateAdmin } = require('../middleware/adminAuth');
|
|
13
|
+
const { db } = require('../models/db');
|
|
14
|
+
|
|
15
|
+
// ─── Schema ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS workspace_subscriptions (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
user_id TEXT NOT NULL,
|
|
21
|
+
plan TEXT NOT NULL DEFAULT 'free' CHECK(plan IN ('free','starter','pro','enterprise')),
|
|
22
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','cancelled','expired','suspended')),
|
|
23
|
+
tasks_today INTEGER DEFAULT 0,
|
|
24
|
+
tasks_total INTEGER DEFAULT 0,
|
|
25
|
+
deals_completed INTEGER DEFAULT 0,
|
|
26
|
+
total_savings REAL DEFAULT 0,
|
|
27
|
+
last_task_date TEXT,
|
|
28
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
29
|
+
expires_at TEXT,
|
|
30
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS workspace_sessions (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
user_id TEXT NOT NULL,
|
|
36
|
+
session_token TEXT NOT NULL,
|
|
37
|
+
active INTEGER DEFAULT 1,
|
|
38
|
+
panels_state TEXT DEFAULT '{}',
|
|
39
|
+
last_activity TEXT DEFAULT (datetime('now')),
|
|
40
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
41
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS workspace_deals (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
user_id TEXT NOT NULL,
|
|
47
|
+
task_id TEXT,
|
|
48
|
+
offer_source TEXT,
|
|
49
|
+
offer_title TEXT,
|
|
50
|
+
original_price REAL,
|
|
51
|
+
final_price REAL,
|
|
52
|
+
savings REAL DEFAULT 0,
|
|
53
|
+
status TEXT DEFAULT 'presented' CHECK(status IN ('presented','clicked','agent_executing','login_required','completed','failed')),
|
|
54
|
+
deal_url TEXT,
|
|
55
|
+
requires_login INTEGER DEFAULT 0,
|
|
56
|
+
login_method TEXT,
|
|
57
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
58
|
+
completed_at TEXT,
|
|
59
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS workspace_analytics (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
user_id TEXT,
|
|
65
|
+
event_type TEXT NOT NULL,
|
|
66
|
+
event_data TEXT DEFAULT '{}',
|
|
67
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS workspace_favorites (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
user_id TEXT NOT NULL,
|
|
73
|
+
url TEXT NOT NULL,
|
|
74
|
+
title TEXT DEFAULT '',
|
|
75
|
+
description TEXT DEFAULT '',
|
|
76
|
+
favicon TEXT DEFAULT '',
|
|
77
|
+
folder TEXT DEFAULT 'default',
|
|
78
|
+
position INTEGER DEFAULT 0,
|
|
79
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
80
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS workspace_history (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
user_id TEXT NOT NULL,
|
|
86
|
+
url TEXT NOT NULL,
|
|
87
|
+
title TEXT DEFAULT '',
|
|
88
|
+
visit_count INTEGER DEFAULT 1,
|
|
89
|
+
last_visited TEXT DEFAULT (datetime('now')),
|
|
90
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
91
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_ws_subs_user ON workspace_subscriptions(user_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_ws_sessions_user ON workspace_sessions(user_id);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_ws_deals_user ON workspace_deals(user_id);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_ws_analytics_type ON workspace_analytics(event_type);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_ws_analytics_date ON workspace_analytics(created_at);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_ws_favorites_user ON workspace_favorites(user_id);
|
|
100
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_ws_favorites_user_url ON workspace_favorites(user_id, url);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_ws_history_user ON workspace_history(user_id);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_ws_history_visited ON workspace_history(last_visited);
|
|
103
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_ws_history_user_url ON workspace_history(user_id, url);
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
// ─── Plan Limits ─────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const PLAN_LIMITS = {
|
|
109
|
+
free: { dailyTasks: 5, maxResults: 3, negotiation: false, agentExecute: false },
|
|
110
|
+
starter: { dailyTasks: 30, maxResults: 10, negotiation: true, agentExecute: false },
|
|
111
|
+
pro: { dailyTasks: -1, maxResults: 20, negotiation: true, agentExecute: true },
|
|
112
|
+
enterprise: { dailyTasks: -1, maxResults: 50, negotiation: true, agentExecute: true },
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ─── Prepared Statements ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
const stmts = {
|
|
118
|
+
getSub: db.prepare('SELECT * FROM workspace_subscriptions WHERE user_id = ? AND status = ?'),
|
|
119
|
+
insertSub: db.prepare(`INSERT INTO workspace_subscriptions (id, user_id, plan, status) VALUES (?, ?, ?, 'active')`),
|
|
120
|
+
updateSubPlan: db.prepare('UPDATE workspace_subscriptions SET plan = ?, status = ? WHERE user_id = ? AND status = ?'),
|
|
121
|
+
incrementTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = tasks_today + 1, tasks_total = tasks_total + 1, last_task_date = date('now') WHERE user_id = ? AND status = 'active'`),
|
|
122
|
+
resetDailyTasks: db.prepare(`UPDATE workspace_subscriptions SET tasks_today = 0 WHERE last_task_date < date('now')`),
|
|
123
|
+
addDealSavings: db.prepare(`UPDATE workspace_subscriptions SET deals_completed = deals_completed + 1, total_savings = total_savings + ? WHERE user_id = ? AND status = 'active'`),
|
|
124
|
+
|
|
125
|
+
insertSession: db.prepare('INSERT INTO workspace_sessions (id, user_id, session_token) VALUES (?, ?, ?)'),
|
|
126
|
+
getSession: db.prepare('SELECT * FROM workspace_sessions WHERE session_token = ? AND active = 1'),
|
|
127
|
+
endSession: db.prepare("UPDATE workspace_sessions SET active = 0 WHERE session_token = ?"),
|
|
128
|
+
|
|
129
|
+
insertDeal: db.prepare('INSERT INTO workspace_deals (id, user_id, task_id, offer_source, offer_title, original_price, final_price, savings, deal_url, requires_login) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
130
|
+
updateDealStatus: db.prepare('UPDATE workspace_deals SET status = ?, completed_at = datetime(\'now\') WHERE id = ?'),
|
|
131
|
+
getUserDeals: db.prepare('SELECT * FROM workspace_deals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'),
|
|
132
|
+
|
|
133
|
+
// Favorites
|
|
134
|
+
insertFavorite: db.prepare('INSERT INTO workspace_favorites (id, user_id, url, title, description, favicon, folder, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'),
|
|
135
|
+
getFavorites: db.prepare('SELECT * FROM workspace_favorites WHERE user_id = ? ORDER BY folder, position, created_at DESC'),
|
|
136
|
+
getFavoritesByFolder: db.prepare('SELECT * FROM workspace_favorites WHERE user_id = ? AND folder = ? ORDER BY position, created_at DESC'),
|
|
137
|
+
deleteFavorite: db.prepare('DELETE FROM workspace_favorites WHERE id = ? AND user_id = ?'),
|
|
138
|
+
deleteFavoriteByUrl: db.prepare('DELETE FROM workspace_favorites WHERE user_id = ? AND url = ?'),
|
|
139
|
+
updateFavorite: db.prepare('UPDATE workspace_favorites SET title = ?, folder = ?, position = ? WHERE id = ? AND user_id = ?'),
|
|
140
|
+
|
|
141
|
+
// History
|
|
142
|
+
upsertHistory: db.prepare(`INSERT INTO workspace_history (id, user_id, url, title, visit_count, last_visited)
|
|
143
|
+
VALUES (?, ?, ?, ?, 1, datetime('now'))
|
|
144
|
+
ON CONFLICT(user_id, url) DO UPDATE SET
|
|
145
|
+
title = excluded.title,
|
|
146
|
+
visit_count = visit_count + 1,
|
|
147
|
+
last_visited = datetime('now')`),
|
|
148
|
+
getHistory: db.prepare('SELECT * FROM workspace_history WHERE user_id = ? ORDER BY last_visited DESC LIMIT ? OFFSET ?'),
|
|
149
|
+
searchHistory: db.prepare("SELECT * FROM workspace_history WHERE user_id = ? AND (url LIKE ? OR title LIKE ?) ORDER BY last_visited DESC LIMIT ?"),
|
|
150
|
+
deleteHistoryItem: db.prepare('DELETE FROM workspace_history WHERE id = ? AND user_id = ?'),
|
|
151
|
+
clearHistory: db.prepare('DELETE FROM workspace_history WHERE user_id = ?'),
|
|
152
|
+
|
|
153
|
+
logEvent: db.prepare('INSERT INTO workspace_analytics (id, user_id, event_type, event_data) VALUES (?, ?, ?, ?)'),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Reset daily task counts
|
|
157
|
+
try { stmts.resetDailyTasks.run(); } catch (_) {}
|
|
158
|
+
|
|
159
|
+
// ─── User Routes ─────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* GET /api/workspace/subscription — Get user's workspace subscription
|
|
163
|
+
*/
|
|
164
|
+
router.get('/subscription', authenticateToken, (req, res) => {
|
|
165
|
+
let sub = stmts.getSub.get(req.user.id, 'active');
|
|
166
|
+
|
|
167
|
+
if (!sub) {
|
|
168
|
+
// Auto-create free subscription
|
|
169
|
+
const id = crypto.randomUUID();
|
|
170
|
+
stmts.insertSub.run(id, req.user.id, 'free');
|
|
171
|
+
sub = stmts.getSub.get(req.user.id, 'active');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
|
|
175
|
+
const remaining = limits.dailyTasks < 0 ? -1 : Math.max(0, limits.dailyTasks - (sub.tasks_today || 0));
|
|
176
|
+
|
|
177
|
+
res.json({
|
|
178
|
+
plan: sub.plan,
|
|
179
|
+
status: sub.status,
|
|
180
|
+
tasksToday: sub.tasks_today,
|
|
181
|
+
tasksTotal: sub.tasks_total,
|
|
182
|
+
dealsCompleted: sub.deals_completed,
|
|
183
|
+
totalSavings: sub.total_savings,
|
|
184
|
+
limits,
|
|
185
|
+
remainingTasks: remaining,
|
|
186
|
+
createdAt: sub.created_at,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* POST /api/workspace/subscription — Upgrade/change plan
|
|
192
|
+
*/
|
|
193
|
+
router.post('/subscription', authenticateToken, (req, res) => {
|
|
194
|
+
const { plan } = req.body;
|
|
195
|
+
if (!PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
|
|
196
|
+
|
|
197
|
+
let sub = stmts.getSub.get(req.user.id, 'active');
|
|
198
|
+
if (sub) {
|
|
199
|
+
stmts.updateSubPlan.run(plan, 'active', req.user.id, 'active');
|
|
200
|
+
} else {
|
|
201
|
+
const id = crypto.randomUUID();
|
|
202
|
+
stmts.insertSub.run(id, req.user.id, plan);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logEvent(req.user.id, 'plan_changed', { plan });
|
|
206
|
+
res.json({ success: true, plan });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* POST /api/workspace/check-limits — Check if user can execute a task
|
|
211
|
+
*/
|
|
212
|
+
router.post('/check-limits', authenticateToken, (req, res) => {
|
|
213
|
+
const sub = stmts.getSub.get(req.user.id, 'active');
|
|
214
|
+
if (!sub) return res.json({ allowed: true, plan: 'free' }); // New user
|
|
215
|
+
|
|
216
|
+
const limits = PLAN_LIMITS[sub.plan] || PLAN_LIMITS.free;
|
|
217
|
+
if (limits.dailyTasks >= 0 && sub.tasks_today >= limits.dailyTasks) {
|
|
218
|
+
return res.json({
|
|
219
|
+
allowed: false,
|
|
220
|
+
reason: 'daily_limit',
|
|
221
|
+
plan: sub.plan,
|
|
222
|
+
limit: limits.dailyTasks,
|
|
223
|
+
used: sub.tasks_today,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
stmts.incrementTasks.run(req.user.id);
|
|
228
|
+
res.json({ allowed: true, plan: sub.plan, remaining: limits.dailyTasks < 0 ? -1 : limits.dailyTasks - sub.tasks_today - 1 });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* POST /api/workspace/deal — Record a deal action
|
|
233
|
+
*/
|
|
234
|
+
router.post('/deal', authenticateToken, (req, res) => {
|
|
235
|
+
const { taskId, source, title, originalPrice, finalPrice, url, requiresLogin } = req.body;
|
|
236
|
+
const savings = originalPrice && finalPrice ? originalPrice - finalPrice : 0;
|
|
237
|
+
const id = crypto.randomUUID();
|
|
238
|
+
|
|
239
|
+
stmts.insertDeal.run(id, req.user.id, taskId || null, source || '', title || '', originalPrice || 0, finalPrice || 0, savings, url || '', requiresLogin ? 1 : 0);
|
|
240
|
+
logEvent(req.user.id, 'deal_created', { dealId: id, source, savings });
|
|
241
|
+
|
|
242
|
+
res.json({ dealId: id, savings });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* POST /api/workspace/deal/:id/status — Update deal status
|
|
247
|
+
*/
|
|
248
|
+
router.post('/deal/:id/status', authenticateToken, (req, res) => {
|
|
249
|
+
const { status } = req.body;
|
|
250
|
+
const valid = ['clicked', 'agent_executing', 'login_required', 'completed', 'failed'];
|
|
251
|
+
if (!valid.includes(status)) return res.status(400).json({ error: 'Invalid status' });
|
|
252
|
+
|
|
253
|
+
stmts.updateDealStatus.run(status, req.params.id);
|
|
254
|
+
|
|
255
|
+
if (status === 'completed') {
|
|
256
|
+
// Update savings in subscription
|
|
257
|
+
const deal = db.prepare('SELECT savings FROM workspace_deals WHERE id = ?').get(req.params.id);
|
|
258
|
+
if (deal) {
|
|
259
|
+
stmts.addDealSavings.run(deal.savings || 0, req.user.id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
logEvent(req.user.id, 'deal_status', { dealId: req.params.id, status });
|
|
264
|
+
res.json({ success: true });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* GET /api/workspace/deals — Get user's deal history
|
|
269
|
+
*/
|
|
270
|
+
router.get('/deals', authenticateToken, (req, res) => {
|
|
271
|
+
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
272
|
+
const deals = stmts.getUserDeals.all(req.user.id, limit);
|
|
273
|
+
res.json({ deals });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* POST /api/workspace/event — Log workspace analytics event
|
|
278
|
+
*/
|
|
279
|
+
router.post('/event', authenticateToken, (req, res) => {
|
|
280
|
+
const { type, data } = req.body;
|
|
281
|
+
if (!type) return res.status(400).json({ error: 'Event type required' });
|
|
282
|
+
logEvent(req.user.id, type, data || {});
|
|
283
|
+
res.json({ ok: true });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── Favorites API ───────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* GET /api/workspace/favorites — Get user's bookmarks
|
|
290
|
+
*/
|
|
291
|
+
router.get('/favorites', authenticateToken, (req, res) => {
|
|
292
|
+
const folder = req.query.folder;
|
|
293
|
+
const favorites = folder
|
|
294
|
+
? stmts.getFavoritesByFolder.all(req.user.id, folder)
|
|
295
|
+
: stmts.getFavorites.all(req.user.id);
|
|
296
|
+
res.json({ favorites });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* POST /api/workspace/favorites — Add a bookmark
|
|
301
|
+
*/
|
|
302
|
+
router.post('/favorites', authenticateToken, (req, res) => {
|
|
303
|
+
const { url, title, description, favicon, folder } = req.body;
|
|
304
|
+
if (!url) return res.status(400).json({ error: 'URL is required' });
|
|
305
|
+
const id = crypto.randomUUID();
|
|
306
|
+
const maxPos = db.prepare('SELECT MAX(position) as m FROM workspace_favorites WHERE user_id = ? AND folder = ?')
|
|
307
|
+
.get(req.user.id, folder || 'default');
|
|
308
|
+
const position = (maxPos?.m || 0) + 1;
|
|
309
|
+
try {
|
|
310
|
+
stmts.insertFavorite.run(id, req.user.id, url, title || '', description || '', favicon || '', folder || 'default', position);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: 'Already bookmarked' });
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
logEvent(req.user.id, 'favorite_added', { url });
|
|
316
|
+
res.json({ id, url, title, folder: folder || 'default', position });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* PUT /api/workspace/favorites/:id — Update a bookmark
|
|
321
|
+
*/
|
|
322
|
+
router.put('/favorites/:id', authenticateToken, (req, res) => {
|
|
323
|
+
const { title, folder, position } = req.body;
|
|
324
|
+
const changes = stmts.updateFavorite.run(title ?? '', folder ?? 'default', position ?? 0, req.params.id, req.user.id);
|
|
325
|
+
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
326
|
+
res.json({ success: true });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* DELETE /api/workspace/favorites/:id — Remove a bookmark by ID
|
|
331
|
+
*/
|
|
332
|
+
router.delete('/favorites/:id', authenticateToken, (req, res) => {
|
|
333
|
+
const changes = stmts.deleteFavorite.run(req.params.id, req.user.id);
|
|
334
|
+
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
335
|
+
logEvent(req.user.id, 'favorite_removed', { id: req.params.id });
|
|
336
|
+
res.json({ success: true });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* DELETE /api/workspace/favorites?url=... — Remove a bookmark by URL
|
|
341
|
+
*/
|
|
342
|
+
router.delete('/favorites', authenticateToken, (req, res) => {
|
|
343
|
+
const { url } = req.query;
|
|
344
|
+
if (!url) return res.status(400).json({ error: 'URL query parameter required' });
|
|
345
|
+
const changes = stmts.deleteFavoriteByUrl.run(req.user.id, url);
|
|
346
|
+
if (!changes.changes) return res.status(404).json({ error: 'Favorite not found' });
|
|
347
|
+
logEvent(req.user.id, 'favorite_removed', { url });
|
|
348
|
+
res.json({ success: true });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ─── History API ─────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* GET /api/workspace/history — Get browsing history
|
|
355
|
+
*/
|
|
356
|
+
router.get('/history', authenticateToken, (req, res) => {
|
|
357
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
358
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
359
|
+
const q = req.query.q;
|
|
360
|
+
let items;
|
|
361
|
+
if (q) {
|
|
362
|
+
const pattern = `%${q}%`;
|
|
363
|
+
items = stmts.searchHistory.all(req.user.id, pattern, pattern, limit);
|
|
364
|
+
} else {
|
|
365
|
+
items = stmts.getHistory.all(req.user.id, limit, offset);
|
|
366
|
+
}
|
|
367
|
+
res.json({ history: items });
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* POST /api/workspace/history — Record a history entry
|
|
372
|
+
*/
|
|
373
|
+
router.post('/history', authenticateToken, (req, res) => {
|
|
374
|
+
const { url, title } = req.body;
|
|
375
|
+
if (!url) return res.status(400).json({ error: 'URL is required' });
|
|
376
|
+
const id = crypto.randomUUID();
|
|
377
|
+
stmts.upsertHistory.run(id, req.user.id, url, title || '');
|
|
378
|
+
res.json({ success: true });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* DELETE /api/workspace/history/:id — Delete a single history entry
|
|
383
|
+
*/
|
|
384
|
+
router.delete('/history/:id', authenticateToken, (req, res) => {
|
|
385
|
+
const changes = stmts.deleteHistoryItem.run(req.params.id, req.user.id);
|
|
386
|
+
if (!changes.changes) return res.status(404).json({ error: 'History entry not found' });
|
|
387
|
+
res.json({ success: true });
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* DELETE /api/workspace/history — Clear all history
|
|
392
|
+
*/
|
|
393
|
+
router.delete('/history', authenticateToken, (req, res) => {
|
|
394
|
+
stmts.clearHistory.run(req.user.id);
|
|
395
|
+
logEvent(req.user.id, 'history_cleared', {});
|
|
396
|
+
res.json({ success: true });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ─── Admin Routes ────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* GET /api/workspace/admin/stats — Global workspace stats
|
|
403
|
+
*/
|
|
404
|
+
router.get('/admin/stats', authenticateAdmin, (req, res) => {
|
|
405
|
+
const totalUsers = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
406
|
+
const activeToday = db.prepare("SELECT COUNT(*) as c FROM workspace_subscriptions WHERE last_task_date = date('now')").get()?.c || 0;
|
|
407
|
+
const totalTasks = db.prepare('SELECT SUM(tasks_total) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
408
|
+
const totalDeals = db.prepare('SELECT SUM(deals_completed) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
409
|
+
const totalSavings = db.prepare('SELECT SUM(total_savings) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
410
|
+
|
|
411
|
+
const planBreakdown = db.prepare(`
|
|
412
|
+
SELECT plan, COUNT(*) as count, SUM(tasks_total) as tasks, SUM(total_savings) as savings
|
|
413
|
+
FROM workspace_subscriptions WHERE status = 'active' GROUP BY plan
|
|
414
|
+
`).all();
|
|
415
|
+
|
|
416
|
+
const recentEvents = db.prepare(`
|
|
417
|
+
SELECT event_type, COUNT(*) as count
|
|
418
|
+
FROM workspace_analytics
|
|
419
|
+
WHERE created_at > datetime('now', '-24 hours')
|
|
420
|
+
GROUP BY event_type ORDER BY count DESC LIMIT 10
|
|
421
|
+
`).all();
|
|
422
|
+
|
|
423
|
+
const dailyTasks = db.prepare(`
|
|
424
|
+
SELECT date(created_at) as day, COUNT(*) as count
|
|
425
|
+
FROM workspace_analytics
|
|
426
|
+
WHERE event_type IN ('task_started','deal_created') AND created_at > datetime('now', '-30 days')
|
|
427
|
+
GROUP BY day ORDER BY day
|
|
428
|
+
`).all();
|
|
429
|
+
|
|
430
|
+
res.json({
|
|
431
|
+
totalUsers,
|
|
432
|
+
activeToday,
|
|
433
|
+
totalTasks,
|
|
434
|
+
totalDeals,
|
|
435
|
+
totalSavings,
|
|
436
|
+
planBreakdown,
|
|
437
|
+
recentEvents,
|
|
438
|
+
dailyTasks,
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* GET /api/workspace/admin/subscriptions — List all subscriptions
|
|
444
|
+
*/
|
|
445
|
+
router.get('/admin/subscriptions', authenticateAdmin, (req, res) => {
|
|
446
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
447
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
448
|
+
const offset = (page - 1) * limit;
|
|
449
|
+
|
|
450
|
+
const subs = db.prepare(`
|
|
451
|
+
SELECT ws.*, u.email, u.name
|
|
452
|
+
FROM workspace_subscriptions ws
|
|
453
|
+
LEFT JOIN users u ON ws.user_id = u.id
|
|
454
|
+
ORDER BY ws.created_at DESC LIMIT ? OFFSET ?
|
|
455
|
+
`).all(limit, offset);
|
|
456
|
+
|
|
457
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM workspace_subscriptions').get()?.c || 0;
|
|
458
|
+
|
|
459
|
+
res.json({ subscriptions: subs, total, page, pages: Math.ceil(total / limit) });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* PUT /api/workspace/admin/subscription/:userId — Admin update subscription
|
|
464
|
+
*/
|
|
465
|
+
router.put('/admin/subscription/:userId', authenticateAdmin, (req, res) => {
|
|
466
|
+
const { plan, status } = req.body;
|
|
467
|
+
if (plan && !PLAN_LIMITS[plan]) return res.status(400).json({ error: 'Invalid plan' });
|
|
468
|
+
|
|
469
|
+
if (plan) {
|
|
470
|
+
stmts.updateSubPlan.run(plan, status || 'active', req.params.userId, 'active');
|
|
471
|
+
}
|
|
472
|
+
if (status && !plan) {
|
|
473
|
+
db.prepare('UPDATE workspace_subscriptions SET status = ? WHERE user_id = ? AND status = ?')
|
|
474
|
+
.run(status, req.params.userId, 'active');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
res.json({ success: true });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* GET /api/workspace/admin/deals — List all deals
|
|
482
|
+
*/
|
|
483
|
+
router.get('/admin/deals', authenticateAdmin, (req, res) => {
|
|
484
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
485
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
486
|
+
const offset = (page - 1) * limit;
|
|
487
|
+
|
|
488
|
+
const deals = db.prepare(`
|
|
489
|
+
SELECT wd.*, u.email, u.name as user_name
|
|
490
|
+
FROM workspace_deals wd
|
|
491
|
+
LEFT JOIN users u ON wd.user_id = u.id
|
|
492
|
+
ORDER BY wd.created_at DESC LIMIT ? OFFSET ?
|
|
493
|
+
`).all(limit, offset);
|
|
494
|
+
|
|
495
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM workspace_deals').get()?.c || 0;
|
|
496
|
+
const totalSavings = db.prepare('SELECT SUM(savings) as s FROM workspace_deals WHERE status = ?').get('completed')?.s || 0;
|
|
497
|
+
|
|
498
|
+
res.json({ deals, total, totalSavings, page, pages: Math.ceil(total / limit) });
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* GET /api/workspace/admin/analytics — Workspace analytics dashboard
|
|
503
|
+
*/
|
|
504
|
+
router.get('/admin/analytics', authenticateAdmin, (req, res) => {
|
|
505
|
+
const days = Math.min(parseInt(req.query.days) || 30, 90);
|
|
506
|
+
|
|
507
|
+
const eventsByType = db.prepare(`
|
|
508
|
+
SELECT event_type, COUNT(*) as count
|
|
509
|
+
FROM workspace_analytics
|
|
510
|
+
WHERE created_at > datetime('now', '-${days} days')
|
|
511
|
+
GROUP BY event_type ORDER BY count DESC
|
|
512
|
+
`).all();
|
|
513
|
+
|
|
514
|
+
const dailyActivity = db.prepare(`
|
|
515
|
+
SELECT date(created_at) as day, COUNT(*) as events,
|
|
516
|
+
COUNT(DISTINCT user_id) as unique_users
|
|
517
|
+
FROM workspace_analytics
|
|
518
|
+
WHERE created_at > datetime('now', '-${days} days')
|
|
519
|
+
GROUP BY day ORDER BY day
|
|
520
|
+
`).all();
|
|
521
|
+
|
|
522
|
+
const topUsers = db.prepare(`
|
|
523
|
+
SELECT ws.user_id, u.email, u.name, ws.plan, ws.tasks_total, ws.deals_completed, ws.total_savings
|
|
524
|
+
FROM workspace_subscriptions ws
|
|
525
|
+
LEFT JOIN users u ON ws.user_id = u.id
|
|
526
|
+
ORDER BY ws.tasks_total DESC LIMIT 20
|
|
527
|
+
`).all();
|
|
528
|
+
|
|
529
|
+
res.json({ eventsByType, dailyActivity, topUsers });
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
function logEvent(userId, type, data) {
|
|
535
|
+
try {
|
|
536
|
+
stmts.logEvent.run(crypto.randomUUID(), userId || null, type, JSON.stringify(data));
|
|
537
|
+
} catch (_) {}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = router;
|