ObsiGate/plan.md

14 KiB
Raw Blame History

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 89311):

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.htmlafter 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 disk
  • create_webhook(name, url, events, secret=None) -> dict
  • update_webhook(id, updates) -> dict
  • delete_webhook(id) -> bool
  • async def dispatch_webhooks(event_type: str, data: dict) — calls all webhooks subscribed to event_type, sends JSON POST with HMAC-SHA256 signature header if secret is set, timeout 5s, logs failures
  • Model: WebhookConfig with id, 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 12. 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 info
  • get_share_by_token(token) -> dict | None — validates expiry, returns share
  • revoke_share(id) -> bool
  • list_shares(vault_filter=None) -> list — for admin/settings page
  • record_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:

  1. Looks up token via get_share_by_token(token)
  2. Reads the file content
  3. Renders markdown with redacted secrets
  4. Returns simple HTML page (not SPA) with rendered content
  5. 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.jsloadShares() 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/conflicts
  • render() → 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)

  1. Item 1 — OpenAPI docs (quick win, no risk)
  2. Item 2 — Dashboard stats (standalone, visible result)
  3. Item 3 — Webhooks (new module + integration, most code)
  4. Item 4 — Public shares (new module + public view, security-sensitive)
  5. 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)