From d1d1a85bf9c35366ece7fa5981345d714e077445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:25:41 +0000 Subject: [PATCH 1/6] Initial plan From 6eb31d416b4e8ffe7cb6e918e7f0217412889c91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:28:47 +0000 Subject: [PATCH 2/6] Add backend support for note rename with YYYY_MM_DD_HH_MM format Co-authored-by: satyama2027-debug <240024655+satyama2027-debug@users.noreply.github.com> --- app/v2_routes.py | 122 +++++++++++++++++++++++++++++++++++++++++++++ engine/config.py | 4 +- engine/markdown.py | 18 ++++--- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/app/v2_routes.py b/app/v2_routes.py index 465ff0b..f765bf6 100644 --- a/app/v2_routes.py +++ b/app/v2_routes.py @@ -42,6 +42,10 @@ class NoteUpdate(BaseModel): tags: Optional[List[str]] = None +class NoteRename(BaseModel): + new_slug: str + + class TaskUpdate(BaseModel): completed: bool @@ -1923,3 +1927,121 @@ async def api_registry_audio(note_id: int): media_type=mime, filename=row["filename"], ) + + +@router.post("/api/registry/{note_id}/rename") +async def api_rename_note(note_id: int, rename_data: NoteRename): + """Rename a processed note file (slug only, timestamp locked).""" + import sqlite3 + import re + import os + + registry_path = _get_registry_path() + if not registry_path.exists(): + raise HTTPException(status_code=404, detail="Registry not found") + + # Get the current note info + conn = sqlite3.connect(str(registry_path)) + conn.row_factory = sqlite3.Row + row = conn.execute( + "SELECT note_path, success FROM processed_files WHERE id = ?", (note_id,) + ).fetchone() + + if not row: + conn.close() + raise HTTPException(status_code=404, detail="Note not found") + + if not row["success"]: + conn.close() + raise HTTPException(status_code=400, detail="Cannot rename failed or pending notes") + + old_path_str = row["note_path"] + if not old_path_str: + conn.close() + raise HTTPException(status_code=404, detail="Note path not found in registry") + + old_path = Path(old_path_str) + if not old_path.exists(): + conn.close() + raise HTTPException(status_code=404, detail="Note file not found on disk") + + # Sanitize the new slug + new_slug = rename_data.new_slug.strip() + # Remove invalid filesystem characters: / \ : ? * < > | and leading/trailing spaces + new_slug = re.sub(r'[/\\:?*<>|]', '', new_slug) + # Replace spaces with hyphens + new_slug = re.sub(r'\s+', '-', new_slug) + # Remove leading/trailing hyphens + new_slug = new_slug.strip('-') + # Convert to lowercase + new_slug = new_slug.lower() + + if not new_slug: + conn.close() + raise HTTPException(status_code=400, detail="Invalid slug: cannot be empty after sanitization") + + # Parse the old filename to extract the timestamp prefix + old_filename = old_path.stem # without .md extension + + # Expected format: YYYY_MM_DD_HH_MM_slug + # Find the timestamp prefix (first 16 chars: YYYY_MM_DD_HH_MM) + # The timestamp format is: 2026_02_16_14_30 (16 characters) + if len(old_filename) < 17 or old_filename[16] != '_': + # Try to match the pattern more flexibly + match = re.match(r'^(\d{4}_\d{2}_\d{2}_\d{2}_\d{2})_(.+)$', old_filename) + if not match: + conn.close() + raise HTTPException( + status_code=400, + detail="Filename format not recognized. Expected YYYY_MM_DD_HH_MM_slug format" + ) + timestamp_prefix = match.group(1) + else: + timestamp_prefix = old_filename[:16] # YYYY_MM_DD_HH_MM + + # Build new filename with locked timestamp prefix + new_filename = f"{timestamp_prefix}_{new_slug}.md" + new_path = old_path.parent / new_filename + + # Check for conflicts + if new_path.exists() and new_path != old_path: + conn.close() + raise HTTPException( + status_code=409, + detail=f"A note with this name already exists: {new_filename}" + ) + + # Perform the rename + try: + os.rename(old_path, new_path) + except Exception as e: + conn.close() + logger.error(f"Failed to rename file: {e}") + raise HTTPException(status_code=500, detail=f"Failed to rename file: {str(e)}") + + # Update the registry + try: + conn.execute( + "UPDATE processed_files SET note_path = ? WHERE id = ?", + (str(new_path), note_id) + ) + conn.commit() + except Exception as e: + # Try to rollback the filesystem rename + try: + os.rename(new_path, old_path) + except: + pass + conn.close() + logger.error(f"Failed to update registry: {e}") + raise HTTPException(status_code=500, detail=f"Failed to update registry: {str(e)}") + + conn.close() + + return { + "success": True, + "old_path": str(old_path), + "new_path": str(new_path), + "new_filename": new_filename, + "sanitized_slug": new_slug + } diff --git a/engine/config.py b/engine/config.py index be1158a..e684494 100644 --- a/engine/config.py +++ b/engine/config.py @@ -73,8 +73,8 @@ class EngineConfig: ) # Filename format settings - # Supported: "DD_MM_YY", "YYYY-MM-DD", "MM-DD-YYYY", "YYMMDD" - filename_date_format: str = "DD_MM_YY" + # Supported: "DD_MM_YY", "YYYY-MM-DD", "MM-DD-YYYY", "YYMMDD", "YYYY_MM_DD_HH_MM" + filename_date_format: str = "YYYY_MM_DD_HH_MM" @property def notes_output_dir(self) -> Path: diff --git a/engine/markdown.py b/engine/markdown.py index 3d56312..505fc6e 100644 --- a/engine/markdown.py +++ b/engine/markdown.py @@ -19,13 +19,14 @@ logger = logging.getLogger(__name__) -def get_filename_base(result: ProcessingResult, date_format: str = "DD_MM_YY") -> tuple[str, datetime]: +def get_filename_base(result: ProcessingResult, date_format: str = "YYYY_MM_DD_HH_MM") -> tuple[str, datetime]: """Generate the base filename (without extension) in date_slug format. Args: result: ProcessingResult with title and metadata date_format: Format string. Supported values: - - "DD_MM_YY" (default): 25_01_25 + - "YYYY_MM_DD_HH_MM" (default): 2026_02_16_14_30 + - "DD_MM_YY": 25_01_25 - "YYYY-MM-DD": 2025-01-25 - "MM-DD-YYYY": 01-25-2025 - "YYMMDD": 250125 @@ -42,12 +43,13 @@ def get_filename_base(result: ProcessingResult, date_format: str = "DD_MM_YY") - # Map format names to strftime patterns format_map = { + "YYYY_MM_DD_HH_MM": "%Y_%m_%d_%H_%M", "DD_MM_YY": "%d_%m_%y", "YYYY-MM-DD": "%Y-%m-%d", "MM-DD-YYYY": "%m-%d-%Y", "YYMMDD": "%y%m%d", } - strftime_fmt = format_map.get(date_format, "%d_%m_%y") + strftime_fmt = format_map.get(date_format, "%Y_%m_%d_%H_%M") filename_base = f"{ts.strftime(strftime_fmt)}_{slug}" @@ -72,7 +74,7 @@ def _resolve_path_collision(path: Path) -> Path: # TRANSCRIPT OUTPUT (Raw verbatim transcript) # ============================================================================= -def build_transcript_note(result: ProcessingResult, engine_version: str = "2.0.0", date_format: str = "DD_MM_YY") -> str: +def build_transcript_note(result: ProcessingResult, engine_version: str = "2.0.0", date_format: str = "YYYY_MM_DD_HH_MM") -> str: """Build a minimal transcript-only markdown file. Format: @@ -118,7 +120,7 @@ def save_transcript( result: ProcessingResult, transcripts_dir: Path, engine_version: str = "2.0.0", - date_format: str = "DD_MM_YY", + date_format: str = "YYYY_MM_DD_HH_MM", ) -> Path: """Save the raw transcript to the Transcripts folder. @@ -144,7 +146,7 @@ def build_inbox_note( result: ProcessingResult, engine_version: str = "2.0.0", transcript_path: Optional[Path] = None, - date_format: str = "DD_MM_YY", + date_format: str = "YYYY_MM_DD_HH_MM", ) -> str: """Build a complete Obsidian-compatible structured note for Inbox. @@ -231,7 +233,7 @@ def save_inbox_note( inbox_dir: Path, engine_version: str = "2.0.0", transcript_path: Optional[Path] = None, - date_format: str = "DD_MM_YY", + date_format: str = "YYYY_MM_DD_HH_MM", ) -> Path: """Save the structured note to the Inbox folder. @@ -258,7 +260,7 @@ def save_dual_output( inbox_dir: Path, transcripts_dir: Path, engine_version: str = "2.0.0", - date_format: str = "DD_MM_YY", + date_format: str = "YYYY_MM_DD_HH_MM", ) -> tuple[Path, Path]: """Save both transcript and structured note. From 695a0355eb623cc25764b88846712588f18a454e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:30:44 +0000 Subject: [PATCH 3/6] Add frontend rename UI with inline editing and toast notifications Co-authored-by: satyama2027-debug <240024655+satyama2027-debug@users.noreply.github.com> --- app/static/js/v2/app.js | 155 +++++++++++++++++++++++++++++++++ app/templates/v2/inbox.html | 75 ++++++++++++++-- app/templates/v2/projects.html | 60 ++++++++++++- 3 files changed, 279 insertions(+), 11 deletions(-) diff --git a/app/static/js/v2/app.js b/app/static/js/v2/app.js index 82cc7a5..445fb19 100644 --- a/app/static/js/v2/app.js +++ b/app/static/js/v2/app.js @@ -1107,3 +1107,158 @@ window.formatTimestamp = function(isoStr) { var d = new Date(isoStr); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); }; + +// ============================================================================ +// TOAST NOTIFICATION SYSTEM +// ============================================================================ +window.showToast = function(message, type) { + type = type || 'success'; + var toast = document.createElement('div'); + toast.className = 'toast toast-' + type; + toast.textContent = message; + toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:8px;padding:12px 16px;z-index:10000;min-width:200px;max-width:400px;box-shadow:0 4px 16px rgba(0,0,0,0.3);animation:slideIn 0.2s ease-out;'; + + if (type === 'success') { + toast.style.borderColor = 'var(--success)'; + toast.style.color = 'var(--success)'; + } else if (type === 'error') { + toast.style.borderColor = 'var(--error)'; + toast.style.color = 'var(--error)'; + } + + document.body.appendChild(toast); + + setTimeout(function() { + toast.style.animation = 'slideOut 0.2s ease-in'; + setTimeout(function() { + if (toast.parentNode) toast.parentNode.removeChild(toast); + }, 200); + }, 3000); +}; + +// Add CSS animations for toast +if (!document.getElementById('toast-styles')) { + var style = document.createElement('style'); + style.id = 'toast-styles'; + style.textContent = '@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }'; + document.head.appendChild(style); +} + +// ============================================================================ +// INLINE RENAME COMPONENT +// ============================================================================ +window.initRename = function(noteId, currentFilename, onSuccess) { + return { + noteId: noteId, + currentFilename: currentFilename, + isEditing: false, + editingSlug: '', + timestampPrefix: '', + error: '', + saving: false, + onSuccess: onSuccess || function() {}, + + init: function() { + this.parseFilename(); + }, + + parseFilename: function() { + // Parse YYYY_MM_DD_HH_MM_slug.md format + var match = this.currentFilename.match(/^(\d{4}_\d{2}_\d{2}_\d{2}_\d{2})_(.+)\.md$/); + if (match) { + this.timestampPrefix = match[1]; + this.editingSlug = match[2]; + } else { + // Fallback: try to find any timestamp-like prefix + var parts = this.currentFilename.replace('.md', '').split('_'); + if (parts.length >= 5) { + this.timestampPrefix = parts.slice(0, 5).join('_'); + this.editingSlug = parts.slice(5).join('_'); + } else { + this.timestampPrefix = ''; + this.editingSlug = this.currentFilename.replace('.md', ''); + } + } + }, + + startEdit: function() { + this.isEditing = true; + this.error = ''; + var self = this; + this.$nextTick(function() { + var input = self.$el.querySelector('.rename-input'); + if (input) { + input.focus(); + input.select(); + } + }); + }, + + cancelEdit: function() { + this.isEditing = false; + this.error = ''; + this.parseFilename(); + }, + + confirmEdit: function() { + var self = this; + if (self.saving) return; + + var newSlug = self.editingSlug.trim(); + if (!newSlug) { + self.error = 'Slug cannot be empty'; + return; + } + + // Sanitize on frontend (server will also sanitize) + newSlug = newSlug.replace(/[\/\\:?*<>|]/g, '').replace(/\s+/g, '-').replace(/^-+|-+$/g, '').toLowerCase(); + + if (!newSlug) { + self.error = 'Invalid characters'; + return; + } + + self.saving = true; + self.error = ''; + + fetch('/v2/api/registry/' + self.noteId + '/rename', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ new_slug: newSlug }) + }) + .then(function(res) { + if (!res.ok) { + return res.json().then(function(err) { + throw new Error(err.detail || 'Rename failed'); + }); + } + return res.json(); + }) + .then(function(data) { + self.currentFilename = data.new_filename; + self.isEditing = false; + self.parseFilename(); + window.showToast('Renamed successfully', 'success'); + if (self.onSuccess) self.onSuccess(data); + }) + .catch(function(err) { + self.error = err.message; + if (err.message.includes('already exists')) { + self.error = 'Name already exists'; + } + window.showToast(self.error, 'error'); + }) + .finally(function() { + self.saving = false; + }); + }, + + handleKeydown: function(e) { + if (e.key === 'Enter') { + this.confirmEdit(); + } else if (e.key === 'Escape') { + this.cancelEdit(); + } + } + }; +}; diff --git a/app/templates/v2/inbox.html b/app/templates/v2/inbox.html index ff4de2a..9133b84 100644 --- a/app/templates/v2/inbox.html +++ b/app/templates/v2/inbox.html @@ -8,7 +8,53 @@ {% endblock %} {% block content %} -
+
+ + +
- - Open - - +
+ + + Open + + +
diff --git a/app/templates/v2/projects.html b/app/templates/v2/projects.html index ac22759..7eca05a 100644 --- a/app/templates/v2/projects.html +++ b/app/templates/v2/projects.html @@ -7,7 +7,54 @@ {% endblock %} {% block content %} -
+
+ + +