fix: handle CRLF line endings in aggregator task updates

The updateTaskField, addUpdatedDate, and updateTaskDescription functions
only matched Unix line endings (\n), causing task updates to silently
fail when files had Windows line endings (\r\n).

Updated all three functions to:
- Match both \r\n and \n line endings in regexes
- Detect and preserve the original line ending style when writing back

This fixes drag & drop status updates in the aggregator web UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-05 12:28:39 -08:00
parent 0368963bff
commit 6c3563c7e4
3 changed files with 81 additions and 11 deletions

View File

@ -0,0 +1,38 @@
---
id: task-001
title: Fix aggregator drag & drop line ending bug
status: Done
assignee: [@claude]
created_date: '2025-12-05 12:21'
labels: [dev-ops, bug-fix]
priority: high
dependencies: []
---
## Description
Fixed the aggregator web UI drag & drop functionality that was failing to update task status. The issue was that the `updateTaskField`, `addUpdatedDate`, and `updateTaskDescription` functions only handled Unix line endings (`\n`), while the markdown parser correctly handled both Windows (`\r\n`) and Unix line endings.
When task files had Windows line endings (CRLF), the frontmatter regex would fail to match, causing the update to silently fail (returning unchanged content). The PATCH request would return success, but the actual file wasn't modified, so the WebSocket broadcast would revert the UI back to the old status.
## Plan
1. [x] Investigate drag & drop functionality in aggregator web UI
2. [x] Identify root cause: line ending mismatch between parser and update functions
3. [x] Update `updateTaskField` to handle both CRLF and LF line endings
4. [x] Update `addUpdatedDate` with same fix
5. [x] Update `updateTaskDescription` with same fix
6. [x] Test the fix with both line ending types
7. [x] Push changes to remote
## Acceptance Criteria
- [x] Drag & drop works with Unix (LF) line endings
- [x] Drag & drop works with Windows (CRLF) line endings
- [x] Original line ending style is preserved when writing back
## Notes
Files modified: `src/aggregator/index.ts`
The fix detects the original line ending style and preserves it when writing back to maintain file consistency.

22
backlog/config.yml Normal file
View File

@ -0,0 +1,22 @@
project_name: Backlog.md
default_status: To Do
statuses:
- To Do
- In Progress
- Done
labels:
- dev-ops
- bug-fix
- feature
- enhancement
milestones: []
date_format: yyyy-mm-dd
max_column_width: 20
auto_open_browser: true
default_port: 6420
remote_operations: true
auto_commit: true
zero_padded_ids: 3
bypass_git_hooks: false
check_active_branches: true
active_branch_days: 60

View File

@ -67,7 +67,7 @@ export class BacklogAggregator {
constructor(config: Partial<AggregatorConfig> = {}) {
this.config = {
scanPaths: config.scanPaths || ["/opt/websites", "/opt/apps"],
scanPaths: config.scanPaths || ["/opt/websites", "/opt/apps", "/opt/ops"],
port: config.port || 6420,
colors: config.colors || DEFAULT_COLORS,
};
@ -810,11 +810,14 @@ created_date: '${dateStr}'
private updateTaskField(content: string, field: string, value: string): string {
// Parse frontmatter and update the field
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
// Handle both Windows (\r\n) and Unix (\n) line endings
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch || !frontmatterMatch[1]) {
return content;
}
// Detect the line ending style used in the file
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
const frontmatter = frontmatterMatch[1];
const fieldRegex = new RegExp(`^${field}:\\s*.+$`, "m");
@ -824,43 +827,50 @@ created_date: '${dateStr}'
updatedFrontmatter = frontmatter.replace(fieldRegex, `${field}: ${value}`);
} else {
// Add new field
updatedFrontmatter = frontmatter + `\n${field}: ${value}`;
updatedFrontmatter = frontmatter + `${lineEnding}${field}: ${value}`;
}
return content.replace(/^---\n[\s\S]*?\n---/, `---\n${updatedFrontmatter}\n---`);
return content.replace(/^---\r?\n[\s\S]*?\r?\n---/, `---${lineEnding}${updatedFrontmatter}${lineEnding}---`);
}
private updateTaskDescription(content: string, newDescription: string): string {
// Detect the line ending style used in the file
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
// Find and replace the description section
const descriptionRegex = /## Description\n\n[\s\S]*?(?=\n## |\n---|\Z)/;
const newDescriptionSection = `## Description\n\n${newDescription}\n`;
// Handle both Windows (\r\n) and Unix (\n) line endings
const descriptionRegex = /## Description\r?\n\r?\n[\s\S]*?(?=\r?\n## |\r?\n---|\Z)/;
const newDescriptionSection = `## Description${lineEnding}${lineEnding}${newDescription}${lineEnding}`;
if (descriptionRegex.test(content)) {
return content.replace(descriptionRegex, newDescriptionSection);
}
// If no description section, add one after frontmatter
return content.replace(/^(---\n[\s\S]*?\n---\n\n?)/, `$1${newDescriptionSection}\n`);
return content.replace(/^(---\r?\n[\s\S]*?\r?\n---\r?\n\r?\n?)/, `$1${newDescriptionSection}${lineEnding}`);
}
private addUpdatedDate(content: string): string {
const now = new Date();
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
// Handle both Windows (\r\n) and Unix (\n) line endings
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch || !frontmatterMatch[1]) {
return content;
}
// Detect the line ending style used in the file
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
const frontmatter = frontmatterMatch[1];
let updatedFrontmatter: string;
if (frontmatter.includes("updated_date:")) {
updatedFrontmatter = frontmatter.replace(/^updated_date:\s*.+$/m, `updated_date: '${dateStr}'`);
} else {
updatedFrontmatter = frontmatter + `\nupdated_date: '${dateStr}'`;
updatedFrontmatter = frontmatter + `${lineEnding}updated_date: '${dateStr}'`;
}
return content.replace(/^---\n[\s\S]*?\n---/, `---\n${updatedFrontmatter}\n---`);
return content.replace(/^---\r?\n[\s\S]*?\r?\n---/, `---${lineEnding}${updatedFrontmatter}${lineEnding}---`);
}
private async getNextTaskId(projectPath: string): Promise<string> {
@ -894,7 +904,7 @@ if (import.meta.main) {
const port = portIndex !== -1 ? Number.parseInt(args[portIndex + 1], 10) : 6420;
const pathsIndex = args.indexOf("--paths");
const paths = pathsIndex !== -1 ? args[pathsIndex + 1].split(",") : ["/opt/websites", "/opt/apps"];
const paths = pathsIndex !== -1 ? args[pathsIndex + 1].split(",") : ["/opt/websites", "/opt/apps", "/opt/ops"];
const aggregator = new BacklogAggregator({ port, scanPaths: paths });