Files
flowdeck/static/extension/background.js
T
bruno 0b251649e5
FlowDeck CI / lint (push) Successful in 1m11s
FlowDeck CI / test (push) Failing after 8m32s
FlowDeck CI / docker (push) Skipped
feat: v6.2.0 Web Clipper — extension navigateur (capture article/selection/bookmark/screenshot)
- 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
2026-09-19 23:26:03 -04:00

121 lines
3.6 KiB
JavaScript

// FlowDeck Web Clipper — background service worker (Manifest V3)
const STORAGE_KEY = "flowdeck_clipper";
const DEFAULT_SERVER = "http://localhost:8080";
async function getConfig() {
const data = await chrome.storage.local.get(STORAGE_KEY);
return data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" };
}
async function setConfig(cfg) {
await chrome.storage.local.set({ [STORAGE_KEY]: cfg });
}
function genDeviceId() {
return "dev_" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 10);
}
async function ensureDeviceId() {
const cfg = await getConfig();
if (!cfg.deviceId) {
cfg.deviceId = genDeviceId();
await setConfig(cfg);
}
return cfg.deviceId;
}
chrome.runtime.onInstalled.addListener(async () => {
await ensureDeviceId();
chrome.contextMenus.create({
id: "fd-clip-selection",
title: "📌 Clip selection to FlowDeck",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "fd-clip-page",
title: "📌 Clip page to FlowDeck",
contexts: ["page"]
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (!tab || !tab.id) return;
if (info.menuItemId === "fd-clip-selection") {
chrome.tabs.sendMessage(tab.id, { action: "clipSelection" });
} else if (info.menuItemId === "fd-clip-page") {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.__fdOpenClipper && window.__fdOpenClipper("article")
});
}
});
// Unified clip handler
async function handleClip(data) {
const cfg = await getConfig();
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
const deviceId = cfg.deviceId || (await ensureDeviceId());
const token = cfg.token || "";
// Require token or let server handle session cookie fallback
if (!token) {
// Try clipboard without token; server may accept session if user logged in flowdeck tab
// but background fetch cannot send cookie cross-origin without credentials; warn
}
const payload = {
url: data.url,
title: data.title,
content: data.html || data.content,
content_type: data.content_type || "article",
selection_html: data.selection_html || "",
device_id: deviceId,
metadata: data.metadata || {}
};
const headers = { "Content-Type": "application/json", "X-Device-Id": deviceId };
if (token) headers["Authorization"] = "Bearer " + token;
const resp = await fetch(serverUrl + "/api/v2/web-clipper/clip", {
method: "POST",
headers,
body: JSON.stringify(payload)
});
const body = await resp.json().catch(() => ({}));
if (!resp.ok) {
return { error: body.detail || body.error || "clip_failed", status: resp.status, ...body };
}
return body;
}
async function handleAuth() {
// Token is set via popup; verify it
const cfg = await getConfig();
if (!cfg.token) return { error: "no_token" };
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
const resp = await fetch(serverUrl + "/api/v2/web-clipper/status", {
headers: { Authorization: "Bearer " + cfg.token }
});
const body = await resp.json().catch(() => ({}));
return body;
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "clip") {
handleClip(message.data).then((r) => sendResponse(r));
return true;
}
if (message.action === "auth") {
handleAuth().then((r) => sendResponse(r));
return true;
}
if (message.action === "getConfig") {
getConfig().then((c) => sendResponse(c));
return true;
}
if (message.action === "setConfig") {
setConfig(message.config).then(() => sendResponse({ ok: true }));
return true;
}
});