14 KiB
Implementation Plan — Remaining Roadmap Items
1. 📝 Documentation OpenAPI enrichie (P3) — 5 min
Goal: Add Field(description=...) to all Pydantic models without descriptions in backend/main.py.
Models to update (lines 89–311):
| Line | Model | Fields to annotate |
|---|---|---|
| 89 | FileContentResponse |
vault, path, title, tags, frontmatter, html, raw_length, extension, is_markdown, unsupported, size_bytes |
| 103 | FileRawResponse |
vault, path, raw |
| 110 | FileSaveResponse |
status, vault, path, size |
| 118 | FileDeleteResponse |
status, vault, path |
| 125 | SearchResultItem |
vault, path, title, tags, score, snippet, modified |
| 136 | SearchResponse |
query, vault_filter, tag_filter, count, results (total, offset, limit already have Field) |
| 146 | TagsResponse |
vault_filter, tags |
| 152 | TreeSearchResult |
vault, path, name, matched_path (type has Field) |
| 161 | TreeSearchResponse |
query, vault_filter, results |
| 168 | AdvancedSearchResultItem |
vault, path, title, tags, score, snippet, modified |
| 179 | SearchFacets |
tags, vaults (already have default_factory) |
| 185 | AdvancedSearchResponse |
results, total, offset, limit, facets (query_time_ms has Field) |
| 196 | TitleSuggestion |
vault, path, title |
| 203 | SuggestResponse |
query, suggestions |
| 209 | TagSuggestion |
tag, count |
| 215 | TagSuggestResponse |
query, suggestions |
| 221 | GraphNode |
(all fields already have Field) |
| 231 | GraphEdge |
(all fields already have Field) |
| 239 | GraphResponse |
vault, path, nodes, edges |
| 247 | ReloadResponse |
status, vaults |
| 253 | HealthResponse |
status, version, vaults, total_files |
| 265 | DirectoryCreateResponse |
success, path |
| 284 | DirectoryDeleteResponse |
success, deleted_count |
| 296 | FileCreateResponse |
success, path |
| 307 | FileRenameResponse |
success |
Dependency: None. Pure documentation change.
2. 📊 Dashboard statistiques (P3) — 30 min
2a. Backend: GET /api/dashboard (new endpoint)
File: backend/main.py — insert at line ~2547 (after /api/diagnostics)
@app.get("/api/dashboard")
async def api_dashboard(current_user=Depends(require_auth)):
"""Aggregated dashboard statistics across all accessible vaults."""
from backend.indexer import index, vault_config, path_index
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
vault_stats = []
total_files = 0
total_tags = set()
total_size = 0
for vname, vdata in index.items():
if "*" not in user_vaults and vname not in user_vaults:
continue
files = vdata.get("files", [])
file_count = len(files)
total_files += file_count
tags = set()
for f in files:
tags.update(f.get("tags", []))
total_size += f.get("size", 0)
total_tags.update(tags)
vault_stats.append({
"name": vname,
"file_count": file_count,
"tag_count": len(tags),
"total_size_bytes": sum(f.get("size", 0) for f in files),
})
return {
"vaults": vault_stats,
"total_files": total_files,
"total_tags": len(total_tags),
"total_size_bytes": total_size,
}
No new model needed — return plain dict (or add optional DashboardResponse model).
2b. Frontend: Insert stats widget in dashboard-home
File: frontend/index.html — after line 364 (</div> closing bookmarks section, before <!-- Recently Opened Section -->)
Add:
<!-- Stats Section -->
<div id="dashboard-stats-section" class="dashboard-section">
<div class="dashboard-header">
<div class="dashboard-title-row">
<i data-lucide="bar-chart-3" class="dashboard-icon" style="color:var(--accent)"></i>
<h2>Statistiques</h2>
</div>
</div>
<div id="dashboard-stats-grid" class="dashboard-stats-grid">
<div class="dashboard-stats-loading">Chargement...</div>
</div>
</div>
File: frontend/app.js — add DashboardStatsWidget module (insert at line ~3343, before DashboardRecentWidget):
const DashboardStatsWidget = {
async load() {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
grid.innerHTML = '<div class="dashboard-stats-loading">Chargement...</div>';
try {
const data = await api("/api/dashboard");
this.render(data);
} catch (err) {
grid.innerHTML = `<div class="dashboard-recent-empty">Erreur: ${escapeHtml(err.message)}</div>`;
}
},
render(data) {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
const items = [
{ icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() },
{ icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() },
{ icon: "hard-drive", label: "Taille totale", value: this._formatSize(data.total_size_bytes) },
{ icon: "folder", label: "Vaults", value: data.vaults.length.toString() },
];
grid.innerHTML = items.map(i => `
<div class="stat-card">
<i data-lucide="${i.icon}" class="stat-icon"></i>
<span class="stat-value">${i.value}</span>
<span class="stat-label">${i.label}</span>
</div>
`).join("");
safeCreateIcons();
},
_formatSize(bytes) { /* KB/MB/GB formatter */ }
};
Also update showWelcome() at line ~5417 — the dashboard rebuild HTML must include the stats section div. And line ~5490 — add DashboardStatsWidget.load() call.
File: frontend/style.css — add CSS for .dashboard-stats-grid, .stat-card, .stat-icon, .stat-value, .stat-label.
Dependency: Item 1 (Pydantic models) — none. Standalone.
3. 🔔 Webhooks (P3) — 45 min
3a. New backend module: backend/webhooks.py
Create full module with:
WEBHOOKS_FILE = Path("data/webhooks.json")— persistence_DEFAULT_WEBHOOKS = []get_webhooks() -> list— reads from diskcreate_webhook(name, url, events, secret=None) -> dictupdate_webhook(id, updates) -> dictdelete_webhook(id) -> boolasync def dispatch_webhooks(event_type: str, data: dict)— calls all webhooks subscribed toevent_type, sends JSON POST with HMAC-SHA256 signature header if secret is set, timeout 5s, logs failures- Model:
WebhookConfigwithid,name,url,events(list of event type strings),secret(optional),enabled,created_at,last_fired_at
3b. Backend: CRUD endpoints in backend/main.py
Insert at line ~2470 (before GET /api/config):
@app.get("/api/webhooks")
@app.post("/api/webhooks")
@app.patch("/api/webhooks/{webhook_id}")
@app.delete("/api/webhooks/{webhook_id}")
Import from backend.webhooks import get_webhooks, create_webhook, update_webhook, delete_webhook, dispatch_webhooks
3c. Backend: Hook dispatch_webhooks into file events
Add await dispatch_webhooks("file_created", {...}) calls alongside each sse_manager.broadcast(...) call:
| Line | Event | Add dispatch |
|---|---|---|
| ~1252 | file_deleted |
dispatch_webhooks("file_deleted", {"vault":..., "path":...}) |
| ~1330 | directory_created |
dispatch_webhooks("directory_created", {...}) |
| ~1401 | directory_renamed |
dispatch_webhooks("directory_renamed", {...}) |
| ~1462 | directory_deleted |
dispatch_webhooks("directory_deleted", {...}) |
| ~1532 | file_created |
dispatch_webhooks("file_created", {...}) |
| ~1607 | file_renamed |
dispatch_webhooks("file_renamed", {...}) |
3d. Frontend: Webhooks management UI in Configurations modal
File: frontend/index.html — insert at line ~633 (after <!-- À propos --> section, before </div> closing config-content):
<section class="config-section">
<h2>🔔 Webhooks</h2>
<p class="config-description">Notifications HTTP vers des services externes lors des changements de fichiers.</p>
<div id="webhooks-list"></div>
<div class="config-add-pattern">
<input type="text" id="webhook-name-input" placeholder="Nom" class="config-input" style="width:120px">
<input type="text" id="webhook-url-input" placeholder="https://..." class="config-input" style="flex:1">
<button id="webhook-add-btn" class="config-btn-add">Ajouter</button>
</div>
</section>
File: frontend/app.js — in initConfigModal() at line ~3918, add:
loadWebhooks(); // in the open handler
// Event binding for webhook add/save/delete buttons
Add functions: loadWebhooks(), renderWebhooks(webhooks), addWebhook(), deleteWebhook(id), toggleWebhook(id, enabled). All use api("/api/webhooks", ...).
File: frontend/style.css — add .webhook-item, .webhook-toggle, .webhook-delete styles.
Dependency: None on items 1–2. Standalone.
4. 📤 Publication publique de documents (P3) — 60 min
4a. New backend module: backend/share.py
Create full module:
SHARES_FILE = Path("data/shares.json")- ShareToken model:
id,vault,path,token(64-char hex),created_by,created_at,expires_at(optional, null = never),access_count,last_accessed create_share(vault, path, created_by, expires_in_hours=None) -> dict— generates token, stores, returns share infoget_share_by_token(token) -> dict | None— validates expiry, returns sharerevoke_share(id) -> boollist_shares(vault_filter=None) -> list— for admin/settings pagerecord_access(token)— increments access_count
4b. Backend: Endpoints in backend/main.py
Insert at line ~1619 (before GET /api/file/{vault_name}):
# Share management
@app.post("/api/share/{vault_name}")
@app.get("/api/shares")
@app.delete("/api/share/{share_id}")
# Public view (no auth required!)
@app.get("/s/{token}")
async def public_share_view(token: str): ...
The public view endpoint:
- Looks up token via
get_share_by_token(token) - Reads the file content
- Renders markdown with redacted secrets
- Returns simple HTML page (not SPA) with rendered content
- Increments access count
4c. Frontend: Share button in file actions
File: frontend/app.js — in renderFile(), at line ~3250 (after pop-out button):
const shareBtn = el("button", { class: "btn-action", title: "Partager" }, [icon("share-2", 14), document.createTextNode("Partager")]);
shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
Add shareBtn to the file-actions div at line ~3300.
Add openShareDialog(vault, path) function that:
- Calls
POST /api/share/{vault}to create a share - Shows a modal with the share URL (copyable) and expiration options
- Shows existing shares list with revoke buttons
4d. Frontend: Share management in Configurations
File: frontend/index.html — add share management section in config modal (alongside webhooks).
File: frontend/app.js — loadShares() and renderShares() functions.
File: frontend/style.css — add .share-dialog, .share-url, .share-item styles.
Dependency: None. Standalone, but needs item 1 for clean models.
5. 🔄 Gestion conflits Syncthing (P2) — 45 min
5a. Backend: Conflict file detection
File: backend/indexer.py — add after _backlink_index:
def get_conflicts() -> list:
"""Scan all vaults for Syncthing/Nextcloud sync-conflict files."""
conflicts = []
pattern = re.compile(r'\.sync-conflict-(\d{8}-\d{6})\.')
for vname, vdata in index.items():
for f in vdata.get("files", []):
m = pattern.search(f["path"])
if m:
# Find the original file
orig_path = pattern.sub("", f["path"])
conflicts.append({
"vault": vname,
"conflict_path": f["path"],
"original_path": orig_path,
"conflict_date": m.group(1),
"conflict_title": f.get("title", ""),
})
return conflicts
5b. Backend: Endpoints
File: backend/main.py — insert at line ~2547:
@app.get("/api/conflicts")
async def api_conflicts(current_user=Depends(require_auth)):
"""List sync-conflict files across accessible vaults."""
...
@app.post("/api/conflicts/resolve")
async def api_conflict_resolve(body: dict, current_user=Depends(require_auth)):
"""Resolve a conflict: keep_local (delete conflict), keep_conflict (replace original)."""
...
5c. Backend: Diff endpoint
@app.get("/api/conflicts/diff")
async def api_conflict_diff(vault: str, original: str, conflict: str, current_user=Depends(require_auth)):
"""Return unified diff between original and conflict file."""
import difflib
...
5d. Frontend: Conflict dashboard widget
File: frontend/index.html — add #dashboard-conflicts-section in dashboard after stats section.
File: frontend/app.js — add DashboardConflictsWidget (pattern similar to recent/bookmarks):
load()→GET /api/conflictsrender()→ shows conflict cards with file names and dates- Click → opens diff modal showing side-by-side comparison
- Action buttons: "Garder l'original", "Garder le conflit"
File: frontend/style.css — add .conflict-card, .conflict-diff, .conflict-actions styles.
Dependency: None. Standalone.
Execution Order (optimal)
- Item 1 — OpenAPI docs (quick win, no risk)
- Item 2 — Dashboard stats (standalone, visible result)
- Item 3 — Webhooks (new module + integration, most code)
- Item 4 — Public shares (new module + public view, security-sensitive)
- Item 5 — Syncthing conflicts (standalone, nice-to-have)
Total estimated effort: ~3 hours
Files Summary
| File | Action | Items |
|---|---|---|
backend/main.py |
Edit | 1 (models), 2a (endpoint), 3b+c (webhook CRUD+dispatch), 4b (share+public view), 5b+c (conflicts) |
backend/webhooks.py |
Create | 3a |
backend/share.py |
Create | 4a |
backend/indexer.py |
Edit | 5a (get_conflicts) |
frontend/index.html |
Edit | 2b, 3d, 4d, 5d (dashboard + config sections) |
frontend/app.js |
Edit | 2b, 3d, 4c, 5d (widgets + share button + webhook UI) |
frontend/style.css |
Edit | 2b, 3d, 4c, 5d (all new CSS classes) |