fix: add missing libraryService.js and fix two bugs

- Create missing src/services/libraryService.js (was imported but never created)
- Fix readJSONFile import in tagIndexer.js (unused import)
- Fix found.found?.libId typo in deleteDocument (double .found lookup)

These were blocking the application from starting.
This commit is contained in:
Erwin
2026-03-28 03:31:14 +00:00
parent 825dfba2a7
commit 8b0b60db33
4 changed files with 583 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
* Rebuild and query the global tag index * Rebuild and query the global tag index
*/ */
import { readJSON, writeJSON, pathExists, listDir, readJSONFile } from '../utils/fsHelper.js'; import { readJSON, writeJSON, pathExists, listDir } from '../utils/fsHelper.js';
import { join } from 'path'; import { join } from 'path';
import config from '../config/index.js'; import config from '../config/index.js';

View File

@@ -276,7 +276,6 @@ export class DocumentService {
} }
const meta = readJSON(found.metaPath); const meta = readJSON(found.metaPath);
deletePath(found.found?.libId ? join(this.librariesPath, found.libId, 'documents', docId) : null);
deletePath(this._docPath(found.libId, docId)); deletePath(this._docPath(found.libId, docId));
this.tagIndexer.removeDocument(docId); this.tagIndexer.removeDocument(docId);

View File

