Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac55d0e8d8 | |||
| 47d760550d | |||
| 89a89e0ded | |||
| dc3bce7fc1 | |||
| f657582f30 | |||
| 111b952535 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user