Add share, webhook, and conflict management features
This commit is contained in:
+259
-268
@@ -1,321 +1,312 @@
|
||||
# Code Context
|
||||
# Code Context — ObsiGate Roadmap Implementation
|
||||
|
||||
## Files Retrieved
|
||||
|
||||
1. `C:/dev/git/python/ObsiGate/backend/main.py` (lines 1-2504) - Core API endpoints, markdown rendering, SSE
|
||||
2. `C:/dev/git/python/ObsiGate/backend/auth/router.py` (full file, 263 lines) - Auth endpoints (login, logout, refresh, admin CRUD)
|
||||
3. `C:/dev/git/python/ObsiGate/backend/auth/jwt_handler.py` (full file, 153 lines) - JWT token creation, validation, revocation
|
||||
4. `C:/dev/git/python/ObsiGate/backend/indexer.py` (full file, ~728 lines) - File indexing, vault config, file lookup
|
||||
5. `C:/dev/git/python/ObsiGate/backend/search.py` (full file, ~700 lines) - Full-text search, TF-IDF, suggestions
|
||||
6. `C:/dev/git/python/ObsiGate/frontend/app.js` (lines 1-8046) - Frontend SPA (TOC, tabs, tree, search)
|
||||
7. `C:/dev/git/python/ObsiGate/frontend/style.css` (lines 5379-5476) - Tab bar styles
|
||||
### Backend
|
||||
1. `backend/main.py` (2599 lines total) — main FastAPI app with all endpoints, Pydantic models, SSE manager
|
||||
2. `backend/vault_settings.py` (138 lines) — per-vault settings persistence (hideHiddenFiles, etc.)
|
||||
|
||||
### Frontend
|
||||
3. `frontend/app.js` (~8187 lines) — vanilla JS SPA, all UI logic
|
||||
4. `frontend/index.html` (1083 lines) — page structure with modals and dashboard
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend: `main.py`
|
||||
## 1. Pydantic Models Section — `backend/main.py`
|
||||
|
||||
### PUT endpoint for saving files
|
||||
**Location: lines 56–284** — All Pydantic response/request models defined in a single block after imports and before SSE Manager.
|
||||
|
||||
**Location:** Line **717** (`@app.put("/api/file/{vault_name}/save", response_model=FileSaveResponse)`)
|
||||
- Function: `api_file_save` (line **718**)
|
||||
- Body expects `{"content": "..."}`
|
||||
- Ends at line ~750 with return `{"status": "ok", "vault": vault_name, "path": path, "size": len(content)}`
|
||||
|
||||
### DELETE endpoint for files
|
||||
|
||||
**Location:** Line **753** (`@app.delete("/api/file/{vault_name}", response_model=FileDeleteResponse)`)
|
||||
- Function: `api_file_delete` (line **754**)
|
||||
- Path provided as query parameter: `path: str = Query(...)`
|
||||
- Also calls `remove_single_file()` and broadcasts SSE `file_deleted` event
|
||||
- Ends at line ~797
|
||||
|
||||
### `_heading_slugify` function
|
||||
|
||||
**Location:** Lines **476–503** (inside the Markdown rendering helpers section)
|
||||
### Key models with Field descriptions (lines 60–69):
|
||||
```python
|
||||
def _heading_slugify(text: str) -> str:
|
||||
class VaultInfo(BaseModel):
|
||||
name: str = Field(description="Display name of the vault")
|
||||
file_count: int = Field(description="Number of indexed files")
|
||||
tag_count: int = Field(description="Number of unique tags")
|
||||
type: str = Field(default="VAULT", description="Type of the vault mapping (VAULT or DIR)")
|
||||
|
||||
class BrowseItem(BaseModel): # line 72
|
||||
name: str
|
||||
path: str
|
||||
type: str = Field(description="'file' or 'directory'")
|
||||
```
|
||||
- Matches the JavaScript `slugify()` exactly:
|
||||
1. Lowercase
|
||||
2. NFD normalize + strip combining marks
|
||||
3. Keep only Unicode letters, numbers, spaces, hyphens
|
||||
4. Spaces → hyphens, collapse multiple hyphens
|
||||
5. Strip leading/trailing hyphens, fallback to `"heading"`
|
||||
|
||||
### `_add_heading_ids` function
|
||||
|
||||
**Location:** Lines **506–527**
|
||||
- Post-processes HTML to inject `id=""` attributes on `<h1>`–`<h6>` tags
|
||||
- Handles duplicate slugs with `-2`, `-3` suffix
|
||||
|
||||
### Health endpoint
|
||||
|
||||
**Location:** Lines **562–571** (`@app.get("/api/health", response_model=HealthResponse)`)
|
||||
- Returns `{ status, version, vaults, total_files }`
|
||||
- No authentication required
|
||||
|
||||
### Markdown rendering pipeline (wikilinks)
|
||||
|
||||
- `_convert_wikilinks()`: lines **528–549** — converts `[[target]]` / `[[target|display]]` to clickable HTML anchors
|
||||
- `_render_markdown()`: lines **552–577** — master renderer: preprocesses images → converts wikilinks → renders with mistune → adds heading IDs
|
||||
- Wikilinks render as `<a class="wikilink" data-vault="..." data-path="...">` when resolved, `<span class="wikilink-missing">` otherwise
|
||||
### Models WITHOUT Field descriptions (need updating):
|
||||
- `FileContentResponse` — **lines 89–100**
|
||||
- `FileRawResponse` — **lines 103–107**
|
||||
- `FileSaveResponse` — **lines 110–115**
|
||||
- `FileDeleteResponse` — **lines 118–122**
|
||||
- `SearchResultItem` — **lines 125–133**
|
||||
- `SearchResponse` — **lines 136–143** (has Field on `total`, `offset`, `limit`)
|
||||
- `TagsResponse` — **lines 146–149**
|
||||
- `TreeSearchResult` — **lines 152–158**
|
||||
- `TreeSearchResponse` — **lines 161–165**
|
||||
- `AdvancedSearchResultItem` — **lines 168–176**
|
||||
- `SearchFacets` — **lines 179–182**
|
||||
- `AdvancedSearchResponse` — **lines 185–193** (has Field on `query_time_ms`)
|
||||
- `TitleSuggestion` — **lines 196–200**
|
||||
- `SuggestResponse` — **lines 203–206**
|
||||
- `TagSuggestion` — **lines 209–212**
|
||||
- `TagSuggestResponse` — **lines 215–218**
|
||||
- `GraphNode` — **lines 221–228**
|
||||
- `GraphEdge` — **lines 231–236**
|
||||
- `GraphResponse` — **lines 239–244**
|
||||
- `ReloadResponse` — **lines 247–250**
|
||||
- `HealthResponse` — **lines 253–257**
|
||||
- `DirectoryCreateRequest` — **lines 260–262** (has Field)
|
||||
- `DirectoryCreateResponse` — **lines 265–269**
|
||||
- `DirectoryRenameRequest` — **lines 272–274** (has Field)
|
||||
- `DirectoryRenameResponse` — **lines 277–281**
|
||||
- `DirectoryDeleteResponse` — **lines 284–288**
|
||||
- `FileCreateRequest` — **lines 291–293** (has Field)
|
||||
- `FileCreateResponse` — **lines 296–299**
|
||||
- `FileRenameRequest` — **lines 302–304** (has Field)
|
||||
- `FileRenameResponse` — **lines 307–311**
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend: `auth/router.py`
|
||||
## 2. Dashboard/Stats Endpoint — `backend/main.py`
|
||||
|
||||
### Login endpoint
|
||||
|
||||
**Location:** Line **97** (`@router.post("/login")`)
|
||||
- Function: `login` (line **98**)
|
||||
- Accepts `LoginRequest` with `username`, `password`, `remember_me`
|
||||
- Rate limiting via lockout:
|
||||
- `is_locked()` check at line **108** → returns 429 after too many failures
|
||||
- `record_login_failure()` at line **112** → increments failure counter
|
||||
- Lockout message: `"Compte temporairement verrouillé (15min)"` (line **109**)
|
||||
- Success: calls `create_access_token()` + `create_refresh_token()`, sets cookies
|
||||
|
||||
### Rate limiting
|
||||
|
||||
**Location:** Lines **108–117** (inside `login` endpoint)
|
||||
- **There is NO decorator-based or middleware rate limiting.** Rate limiting is manual, login-only:
|
||||
- Checks `is_locked()` (line **108**) — if true, raises HTTP 429
|
||||
- Calls `record_login_failure()` (line **112**) on bad password
|
||||
- Shows remaining attempts when <= 2 (line **114–115**)
|
||||
- Implementation lives in `backend/auth/user_store.py`:
|
||||
- `record_login_failure()` at line **142**
|
||||
- `is_locked()` at line **167**
|
||||
- No rate limiting on other endpoints (no slowapi, no middleware, no global limiter)
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend: `auth/jwt_handler.py`
|
||||
|
||||
### JWT TTL / expiration settings
|
||||
|
||||
**Location:** Lines **22–23**
|
||||
### `/api/diagnostics` — **lines 2500–2547**
|
||||
```python
|
||||
ACCESS_TOKEN_EXPIRE_SECONDS = 3600 # 1 hour
|
||||
REFRESH_TOKEN_EXPIRE_SECONDS = 604800 # 7 days
|
||||
@app.get("/api/diagnostics") # line 2500
|
||||
async def api_diagnostics(current_user=Depends(require_admin)):
|
||||
```
|
||||
- Algorithm: `HS256` (line **20**)
|
||||
- Secret key auto-generated to `data/secret.key` on first run (line **29**)
|
||||
- Refresh cookie max_age in router.py: 30 days if `remember_me`, else 7 days (line **131**)
|
||||
Returns index stats, inverted index stats, config, and search executor info. Requires admin auth.
|
||||
|
||||
### `create_access_token` function
|
||||
|
||||
**Location:** Lines **48–60**
|
||||
### `/api/health` — **lines 1048–1059**
|
||||
```python
|
||||
def create_access_token(user: dict) -> str:
|
||||
@app.get("/api/health", response_model=HealthResponse) # line 1048
|
||||
async def api_health():
|
||||
```
|
||||
- Payload: `{ sub, role, vaults, jti, iat, exp, type: "access" }`
|
||||
- Encoded with `jwt.encode()` using HS256 and the secret key from `get_secret_key()`
|
||||
|
||||
### `create_refresh_token` function
|
||||
|
||||
**Location:** Lines **63–73**
|
||||
- Returns `(token_string, jti)` tuple
|
||||
- Payload: `{ sub, jti, iat, exp, type: "refresh" }`
|
||||
- Uses `REFRESH_TOKEN_EXPIRE_SECONDS`
|
||||
Public health check (no auth). Returns status, version, vaults count, total_files.
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend: `indexer.py`
|
||||
## 3. SSE File Event Broadcasting — `backend/main.py`
|
||||
|
||||
### IGNORED_DIRS or similar
|
||||
**SSE Manager class: lines 349–381** — `SSEManager` with `connect()`, `disconnect()`, and `broadcast()` methods.
|
||||
|
||||
**There is NO `IGNORED_DIRS` constant.** The indexer indexes **everything** including hidden files (starting with `.`). This is stated explicitly in the docstring at line **206**:
|
||||
> "All files and directories are indexed, including hidden files (starting with '.')."
|
||||
**All `sse_manager.broadcast()` calls:**
|
||||
|
||||
Hidden-file filtering is handled at the **UI/browse level** via vault settings (`hideHiddenFiles`) in `main.py` and `vault_settings.py`.
|
||||
| Line | Event Type | Context |
|
||||
|------|-----------|---------|
|
||||
| 413 | `index_updated` | File watcher callback (`_on_vault_change`) — partial index changes |
|
||||
| 506 | `index_<event_type>` | Background indexing progress |
|
||||
| 1252 | `file_deleted` | DELETE file endpoint |
|
||||
| 1330 | `directory_created` | POST create directory |
|
||||
| 1401 | `directory_renamed` | PATCH rename directory |
|
||||
| 1462 | `directory_deleted` | DELETE directory |
|
||||
| 1532 | `file_created` | POST create file |
|
||||
| 1607 | `file_renamed` | PATCH rename file |
|
||||
| 1949 | `index_reloaded` | Force reindex endpoint |
|
||||
| 2114 | `vault_reloaded` | (likely during vault reload) |
|
||||
| 2196 | `vault_added` | POST /api/vaults/add |
|
||||
| 2215 | `vault_removed` | DELETE /api/vaults/remove |
|
||||
|
||||
### `vault_config` handling
|
||||
**SSE endpoint: lines 2127–2169** — `GET /api/events` returns `StreamingResponse` with `text/event-stream`.
|
||||
|
||||
**Location:** Lines **15–16** (global)
|
||||
**Frontend SSE client: `frontend/app.js` lines 5773–6015**
|
||||
- `IndexUpdateManager` (IIFE module)
|
||||
- Listens for events: `connected`, `index_updated`, `index_reloaded`, `vault_added`, `vault_removed`, `index_start`, `index_progress`, `index_complete`
|
||||
- Auto-reconnects with exponential backoff (1s → 30s max)
|
||||
- The `_onIndexUpdated()` handler (line 5912) refreshes sidebar tree and tags when affected vault matches context
|
||||
|
||||
**⚠️ Note:** The frontend SSE client does NOT currently listen for `file_created`, `file_deleted`, `file_modified`, `file_renamed`, `directory_created`, `directory_renamed`, `directory_deleted` events — these are broadcast by the backend but not consumed by the frontend. The frontend only reacts to `index_updated` (which already includes all changes).
|
||||
|
||||
---
|
||||
|
||||
## 4. Configurations Endpoint — `backend/main.py`
|
||||
|
||||
### Config storage — **lines 2414–2458**
|
||||
```python
|
||||
vault_config: Dict[str, Dict[str, Any]] = {}
|
||||
```
|
||||
- Type: `{name: {path, attachmentsPath, scanAttachmentsOnStartup, type}}`
|
||||
- Populated by `load_vault_config()` at lines **50–104**
|
||||
- Reads `VAULT_N_NAME`/`VAULT_N_PATH` and `DIR_N_NAME`/`DIR_N_PATH` env vars
|
||||
- Also has `vault_config.update(load_vault_config())` in `build_index()` at line **312**
|
||||
_CONFIG_PATH = _BASE_DIR / "data" / "config.json" # line 2414
|
||||
|
||||
### Key data structures
|
||||
|
||||
- `index`: dict of vaults → `{files, tags, path, paths, config}` (line **11**)
|
||||
- `_file_lookup`: `{filename_lower: [{vault, path}, ...]}` — O(1) wikilink resolution (line **22**)
|
||||
- `path_index`: `{vault_name: [{path, name, type}, ...]}` — tree filtering (line **25**)
|
||||
- `_index_lock`: `threading.Lock()` (line **18**)
|
||||
- `_index_generation`: int counter for staleness detection (line **24**)
|
||||
|
||||
---
|
||||
|
||||
## 5. Backend: `search.py`
|
||||
|
||||
### Wikilink / backlink functions
|
||||
|
||||
**There are NO wikilink or backlink functions in `search.py`.** The file handles:
|
||||
- Full-text search with TF-IDF via `InvertedIndex` class (line **218**)
|
||||
- `advanced_search()` (line **426**) — supports operators: `tag:`, `vault:`, `title:`, `path:`, `ext:`
|
||||
- Title suggestions: `suggest_titles()` (line **594**)
|
||||
- Tag suggestions: `suggest_tags()` (line **620**)
|
||||
|
||||
**Wikilink resolution** lives in `backend/indexer.py` via `find_file_in_index()` (line **653**) using `_file_lookup`.
|
||||
**Wikilink rendering** lives in `backend/main.py` via `_convert_wikilinks()` (line **528**).
|
||||
**Backlinks do not exist** anywhere in the codebase — no function computes "what links to this file."
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend: `app.js`
|
||||
|
||||
### Tree item click handler (sidebar file opening)
|
||||
|
||||
**Primary location:** Lines **2349–2360** (inside `_renderDirectoryInContainer` during tree rendering)
|
||||
```javascript
|
||||
fileItem.addEventListener("click", () => {
|
||||
scrollTreeItemIntoView(fileItem, false);
|
||||
openFile(vaultName, item.path);
|
||||
closeMobileSidebar();
|
||||
});
|
||||
```
|
||||
|
||||
**Second location (search results):** Lines **2637–2642** — same pattern in a different tree-rendering path.
|
||||
**Third location (tree search filter results):** Lines **2790–2795** — filter results click handler.
|
||||
|
||||
### `openFile` function
|
||||
|
||||
**Location:** Lines **3085–3106** (original `openFile`)
|
||||
- Sets `currentVault`, `currentPath`, fetches `/api/file/{vault}?path={path}`
|
||||
- Calls `renderFile(data)` which builds breadcrumb, tags, action buttons, then renders HTML
|
||||
|
||||
**Overridden at line 7604:**
|
||||
```javascript
|
||||
openFile = function(vault, path) {
|
||||
TabManager.open(vault, path);
|
||||
};
|
||||
```
|
||||
This wraps the original to use tab-based navigation. `TabManager.open()` creates/focuses a tab.
|
||||
|
||||
### Tab management functions (TabManager)
|
||||
|
||||
**Location:** Lines **7234–7598** (`const TabManager = { ... }`)
|
||||
- `init()` — line **7243** — grabs DOM refs
|
||||
- `open(vault, path, options)` — line **7247** — opens a file in a new/focused tab
|
||||
- `activate(tabId)` — line **7274** — switches to a tab, saves/restores state
|
||||
- `close(tabId)` — line **7330** — closes a tab, switches to adjacent
|
||||
- `closeAll()` — line **7348** — closes all, shows dashboard
|
||||
- `closeRight(tabId)` — line **7358** — closes tabs to the right
|
||||
- `closeOthers(tabId)` — line **7374** — closes all except current
|
||||
- `moveTab(fromIdx, toIdx)` — line **7387** — drag-and-drop reorder
|
||||
- `_renderTabs()` — line **7438** — DOM rendering of tab bar with icons, names, close buttons, drag & drop
|
||||
- `_showTabContextMenu(x, y, tabId)` — line **7558** — right-click menu (Close, Close Others, Close Right, Close All)
|
||||
|
||||
### TOC `slugify` function
|
||||
|
||||
**Location:** Lines **766–776** (inside `OutlineManager`)
|
||||
```javascript
|
||||
slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.trim() || "heading";
|
||||
_DEFAULT_CONFIG = { # line 2416
|
||||
"search_workers": 2,
|
||||
"debounce_ms": 300,
|
||||
"results_per_page": 50,
|
||||
"min_query_length": 2,
|
||||
"search_timeout_ms": 30000,
|
||||
"max_content_size": 100000,
|
||||
"snippet_context_chars": 120,
|
||||
"max_snippet_highlights": 5,
|
||||
"title_boost": 3.0,
|
||||
"path_boost": 1.5,
|
||||
"watcher_enabled": True,
|
||||
"watcher_use_polling": False,
|
||||
"watcher_polling_interval": 5.0,
|
||||
"watcher_debounce": 2.0,
|
||||
"tag_boost": 2.0,
|
||||
"prefix_max_expansions": 50,
|
||||
"recent_files_limit": 20,
|
||||
}
|
||||
```
|
||||
|
||||
### Backlinks UI
|
||||
|
||||
**There is NO backlinks UI or functionality anywhere in the frontend.**
|
||||
- No `backlink` string found in `app.js`, `style.css`, or `index.html`
|
||||
- No "Links to this page" panel, no backlink section in the editor, no backlink search in the sidebar
|
||||
### `GET /api/config` — **line 2464**: Returns merged config (requires auth)
|
||||
### `POST /api/config` — **line 2470**: Updates config (requires admin), validates types against `_DEFAULT_CONFIG`
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend: `style.css`
|
||||
## 5. Dashboard-Home Element and Rendering — `frontend/app.js`
|
||||
|
||||
### Tab-related styles
|
||||
### Dashboard DOM structure → `frontend/index.html` lines 341–392
|
||||
```html
|
||||
<div id="dashboard-home" class="dashboard-home" role="region" aria-label="Tableau de bord">
|
||||
<div id="dashboard-bookmarks-section" class="dashboard-section">...</div>
|
||||
<div id="dashboard-recent-section" class="dashboard-section">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Location:** Lines **5379–5480** (`.tab-bar` through `.tab-drop-indicator`)
|
||||
- `.tab-bar` (line **5379**): flex container, 36px min-height, border-bottom
|
||||
- `.tab-bar[hidden]` (line **5389**): `display: none`
|
||||
- `.tab-list` (line **5393**): horizontal flex with overflow-x auto
|
||||
- `.tab-item` (line **5406**): padding 6px 12px, 0.8rem, border-right, transitions
|
||||
- `.tab-item:hover` (line **5424**): bg hover, color change
|
||||
- `.tab-item.active` (line **5429**): bg primary, bottom accent border
|
||||
- `.tab-item .tab-icon` (line **5436**): 14×14, flex-shrink
|
||||
- `.tab-item .tab-name` (line **5443**): overflow ellipsis, max-width 150px
|
||||
- `.tab-item .tab-close` (line **5449**): 16×16, hidden by default (opacity: 0)
|
||||
- `.tab-item:hover .tab-close, .tab-item.active .tab-close` (lines **5461–5462**): opacity 0.6
|
||||
- `.tab-item .tab-close:hover` (line **5466**): opacity 1
|
||||
- `.tab-item.dragging` (line **5471**): opacity 0.5
|
||||
- `.tab-drop-indicator` (line **5476**): 2px accent bar for drag-drop
|
||||
### Dashboard regeneration fallback → `app.js` lines 5417–5482 (`showWelcome()`)
|
||||
When `dashboard-home` or its children are missing, `showWelcome()` rebuilds the entire HTML structure inline.
|
||||
|
||||
### Sidebar tab styles (sidebar-tab, not content-tab)
|
||||
### Dashboard Recent Widget → `app.js` lines 3344–3580 (`DashboardRecentWidget`)
|
||||
- `load(vaultFilter)` — line 3347
|
||||
- `render()` — line 3410
|
||||
- `_createCard(file, index)` — line 3436
|
||||
- `showLoading()` — line 3397
|
||||
- `showEmpty()` — line 3526
|
||||
|
||||
**Location:** Lines **744–802**
|
||||
- `.sidebar-tabs` (line **745**)
|
||||
- `.sidebar-tab` (line **754**): uppercase, accent border on active
|
||||
- `.sidebar-tab-panel` (line **793**): display none, scrollable
|
||||
### Dashboard Bookmarks Widget → `app.js` lines 3583–3660 (`DashboardBookmarkWidget`)
|
||||
- `load(vaultFilter)` — line 3587
|
||||
- `render()` — line 3613
|
||||
- `_createCard(file, index)` — around line 3628
|
||||
|
||||
### Dashboard visibility toggling:
|
||||
- Show: `app.js` line 7590–7593 — `dashboard.style.display = ""`
|
||||
- Hide: `app.js` line 7462–7464 — `dashboard.style.display = "none"`
|
||||
|
||||
---
|
||||
|
||||
## 6. Configurations/Settings Modal — `frontend/app.js`
|
||||
|
||||
### Modal initialization → **lines 3906–3990** (`initConfigModal()`)
|
||||
Event binding for open/close, config fields, save buttons, reindex, reset, diary refresh, hidden files.
|
||||
|
||||
### Config modal opening → **line 3914**:
|
||||
```javascript
|
||||
openBtn.addEventListener("click", async () => {
|
||||
modal.classList.add("active");
|
||||
renderConfigFilters();
|
||||
loadConfigFields(); // loads frontend+backend config
|
||||
loadDiagnostics(); // loads /api/diagnostics
|
||||
loadAbout(); // loads /api/health
|
||||
await loadHiddenFilesSettings();
|
||||
});
|
||||
```
|
||||
|
||||
### Config field loading → **lines 4043–4070** (`loadConfigFields()`)
|
||||
Loads frontend config from localStorage and backend config from `GET /api/config`.
|
||||
|
||||
### Diagnostics rendering → **lines 4157–4207** (`loadDiagnostics()`, `renderDiagnostics()`)
|
||||
Fetches `GET /api/diagnostics` and renders in `#config-diagnostics`.
|
||||
|
||||
### About section → **lines 4211–4250+** (`loadAbout()`)
|
||||
Fetches `GET /api/health`.
|
||||
|
||||
### Config Modal HTML → `frontend/index.html` lines 395–564
|
||||
All the config sections: search params, recent history, backend params, tag filtering, watcher, hidden files, diagnostics, about.
|
||||
|
||||
---
|
||||
|
||||
## 7. File Action Buttons — `frontend/app.js`
|
||||
|
||||
### Button creation → **lines 3213–3260**
|
||||
All 6 action buttons created in `renderFile()`:
|
||||
|
||||
| Button | Line | Icon | Action |
|
||||
|--------|------|------|--------|
|
||||
| Copy | 3213 | `copy` | Copies raw content to clipboard (fetches if needed) |
|
||||
| Source | 3231 | `code` | Toggles raw source view |
|
||||
| Download | 3233 | `download` | Triggers file download via `/api/file/{vault}/download` |
|
||||
| Edit | 3244 | `edit` | Calls `openEditor(vault, path)` |
|
||||
| Pop-out | 3250 | `external-link` | Opens in new window via `/popout/{vault}/{path}` |
|
||||
| TOC | 3256 | `list` | Toggles right sidebar TOC |
|
||||
|
||||
### Button assembly → **line 3300**:
|
||||
```javascript
|
||||
area.appendChild(el("div", { class: "file-header" }, [...,
|
||||
el("div", { class: "file-actions" }, [
|
||||
copyBtn, sourceBtn, downloadBtn, editBtn, openNewWindowBtn, tocBtn
|
||||
])
|
||||
]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Vault Settings — `backend/vault_settings.py`
|
||||
|
||||
**Full file: 138 lines** — Per-vault UI display preferences stored in `/app/data/vault_settings.json`.
|
||||
|
||||
### Exports used by `main.py`:
|
||||
```python
|
||||
from backend.vault_settings import get_vault_setting, update_vault_setting, get_all_vault_settings, delete_vault_setting
|
||||
```
|
||||
(imported at line 46 of `main.py`)
|
||||
|
||||
### Key functions:
|
||||
- `get_vault_setting(vault_name)` — line 82 — returns settings dict or None
|
||||
- `update_vault_setting(vault_name, settings)` — line 93 — partial update, auto-saves
|
||||
- `get_all_vault_settings()` — line 125 — returns all vault settings
|
||||
- `delete_vault_setting(vault_name)` — line 111 — removes vault settings
|
||||
- Storage format: `{"vault_name": {"hideHiddenFiles": true/false}, ...}`
|
||||
|
||||
### Current usage in `main.py`:
|
||||
- `get_vault_setting(vault_name)` used in browse (line 776) and graph (line 1981) endpoints for `hideHiddenFiles`
|
||||
|
||||
### Config Modal Hidden Files → `app.js` (search for `loadHiddenFilesSettings`)
|
||||
Front-facing CRUD for per-vault `hideHiddenFiles` setting in the Configurations modal.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Frontend (app.js ~8000 lines) │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Sidebar │ │ Content Area │ │ Right Sidebar │ │
|
||||
│ │ Tree │ │ TabManager │ │ TOC/Outline │ │
|
||||
│ │ Tags │ │ renderFile() │ │ (slugify) │ │
|
||||
│ │ Filter │ │ Breadcrumbs │ │ ReadingProgress │ │
|
||||
│ └──────────┘ └──────────────┘ └─────────────────┘ │
|
||||
│ ← openFile() → TabManager.open() → api() → backend│
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Backend (FastAPI) │
|
||||
│ main.py: │
|
||||
│ PUT /api/file/{vault}/save (line 717) │
|
||||
│ DELETE /api/file/{vault} (line 753) │
|
||||
│ GET /api/file/{vault} (rendered HTML) │
|
||||
│ GET /api/health (line 562) │
|
||||
│ _heading_slugify() (line 476) │
|
||||
│ _convert_wikilinks() (line 528) │
|
||||
│ │
|
||||
│ auth/router.py: │
|
||||
│ POST /api/auth/login (line 97) │
|
||||
│ Rate limiting: lockout only (lines 108-117) │
|
||||
│ │
|
||||
│ auth/jwt_handler.py: │
|
||||
│ ACCESS_TOKEN_EXPIRE_SECONDS = 3600 (line 22) │
|
||||
│ REFRESH_TOKEN_EXPIRE_SECONDS = 604800 (line 23) │
|
||||
│ create_access_token() (line 48) │
|
||||
│ │
|
||||
│ indexer.py: │
|
||||
│ vault_config {} (line 15-16) │
|
||||
│ load_vault_config() (line 50) │
|
||||
│ ⚠ No IGNORED_DIRS — indexes everything │
|
||||
│ │
|
||||
│ search.py: │
|
||||
│ ⚠ No wikilink/backlink functions │
|
||||
│ Wikilinks resolved via indexer.find_file_in_index│
|
||||
└─────────────────────────────────────────────────────┘
|
||||
backend/main.py
|
||||
├── Pydantic models (lines 56-284) — request/response schemas
|
||||
├── SSEManager (lines 349-381) — broadcast to clients
|
||||
├── _on_vault_change (lines 390-417) — watcher callback → index update + SSE broadcast
|
||||
├── API endpoints:
|
||||
│ ├── /api/health (1048) — public health
|
||||
│ ├── /api/events (2127) — SSE stream
|
||||
│ ├── /api/config GET/POST (2464/2470) — app config CRUD
|
||||
│ ├── /api/diagnostics (2500) — index stats (admin only)
|
||||
│ ├── /api/file/* — CRUD with SSE broadcasts on create/delete/rename
|
||||
│ └── /api/directory/* — CRUD with SSE broadcasts
|
||||
├── _CONFIG_PATH, _DEFAULT_CONFIG (2414-2458)
|
||||
|
||||
backend/vault_settings.py
|
||||
├── Per-vault settings (hideHiddenFiles)
|
||||
├── JSON persistence in /app/data/vault_settings.json
|
||||
|
||||
frontend/index.html
|
||||
├── #dashboard-home (341-392) — bookmarks + recent sections
|
||||
├── #config-modal (395-564) — full config UI
|
||||
├── #editor-modal — CodeMirror editor
|
||||
├── #graph-modal — D3 graph view
|
||||
└── #help-modal — user guide
|
||||
|
||||
frontend/app.js
|
||||
├── AuthManager (~1532+)
|
||||
├── DashboardRecentWidget (3344-3580)
|
||||
├── DashboardBookmarkWidget (3583-3660)
|
||||
├── initConfigModal (3906-3990)
|
||||
├── loadConfigFields (4043-4070)
|
||||
├── loadDiagnostics / renderDiagnostics (4157-4207)
|
||||
├── showWelcome (5417-5482) — dashboard rebuild + render
|
||||
├── IndexUpdateManager / SSE client (5773-6015)
|
||||
├── renderFile (3075-3328) — file view with action buttons
|
||||
└── TabManager (7307+) — multi-tab support
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Start Here
|
||||
|
||||
For any feature work, start with **`C:/dev/git/python/ObsiGate/backend/main.py`** — it contains the API surface, markdown rendering (wikilinks, heading IDs, slugify), and all the endpoint definitions that tie the frontend to the backend index/search/auth subsystems.
|
||||
1. **For Pydantic Field descriptions**: Open `backend/main.py` at **line 89** (`FileContentResponse`) and add `Field(description=...)` to each field for models without descriptions through line 311.
|
||||
|
||||
### Key Findings / Gaps
|
||||
- **No rate limiting** except manual lockout on login
|
||||
- **No backlinks** — neither computed in backend nor displayed in frontend
|
||||
- **No IGNORED_DIRS** — the indexer indexes everything; hidden-file hiding is at the UI layer
|
||||
- **No wikilink/backlink in search.py** — wikilink resolution is in `indexer.py`, rendering in `main.py`
|
||||
- **TabManager** is a self-contained singleton at the end of `app.js` (line 7234) wrapping the original `openFile`
|
||||
2. **For dashboard stats widget**: Open `frontend/index.html` at **line 341** (`#dashboard-home`) and `frontend/app.js` at **line 5417** (`showWelcome()`). The dashboard currently has two sections (Bookmarks + Recently Opened). A new stats section would be added between those divs.
|
||||
|
||||
3. **For webhooks on file events**: The SSE broadcasts happen in `backend/main.py` at lines 1252, 1532, 1607 (file events) and 1330, 1401, 1462 (directory events). The frontend SSE client in `app.js` at line 5773 doesn't listen for individual file events — it only handles `index_updated`. Webhook firing should be added alongside the `sse_manager.broadcast()` calls.
|
||||
|
||||
4. **For Configurations modal**: Open `frontend/index.html` at **line 395** (`#config-modal`) and `frontend/app.js` at **line 3906** (`initConfigModal()`).
|
||||
|
||||
Reference in New Issue
Block a user