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/
run: |
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
echo "No XPI produced by web-ext sign — exiting"
ls -la extension/web-ext-artifacts/ || true
exit 1
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
cp "$XPI" frontend/public/extension/
# Also copy as -latest.xpi so the FC server can serve a stable URL.
# Wipe any prior versions so the directory doesn't grow each release.
rm -f frontend/public/extension/fabledcurator-*.xpi
cp "$XPI" "$DEST"
cp "$XPI" "frontend/public/extension/fabledcurator-latest.xpi"
git config user.name "FC extension CI"
git config user.email "noreply@fabledsword.com"
@@ -54,6 +66,6 @@ jobs:
if git diff --cached --quiet; then
echo "No changes to commit"
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
fi
+2
View File
@@ -47,6 +47,8 @@ async def status():
if active:
payload["active_batch"] = {
"id": active.id,
"source_path": active.source_path,
"scan_mode": active.scan_mode,
"total_files": active.total_files,
"imported": active.imported,
"skipped": active.skipped,
+13 -2
View File
@@ -34,10 +34,21 @@ class Embedder:
if self._model is not None:
return
import torch
from transformers import AutoModel, AutoProcessor
from transformers import AutoModel, SiglipImageProcessor
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.eval()
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"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.",
"browser_specific_settings": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fabledcurator-extension",
"version": "1.0.0",
"version": "1.0.1",
"private": true,
"description": "Firefox extension for FabledCurator",
"scripts": {
@@ -7,7 +7,8 @@
indeterminate color="accent" size="20"
/>
<span>
Scanning {{ store.activeBatch.source_path }}
{{ store.activeBatch.scan_mode === 'deep' ? 'Deep scanning' : 'Scanning' }}
{{ store.activeBatch.source_path || '/import' }}
imported {{ store.activeBatch.imported }},
skipped {{ store.activeBatch.skipped }},
failed {{ store.activeBatch.failed }} /
@@ -24,8 +25,12 @@
<p class="text-body-2 mb-3">
<span v-if="!store.activeBatch">
Run a quick scan of the import directory. Deep scan (pHash dedup,
archives) lands in FC-2d.
<strong>Quick scan</strong> walks <code>/import</code> and enqueues
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 v-else>
An active batch is in progress. Wait for it to finish, or click
@@ -34,15 +39,26 @@
</span>
</p>
<v-btn
color="primary" rounded="pill"
:disabled="!!store.activeBatch"
:loading="busy"
@click="trigger"
>
<v-icon start>mdi-magnify-scan</v-icon>
Quick scan
</v-btn>
<div class="d-flex flex-wrap" style="gap: 12px;">
<v-btn
color="primary" rounded="pill"
:disabled="!!store.activeBatch"
:loading="busy === 'quick'"
@click="trigger('quick')"
>
<v-icon start>mdi-magnify-scan</v-icon>
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>
{{ store.triggerError }}
@@ -56,12 +72,12 @@ import { ref } from 'vue'
import { useImportStore } from '../../stores/import.js'
const store = useImportStore()
const busy = ref(false)
const busy = ref(null)
const clearing = ref(false)
async function trigger() {
busy.value = true
try { await store.triggerScan() } catch {} finally { busy.value = false }
async function trigger(mode) {
busy.value = mode
try { await store.triggerScan(mode) } catch {} finally { busy.value = null }
}
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
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
// can finalize the batch synchronously when every file in /import is
// already on a non-failed ImportTask (operator-flagged 2026-05-25:
// 233k existing tasks → all paths in skip-set → files_seen=0 →
// batch flashes 'running' for <100ms then 'complete' before the
// 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()
// 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 () => {
await refreshStatus()
if (!activeBatch.value) {
// Either scan completed with zero new files, or it never visibly
// started. Fetch the freshest task to differentiate.
await loadTasks(true)
if (!activeBatch.value && mode !== 'verify') {
window.__fcToast?.({
text: 'Scan complete — no new files (everything already on an ImportTask row)',
type: 'info',
+16 -3
View File
@@ -1,6 +1,16 @@
<template>
<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="activity">Activity</v-tab>
<v-tab value="import">Import</v-tab>
@@ -28,11 +38,14 @@
</v-window-item>
<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 />
<v-divider class="my-6" />
<ImportFiltersForm />
<v-divider class="my-6" />
<ImportTaskList />
<v-divider class="my-6" />
<ImportFiltersForm />
</v-window-item>
<v-window-item value="maintenance">