@@ -0,0 +1,273 @@
/**
* SimpleNote Web - Library Service
* Library CRUD with filesystem storage
*/
import { join } from 'path';
import config from '../config/index.js';
import { ensureDir, readJSON, writeJSON, pathExists, deletePath, listDir, isDirectory } from '../utils/fsHelper.js';
import { generateId } from '../utils/uuid.js';
import { NotFoundError, ValidationError } from '../utils/errors.js';
const LIBRARIES_DIR = 'libraries';
const LIBRARY_META_FILE = '.library.json';
export class LibraryService {
constructor(dataRoot = config.dataRoot) {
this.dataRoot = dataRoot;
this.librariesPath = join(dataRoot, LIBRARIES_DIR);
}
_libPath(libId) {
return join(this.librariesPath, libId);
}
_libMetaPath(libId) {
return join(this._libPath(libId), LIBRARY_META_FILE);
}
_libDocumentsPath(libId) {
return join(this._libPath(libId), 'documents');
}
_libSubLibrariesPath(libId) {
return join(this._libPath(libId), 'sub-libraries');
}
_readLibMeta(libId) {
const meta = readJSON(this._libMetaPath(libId));
if (!meta) throw new NotFoundError('Library');
return meta;
}
_findLibraryById(libId) {
const metaPath = this._libMetaPath(libId);
if (pathExists(metaPath)) {
return { id: libId, metaPath };
}
return null;
}
async createLibrary({ name, parentId = null }) {
if (!name || !name.trim()) {
throw new ValidationError('Library name is required');
}
const libId = generateId();
const now = new Date().toISOString();
if (parentId) {
// Verify parent exists
const parentMeta = this._readLibMeta(parentId);
const parentSubLibsPath = this._libSubLibrariesPath(parentId);
ensureDir(parentSubLibsPath);
const libMeta = {
id: libId,
name: name.trim(),
parentId,
createdAt: now,
updatedAt: now,
};
const libPath = join(parentSubLibsPath, libId);
ensureDir(libPath);
ensureDir(join(libPath, 'documents'));
writeJSON(this._libMetaPath(libId), libMeta);
return libMeta;
} else {
ensureDir(this.librariesPath);
const libMeta = {
id: libId,
name: name.trim(),
parentId: null,
createdAt: now,
updatedAt: now,
};
const libPath = this._libPath(libId);
ensureDir(libPath);
ensureDir(join(libPath, 'documents'));
writeJSON(this._libMetaPath(libId), libMeta);
return libMeta;
}
}
async listRootLibraries() {
ensureDir(this.librariesPath);
const entries = listDir(this.librariesPath);
const libraries = [];
for (const entry of entries) {
const libPath = join(this.librariesPath, entry);
if (!isDirectory(libPath)) continue;
const meta = readJSON(this._libMetaPath(entry));
if (!meta) continue;
const docCount = this._countDocuments(this._libDocumentsPath(entry));
libraries.push({
...meta,
documentCount: docCount,
});
}
return libraries;
}
_countDocuments(docsPath) {
if (!pathExists(docsPath)) return 0;
const entries = listDir(docsPath);
return entries.filter(e => {
const metaPath = join(docsPath, e, '.meta.json');
return pathExists(metaPath);
}).length;
}
async getLibrary(libId) {
const found = this._findLibraryById(libId);
if (!found) {
throw new NotFoundError('Library');
}
const meta = readJSON(this._libMetaPath(libId));
const docsPath = this._libDocumentsPath(libId);
const subLibsPath = this._libSubLibrariesPath(libId);
// List documents
const documents = [];
if (pathExists(docsPath)) {
const docEntries = listDir(docsPath);
for (const docEntry of docEntries) {
const docMetaPath = join(docsPath, docEntry, '.meta.json');
if (pathExists(docMetaPath)) {
const docMeta = readJSON(docMetaPath);
if (docMeta?.id) {
documents.push({
id: docMeta.id,
title: docMeta.title,
type: docMeta.type,
status: docMeta.status,
tags: docMeta.tags || [],
updatedAt: docMeta.updatedAt,
});
}
}
}
}
// List sub-libraries
const subLibraries = [];
if (pathExists(subLibsPath)) {
const subEntries = listDir(subLibsPath);
for (const subEntry of subEntries) {
const subMetaPath = join(subLibsPath, subEntry, LIBRARY_META_FILE);
if (pathExists(subMetaPath)) {
const subMeta = readJSON(subMetaPath);
if (subMeta?.id) {
const subDocCount = this._countDocuments(join(subLibsPath, subEntry, 'documents'));
subLibraries.push({
id: subMeta.id,
name: subMeta.name,
documentCount: subDocCount,
});
}
}
}
}
return {
library: meta,
documents,
subLibraries,
};
}
async getLibraryTree(libId) {
const found = this._findLibraryById(libId);
if (!found) {
throw new NotFoundError('Library');
}
const meta = readJSON(this._libMetaPath(libId));
const buildTree = (id) => {
const libMeta = readJSON(this._libMetaPath(id));
const docsPath = this._libDocumentsPath(id);
const subLibsPath = this._libSubLibrariesPath(id);
const documents = [];
if (pathExists(docsPath)) {
const docEntries = listDir(docsPath);
for (const docEntry of docEntries) {
const docMetaPath = join(docsPath, docEntry, '.meta.json');
if (pathExists(docMetaPath)) {
const docMeta = readJSON(docMetaPath);
if (docMeta?.id) {
documents.push({
id: docMeta.id,
title: docMeta.title,
type: docMeta.type,
});
}
}
}
}
const subLibraries = [];
if (pathExists(subLibsPath)) {
const subEntries = listDir(subLibsPath);
for (const subEntry of subEntries) {
const subMetaPath = join(subLibsPath, subEntry, LIBRARY_META_FILE);
if (pathExists(subMetaPath)) {
subLibraries.push(buildTree(subEntry));
}
}
}
return {
id: libMeta.id,
name: libMeta.name,
documents,
subLibraries,
};
};
return buildTree(libId);
}
async listDocumentsInLibrary(libId) {
const found = this._findLibraryById(libId);
if (!found) {
throw new NotFoundError('Library');
}
const result = await this.getLibrary(libId);
return { documents: result.documents, total: result.documents.length };
}
async deleteLibrary(libId) {
const found = this._findLibraryById(libId);
if (!found) {
throw new NotFoundError('Library');
}
const libPath = this._libPath(libId);
deletePath(libPath);
return { deleted: true, id: libId };
}
}
let globalLibraryService = null;
export function getLibraryService(dataRoot = config.dataRoot) {
if (!globalLibraryService) {
globalLibraryService = new LibraryService(dataRoot);
}
return globalLibraryService;
}
export default LibraryService;

309
ui/SPEC.md Normal file
View File

