devguard 0.2.0 → 0.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/README.md +40 -9
- package/dist/index.js +2 -0
- package/dist/tools/daily-view.d.ts +2 -0
- package/dist/tools/daily-view.js +87 -0
- package/dist/utils/daily-view-html.d.ts +6 -0
- package/dist/utils/daily-view-html.js +596 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# devguard
|
|
2
2
|
|
|
3
|
-
MCP server that
|
|
3
|
+
MCP server that keeps a dev diary for you — what changed, what decisions were made, what broke, what's next. Picks up where you left off so you never lose context between sessions.
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|
|
7
|
-
You vibe code for 3 hours, close your laptop, and come back the next day with no idea what you were doing.
|
|
7
|
+
You vibe code for 3 hours, close your laptop, and come back the next day with no idea what you were doing. Devguard fixes that. It reads your git state, tracks your branches, and writes diary entries automatically.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
Add to Claude Code:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
claude mcp add
|
|
14
|
+
claude mcp add devguard -- npx devguard
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
That's it. On first run,
|
|
18
|
-
- Adds `.
|
|
17
|
+
That's it. On first run, devguard automatically:
|
|
18
|
+
- Adds `.devguard/` to your `.gitignore`
|
|
19
19
|
- Adds an auto-logging instruction to your `CLAUDE.md` (or `.cursorrules` if that exists)
|
|
20
20
|
|
|
21
21
|
From then on, your AI writes diary entries on its own — after finishing a feature, after a big commit, before context gets lost. You never think about it. The diary just fills itself.
|
|
@@ -25,9 +25,40 @@ From then on, your AI writes diary entries on its own — after finishing a feat
|
|
|
25
25
|
| Tool | What it does |
|
|
26
26
|
|------|-------------|
|
|
27
27
|
| `get_context` | Reads git branch, status, recent commits, and diffs |
|
|
28
|
-
| `write_entry` | Saves a diary entry
|
|
28
|
+
| `write_entry` | Saves a diary entry with what changed, decisions, issues, and next steps |
|
|
29
29
|
| `read_entries` | Reads recent entries to catch you up |
|
|
30
|
-
| `catch_me_up` | Morning briefing — diary entries + git state in one shot |
|
|
30
|
+
| `catch_me_up` | Morning briefing — diary entries + git state + branch map in one shot |
|
|
31
|
+
| `branch_map` | Opens a visual branch map in your browser |
|
|
31
32
|
| `setup` | Re-run setup manually if needed |
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## Branch Map
|
|
35
|
+
|
|
36
|
+
Run `branch_map` to open an interactive HTML visualization of your repo in the browser. It shows:
|
|
37
|
+
|
|
38
|
+
- **All branches** with status (ahead/behind main), files changed, and latest commit
|
|
39
|
+
- **Collapsible diary summaries** per branch — what changed, decisions made, issues hit, next steps
|
|
40
|
+
- **Commit navigator** — click any branch to explore its commits on a visual timeline
|
|
41
|
+
- **Per-commit detail** — files changed, insertions/deletions, and diary entries linked to each commit
|
|
42
|
+
- **Auto-generated summaries** for commits without diary entries — categorized by type (Feature, Fix, Refactor, etc.) with affected areas and change scale
|
|
43
|
+
|
|
44
|
+
Designed for people who don't want to think about git.
|
|
45
|
+
|
|
46
|
+
## Branch-Aware Diary
|
|
47
|
+
|
|
48
|
+
Entries are automatically routed by branch:
|
|
49
|
+
- **main/master** → `.devguard/entries/` (daily files)
|
|
50
|
+
- **feature branches** → `.devguard/branches/<branch-name>.md` (one file per branch)
|
|
51
|
+
|
|
52
|
+
This means `catch_me_up` shows you the current branch's full story first, then the main stem, then other active branches — so you always know what's happening everywhere.
|
|
53
|
+
|
|
54
|
+
## How It Works
|
|
55
|
+
|
|
56
|
+
Entries are markdown files stored locally in your project under `.devguard/`. Each entry captures:
|
|
57
|
+
- **Summary** — one-line description of what happened
|
|
58
|
+
- **What Changed** — files modified, features added, bugs fixed
|
|
59
|
+
- **Decisions** — key choices made and why
|
|
60
|
+
- **Issues** — what broke, what's stuck
|
|
61
|
+
- **Next Steps** — what to do next session
|
|
62
|
+
- **Commit hash** — links the entry to a specific commit for traceability
|
|
63
|
+
|
|
64
|
+
Multiple sessions and agents all append to the same file. The diary builds up over time, making summaries richer and the branch map more useful with every session.
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const read_entries_js_1 = require("./tools/read-entries.js");
|
|
|
9
9
|
const catch_me_up_js_1 = require("./tools/catch-me-up.js");
|
|
10
10
|
const setup_js_1 = require("./tools/setup.js");
|
|
11
11
|
const branch_map_js_1 = require("./tools/branch-map.js");
|
|
12
|
+
const daily_view_js_1 = require("./tools/daily-view.js");
|
|
12
13
|
const auto_setup_js_1 = require("./utils/auto-setup.js");
|
|
13
14
|
// Auto-setup on first run: adds .devdiary/ to .gitignore
|
|
14
15
|
// and auto-logging instruction to CLAUDE.md or .cursorrules
|
|
@@ -23,6 +24,7 @@ const server = new mcp_js_1.McpServer({
|
|
|
23
24
|
(0, catch_me_up_js_1.registerCatchMeUp)(server);
|
|
24
25
|
(0, setup_js_1.registerSetup)(server);
|
|
25
26
|
(0, branch_map_js_1.registerBranchMap)(server);
|
|
27
|
+
(0, daily_view_js_1.registerDailyView)(server);
|
|
26
28
|
async function main() {
|
|
27
29
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
28
30
|
await server.connect(transport);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerDailyView = registerDailyView;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const git = __importStar(require("../utils/git.js"));
|
|
39
|
+
const storage = __importStar(require("../utils/storage.js"));
|
|
40
|
+
const daily_view_html_js_1 = require("../utils/daily-view-html.js");
|
|
41
|
+
function registerDailyView(server) {
|
|
42
|
+
server.tool("daily_view", "Opens a calendar dashboard in the browser. Each day shows bullet points of what you worked on — pulled from diary entries across all branches.", {
|
|
43
|
+
project_path: zod_1.z.string().describe("Absolute path to the project directory"),
|
|
44
|
+
}, async ({ project_path }) => {
|
|
45
|
+
if (!git.isGitRepo(project_path)) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: "This project isn't using git yet — nothing to show." }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const currentBranch = git.getBranch(project_path);
|
|
51
|
+
// Collect all diary entries from main stem
|
|
52
|
+
const mainEntries = storage.readEntries(project_path, 90);
|
|
53
|
+
const allParsed = [];
|
|
54
|
+
for (const content of mainEntries) {
|
|
55
|
+
const entries = storage.parseDiaryEntries(content);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const date = entry.date ? entry.date.slice(0, 10) : "";
|
|
58
|
+
if (date)
|
|
59
|
+
allParsed.push({ date, entry });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Collect diary entries from all branches
|
|
63
|
+
const branchFiles = storage.listBranchFiles(project_path);
|
|
64
|
+
for (const bf of branchFiles) {
|
|
65
|
+
const entries = storage.parseDiaryEntries(bf.content);
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const date = entry.date ? entry.date.slice(0, 10) : "";
|
|
68
|
+
if (date)
|
|
69
|
+
allParsed.push({ date, entry });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Group by date
|
|
73
|
+
const dateMap = new Map();
|
|
74
|
+
for (const { date, entry } of allParsed) {
|
|
75
|
+
if (!dateMap.has(date))
|
|
76
|
+
dateMap.set(date, []);
|
|
77
|
+
dateMap.get(date).push(entry);
|
|
78
|
+
}
|
|
79
|
+
const days = Array.from(dateMap.entries())
|
|
80
|
+
.map(([date, entries]) => ({ date, entries }))
|
|
81
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
82
|
+
const filePath = (0, daily_view_html_js_1.generateAndOpenDailyView)(project_path, days, currentBranch);
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `Daily view opened in your browser.\n\nSaved to: ${filePath}` }],
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateAndOpenDailyView = generateAndOpenDailyView;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
function generateAndOpenDailyView(projectPath, days, currentBranch) {
|
|
8
|
+
const projectName = projectPath.split("/").pop() || "project";
|
|
9
|
+
const html = buildHtml(days, projectName, currentBranch);
|
|
10
|
+
const dir = (0, path_1.join)(projectPath, ".devguard");
|
|
11
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
12
|
+
const filePath = (0, path_1.join)(dir, "daily-view.html");
|
|
13
|
+
(0, fs_1.writeFileSync)(filePath, html, "utf-8");
|
|
14
|
+
try {
|
|
15
|
+
const platform = process.platform;
|
|
16
|
+
if (platform === "darwin") {
|
|
17
|
+
(0, child_process_1.execSync)(`open "${filePath}"`, { timeout: 5000 });
|
|
18
|
+
}
|
|
19
|
+
else if (platform === "linux") {
|
|
20
|
+
(0, child_process_1.execSync)(`xdg-open "${filePath}"`, { timeout: 5000 });
|
|
21
|
+
}
|
|
22
|
+
else if (platform === "win32") {
|
|
23
|
+
(0, child_process_1.execSync)(`start "" "${filePath}"`, { timeout: 5000 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Silently fail — file is still written
|
|
28
|
+
}
|
|
29
|
+
return filePath;
|
|
30
|
+
}
|
|
31
|
+
function esc(str) {
|
|
32
|
+
return str
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/</g, "<")
|
|
35
|
+
.replace(/>/g, ">")
|
|
36
|
+
.replace(/"/g, """)
|
|
37
|
+
.replace(/'/g, "'");
|
|
38
|
+
}
|
|
39
|
+
function buildHtml(days, projectName, currentBranch) {
|
|
40
|
+
// Build a JSON-safe data structure for the JS side
|
|
41
|
+
const daysJson = JSON.stringify(days.map((d) => ({
|
|
42
|
+
date: d.date,
|
|
43
|
+
entries: d.entries.map((e) => ({
|
|
44
|
+
title: e.title,
|
|
45
|
+
summary: e.summary,
|
|
46
|
+
date: e.date,
|
|
47
|
+
commit: e.commit,
|
|
48
|
+
whatChanged: e.whatChanged,
|
|
49
|
+
decisions: e.decisions,
|
|
50
|
+
issues: e.issues,
|
|
51
|
+
nextSteps: e.nextSteps,
|
|
52
|
+
})),
|
|
53
|
+
})));
|
|
54
|
+
return `<!DOCTYPE html>
|
|
55
|
+
<html lang="en">
|
|
56
|
+
<head>
|
|
57
|
+
<meta charset="UTF-8">
|
|
58
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
59
|
+
<title>${esc(projectName)} — Daily View</title>
|
|
60
|
+
<style>
|
|
61
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
62
|
+
|
|
63
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
64
|
+
|
|
65
|
+
body {
|
|
66
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
67
|
+
background: #1a1a2e;
|
|
68
|
+
color: #e0e0e0;
|
|
69
|
+
min-height: 100vh;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Header */
|
|
73
|
+
.header {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
padding: 20px 32px;
|
|
78
|
+
border-bottom: 1px solid #2a2a4a;
|
|
79
|
+
background: #16162a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.header h1 {
|
|
83
|
+
font-size: 20px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
color: #fff;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.header h1 span {
|
|
89
|
+
color: #4fc3f7;
|
|
90
|
+
font-weight: 400;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.header .branch-badge {
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
background: #2a2a4a;
|
|
96
|
+
color: #a0a0c0;
|
|
97
|
+
padding: 4px 12px;
|
|
98
|
+
border-radius: 12px;
|
|
99
|
+
font-family: 'JetBrains Mono', monospace;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Nav */
|
|
103
|
+
.nav {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
gap: 16px;
|
|
108
|
+
padding: 16px 32px;
|
|
109
|
+
border-bottom: 1px solid #2a2a4a;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nav button {
|
|
113
|
+
background: #2a2a4a;
|
|
114
|
+
color: #e0e0e0;
|
|
115
|
+
border: none;
|
|
116
|
+
padding: 8px 16px;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
font-family: inherit;
|
|
120
|
+
font-size: 14px;
|
|
121
|
+
transition: background 0.15s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.nav button:hover { background: #3a3a5a; }
|
|
125
|
+
|
|
126
|
+
.nav .month-label {
|
|
127
|
+
font-size: 18px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
min-width: 200px;
|
|
130
|
+
text-align: center;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Calendar grid */
|
|
134
|
+
.calendar-container {
|
|
135
|
+
max-width: 1200px;
|
|
136
|
+
margin: 24px auto;
|
|
137
|
+
padding: 0 24px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.weekday-header {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: repeat(7, 1fr);
|
|
143
|
+
gap: 4px;
|
|
144
|
+
margin-bottom: 4px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.weekday-header div {
|
|
148
|
+
text-align: center;
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: #808098;
|
|
152
|
+
padding: 8px 0;
|
|
153
|
+
text-transform: uppercase;
|
|
154
|
+
letter-spacing: 0.5px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.calendar-grid {
|
|
158
|
+
display: grid;
|
|
159
|
+
grid-template-columns: repeat(7, 1fr);
|
|
160
|
+
gap: 4px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.day-cell {
|
|
164
|
+
min-height: 120px;
|
|
165
|
+
background: #20203a;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
padding: 8px;
|
|
168
|
+
cursor: default;
|
|
169
|
+
transition: background 0.15s, box-shadow 0.15s;
|
|
170
|
+
position: relative;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.day-cell.empty {
|
|
175
|
+
background: transparent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.day-cell.today {
|
|
179
|
+
box-shadow: inset 0 0 0 2px #4fc3f7;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.day-cell.has-entries {
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.day-cell.has-entries:hover {
|
|
187
|
+
background: #2a2a4a;
|
|
188
|
+
box-shadow: 0 2px 12px rgba(79, 195, 247, 0.1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.day-cell.selected {
|
|
192
|
+
background: #2a2a4a;
|
|
193
|
+
box-shadow: inset 0 0 0 2px #4fc3f7, 0 2px 12px rgba(79, 195, 247, 0.15);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.day-number {
|
|
197
|
+
font-size: 13px;
|
|
198
|
+
font-weight: 600;
|
|
199
|
+
color: #808098;
|
|
200
|
+
margin-bottom: 6px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.day-cell.has-entries .day-number {
|
|
204
|
+
color: #4fc3f7;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.day-cell.today .day-number {
|
|
208
|
+
color: #fff;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.day-bullets {
|
|
212
|
+
list-style: none;
|
|
213
|
+
padding: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.day-bullets li {
|
|
217
|
+
font-size: 11px;
|
|
218
|
+
color: #b0b0c8;
|
|
219
|
+
padding: 2px 0;
|
|
220
|
+
line-height: 1.4;
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
text-overflow: ellipsis;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.day-bullets li::before {
|
|
227
|
+
content: "";
|
|
228
|
+
display: inline-block;
|
|
229
|
+
width: 5px;
|
|
230
|
+
height: 5px;
|
|
231
|
+
background: #4fc3f7;
|
|
232
|
+
border-radius: 50%;
|
|
233
|
+
margin-right: 6px;
|
|
234
|
+
vertical-align: middle;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.day-entry-count {
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: 8px;
|
|
240
|
+
right: 8px;
|
|
241
|
+
background: #4fc3f7;
|
|
242
|
+
color: #1a1a2e;
|
|
243
|
+
font-size: 10px;
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
width: 18px;
|
|
246
|
+
height: 18px;
|
|
247
|
+
border-radius: 50%;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* Detail panel */
|
|
254
|
+
.detail-overlay {
|
|
255
|
+
display: none;
|
|
256
|
+
position: fixed;
|
|
257
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
258
|
+
background: rgba(10, 10, 20, 0.7);
|
|
259
|
+
z-index: 100;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.detail-overlay.open { display: flex; align-items: center; justify-content: center; }
|
|
263
|
+
|
|
264
|
+
.detail-panel {
|
|
265
|
+
background: #1e1e38;
|
|
266
|
+
border-radius: 16px;
|
|
267
|
+
width: 90%;
|
|
268
|
+
max-width: 700px;
|
|
269
|
+
max-height: 80vh;
|
|
270
|
+
overflow-y: auto;
|
|
271
|
+
padding: 28px 32px;
|
|
272
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.detail-panel::-webkit-scrollbar { width: 6px; }
|
|
276
|
+
.detail-panel::-webkit-scrollbar-track { background: transparent; }
|
|
277
|
+
.detail-panel::-webkit-scrollbar-thumb { background: #3a3a5a; border-radius: 3px; }
|
|
278
|
+
|
|
279
|
+
.detail-header {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
margin-bottom: 20px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.detail-header h2 {
|
|
287
|
+
font-size: 18px;
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
color: #fff;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.detail-close {
|
|
293
|
+
background: none;
|
|
294
|
+
border: none;
|
|
295
|
+
color: #808098;
|
|
296
|
+
font-size: 22px;
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
padding: 4px 8px;
|
|
299
|
+
border-radius: 6px;
|
|
300
|
+
transition: background 0.15s;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.detail-close:hover { background: #2a2a4a; color: #e0e0e0; }
|
|
304
|
+
|
|
305
|
+
.detail-entry {
|
|
306
|
+
margin-bottom: 20px;
|
|
307
|
+
padding: 16px;
|
|
308
|
+
background: #252545;
|
|
309
|
+
border-radius: 10px;
|
|
310
|
+
border-left: 3px solid #4fc3f7;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.detail-entry:last-child { margin-bottom: 0; }
|
|
314
|
+
|
|
315
|
+
.detail-entry h3 {
|
|
316
|
+
font-size: 15px;
|
|
317
|
+
font-weight: 600;
|
|
318
|
+
color: #e0e0e0;
|
|
319
|
+
margin-bottom: 4px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.detail-entry .entry-meta {
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
color: #808098;
|
|
325
|
+
font-family: 'JetBrains Mono', monospace;
|
|
326
|
+
margin-bottom: 12px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.detail-section {
|
|
330
|
+
margin-top: 10px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.detail-section .section-label {
|
|
334
|
+
font-size: 11px;
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
text-transform: uppercase;
|
|
337
|
+
letter-spacing: 0.5px;
|
|
338
|
+
margin-bottom: 4px;
|
|
339
|
+
display: block;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.detail-section.changes .section-label { color: #4fc3f7; }
|
|
343
|
+
.detail-section.decisions .section-label { color: #ce93d8; }
|
|
344
|
+
.detail-section.issues .section-label { color: #ef9a9a; }
|
|
345
|
+
.detail-section.next-steps .section-label { color: #a5d6a7; }
|
|
346
|
+
|
|
347
|
+
.detail-section ul {
|
|
348
|
+
list-style: none;
|
|
349
|
+
padding: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.detail-section ul li {
|
|
353
|
+
font-size: 13px;
|
|
354
|
+
color: #c0c0d8;
|
|
355
|
+
padding: 3px 0;
|
|
356
|
+
padding-left: 14px;
|
|
357
|
+
position: relative;
|
|
358
|
+
line-height: 1.5;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.detail-section ul li::before {
|
|
362
|
+
content: "";
|
|
363
|
+
position: absolute;
|
|
364
|
+
left: 0;
|
|
365
|
+
top: 10px;
|
|
366
|
+
width: 5px;
|
|
367
|
+
height: 5px;
|
|
368
|
+
border-radius: 50%;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.detail-section.changes ul li::before { background: #4fc3f7; }
|
|
372
|
+
.detail-section.decisions ul li::before { background: #ce93d8; }
|
|
373
|
+
.detail-section.issues ul li::before { background: #ef9a9a; }
|
|
374
|
+
.detail-section.next-steps ul li::before { background: #a5d6a7; }
|
|
375
|
+
|
|
376
|
+
/* Empty state */
|
|
377
|
+
.empty-state {
|
|
378
|
+
text-align: center;
|
|
379
|
+
padding: 80px 32px;
|
|
380
|
+
color: #808098;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.empty-state h2 { font-size: 18px; margin-bottom: 8px; color: #a0a0c0; }
|
|
384
|
+
.empty-state p { font-size: 14px; }
|
|
385
|
+
</style>
|
|
386
|
+
</head>
|
|
387
|
+
<body>
|
|
388
|
+
|
|
389
|
+
<div class="header">
|
|
390
|
+
<h1>${esc(projectName)} <span>/ daily view</span></h1>
|
|
391
|
+
<div class="branch-badge">${esc(currentBranch)}</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div class="nav">
|
|
395
|
+
<button onclick="prevMonth()">← Prev</button>
|
|
396
|
+
<div class="month-label" id="month-label"></div>
|
|
397
|
+
<button onclick="nextMonth()">Next →</button>
|
|
398
|
+
<button onclick="goToday()" style="margin-left: 12px; background: #4fc3f7; color: #1a1a2e; font-weight: 600;">Today</button>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div class="calendar-container">
|
|
402
|
+
<div class="weekday-header">
|
|
403
|
+
<div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div><div>Thu</div><div>Fri</div><div>Sat</div>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="calendar-grid" id="calendar-grid"></div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div class="detail-overlay" id="detail-overlay" onclick="closeDetail(event)">
|
|
409
|
+
<div class="detail-panel" id="detail-panel" onclick="event.stopPropagation()">
|
|
410
|
+
<div class="detail-header">
|
|
411
|
+
<h2 id="detail-title"></h2>
|
|
412
|
+
<button class="detail-close" onclick="closeDetail()">×</button>
|
|
413
|
+
</div>
|
|
414
|
+
<div id="detail-body"></div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<script>
|
|
419
|
+
const DAYS_DATA = ${daysJson};
|
|
420
|
+
|
|
421
|
+
// Index entries by date
|
|
422
|
+
const dateMap = {};
|
|
423
|
+
DAYS_DATA.forEach(d => { dateMap[d.date] = d.entries; });
|
|
424
|
+
|
|
425
|
+
// State
|
|
426
|
+
const today = new Date();
|
|
427
|
+
let viewYear = today.getFullYear();
|
|
428
|
+
let viewMonth = today.getMonth();
|
|
429
|
+
|
|
430
|
+
const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
431
|
+
|
|
432
|
+
function todayStr() {
|
|
433
|
+
const y = today.getFullYear();
|
|
434
|
+
const m = String(today.getMonth() + 1).padStart(2, "0");
|
|
435
|
+
const d = String(today.getDate()).padStart(2, "0");
|
|
436
|
+
return y + "-" + m + "-" + d;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function dateStr(y, m, d) {
|
|
440
|
+
return y + "-" + String(m + 1).padStart(2, "0") + "-" + String(d).padStart(2, "0");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function esc(s) {
|
|
444
|
+
const el = document.createElement("span");
|
|
445
|
+
el.textContent = s;
|
|
446
|
+
return el.innerHTML;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function render() {
|
|
450
|
+
document.getElementById("month-label").textContent = MONTH_NAMES[viewMonth] + " " + viewYear;
|
|
451
|
+
|
|
452
|
+
const grid = document.getElementById("calendar-grid");
|
|
453
|
+
grid.innerHTML = "";
|
|
454
|
+
|
|
455
|
+
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
|
456
|
+
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
457
|
+
const todayString = todayStr();
|
|
458
|
+
|
|
459
|
+
// Empty cells before first day
|
|
460
|
+
for (let i = 0; i < firstDay; i++) {
|
|
461
|
+
const cell = document.createElement("div");
|
|
462
|
+
cell.className = "day-cell empty";
|
|
463
|
+
grid.appendChild(cell);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
467
|
+
const ds = dateStr(viewYear, viewMonth, d);
|
|
468
|
+
const entries = dateMap[ds] || [];
|
|
469
|
+
const isToday = ds === todayString;
|
|
470
|
+
const hasEntries = entries.length > 0;
|
|
471
|
+
|
|
472
|
+
const cell = document.createElement("div");
|
|
473
|
+
cell.className = "day-cell" + (isToday ? " today" : "") + (hasEntries ? " has-entries" : "");
|
|
474
|
+
|
|
475
|
+
if (hasEntries) {
|
|
476
|
+
cell.onclick = function() { openDetail(ds, entries); };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let inner = '<div class="day-number">' + d + '</div>';
|
|
480
|
+
|
|
481
|
+
if (hasEntries) {
|
|
482
|
+
inner += '<div class="day-entry-count">' + entries.length + '</div>';
|
|
483
|
+
inner += '<ul class="day-bullets">';
|
|
484
|
+
// Show up to 4 bullet items from whatChanged across all entries
|
|
485
|
+
const bullets = [];
|
|
486
|
+
for (const e of entries) {
|
|
487
|
+
for (const item of e.whatChanged) {
|
|
488
|
+
bullets.push(item);
|
|
489
|
+
if (bullets.length >= 4) break;
|
|
490
|
+
}
|
|
491
|
+
if (bullets.length >= 4) break;
|
|
492
|
+
}
|
|
493
|
+
if (bullets.length === 0) {
|
|
494
|
+
// Fallback to entry titles
|
|
495
|
+
for (const e of entries) {
|
|
496
|
+
if (e.title) bullets.push(e.title);
|
|
497
|
+
if (bullets.length >= 4) break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const b of bullets) {
|
|
501
|
+
inner += '<li>' + esc(b) + '</li>';
|
|
502
|
+
}
|
|
503
|
+
inner += '</ul>';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
cell.innerHTML = inner;
|
|
507
|
+
grid.appendChild(cell);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function openDetail(ds, entries) {
|
|
512
|
+
const overlay = document.getElementById("detail-overlay");
|
|
513
|
+
const d = new Date(ds + "T12:00:00");
|
|
514
|
+
document.getElementById("detail-title").textContent =
|
|
515
|
+
MONTH_NAMES[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
|
|
516
|
+
|
|
517
|
+
let html = "";
|
|
518
|
+
for (const e of entries) {
|
|
519
|
+
html += '<div class="detail-entry">';
|
|
520
|
+
html += '<h3>' + esc(e.title) + '</h3>';
|
|
521
|
+
|
|
522
|
+
const meta = [];
|
|
523
|
+
if (e.commit) meta.push(e.commit);
|
|
524
|
+
if (e.date) {
|
|
525
|
+
try { meta.push(new Date(e.date).toLocaleTimeString()); } catch(x) {}
|
|
526
|
+
}
|
|
527
|
+
if (meta.length) html += '<div class="entry-meta">' + esc(meta.join(" / ")) + '</div>';
|
|
528
|
+
|
|
529
|
+
if (e.whatChanged.length) {
|
|
530
|
+
html += '<div class="detail-section changes"><span class="section-label">What Changed</span><ul>';
|
|
531
|
+
e.whatChanged.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
|
|
532
|
+
html += '</ul></div>';
|
|
533
|
+
}
|
|
534
|
+
if (e.decisions.length) {
|
|
535
|
+
html += '<div class="detail-section decisions"><span class="section-label">Decisions</span><ul>';
|
|
536
|
+
e.decisions.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
|
|
537
|
+
html += '</ul></div>';
|
|
538
|
+
}
|
|
539
|
+
if (e.issues.length) {
|
|
540
|
+
html += '<div class="detail-section issues"><span class="section-label">Issues</span><ul>';
|
|
541
|
+
e.issues.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
|
|
542
|
+
html += '</ul></div>';
|
|
543
|
+
}
|
|
544
|
+
if (e.nextSteps.length) {
|
|
545
|
+
html += '<div class="detail-section next-steps"><span class="section-label">Next Steps</span><ul>';
|
|
546
|
+
e.nextSteps.forEach(function(item) { html += '<li>' + esc(item) + '</li>'; });
|
|
547
|
+
html += '</ul></div>';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
html += '</div>';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!html) html = '<div style="color:#808098;text-align:center;padding:20px;">No details available</div>';
|
|
554
|
+
|
|
555
|
+
document.getElementById("detail-body").innerHTML = html;
|
|
556
|
+
overlay.classList.add("open");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function closeDetail(event) {
|
|
560
|
+
if (event && event.target !== document.getElementById("detail-overlay")) return;
|
|
561
|
+
document.getElementById("detail-overlay").classList.remove("open");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function prevMonth() {
|
|
565
|
+
viewMonth--;
|
|
566
|
+
if (viewMonth < 0) { viewMonth = 11; viewYear--; }
|
|
567
|
+
render();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function nextMonth() {
|
|
571
|
+
viewMonth++;
|
|
572
|
+
if (viewMonth > 11) { viewMonth = 0; viewYear++; }
|
|
573
|
+
render();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function goToday() {
|
|
577
|
+
viewYear = today.getFullYear();
|
|
578
|
+
viewMonth = today.getMonth();
|
|
579
|
+
render();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
document.addEventListener("keydown", function(e) {
|
|
583
|
+
if (e.key === "Escape") {
|
|
584
|
+
document.getElementById("detail-overlay").classList.remove("open");
|
|
585
|
+
} else if (e.key === "ArrowLeft") {
|
|
586
|
+
prevMonth();
|
|
587
|
+
} else if (e.key === "ArrowRight") {
|
|
588
|
+
nextMonth();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
render();
|
|
593
|
+
</script>
|
|
594
|
+
</body>
|
|
595
|
+
</html>`;
|
|
596
|
+
}
|