Compare commits

..

6 Commits

8 changed files with 94 additions and 34 deletions
+16 -4
View File
@@ -39,14 +39,26 @@ jobs:
- name: Commit signed XPI to frontend/public/extension/ - name: Commit signed XPI to frontend/public/extension/
run: | run: |
set -e set -e
XPI=$(ls extension/web-ext-artifacts/fabledcurator-*.xpi | head -1) # AMO renames signed XPIs using its internal addon-id-safe-string
# (e.g. 997017ca3e104e30a75a-1.0.1.xpi), NOT our gecko ID. The
# original 'fabledcurator-*' glob never matched and the step
# exited 1 even on a successful sign (operator-flagged
# 2026-05-25). Match any *.xpi in the artifacts dir — fresh
# CI runner means there's exactly one — and rename it on copy
# so the FC server's whitelist (backend/app/frontend.py expects
# 'fabledcurator-*.xpi') keeps working.
XPI=$(ls extension/web-ext-artifacts/*.xpi 2>/dev/null | head -1)
if [ -z "$XPI" ]; then if [ -z "$XPI" ]; then
echo "No XPI produced by web-ext sign — exiting" echo "No XPI produced by web-ext sign — exiting"
ls -la extension/web-ext-artifacts/ || true
exit 1 exit 1
fi fi
VERSION=$(grep -E '"version"' extension/package.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
DEST="frontend/public/extension/fabledcurator-${VERSION}.xpi"
mkdir -p frontend/public/extension mkdir -p frontend/public/extension
cp "$XPI" frontend/public/extension/ # Wipe any prior versions so the directory doesn't grow each release.
# Also copy as -latest.xpi so the FC server can serve a stable URL. rm -f frontend/public/extension/fabledcurator-*.xpi
cp "$XPI" "$DEST"
cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi" cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi"
git config user.name "FC extension CI" git config user.name "FC extension CI"
git config user.email "noreply@fabledsword.com" git config user.email "noreply@fabledsword.com"
@@ -54,6 +66,6 @@ jobs:
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No changes to commit" echo "No changes to commit"
else else
git commit -m "ext: publish signed XPI $(basename $XPI)" git commit -m "ext: publish signed XPI fabledcurator-${VERSION}.xpi"
git push origin HEAD:main git push origin HEAD:main
fi fi
+2
View File
@@ -47,6 +47,8 @@ async def status():
if active: if active:
payload["active_batch"] = { payload["active_batch"] = {
"id": active.id, "id": active.id,
"source_path": active.source_path,
"scan_mode": active.scan_mode,
"total_files": active.total_files, "total_files": active.total_files,
"imported": active.imported, "imported": active.imported,
"skipped": active.skipped, "skipped": active.skipped,
+13 -2
View File
@@ -34,10 +34,21 @@ class Embedder:
if self._model is not None: if self._model is not None:
return return
import torch import torch
from transformers import AutoModel, AutoProcessor from transformers import AutoModel, SiglipImageProcessor
self._torch = torch self._torch = torch
self._processor = AutoProcessor.from_pretrained(str(self._model_dir)) # FC's embedder only does IMAGE inference — never text. AutoProcessor
# loads the full processor including SiglipTokenizer, which requires
# the sentencepiece library at import time even if we never call it.
# SiglipImageProcessor loads ONLY preprocessor_config.json (image
# side) and skips the tokenizer config entirely. Operator hit the
# ImportError 2026-05-25 once the ml-worker started actually running
# tag_and_embed; switching to the image-only loader avoids the
# tokenizer dep without adding ~30 MB of unused C++ build to the
# lean ml-worker image.
self._processor = SiglipImageProcessor.from_pretrained(
str(self._model_dir)
)
self._model = AutoModel.from_pretrained(str(self._model_dir)) self._model = AutoModel.from_pretrained(str(self._model_dir))
self._model.eval() self._model.eval()
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "FabledCurator", "name": "FabledCurator",
"version": "1.0.0", "version": "1.0.1",
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
"browser_specific_settings": { "browser_specific_settings": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "fabledcurator-extension", "name": "fabledcurator-extension",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"description": "Firefox extension for FabledCurator", "description": "Firefox extension for FabledCurator",
"scripts": { "scripts": {
@@ -7,7 +7,8 @@
indeterminate color="accent" size="20" indeterminate color="accent" size="20"
/> />
<span> <span>
Scanning {{ store.activeBatch.source_path }} {{ store.activeBatch.scan_mode === 'deep' ? 'Deep scanning' : 'Scanning' }}
{{ store.activeBatch.source_path || '/import' }}
imported {{ store.activeBatch.imported }}, imported {{ store.activeBatch.imported }},
skipped {{ store.activeBatch.skipped }}, skipped {{ store.activeBatch.skipped }},
failed {{ store.activeBatch.failed }} / failed {{ store.activeBatch.failed }} /
@@ -24,8 +25,12 @@
<p class="text-body-2 mb-3"> <p class="text-body-2 mb-3">
<span v-if="!store.activeBatch"> <span v-if="!store.activeBatch">
Run a quick scan of the import directory. Deep scan (pHash dedup, <strong>Quick scan</strong> walks <code>/import</code> and enqueues
archives) lands in FC-2d. new files only (skips paths already on a non-failed ImportTask).
<strong>Deep scan</strong> additionally chains a phash backfill
across the existing library use after bulk-imports to catch
near-duplicates that slipped through. Both modes route non-media
+ sidecar pairs through PostAttachment capture.
</span> </span>
<span v-else> <span v-else>
An active batch is in progress. Wait for it to finish, or click An active batch is in progress. Wait for it to finish, or click
@@ -34,15 +39,26 @@
</span> </span>
</p> </p>
<v-btn <div class="d-flex flex-wrap" style="gap: 12px;">
color="primary" rounded="pill" <v-btn
:disabled="!!store.activeBatch" color="primary" rounded="pill"
:loading="busy" :disabled="!!store.activeBatch"
@click="trigger" :loading="busy === 'quick'"
> @click="trigger('quick')"
<v-icon start>mdi-magnify-scan</v-icon> >
Quick scan <v-icon start>mdi-magnify-scan</v-icon>
</v-btn> Quick scan
</v-btn>
<v-btn
color="secondary" rounded="pill" variant="tonal"
:disabled="!!store.activeBatch"
:loading="busy === 'deep'"
@click="trigger('deep')"
>
<v-icon start>mdi-magnify-plus-outline</v-icon>
Deep scan
</v-btn>
</div>
<v-alert v-if="store.triggerError" type="error" variant="tonal" class="mt-3" closable> <v-alert v-if="store.triggerError" type="error" variant="tonal" class="mt-3" closable>
{{ store.triggerError }} {{ store.triggerError }}
@@ -56,12 +72,12 @@ import { ref } from 'vue'
import { useImportStore } from '../../stores/import.js' import { useImportStore } from '../../stores/import.js'
const store = useImportStore() const store = useImportStore()
const busy = ref(false) const busy = ref(null)
const clearing = ref(false) const clearing = ref(false)
async function trigger() { async function trigger(mode) {
busy.value = true busy.value = mode
try { await store.triggerScan() } catch {} finally { busy.value = false } try { await store.triggerScan(mode) } catch {} finally { busy.value = null }
} }
async function onClearStuck() { async function onClearStuck() {
+13 -7
View File
@@ -52,25 +52,31 @@ export const useImportStore = defineStore('import', () => {
} }
} }
async function triggerScan() { async function triggerScan(mode = 'quick') {
if (!['quick', 'deep', 'verify'].includes(mode)) {
throw new Error(`unsupported scan mode: ${mode}`)
}
triggerError.value = null triggerError.value = null
try { try {
await api.post('/api/import/trigger', { body: { mode: 'quick' } }) await api.post('/api/import/trigger', { body: { mode } })
// Acknowledge immediately so the click isn't invisible. scan_directory // Acknowledge immediately so the click isn't invisible. scan_directory
// can finalize the batch synchronously when every file in /import is // can finalize the batch synchronously when every file in /import is
// already on a non-failed ImportTask (operator-flagged 2026-05-25: // already on a non-failed ImportTask (operator-flagged 2026-05-25:
// 233k existing tasks → all paths in skip-set → files_seen=0 → // 233k existing tasks → all paths in skip-set → files_seen=0 →
// batch flashes 'running' for <100ms then 'complete' before the // batch flashes 'running' for <100ms then 'complete' before the
// first refreshStatus() lands; UI never sees the active state). // first refreshStatus() lands; UI never sees the active state).
window.__fcToast?.({ text: 'Scan triggered', type: 'success' }) const label = mode === 'deep'
? 'Deep scan triggered (pHash backfill chained)'
: mode === 'verify' ? 'Library verify triggered' : 'Quick scan triggered'
window.__fcToast?.({ text: label, type: 'success' })
await refreshStatus() await refreshStatus()
// Re-poll twice over ~5s to catch quick-finalize transitions and // Re-poll twice over ~5s to catch quick-finalize transitions and
// surface a result toast either way. // surface a result toast either way. Skip the "no new files" hint
// for 'verify' since it doesn't walk the import_root.
setTimeout(async () => { setTimeout(async () => {
await refreshStatus() await refreshStatus()
if (!activeBatch.value) { await loadTasks(true)
// Either scan completed with zero new files, or it never visibly if (!activeBatch.value && mode !== 'verify') {
// started. Fetch the freshest task to differentiate.
window.__fcToast?.({ window.__fcToast?.({
text: 'Scan complete — no new files (everything already on an ImportTask row)', text: 'Scan complete — no new files (everything already on an ImportTask row)',
type: 'info', type: 'info',
+16 -3
View File
@@ -1,6 +1,16 @@
<template> <template>
<v-container fluid class="py-6"> <v-container fluid class="py-6">
<v-tabs v-model="tab" color="accent" class="mb-4"> <!-- Sticky tabs: operator-flagged 2026-05-25 long Import / Maintenance
panels pushed the tab strip out of the viewport, forcing a scroll-
to-top just to change tab. AppShell's TopNav is 64px sticky, so the
tab strip lives directly under it. Background uses the theme surface
token so it visually merges with the page rather than the
translucent v-tabs default. -->
<v-tabs
v-model="tab" color="accent" class="mb-4"
style="position: sticky; top: 64px; z-index: 4;
background: rgb(var(--v-theme-surface));"
>
<v-tab value="overview">Overview</v-tab> <v-tab value="overview">Overview</v-tab>
<v-tab value="activity">Activity</v-tab> <v-tab value="activity">Activity</v-tab>
<v-tab value="import">Import</v-tab> <v-tab value="import">Import</v-tab>
@@ -28,11 +38,14 @@
</v-window-item> </v-window-item>
<v-window-item value="import"> <v-window-item value="import">
<!-- Order: trigger → recent tasks → filters. Tasks sit directly
below the trigger so operator sees hit/miss feedback without
scrolling past the filter card (operator-flagged 2026-05-25). -->
<ImportTriggerPanel /> <ImportTriggerPanel />
<v-divider class="my-6" /> <v-divider class="my-6" />
<ImportFiltersForm />
<v-divider class="my-6" />
<ImportTaskList /> <ImportTaskList />
<v-divider class="my-6" />
<ImportFiltersForm />
</v-window-item> </v-window-item>
<v-window-item value="maintenance"> <v-window-item value="maintenance">