@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.4
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/.agent/workflows/dev-workflow-bugfix.md +250 -250
- package/.agent/workflows/dev-workflow-feature.md +201 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
- package/.agent/workflows/dev-workflow-refactor.md +238 -238
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
- package/.github/pull_request_template.md +22 -22
- package/.github/workflows/github-release.yml +56 -0
- package/.github/workflows/npm-publish.yml +50 -50
- package/.husky/commit-msg +0 -0
- package/CHANGELOG.md +759 -741
- package/CODE_OF_CONDUCT.md +79 -79
- package/CONTRIBUTING.md +60 -60
- package/LICENSE +21 -21
- package/README.md +632 -632
- package/SECURITY.md +20 -20
- package/db/index.js +204 -204
- package/docs/NETLIFY_DEPLOYMENT.md +125 -125
- package/docs/PLESK_DEPLOYMENT.md +170 -170
- package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
- package/docs/antigravity/EXAMPLES.md +535 -535
- package/docs/antigravity/GETTING-STARTED.md +292 -292
- package/docs/antigravity/TROUBLESHOOTING.md +742 -742
- package/docs/antigravity/WALKTHROUGH.md +25 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
- package/docs/product/PRD.md +67 -67
- package/docs/release-notes/RELEASE_NOTES.md +233 -226
- package/docs/web-dashboard.md +175 -175
- package/exec.js +49 -49
- package/index.js +279 -279
- package/install.js +73 -73
- package/package.json +4 -4
- package/release-wrapper.js +192 -192
- package/scripts/build-docs.js +63 -63
- package/scripts/extract-release-notes.cjs +78 -0
- package/scripts/hello-world.js +1 -1
- package/scripts/release.cjs +252 -252
- package/scripts/release.js +252 -0
- package/scripts/update-release-notes.cjs +178 -178
- package/scripts/update-release-notes.js +182 -0
- package/server.json +23 -23
- package/tailwind.config.js +8 -8
- package/tests/build.test.js +83 -83
- package/tests/exec.test.js +35 -35
- package/tests/handlers.test.js +1058 -1058
- package/tests/hello-world.test.js +13 -13
- package/tests/mcpRegistry.test.js +65 -65
- package/tests/patch-release.test.js +7 -0
- package/tests/release-wrapper.test.js +107 -107
- package/tests/workflowState.test.js +505 -505
- package/tools/definitions.js +332 -332
- package/tools/handlers.js +1019 -1019
- package/vite.config.js +43 -43
- package/vite.web.config.js +23 -23
- package/web/README.md +36 -36
- package/web/app/docs/page.tsx +202 -202
- package/web/app/globals.css +90 -90
- package/web/app/layout.tsx +36 -36
- package/web/app/page.tsx +15 -15
- package/web/components/ApiReference.tsx +20 -20
- package/web/components/Features.tsx +61 -61
- package/web/components/Footer.tsx +9 -9
- package/web/components/Hero.tsx +96 -96
- package/web/components/History.tsx +143 -143
- package/web/components/Navbar.tsx +64 -64
- package/web/components/Terminal.tsx +114 -114
- package/web/eslint.config.mjs +18 -18
- package/web/lib/db.js +131 -131
- package/web/lib/docs.js +73 -73
- package/web/netlify.toml +3 -3
- package/web/next.config.ts +12 -12
- package/web/package-lock.json +6560 -6560
- package/web/package.json +28 -28
- package/web/postcss.config.mjs +9 -9
- package/web/server.js +31 -31
- package/web/tailwind.config.ts +19 -19
- package/web/tsconfig.json +42 -42
- package/workflow-state.js +612 -612
package/SECURITY.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Supported Versions
|
|
4
|
-
|
|
5
|
-
We release patches for security vulnerabilities under new minor or patch versions. Always use the latest release.
|
|
6
|
-
|
|
7
|
-
## Reporting a Vulnerability
|
|
8
|
-
|
|
9
|
-
If you discover a vulnerability, please email `security@programinglive.com` with:
|
|
10
|
-
|
|
11
|
-
- Description of the issue
|
|
12
|
-
- Steps to reproduce
|
|
13
|
-
- Potential impact
|
|
14
|
-
- Suggested remediation (if known)
|
|
15
|
-
|
|
16
|
-
We aim to acknowledge reports within 72 hours and will keep you informed as we work on a fix. Please do not disclose the vulnerability publicly until we have released an update.
|
|
17
|
-
|
|
18
|
-
## Disclosure Policy
|
|
19
|
-
|
|
20
|
-
Once a fix is available, we will publish a security advisory in the repository releases and notify contributors via the README.
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
We release patches for security vulnerabilities under new minor or patch versions. Always use the latest release.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
If you discover a vulnerability, please email `security@programinglive.com` with:
|
|
10
|
+
|
|
11
|
+
- Description of the issue
|
|
12
|
+
- Steps to reproduce
|
|
13
|
+
- Potential impact
|
|
14
|
+
- Suggested remediation (if known)
|
|
15
|
+
|
|
16
|
+
We aim to acknowledge reports within 72 hours and will keep you informed as we work on a fix. Please do not disclose the vulnerability publicly until we have released an update.
|
|
17
|
+
|
|
18
|
+
## Disclosure Policy
|
|
19
|
+
|
|
20
|
+
Once a fix is available, we will publish a security advisory in the repository releases and notify contributors via the README.
|
package/db/index.js
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
const DB_FILENAME = "dev-workflow.db";
|
|
9
|
-
const FREQUENCY_FORMATS = {
|
|
10
|
-
daily: "%Y-%m-%d",
|
|
11
|
-
monthly: "%Y-%m",
|
|
12
|
-
yearly: "%Y",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function getDbPath() {
|
|
16
|
-
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
17
|
-
let projectRoot = initCwd;
|
|
18
|
-
while (projectRoot !== path.dirname(projectRoot)) {
|
|
19
|
-
if (existsSync(path.join(projectRoot, "package.json"))) break;
|
|
20
|
-
projectRoot = path.dirname(projectRoot);
|
|
21
|
-
}
|
|
22
|
-
return path.join(projectRoot, ".state", DB_FILENAME);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let db;
|
|
26
|
-
export function getDb() {
|
|
27
|
-
if (!db) {
|
|
28
|
-
const dbPath = getDbPath();
|
|
29
|
-
db = new Database(dbPath);
|
|
30
|
-
db.pragma("journal_mode = WAL");
|
|
31
|
-
migrate();
|
|
32
|
-
}
|
|
33
|
-
return db;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function migrate() {
|
|
37
|
-
const createHistory = `
|
|
38
|
-
CREATE TABLE IF NOT EXISTS workflow_history (
|
|
39
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
-
user_id TEXT NOT NULL,
|
|
41
|
-
task_description TEXT NOT NULL,
|
|
42
|
-
task_type TEXT NOT NULL,
|
|
43
|
-
commit_message TEXT,
|
|
44
|
-
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
-
forced INTEGER DEFAULT 0,
|
|
46
|
-
force_reason TEXT,
|
|
47
|
-
dropped INTEGER DEFAULT 0,
|
|
48
|
-
drop_reason TEXT,
|
|
49
|
-
UNIQUE(user_id, timestamp, task_description)
|
|
50
|
-
)
|
|
51
|
-
`;
|
|
52
|
-
const createSummary = `
|
|
53
|
-
CREATE TABLE IF NOT EXISTS project_summary (
|
|
54
|
-
user_id TEXT PRIMARY KEY,
|
|
55
|
-
total_tasks INTEGER DEFAULT 0,
|
|
56
|
-
task_types TEXT DEFAULT '{}',
|
|
57
|
-
last_active DATETIME,
|
|
58
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
59
|
-
recent_tasks TEXT DEFAULT '[]'
|
|
60
|
-
)
|
|
61
|
-
`;
|
|
62
|
-
const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
|
|
63
|
-
const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
|
|
64
|
-
|
|
65
|
-
db.exec(createHistory);
|
|
66
|
-
db.exec(createSummary);
|
|
67
|
-
db.exec(idxHistory);
|
|
68
|
-
db.exec(idxSummary);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function insertHistoryEntry(userId, entry) {
|
|
72
|
-
const stmt = getDb().prepare(`
|
|
73
|
-
INSERT OR REPLACE INTO workflow_history
|
|
74
|
-
(user_id, task_description, task_type, commit_message, timestamp, forced, force_reason, dropped, drop_reason)
|
|
75
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
-
`);
|
|
77
|
-
stmt.run(
|
|
78
|
-
userId,
|
|
79
|
-
entry.taskDescription,
|
|
80
|
-
entry.taskType,
|
|
81
|
-
entry.commitMessage,
|
|
82
|
-
entry.timestamp || new Date().toISOString(),
|
|
83
|
-
entry.forced ? 1 : 0,
|
|
84
|
-
entry.forceReason || null,
|
|
85
|
-
entry.dropped ? 1 : 0,
|
|
86
|
-
entry.dropReason || null
|
|
87
|
-
);
|
|
88
|
-
updateSummaryForUser(userId);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function updateSummaryForUser(userId) {
|
|
92
|
-
const getHistory = getDb().prepare(`
|
|
93
|
-
SELECT task_type, timestamp, task_description
|
|
94
|
-
FROM workflow_history
|
|
95
|
-
WHERE user_id = ?
|
|
96
|
-
ORDER BY timestamp DESC
|
|
97
|
-
LIMIT 20
|
|
98
|
-
`);
|
|
99
|
-
const rows = getHistory.all(userId);
|
|
100
|
-
|
|
101
|
-
const taskTypes = {};
|
|
102
|
-
for (const row of rows) {
|
|
103
|
-
taskTypes[row.task_type] = (taskTypes[row.task_type] || 0) + 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const recentTasks = rows.slice(0, 5).map(r => ({
|
|
107
|
-
description: r.task_description,
|
|
108
|
-
type: r.task_type,
|
|
109
|
-
timestamp: r.timestamp,
|
|
110
|
-
}));
|
|
111
|
-
|
|
112
|
-
const lastActive = rows[0]?.timestamp || null;
|
|
113
|
-
|
|
114
|
-
const upsert = getDb().prepare(`
|
|
115
|
-
INSERT OR REPLACE INTO project_summary
|
|
116
|
-
(user_id, total_tasks, task_types, last_active, recent_tasks, updated_at)
|
|
117
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
118
|
-
`);
|
|
119
|
-
upsert.run(
|
|
120
|
-
userId,
|
|
121
|
-
rows.length,
|
|
122
|
-
JSON.stringify(taskTypes),
|
|
123
|
-
lastActive,
|
|
124
|
-
JSON.stringify(recentTasks),
|
|
125
|
-
new Date().toISOString()
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function getSummaryForUser(userId) {
|
|
130
|
-
const stmt = getDb().prepare(`
|
|
131
|
-
SELECT * FROM project_summary WHERE user_id = ?
|
|
132
|
-
`);
|
|
133
|
-
const row = stmt.get(userId);
|
|
134
|
-
if (!row) return null;
|
|
135
|
-
return {
|
|
136
|
-
totalTasks: row.total_tasks,
|
|
137
|
-
taskTypes: JSON.parse(row.task_types),
|
|
138
|
-
lastActive: row.last_active,
|
|
139
|
-
recentTasks: JSON.parse(row.recent_tasks),
|
|
140
|
-
updatedAt: row.updated_at,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function buildHistoryFilters({ startDate, endDate }) {
|
|
145
|
-
const clauses = [];
|
|
146
|
-
const params = [];
|
|
147
|
-
if (startDate) {
|
|
148
|
-
clauses.push("DATE(timestamp) >= DATE(?)");
|
|
149
|
-
params.push(startDate);
|
|
150
|
-
}
|
|
151
|
-
if (endDate) {
|
|
152
|
-
clauses.push("DATE(timestamp) <= DATE(?)");
|
|
153
|
-
params.push(endDate);
|
|
154
|
-
}
|
|
155
|
-
return { clauses, params };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function getHistoryForUser(userId, options = {}) {
|
|
159
|
-
const { page = 1, pageSize = 20, startDate, endDate } = options;
|
|
160
|
-
const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
|
|
161
|
-
const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
|
|
162
|
-
const offset = (sanitizedPage - 1) * sanitizedPageSize;
|
|
163
|
-
|
|
164
|
-
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
165
|
-
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
166
|
-
const dbInstance = getDb();
|
|
167
|
-
|
|
168
|
-
const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
|
|
169
|
-
const total = countStmt.get(userId, ...params).total;
|
|
170
|
-
|
|
171
|
-
const rowsStmt = dbInstance.prepare(`
|
|
172
|
-
SELECT *
|
|
173
|
-
FROM workflow_history
|
|
174
|
-
WHERE ${where}
|
|
175
|
-
ORDER BY timestamp DESC
|
|
176
|
-
LIMIT ?
|
|
177
|
-
OFFSET ?
|
|
178
|
-
`);
|
|
179
|
-
const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
entries,
|
|
183
|
-
total,
|
|
184
|
-
page: sanitizedPage,
|
|
185
|
-
pageSize: sanitizedPageSize,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function getHistorySummary(userId, options = {}) {
|
|
190
|
-
const { startDate, endDate, frequency = "daily" } = options;
|
|
191
|
-
const format = FREQUENCY_FORMATS[frequency] || FREQUENCY_FORMATS.daily;
|
|
192
|
-
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
193
|
-
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
194
|
-
const stmt = getDb().prepare(`
|
|
195
|
-
SELECT strftime('${format}', timestamp) AS period, COUNT(*) AS count
|
|
196
|
-
FROM workflow_history
|
|
197
|
-
WHERE ${where}
|
|
198
|
-
GROUP BY period
|
|
199
|
-
ORDER BY period DESC
|
|
200
|
-
`);
|
|
201
|
-
return stmt
|
|
202
|
-
.all(userId, ...params)
|
|
203
|
-
.map((row) => ({ period: row.period, count: row.count }));
|
|
204
|
-
}
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const DB_FILENAME = "dev-workflow.db";
|
|
9
|
+
const FREQUENCY_FORMATS = {
|
|
10
|
+
daily: "%Y-%m-%d",
|
|
11
|
+
monthly: "%Y-%m",
|
|
12
|
+
yearly: "%Y",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getDbPath() {
|
|
16
|
+
const initCwd = process.env.INIT_CWD || process.cwd();
|
|
17
|
+
let projectRoot = initCwd;
|
|
18
|
+
while (projectRoot !== path.dirname(projectRoot)) {
|
|
19
|
+
if (existsSync(path.join(projectRoot, "package.json"))) break;
|
|
20
|
+
projectRoot = path.dirname(projectRoot);
|
|
21
|
+
}
|
|
22
|
+
return path.join(projectRoot, ".state", DB_FILENAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let db;
|
|
26
|
+
export function getDb() {
|
|
27
|
+
if (!db) {
|
|
28
|
+
const dbPath = getDbPath();
|
|
29
|
+
db = new Database(dbPath);
|
|
30
|
+
db.pragma("journal_mode = WAL");
|
|
31
|
+
migrate();
|
|
32
|
+
}
|
|
33
|
+
return db;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function migrate() {
|
|
37
|
+
const createHistory = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS workflow_history (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
user_id TEXT NOT NULL,
|
|
41
|
+
task_description TEXT NOT NULL,
|
|
42
|
+
task_type TEXT NOT NULL,
|
|
43
|
+
commit_message TEXT,
|
|
44
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
forced INTEGER DEFAULT 0,
|
|
46
|
+
force_reason TEXT,
|
|
47
|
+
dropped INTEGER DEFAULT 0,
|
|
48
|
+
drop_reason TEXT,
|
|
49
|
+
UNIQUE(user_id, timestamp, task_description)
|
|
50
|
+
)
|
|
51
|
+
`;
|
|
52
|
+
const createSummary = `
|
|
53
|
+
CREATE TABLE IF NOT EXISTS project_summary (
|
|
54
|
+
user_id TEXT PRIMARY KEY,
|
|
55
|
+
total_tasks INTEGER DEFAULT 0,
|
|
56
|
+
task_types TEXT DEFAULT '{}',
|
|
57
|
+
last_active DATETIME,
|
|
58
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
59
|
+
recent_tasks TEXT DEFAULT '[]'
|
|
60
|
+
)
|
|
61
|
+
`;
|
|
62
|
+
const idxHistory = `CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id)`;
|
|
63
|
+
const idxSummary = `CREATE INDEX IF NOT EXISTS idx_project_summary_user ON project_summary(user_id)`;
|
|
64
|
+
|
|
65
|
+
db.exec(createHistory);
|
|
66
|
+
db.exec(createSummary);
|
|
67
|
+
db.exec(idxHistory);
|
|
68
|
+
db.exec(idxSummary);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function insertHistoryEntry(userId, entry) {
|
|
72
|
+
const stmt = getDb().prepare(`
|
|
73
|
+
INSERT OR REPLACE INTO workflow_history
|
|
74
|
+
(user_id, task_description, task_type, commit_message, timestamp, forced, force_reason, dropped, drop_reason)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
`);
|
|
77
|
+
stmt.run(
|
|
78
|
+
userId,
|
|
79
|
+
entry.taskDescription,
|
|
80
|
+
entry.taskType,
|
|
81
|
+
entry.commitMessage,
|
|
82
|
+
entry.timestamp || new Date().toISOString(),
|
|
83
|
+
entry.forced ? 1 : 0,
|
|
84
|
+
entry.forceReason || null,
|
|
85
|
+
entry.dropped ? 1 : 0,
|
|
86
|
+
entry.dropReason || null
|
|
87
|
+
);
|
|
88
|
+
updateSummaryForUser(userId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function updateSummaryForUser(userId) {
|
|
92
|
+
const getHistory = getDb().prepare(`
|
|
93
|
+
SELECT task_type, timestamp, task_description
|
|
94
|
+
FROM workflow_history
|
|
95
|
+
WHERE user_id = ?
|
|
96
|
+
ORDER BY timestamp DESC
|
|
97
|
+
LIMIT 20
|
|
98
|
+
`);
|
|
99
|
+
const rows = getHistory.all(userId);
|
|
100
|
+
|
|
101
|
+
const taskTypes = {};
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
taskTypes[row.task_type] = (taskTypes[row.task_type] || 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const recentTasks = rows.slice(0, 5).map(r => ({
|
|
107
|
+
description: r.task_description,
|
|
108
|
+
type: r.task_type,
|
|
109
|
+
timestamp: r.timestamp,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const lastActive = rows[0]?.timestamp || null;
|
|
113
|
+
|
|
114
|
+
const upsert = getDb().prepare(`
|
|
115
|
+
INSERT OR REPLACE INTO project_summary
|
|
116
|
+
(user_id, total_tasks, task_types, last_active, recent_tasks, updated_at)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
118
|
+
`);
|
|
119
|
+
upsert.run(
|
|
120
|
+
userId,
|
|
121
|
+
rows.length,
|
|
122
|
+
JSON.stringify(taskTypes),
|
|
123
|
+
lastActive,
|
|
124
|
+
JSON.stringify(recentTasks),
|
|
125
|
+
new Date().toISOString()
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getSummaryForUser(userId) {
|
|
130
|
+
const stmt = getDb().prepare(`
|
|
131
|
+
SELECT * FROM project_summary WHERE user_id = ?
|
|
132
|
+
`);
|
|
133
|
+
const row = stmt.get(userId);
|
|
134
|
+
if (!row) return null;
|
|
135
|
+
return {
|
|
136
|
+
totalTasks: row.total_tasks,
|
|
137
|
+
taskTypes: JSON.parse(row.task_types),
|
|
138
|
+
lastActive: row.last_active,
|
|
139
|
+
recentTasks: JSON.parse(row.recent_tasks),
|
|
140
|
+
updatedAt: row.updated_at,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildHistoryFilters({ startDate, endDate }) {
|
|
145
|
+
const clauses = [];
|
|
146
|
+
const params = [];
|
|
147
|
+
if (startDate) {
|
|
148
|
+
clauses.push("DATE(timestamp) >= DATE(?)");
|
|
149
|
+
params.push(startDate);
|
|
150
|
+
}
|
|
151
|
+
if (endDate) {
|
|
152
|
+
clauses.push("DATE(timestamp) <= DATE(?)");
|
|
153
|
+
params.push(endDate);
|
|
154
|
+
}
|
|
155
|
+
return { clauses, params };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getHistoryForUser(userId, options = {}) {
|
|
159
|
+
const { page = 1, pageSize = 20, startDate, endDate } = options;
|
|
160
|
+
const sanitizedPage = Math.max(1, Number.isFinite(Number(page)) ? Math.floor(Number(page)) : 1);
|
|
161
|
+
const sanitizedPageSize = Math.min(100, Math.max(1, Number.isFinite(Number(pageSize)) ? Math.floor(Number(pageSize)) : 20));
|
|
162
|
+
const offset = (sanitizedPage - 1) * sanitizedPageSize;
|
|
163
|
+
|
|
164
|
+
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
165
|
+
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
166
|
+
const dbInstance = getDb();
|
|
167
|
+
|
|
168
|
+
const countStmt = dbInstance.prepare(`SELECT COUNT(*) as total FROM workflow_history WHERE ${where}`);
|
|
169
|
+
const total = countStmt.get(userId, ...params).total;
|
|
170
|
+
|
|
171
|
+
const rowsStmt = dbInstance.prepare(`
|
|
172
|
+
SELECT *
|
|
173
|
+
FROM workflow_history
|
|
174
|
+
WHERE ${where}
|
|
175
|
+
ORDER BY timestamp DESC
|
|
176
|
+
LIMIT ?
|
|
177
|
+
OFFSET ?
|
|
178
|
+
`);
|
|
179
|
+
const entries = rowsStmt.all(userId, ...params, sanitizedPageSize, offset);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
entries,
|
|
183
|
+
total,
|
|
184
|
+
page: sanitizedPage,
|
|
185
|
+
pageSize: sanitizedPageSize,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getHistorySummary(userId, options = {}) {
|
|
190
|
+
const { startDate, endDate, frequency = "daily" } = options;
|
|
191
|
+
const format = FREQUENCY_FORMATS[frequency] || FREQUENCY_FORMATS.daily;
|
|
192
|
+
const { clauses, params } = buildHistoryFilters({ startDate, endDate });
|
|
193
|
+
const where = ["user_id = ?", ...clauses].join(" AND ");
|
|
194
|
+
const stmt = getDb().prepare(`
|
|
195
|
+
SELECT strftime('${format}', timestamp) AS period, COUNT(*) AS count
|
|
196
|
+
FROM workflow_history
|
|
197
|
+
WHERE ${where}
|
|
198
|
+
GROUP BY period
|
|
199
|
+
ORDER BY period DESC
|
|
200
|
+
`);
|
|
201
|
+
return stmt
|
|
202
|
+
.all(userId, ...params)
|
|
203
|
+
.map((row) => ({ period: row.period, count: row.count }));
|
|
204
|
+
}
|