feat: v6.2.0 Web Clipper — extension navigateur (capture article/selection/bookmark/screenshot)
FlowDeck CI / lint (push) Successful in 1m11s
FlowDeck CI / test (push) Failing after 8m32s
FlowDeck CI / docker (push) Skipped

- Service app/services/web_clipper.py: sanitize HTML, html->blocks, extraction article, creation page workspace-aware, rate limit 50/h, device registration
- Router app/routers/web_clipper.py: POST /api/v2/web-clipper/clip, GET /status, POST /auth/verify, GET/DELETE /devices, GET /extensions (download page), auth via session ou Bearer (api_tokens / extension_devices)
- Migration 19: extension_devices + extension_clips (+ indexes)
- Extension Manifest V3: content.js (floating button, selection), background.js (clip + contextMenus), popup.html/js, clipper.css, icons
- Settings UI: onglet Extensions (liste devices, revoke, test clip, liens download), page /extensions
- Tests: 16 tests web_clipper (sanitize, blocks, article/bookmark/selection/screenshot, bearer, rate-limit, devices, extensions page)
- Bump version 6.1.0 -> 6.2.0
This commit is contained in:
2026-09-19 23:26:03 -04:00
parent 13d5f8625a
commit 0b251649e5
31 changed files with 3072 additions and 3 deletions
+47
View File
@@ -796,6 +796,53 @@ def _add_sync_version(conn: sqlite3.Connection, table: str) -> None:
conn.execute(f"ALTER TABLE {table} ADD COLUMN sync_version INTEGER NOT NULL DEFAULT 1")
@register(19, "v6.0.0: web clipper — extension devices & clips")
def _migration_web_clipper(conn: sqlite3.Connection) -> None:
"""v6.0.0 — Web Clipper: extension browser + capture."""
conn.execute(
"""CREATE TABLE IF NOT EXISTS extension_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
extension_name TEXT NOT NULL DEFAULT 'clipper',
device_id TEXT NOT NULL,
device_name TEXT DEFAULT '',
token_hash TEXT NOT NULL DEFAULT '',
scopes TEXT NOT NULL DEFAULT 'read,write',
last_used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
revoked INTEGER NOT NULL DEFAULT 0,
UNIQUE(user_id, extension_name, device_id)
)"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ext_devices_user ON extension_devices(user_id, revoked)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ext_devices_device ON extension_devices(device_id)"
)
conn.execute(
"""CREATE TABLE IF NOT EXISTS extension_clips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
device_id TEXT NOT NULL DEFAULT '',
clip_type TEXT NOT NULL DEFAULT 'article',
source_url TEXT NOT NULL DEFAULT '',
target_page_id INTEGER REFERENCES pages(id) ON DELETE SET NULL,
target_workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
title TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ext_clips_user ON extension_clips(user_id, created_at)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ext_clips_device ON extension_clips(device_id)"
)
@register(17, "v6.0.0: sync_version columns")
def _migration_sync_version_columns(conn: sqlite3.Connection) -> None:
"""v6.0.0 — optimistic-concurrency version counters for offline sync.