@@ -0,0 +1,309 @@
# SimpleNote Web - UI/UX Specification
## 1. Overview
### 1.1 Purpose
SimpleNote Web is the frontend interface for the SimpleNote document management system. It provides a clean, efficient way to browse, create, edit, and manage documents organized in nested libraries with tag-based filtering.
### 1.2 Target Users
- Technical users and agents creating/managing documentation
- Teams using CLI tools to push documents that need visual review
- Anyone preferring a Joplin-like experience with a modern dark-mode interface
### 1.3 Tech Stack
- **Framework**: Vanilla JS with modern ES modules (no framework dependency for simplicity)
- **Styling**: CSS custom properties for theming (dark/light modes)
- **Markdown**: `marked` library for rendering
- **Icons**: Lucide Icons (SVG-based, MIT licensed)
- **HTTP Client**: Native Fetch API
- **State**: Simple pub/sub pattern with localStorage for preferences
---
## 2. Design Principles
### 2.1 Core Principles
1. **Content-First**: Documents are the hero; UI stays out of the way
2. **Keyboard-Friendly**: Common actions accessible via shortcuts
3. **Information Density**: Compact but readable; optimized for power users
4. **Progressive Disclosure**: Show details on demand, not all at once
5. **Dark Mode Primary**: Inspired by Mission Control dashboard aesthetics
### 2.2 Layout Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Header (Logo + Search + User Actions) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content Area │
│ (Library │ │
│ Tree + │ - Document List (Dashboard) │
│ Tags) │ - Document Viewer │
│ │ - Document Editor │
│ │ - Library Browser │
│ │ │
├──────────────┴──────────────────────────────────────────────┤
│ Status Bar (optional: sync status, doc count) │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 Responsive Strategy
- **Desktop (>1024px)**: Full sidebar + content layout
- **Tablet (768-1024px)**: Collapsible sidebar, full content
- **Mobile (<768px)**: Bottom navigation, stacked layout, slide-over panels
---
## 3. Views Specification
### 3.1 Dashboard / Main View
**Purpose**: Central hub showing documents with filtering and navigation.
**Components**:
- **Header Bar**: Logo, global search input, theme toggle, settings
- **Sidebar (left)**:
- Library tree (collapsible, nested)
- Quick filters: All Documents, Recent, Favorites
- Tag cloud/list with counts
- **Main Content**:
- Toolbar: View toggle (list/grid), sort options, bulk actions
- Document cards/list with title, tags, type badge, date, status
- Quick actions: New Document, New Library buttons
**URL**: `/` or `/documents`
**Keyboard Shortcuts**:
- `Ctrl/Cmd + K`: Focus search
- `Ctrl/Cmd + N`: New document
- `Ctrl/Cmd + Shift + N`: New library
- `Escape`: Clear filters/search
### 3.2 Document Viewer
**Purpose**: Read-only view of a document with rendered markdown.
**Components**:
- **Header**: Back button, document title, action buttons (Edit, Export, Delete)
- **Metadata Panel** (collapsible sidebar or dropdown):
- Type badge (requirement/note/spec/general)
- Status badge (draft/approved/implemented)
- Priority indicator (high/medium/low with color)
- Tags as clickable pills
- Author
- Created/Updated timestamps
- **Content Area**: Rendered markdown with:
- Syntax highlighting for code blocks
- Styled tables
- Checkbox rendering for criteria lists
- Anchor links for headings
**URL**: `/documents/:id`
**Keyboard Shortcuts**:
- `E`: Edit document
- `X`: Export markdown
- `Delete/Backspace`: Delete (with confirmation)
- `Escape`: Back to list
### 3.3 Document Editor
**Purpose**: Create or edit documents with live preview.
**Components**:
- **Header**: Cancel/Save buttons, document title (editable)
- **Editor Toolbar**:
- Markdown formatting buttons (bold, italic, headers, lists, code, link)
- Insert template dropdown
- Preview toggle (split view or tabbed)
- **Frontmatter Panel**:
- Title input
- Type selector (dropdown)
- Status selector (dropdown)
- Priority selector (dropdown)
- Tags input (chip-style, autocomplete)
- Library selector (dropdown with tree view)
- **Editor Area**:
- Monaco-style textarea with markdown syntax highlighting
- OR CodeMirror/MarkText integration (future)
- **Preview Pane**: Live rendered markdown (toggleable)
- **Auto-save Indicator**: "Saved" / "Saving..." / "Unsaved changes"
**URL**: `/documents/:id/edit` or `/documents/new`
**Keyboard Shortcuts**:
- `Ctrl/Cmd + S`: Save
- `Ctrl/Cmd + B`: Bold
- `Ctrl/Cmd + I`: Italic
- `Ctrl/Cmd + K`: Insert link
- `Ctrl/Cmd + Shift + P`: Toggle preview
- `Escape`: Discard changes (with confirmation)
### 3.4 Library Browser
**Purpose**: Navigate and manage library hierarchy.
**Components**:
- **Breadcrumbs**: Path from root to current library
- **Library Tree** (sidebar): Expandable/collapsible nested view
- **Main Content**:
- Header: Current library name, document count
- Actions: New Document, New Sub-library, Rename, Delete
- Content list: Documents and sub-libraries mixed
- Drag-and-drop reordering (future)
**URL**: `/libraries/:id`
**Actions**:
- Click library → navigate to library view
- Click document → open viewer
- Right-click/long-press → context menu (Rename, Delete, Move)
- "+" buttons for creating new items
**Keyboard Shortcuts**:
- `Ctrl/Cmd + Shift + N`: New sub-library
- `R`: Rename selected
- `Delete`: Delete (with confirmation)
- `Escape`: Go to parent library
---
## 4. Interaction Patterns
### 4.1 Navigation
- Sidebar persists across views (single-page app behavior)
- Breadcrumbs for deep library navigation
- Browser back/forward support via History API
### 4.2 Search
- Real-time filtering as user types (debounced 300ms)
- Search scope: title, content, tags
- Highlight matching terms in results
- Empty state: "No documents found"
### 4.3 Tag Filtering
- Click tag in sidebar → filter documents
- Click tag pill in document → filter by that tag
- Multiple tags = AND filter
- Active filters shown as removable chips
### 4.4 Confirmation Dialogs
- Delete operations: Modal with confirmation
- Unsaved changes: Modal asking to save/discard
- All modals: Escape to cancel, Enter to confirm (when safe)
### 4.5 Notifications/Toasts
- Success: Green, auto-dismiss 3s
- Error: Red, persistent until dismissed
- Info: Blue, auto-dismiss 5s
- Position: Bottom-right
---
## 5. Data Flow
### 5.1 API Integration
All UI data comes from the REST API at `/api/v1`.
**Auth Flow**:
1. User enters token in settings or login page
2. Token stored in localStorage
3. All API requests include `Authorization: Bearer <token>` header
4. 401 response → clear token, show login
**Endpoints Used by UI**:
```
GET /api/v1/documents → Dashboard document list
GET /api/v1/documents/:id → Document viewer
POST /api/v1/documents → Create document
PUT /api/v1/documents/:id → Update document
DELETE /api/v1/documents/:id → Delete document
GET /api/v1/documents/:id/export → Export markdown
GET /api/v1/libraries → Library tree (root)
GET /api/v1/libraries/:id → Library contents
GET /api/v1/libraries/:id/tree → Full library tree
POST /api/v1/libraries → Create library
DELETE /api/v1/libraries/:id → Delete library
GET /api/v1/tags → Tag list with counts
GET /api/v1/tags/:tag/documents → Filtered documents
```
### 5.2 State Management
```javascript
// App state (simple pub/sub)
const state = {
documents: [],
libraries: [],
tags: [],
currentView: 'dashboard', // dashboard|viewer|editor|library
currentDocument: null,
currentLibrary: null,
filters: { tag: null, library: null, search: '' },
preferences: { theme: 'dark' }
};
```
### 5.3 Auto-save Implementation
```javascript
// Debounced auto-save during editing
let saveTimeout;
function onEditorChange(content) {
clearTimeout(saveTimeout);
ui.setSaving();
saveTimeout = setTimeout(() => {
api.updateDocument(id, { content }).then(() => {
ui.setSaved();
}).catch(() => {
ui.setError('Auto-save failed');
});
}, 2000); // 2 second debounce
}
```
---
## 6. Accessibility
### 6.1 Requirements
- All interactive elements keyboard accessible
- Focus visible indicators (custom styled)
- ARIA labels on icons and non-text elements
- Color contrast ratio ≥ 4.5:1 (WCAG AA)
- Screen reader announcements for dynamic content
### 6.2 Keyboard Navigation
- Tab order follows visual layout
- Focus trapped in modals
- Skip-to-content link
- Arrow keys for tree/list navigation
---
## 7. Error Handling
### 7.1 Network Errors
- Show toast notification
- Retry button for failed requests
- Offline indicator in status bar
### 7.2 Validation Errors
- Inline field errors in forms
- Red border on invalid fields
- Error message below field
### 7.3 Empty States
- No documents: Illustration + "Create your first document" CTA
- No search results: "No matches found" + clear filters button
- No libraries: "Create a library to organize your documents"
---
## 8. Future Enhancements (Out of Scope for v1)
- Drag-and-drop document organization
- Document versioning/history
- Collaborative editing
- Full-text search
- Document templates gallery
- Export to PDF/HTML
- Mobile native app