project-logbook 0.3.4 → 0.4.1

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.
Files changed (40) hide show
  1. package/dist/commands/build.js +25 -2
  2. package/dist/commands/lint.js +18 -0
  3. package/dist/commands/status.js +23 -7
  4. package/dist/lib/build-helpers.js +7 -22
  5. package/dist/lib/build-steps.d.ts +4 -0
  6. package/dist/lib/build-steps.js +18 -1
  7. package/dist/lib/config.d.ts +1 -0
  8. package/dist/lib/config.js +7 -3
  9. package/dist/lib/git-helpers.d.ts +21 -0
  10. package/dist/lib/git-helpers.js +91 -0
  11. package/dist/lib/image-helpers.js +2 -2
  12. package/dist/lib/markdown-processors.d.ts +5 -1
  13. package/dist/lib/markdown-processors.js +28 -2
  14. package/dist/lib/rss.d.ts +29 -0
  15. package/dist/lib/rss.js +77 -0
  16. package/dist/lib/template-helpers.d.ts +3 -3
  17. package/dist/lib/template-helpers.js +19 -6
  18. package/dist/lib/template-types.d.ts +3 -0
  19. package/dist/lib/templates.d.ts +5 -2
  20. package/dist/lib/templates.js +24 -14
  21. package/dist/linters/index.js +2 -0
  22. package/dist/linters/technical-log.d.ts +7 -0
  23. package/dist/linters/technical-log.js +72 -0
  24. package/dist/templates/WELCOME.md +12 -0
  25. package/dist/templates/index.md +4 -0
  26. package/dist/templates/log.md +5 -0
  27. package/dist/templates/logbook-client.js +29 -0
  28. package/dist/templates/steer.txt +21 -5
  29. package/dist/templates/styles.css +182 -0
  30. package/dist/utils/date.d.ts +7 -0
  31. package/dist/utils/date.js +12 -0
  32. package/dist/utils/log-timeline.d.ts +69 -0
  33. package/dist/utils/log-timeline.js +218 -0
  34. package/package.json +3 -1
  35. package/src/templates/WELCOME.md +12 -0
  36. package/src/templates/index.md +4 -0
  37. package/src/templates/log.md +5 -0
  38. package/src/templates/logbook-client.js +29 -0
  39. package/src/templates/steer.txt +21 -5
  40. package/src/templates/styles.css +182 -0
@@ -1,6 +1,7 @@
1
1
  import { linkJiraIds } from './jira-helpers.js';
2
2
  import { formatAbsoluteDate } from '../utils/date.js';
3
3
  import { escapeHtml } from './html-escape.js';
4
+ import { getCommitUrl, getTagUrl } from './git-helpers.js';
4
5
  import { ATTR_ENTRY_SLUG, ATTR_DATA_DATE, CLASS_COMMIT_SHA, CLASS_COMMIT_TIME, CLASS_COMMIT_MESSAGE, } from './html-attributes.js';
5
6
  export const html = (strings, ...values) => {
6
7
  return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
@@ -27,7 +28,7 @@ export const renderTagsAndWorkspaces = (tags, workspaces) => {
27
28
  return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
28
29
  };
29
30
  // Renders a compact list of git commits to embed in the Technical Log tab.
30
- export const renderCommits = (commits) => {
31
+ export const renderCommits = (commits, repositoryUrl) => {
31
32
  if (!commits || commits.length === 0)
32
33
  return '';
33
34
  const rows = commits
@@ -35,7 +36,11 @@ export const renderCommits = (commits) => {
35
36
  const formatted = formatAbsoluteDate(c.timestamp);
36
37
  const sha = escapeHtml(c.sha);
37
38
  const message = escapeHtml(c.message);
38
- return `<div class="commit-item"><span class="${CLASS_COMMIT_SHA}">${sha}</span><span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div>`;
39
+ const commitUrl = getCommitUrl(repositoryUrl, c.sha);
40
+ const shaHtml = commitUrl
41
+ ? `<a href="${escapeHtml(commitUrl)}" class="${CLASS_COMMIT_SHA}" target="_blank" rel="noopener noreferrer">${sha}</a>`
42
+ : `<span class="${CLASS_COMMIT_SHA}">${sha}</span>`;
43
+ return `<div class="commit-item">${shaHtml}<span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div>`;
39
44
  })
40
45
  .join('');
41
46
  return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
@@ -64,16 +69,24 @@ export const renderTimelineEntryItem = (e) => {
64
69
  >
65
70
  </div>`;
66
71
  };
67
- export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
72
+ export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix, repositoryUrl) => {
68
73
  const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : escapeHtml(c.message);
69
74
  const formatted = formatAbsoluteDate(c.timestamp);
70
75
  const sha = escapeHtml(c.sha);
71
- return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="${CLASS_COMMIT_SHA}">${sha}</span><span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div></div>`;
76
+ const commitUrl = getCommitUrl(repositoryUrl, c.sha);
77
+ const shaHtml = commitUrl
78
+ ? `<a href="${escapeHtml(commitUrl)}" class="${CLASS_COMMIT_SHA}" target="_blank" rel="noopener noreferrer">${sha}</a>`
79
+ : `<span class="${CLASS_COMMIT_SHA}">${sha}</span>`;
80
+ return `<div class="timeline-item timeline-item--commit"><div class="commit-chip">${shaHtml}<span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div></div>`;
72
81
  };
73
- export const renderTimelineTagItem = (t) => {
82
+ export const renderTimelineTagItem = (t, repositoryUrl) => {
74
83
  const formatted = formatAbsoluteDate(t.timestamp);
75
84
  const name = escapeHtml(t.name);
76
- return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${name}</span><span class="tag-chip-date" title="${t.timestamp}" ${ATTR_DATA_DATE}="${t.timestamp}">${formatted}</span></div></div>`;
85
+ const tagUrl = getTagUrl(repositoryUrl, t.name);
86
+ const tagHtml = tagUrl
87
+ ? `<a href="${escapeHtml(tagUrl)}" class="tag-chip-name" target="_blank" rel="noopener noreferrer">${name}</a>`
88
+ : `<span class="tag-chip-name">${name}</span>`;
89
+ return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span>${tagHtml}<span class="tag-chip-date" title="${t.timestamp}" ${ATTR_DATA_DATE}="${t.timestamp}">${formatted}</span></div></div>`;
77
90
  };
78
91
  export const paginationLinks = (currentPage, totalPages) => {
79
92
  let links = '';
@@ -16,8 +16,10 @@ export interface TimelineTemplateProps {
16
16
  groups: TimelineGroup[];
17
17
  readmeHtml: string;
18
18
  aboutHtml: string;
19
+ welcomeHtml?: string;
19
20
  jiraBaseUrl?: string;
20
21
  jiraPrefix?: string;
22
+ repositoryUrl?: string;
21
23
  currentPage?: number;
22
24
  totalPages?: number;
23
25
  }
@@ -101,6 +103,7 @@ export interface PostTemplateProps {
101
103
  commits?: GitCommit[];
102
104
  version: string;
103
105
  jiraUrl?: string;
106
+ repositoryUrl?: string;
104
107
  prevEntry: EntryLink | null;
105
108
  nextEntry: EntryLink | null;
106
109
  slug?: string;
@@ -1,6 +1,6 @@
1
1
  import type { TabConfig, TimelineTemplateProps, PostTemplateProps } from './template-types.js';
2
2
  export declare const tabsComponent: (tabs: TabConfig[], defaultActive?: number) => string;
3
- export declare const layout: ({ title, header, content, projectName, basePath, description, bodySlug, buildMeta, }: {
3
+ export declare const layout: ({ title, header, content, projectName, basePath, description, bodySlug, buildMeta, includeRssLink, }: {
4
4
  title: string;
5
5
  header: string;
6
6
  content: string;
@@ -9,8 +9,11 @@ export declare const layout: ({ title, header, content, projectName, basePath, d
9
9
  description?: string;
10
10
  bodySlug?: string;
11
11
  buildMeta?: string;
12
+ includeRssLink?: boolean;
12
13
  }) => string;
13
- export declare const timelineTemplate: (props: TimelineTemplateProps) => {
14
+ export declare const timelineTemplate: (props: TimelineTemplateProps & {
15
+ includeRssLink?: boolean;
16
+ }) => {
14
17
  header: string;
15
18
  content: string;
16
19
  };
@@ -13,39 +13,49 @@ export const tabsComponent = (tabs, defaultActive = 0) => {
13
13
  ${sectionsHtml}
14
14
  </div>`;
15
15
  };
16
- export const layout = ({ title, header, content, projectName, basePath = './', description, bodySlug, buildMeta, }) => html `<!DOCTYPE html>
16
+ export const layout = ({ title, header, content, projectName, basePath = './', description, bodySlug, buildMeta, includeRssLink = false, }) => html `<!DOCTYPE html>
17
17
  <html lang="en">
18
18
  <head>
19
19
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
20
20
  <title>${escapeHtml(title)} | ${escapeHtml(projectName)}</title>
21
21
  ${description ? `<meta name="description" content="${escapeHtml(description).replace(/"/g, '&quot;').substring(0, 160)}">` : ''}
22
+ ${includeRssLink ? `<link rel="alternate" type="application/rss+xml" title="Subscribe to Project RSS Feed" href="${basePath}rss.xml">` : ''}
22
23
  <link rel="icon" type="image/svg+xml" href="${basePath}favicon.svg">
23
24
  <link rel="stylesheet" href="${basePath}style.css">
24
25
  </head>
25
26
  <body${bodySlug ? ` ${ATTR_PAGE_SLUG}="${bodySlug}"` : ''}>
26
27
  ${header}<main>${content}</main>
27
- <footer>${buildMeta ? `<p class="build-time">${buildMeta}</p>` : ''}</footer>
28
+ <footer>
29
+ ${buildMeta ? `<p class="build-time">${buildMeta}${includeRssLink ? ` • <a href="${basePath}rss.xml">RSS Feed</a>` : ''}</p>` : ''}
30
+ </footer>
28
31
  <script src="${basePath}logbook.js"></script>
29
32
  </body></html>`;
30
33
  export const timelineTemplate = (props) => {
31
- const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix, currentPage, totalPages } = props;
32
- const timelineTab = html `<div class="timeline">
33
- ${groups
34
+ const { projectName, groups, readmeHtml, aboutHtml, welcomeHtml, jiraBaseUrl, jiraPrefix, repositoryUrl, currentPage, totalPages, } = props;
35
+ const welcomeBannerHtml = welcomeHtml
36
+ ? `<div id="welcome-banner" class="welcome-banner" style="display:none;">
37
+ <button id="welcome-banner-close" class="welcome-banner-close" aria-label="Dismiss welcome banner">&times;</button>
38
+ <div class="welcome-banner-content">${welcomeHtml}</div>
39
+ </div>`
40
+ : '';
41
+ const timelineTab = html `${welcomeBannerHtml}
42
+ <div class="timeline">
43
+ ${groups
34
44
  .map((g) => html `<section class="month-group">
35
- <h2>${g.month}</h2>
36
- ${g.items
45
+ <h2>${g.month}</h2>
46
+ ${g.items
37
47
  .map((item) => {
38
48
  if (item.kind === 'entry')
39
49
  return renderTimelineEntryItem(item);
40
50
  if (item.kind === 'tag')
41
- return renderTimelineTagItem(item);
42
- return renderTimelineCommitItem(item, jiraBaseUrl, jiraPrefix);
51
+ return renderTimelineTagItem(item, repositoryUrl);
52
+ return renderTimelineCommitItem(item, jiraBaseUrl, jiraPrefix, repositoryUrl);
43
53
  })
44
54
  .join('')}
45
- </section>`)
55
+ </section>`)
46
56
  .join('')}
47
- ${paginationLinks(currentPage, totalPages)}
48
- </div>`;
57
+ ${paginationLinks(currentPage, totalPages)}
58
+ </div>`;
49
59
  const header = html `<header>
50
60
  <h1><a href="./index.html">${escapeHtml(projectName)}</a></h1>
51
61
  <p class="tagline">A brief summary of the recent changes to the project.</p>
@@ -58,7 +68,7 @@ export const timelineTemplate = (props) => {
58
68
  return { header, content };
59
69
  };
60
70
  export const postTemplate = (props) => {
61
- const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
71
+ const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, repositoryUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
62
72
  // Validate jiraUrl scheme to prevent javascript: URLs
63
73
  const validatedJiraUrl = jiraUrl ? validateUrlScheme(jiraUrl) : undefined;
64
74
  const jiraBtn = validatedJiraUrl
@@ -89,7 +99,7 @@ export const postTemplate = (props) => {
89
99
  </div>
90
100
  <div id="spec" class="content-section" role="tabpanel" style="display:none;">${ticketHtml}</div>
91
101
  <div id="log" class="content-section" role="tabpanel" style="display:none;">
92
- ${logHtml}${renderCommits(commits)}
102
+ ${logHtml}${renderCommits(commits, repositoryUrl)}
93
103
  </div>
94
104
  </div>
95
105
  <nav class="post-nav">
@@ -6,6 +6,7 @@ import lockfile from './lockfile.js';
6
6
  import placeholders from './placeholders.js';
7
7
  import projectIntegrity from './project-integrity.js';
8
8
  import readability from './readability.js';
9
+ import technicalLog from './technical-log.js';
9
10
  import workspaces from './workspaces.js';
10
11
  export const linters = [
11
12
  diffToNarrative,
@@ -16,5 +17,6 @@ export const linters = [
16
17
  placeholders,
17
18
  projectIntegrity,
18
19
  readability,
20
+ technicalLog,
19
21
  workspaces,
20
22
  ];
@@ -0,0 +1,7 @@
1
+ import type { Linter } from '../lib/lint-types.js';
2
+ /**
3
+ * Linter that checks for adequate technical logging.
4
+ * Ensures log.md has more than just the initial "Started investigation" entry.
5
+ */
6
+ declare const technicalLog: Linter;
7
+ export default technicalLog;
@@ -0,0 +1,72 @@
1
+ import fs from 'fs-extra';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Linter that checks for adequate technical logging.
5
+ * Ensures log.md has more than just the initial "Started investigation" entry.
6
+ */
7
+ const technicalLog = {
8
+ name: 'technical-log',
9
+ description: 'Checks for adequate technical logging in log.md',
10
+ async check(context) {
11
+ const issues = [];
12
+ // Only check the active entry (the one with the lockfile)
13
+ // Old entries cannot be updated, so we don't warn about them
14
+ if (!context.entryName || !context.entryPath) {
15
+ return issues;
16
+ }
17
+ // Check if this is the active entry by verifying lockfile exists
18
+ const lockfilePath = join(process.cwd(), '.logbook-active');
19
+ if (!(await fs.pathExists(lockfilePath))) {
20
+ // No active entry, skip this linter
21
+ return issues;
22
+ }
23
+ const lockfileContent = await fs.readFile(lockfilePath, 'utf8');
24
+ const lockfileData = JSON.parse(lockfileContent);
25
+ const activeEntrySlug = lockfileData.slug;
26
+ // Only check if this entry matches the active entry slug
27
+ if (context.entryName !== activeEntrySlug) {
28
+ return issues;
29
+ }
30
+ const logPath = join(context.entryPath, 'log.md');
31
+ if (!(await fs.pathExists(logPath))) {
32
+ issues.push({
33
+ category: 'TECHNICAL_LOG',
34
+ level: 'warning',
35
+ message: 'Missing log.md file. Create one to document your technical decisions.',
36
+ });
37
+ return issues;
38
+ }
39
+ const logContent = await fs.readFile(logPath, 'utf8');
40
+ // Extract all protocol entries (lines starting with "- " under ## Protocol)
41
+ const protocolSection = logContent.split('## Protocol')[1];
42
+ if (!protocolSection) {
43
+ // No Protocol section found, might be malformed
44
+ issues.push({
45
+ category: 'TECHNICAL_LOG',
46
+ level: 'warning',
47
+ message: 'log.md missing "## Protocol" section. Add one to structure your log entries.',
48
+ });
49
+ return issues;
50
+ }
51
+ const lines = protocolSection.split('\n').filter((line) => line.trim().startsWith('- '));
52
+ const entryCount = lines.length;
53
+ // Check if there's only the initial "Started investigation" entry
54
+ if (entryCount <= 1) {
55
+ issues.push({
56
+ category: 'TECHNICAL_LOG',
57
+ level: 'warning',
58
+ message: 'log.md only contains the initial "Started investigation" entry. Log your work in real-time using `logbook log "<message>"` after each significant step.',
59
+ });
60
+ }
61
+ else if (entryCount < 5) {
62
+ // Less than 5 entries might indicate insufficient logging
63
+ issues.push({
64
+ category: 'TECHNICAL_LOG',
65
+ level: 'warning',
66
+ message: `log.md has only ${entryCount} entries. Consider adding more detail about your implementation process, decisions, and challenges.`,
67
+ });
68
+ }
69
+ return issues;
70
+ },
71
+ };
72
+ export default technicalLog;
@@ -0,0 +1,12 @@
1
+ # Welcome to the Project Logbook!
2
+
3
+ This website is a **Project Logbook** — a chronological timeline of this project's evolution, design decisions, and development history.
4
+
5
+ Unlike traditional static documentation, a logbook behaves like a **dev log** or **news feed** of the codebase. Each item in the timeline below represents a completed feature or bugfix, containing:
6
+ - **Original Specifications:** The initial requirements and user stories.
7
+ - **Technical Log:** Step-by-step notes and decisions made during development.
8
+ - **The Story:** An engaging narrative explaining the rationale, pivots, and solutions.
9
+
10
+ Click around to explore how this project was built step by step!
11
+
12
+ Wait, did we mention it has an [**RSS Feed**](rss.xml)? Yes, you can subscribe to stay updated on new logbook entries in real-time!
@@ -21,6 +21,10 @@ dateEnd: "[DATE_END]"
21
21
  ## Summary
22
22
  TODO: Write a polished, highly readable ticket summary that reads like an engaging technical narrative (similar to a well-written dev blog post).
23
23
 
24
+ ### Before You Start:
25
+ - **Check `log.md`**: Review your technical log for all the decisions, errors, and pivots you recorded during implementation.
26
+ - **Reference the log**: Use your real-time log entries as source material for the narrative — don't try to reconstruct from memory.
27
+
24
28
  ### Formatting & Style Rules:
25
29
  - **Maintain the narrative tone:** TODO: Keep the storytelling flair (e.g., describing how problems accumulated or how gaps surfaced), but stay strictly factual based on the provided changes.
26
30
  - **Add thematic headings:** TODO: Break the narrative down into logical chapters using Markdown headings (e.g., ### The Friction Points, ### The Fix, ### Closing the Gap).
@@ -1,4 +1,9 @@
1
1
  # Technical Log: {{id}}-{{slug}}
2
2
 
3
+ > **REMINDER**: Log your work in real-time using `logbook log "<message>"`. Don't wait until the end!
4
+ >
5
+ > Log after every significant step: investigation, errors, decisions, code changes, test runs, etc.
6
+ >
7
+
3
8
  ## Protocol
4
9
  - {{fullIso}}: Started investigation.
@@ -168,6 +168,32 @@
168
168
  }
169
169
  }
170
170
 
171
+ /* ── 4. Welcome banner functionality ────────────────────────────────────── */
172
+ function initWelcomeBanner() {
173
+ var banner = document.getElementById('welcome-banner');
174
+ if (!banner) return;
175
+ try {
176
+ if (localStorage.getItem('welcome-banner-dismissed') !== 'true') {
177
+ banner.style.display = 'block';
178
+ }
179
+ } catch (e) {
180
+ // In case localStorage is disabled/unavailable, default to showing
181
+ banner.style.display = 'block';
182
+ }
183
+
184
+ var closeBtn = document.getElementById('welcome-banner-close');
185
+ if (closeBtn) {
186
+ closeBtn.addEventListener('click', function () {
187
+ banner.style.display = 'none';
188
+ try {
189
+ localStorage.setItem('welcome-banner-dismissed', 'true');
190
+ } catch (e) {
191
+ /* localStorage unavailable */
192
+ }
193
+ });
194
+ }
195
+ }
196
+
171
197
  /* ── Bootstrap ─────────────────────────────────────────────────────────── */
172
198
  document.addEventListener('DOMContentLoaded', function () {
173
199
  updateRelativeDates();
@@ -181,6 +207,9 @@
181
207
 
182
208
  // Initialize tab functionality
183
209
  initTabs();
210
+
211
+ // Initialize welcome banner if present
212
+ initWelcomeBanner();
184
213
  });
185
214
 
186
215
  // Expose showTab globally for onclick handlers
@@ -8,11 +8,27 @@ Phase 1: Understand
8
8
  2. Review `AGENTS.md` and `CONTRIBUTING.md` if you need architectural or workflow context.
9
9
 
10
10
  Phase 2: Execute & Trace
11
- 1. Use `log.md` as a live technical scratchpad.
12
- 2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
13
- Example: logbook log "Investigated root cause found issue in src/lib/config.ts"
14
- One message per call. For multiple entries, call the command once per message.
15
- 3. Stick to the active entry; never modify past entries in the logbook folder.
11
+
12
+ **CRITICAL: Real-Time Logging**
13
+ You MUST log your work continuously using `logbook log "<message>"`. This is not optional.
14
+
15
+ 1. Use `log.md` as your live technical scratchpad.
16
+ 2. After EVERY significant step, run: `logbook log "<what you just did>"`
17
+ - Investigated a file? Log it.
18
+ - Found an error? Log it.
19
+ - Made a design decision? Log it.
20
+ - Fixed a bug? Log it.
21
+ - Ran tests? Log the result.
22
+
23
+ Examples:
24
+ - `logbook log "Investigated src/lib/config.ts - found missing validation"`
25
+ - `logbook log "Created new RSS module with generateRssFeed() function"`
26
+ - `logbook log "Test failed: TypeScript error on line 42 - fixed type assertion"`
27
+ - `logbook log "All 127 tests pass, pre-commit suite successful"`
28
+
29
+ 3. **One message per command call.** For multiple entries, call the command multiple times.
30
+ 4. Do NOT reconstruct the log at the end. If the log is empty when you finish, you did it wrong.
31
+ 5. Stick to the active entry; never modify past entries in the logbook folder.
16
32
 
17
33
  Phase 3: Synthesize
18
34
  1. When implementation is finished, write the narrative in `index.md`.
@@ -696,3 +696,185 @@ article img[src$='.webp'],
696
696
  article img[src$='.svg'] {
697
697
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
698
698
  }
699
+
700
+ /* Technical Log Timeline */
701
+ #log .timeline {
702
+ list-style: none;
703
+ padding: 0;
704
+ margin: 0;
705
+ position: relative;
706
+ }
707
+
708
+ #log .timeline::before {
709
+ content: '';
710
+ position: absolute;
711
+ left: 8px;
712
+ top: 0;
713
+ bottom: 0;
714
+ width: 2px;
715
+ background: var(--border);
716
+ }
717
+
718
+ #log .timeline-entry {
719
+ position: relative;
720
+ padding-left: 2rem;
721
+ margin-bottom: 0.75rem;
722
+ min-height: 1.5rem;
723
+ display: grid;
724
+ grid-template-columns: 55px 1fr;
725
+ gap: 0.5rem;
726
+ }
727
+
728
+ #log .timeline-entry.no-time .timeline-message {
729
+ grid-column: 2;
730
+ }
731
+
732
+ #log .timeline-entry.has-time::before {
733
+ content: '';
734
+ position: absolute;
735
+ left: 5px;
736
+ top: 4px;
737
+ width: 8px;
738
+ height: 8px;
739
+ border-radius: 50%;
740
+ background: var(--primary);
741
+ border: 2px solid var(--bg);
742
+ z-index: 1;
743
+ }
744
+
745
+ #log .timeline-entry.no-time::before {
746
+ content: '';
747
+ position: absolute;
748
+ left: 7px;
749
+ top: 0.6rem;
750
+ width: 4px;
751
+ height: 4px;
752
+ border-radius: 50%;
753
+ background: var(--text-muted);
754
+ opacity: 0.6;
755
+ }
756
+
757
+ #log .timeline-time {
758
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
759
+ font-size: 0.75rem;
760
+ font-weight: 600;
761
+ color: var(--primary);
762
+ display: inline-block;
763
+ }
764
+
765
+ #log .timeline-message {
766
+ font-size: 0.9375rem;
767
+ color: var(--text);
768
+ line-height: 1.5;
769
+ }
770
+
771
+ #log .timeline-entry.no-time .timeline-message {
772
+ color: var(--text);
773
+ opacity: 0.85;
774
+ }
775
+
776
+ #log .timeline-gap {
777
+ position: relative;
778
+ padding-left: 2rem;
779
+ margin: 1.5rem 0;
780
+ min-height: 1rem;
781
+ }
782
+
783
+ #log .timeline-gap::before {
784
+ content: '';
785
+ position: absolute;
786
+ left: 7px;
787
+ top: 0.4rem;
788
+ width: 4px;
789
+ height: 4px;
790
+ border-radius: 50%;
791
+ background: var(--text-muted);
792
+ opacity: 0.5;
793
+ }
794
+
795
+ #log .gap-indicator {
796
+ font-size: 0.8125rem;
797
+ color: var(--text-muted);
798
+ font-style: italic;
799
+ display: inline-flex;
800
+ align-items: center;
801
+ gap: 0.25rem;
802
+ }
803
+
804
+ #log .log-fallback {
805
+ font-size: 0.9375rem;
806
+ color: var(--text);
807
+ line-height: 1.6;
808
+ white-space: pre-wrap;
809
+ background: var(--primary-soft);
810
+ padding: 1rem;
811
+ border-radius: 0.5rem;
812
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
813
+ font-size: 0.8125rem;
814
+ }
815
+
816
+ /* Welcome Banner */
817
+ .welcome-banner {
818
+ background: var(--bg-card, #ffffff);
819
+ border: 1px solid var(--border, #e2e8f0);
820
+ padding: 1.5rem;
821
+ border-radius: 0.5rem;
822
+ margin-top: 1.5rem;
823
+ margin-bottom: 2rem;
824
+ position: relative;
825
+ box-shadow:
826
+ 0 1px 3px 0 rgba(0, 0, 0, 0.1),
827
+ 0 1px 2px -1px rgba(0, 0, 0, 0.1);
828
+ }
829
+
830
+ .welcome-banner-content h1 {
831
+ font-size: 1.25rem;
832
+ margin-top: 0;
833
+ margin-bottom: 0.75rem;
834
+ color: var(--text-main, #1e293b);
835
+ display: flex;
836
+ align-items: center;
837
+ gap: 0.5rem;
838
+ }
839
+
840
+ .welcome-banner-content p {
841
+ margin: 0 0 0.75rem 0;
842
+ line-height: 1.5;
843
+ color: var(--text, #334155);
844
+ }
845
+
846
+ .welcome-banner-content ul {
847
+ margin: 0 0 1rem 0;
848
+ padding-left: 1.25rem;
849
+ color: var(--text, #334155);
850
+ }
851
+
852
+ .welcome-banner-content li {
853
+ margin-bottom: 0.5rem;
854
+ line-height: 1.4;
855
+ }
856
+
857
+ .welcome-banner-close {
858
+ position: absolute;
859
+ top: 1rem;
860
+ right: 1rem;
861
+ background: none;
862
+ border: none;
863
+ font-size: 1.25rem;
864
+ cursor: pointer;
865
+ color: var(--text-muted, #64748b);
866
+ line-height: 1;
867
+ padding: 0.25rem;
868
+ border-radius: 0.25rem;
869
+ display: flex;
870
+ align-items: center;
871
+ justify-content: center;
872
+ transition:
873
+ background-color 0.2s,
874
+ color 0.2s;
875
+ }
876
+
877
+ .welcome-banner-close:hover {
878
+ background-color: var(--primary-soft, #f0fdf4);
879
+ color: var(--text-main, #1e293b);
880
+ }
@@ -28,3 +28,10 @@ export declare function getSortTime(dateStart: string | Date, dateEnd?: string |
28
28
  * Alias for formatAbsoluteDate for clarity in build context.
29
29
  */
30
30
  export declare function formatDateTimeForDisplay(date: string | Date): string;
31
+ /**
32
+ * Format a date for RSS feed pubDate element.
33
+ * RSS requires RFC 822 date format: "Mon, 26 May 2026 10:51:00 GMT"
34
+ * @param date - The date to format
35
+ * @returns RFC 822 formatted date string
36
+ */
37
+ export declare function formatDateTimeForRss(date: string | Date): string;
@@ -86,3 +86,15 @@ export function getSortTime(dateStart, dateEnd) {
86
86
  export function formatDateTimeForDisplay(date) {
87
87
  return formatAbsoluteDate(date);
88
88
  }
89
+ /**
90
+ * Format a date for RSS feed pubDate element.
91
+ * RSS requires RFC 822 date format: "Mon, 26 May 2026 10:51:00 GMT"
92
+ * @param date - The date to format
93
+ * @returns RFC 822 formatted date string
94
+ */
95
+ export function formatDateTimeForRss(date) {
96
+ const d = parseDate(date);
97
+ if (!d)
98
+ return new Date().toUTCString();
99
+ return d.toUTCString();
100
+ }