feat: cap-aware autoscaler + token-gated whole-instance tag reset (operator feedback)
Autoscaler (agent 2026-07-02.5): the buffer-occupancy signal alone would peg downloaders at DL_MAX while the bandwidth CAP — not concurrency — is the real constraint (8 streams sharing 8 MB/s move no more data than 4). Growth is now gated on the pipe having headroom (net < 85% of cap) and a pipe pinned at the cap (>= 95%) sheds streams down to 3; dead band prevents flapping. The UI hint says 'holding at the bandwidth cap' and /status reports bw_capped, so the behavior is legible without tests that need the ML stack. Reset content tagging: stays a FULL-instance reset (operator's call), but now lives in a fenced 'Danger zone' section on Cleanup and the apply is gated by a preview-derived confirm token (mirrors the Tier-C bulk-delete pattern — stale counts are rejected server-side). Copy no longer claims suggestions repopulate: it says plainly the heads' training examples are deleted and re-tagging starts fresh. Moved out of TagMaintenanceCard into DangerZoneCard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<MaintenanceTile
|
||||
icon="mdi-nuke"
|
||||
title="Reset content tagging (whole instance)"
|
||||
blurb="Delete ALL general/character tags and their applications — a start-over. Requires a confirmation code."
|
||||
destructive
|
||||
>
|
||||
<p class="text-body-2 mb-2">
|
||||
Deletes every <code>general</code> and <code>character</code> tag and
|
||||
removes them from every image — <strong>including the examples the
|
||||
tagging heads learned from</strong>. Suggestions will <strong>not</strong>
|
||||
repopulate on their own: you re-tag from scratch, and the heads retrain
|
||||
from your new tags as they accumulate. Fandoms and series (with their
|
||||
page order) are kept.
|
||||
</p>
|
||||
<v-alert type="error" variant="tonal" density="compact" class="mb-3">
|
||||
Irreversible — no undo except restoring a DB backup
|
||||
(Settings → Maintenance → Backup). Back one up first.
|
||||
</v-alert>
|
||||
|
||||
<v-btn
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify"
|
||||
:loading="loadingPreview"
|
||||
class="mb-3"
|
||||
@click="onPreview"
|
||||
>Preview content-tag reset</v-btn>
|
||||
|
||||
<div v-if="preview">
|
||||
<p class="text-body-2 mb-2">
|
||||
<strong>{{ preview.count }}</strong> content tag(s)
|
||||
<span v-for="(n, k) in preview.by_kind" :key="k" class="fc-muted">
|
||||
({{ k }}: {{ n }})
|
||||
</span>
|
||||
across <strong>{{ preview.applications }}</strong> image
|
||||
application(s).
|
||||
</p>
|
||||
<SampleNameGrid
|
||||
v-if="preview.sample_names?.length"
|
||||
:names="preview.sample_names" class="mb-3"
|
||||
/>
|
||||
<template v-if="preview.count">
|
||||
<p class="text-body-2 mb-2">
|
||||
To arm the reset, type the confirmation code
|
||||
<code class="fc-code">{{ preview.confirm }}</code> below.
|
||||
</p>
|
||||
<div class="d-flex align-center mb-1" style="gap: 12px">
|
||||
<v-text-field
|
||||
v-model="typed" density="compact" hide-details variant="outlined"
|
||||
label="Confirmation code" style="max-width: 200px"
|
||||
autocomplete="off" spellcheck="false"
|
||||
/>
|
||||
<v-btn
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete-alert"
|
||||
:disabled="typed !== preview.confirm"
|
||||
:loading="committing"
|
||||
@click="onCommit"
|
||||
>Delete {{ preview.count }} tag(s) +
|
||||
{{ preview.applications }} application(s)</v-btn>
|
||||
</div>
|
||||
<p class="fc-muted text-caption mb-0">
|
||||
The code is derived from the counts above — if tagging changes
|
||||
between preview and apply, the server rejects the stale code.
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
import SampleNameGrid from '../common/SampleNameGrid.vue'
|
||||
import { useAdminStore } from '../../stores/admin.js'
|
||||
|
||||
const store = useAdminStore()
|
||||
const preview = ref(null)
|
||||
const loadingPreview = ref(false)
|
||||
const committing = ref(false)
|
||||
const typed = ref('')
|
||||
|
||||
async function onPreview() {
|
||||
loadingPreview.value = true
|
||||
typed.value = ''
|
||||
try {
|
||||
preview.value = await store.resetContentTagging({ dryRun: true })
|
||||
} catch (e) {
|
||||
toast({ text: `Preview failed: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
loadingPreview.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCommit() {
|
||||
committing.value = true
|
||||
try {
|
||||
const res = await store.resetContentTagging({
|
||||
dryRun: false, confirm: typed.value,
|
||||
})
|
||||
toast({ text: `Deleted ${res.deleted} content tag(s) — re-tagging starts fresh`, type: 'success' })
|
||||
preview.value = null
|
||||
typed.value = ''
|
||||
} catch (e) {
|
||||
toast({ text: `Reset rejected: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
committing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
.fc-code {
|
||||
background: rgb(var(--v-theme-surface-light));
|
||||
border-radius: 4px; padding: 2px 8px;
|
||||
font-family: 'JetBrains Mono', monospace; font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
</style>
|
||||
@@ -42,56 +42,6 @@
|
||||
</div>
|
||||
</MaintenanceTile>
|
||||
|
||||
<MaintenanceTile
|
||||
icon="mdi-tag-multiple"
|
||||
title="Reset content tagging"
|
||||
blurb="Delete all general/character tags to re-tag from scratch."
|
||||
destructive
|
||||
>
|
||||
<p class="text-body-2 mb-2">
|
||||
Deletes every <code>general</code> and <code>character</code> tag and
|
||||
removes them from every image, so you can re-tag from scratch with the
|
||||
auto-suggest. <strong>Fandoms and series (with their page order) are
|
||||
kept</strong>, and each image's saved predictions are untouched — open
|
||||
an image and its suggestions reappear.
|
||||
</p>
|
||||
<v-alert type="warning" variant="tonal" density="compact" class="mb-3">
|
||||
Irreversible — there's no undo except restoring a DB backup.
|
||||
Back one up first (Settings → Maintenance → Backup).
|
||||
</v-alert>
|
||||
|
||||
<v-btn
|
||||
color="accent" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-magnify"
|
||||
:loading="loadingResetPreview"
|
||||
class="mb-3"
|
||||
@click="onResetPreview"
|
||||
>Preview content-tag reset</v-btn>
|
||||
|
||||
<div v-if="resetPreview">
|
||||
<p class="text-body-2 mb-2">
|
||||
<strong>{{ resetPreview.count }}</strong> content tag(s)
|
||||
<span v-for="(n, k) in resetPreview.by_kind" :key="k" class="fc-muted">
|
||||
({{ k }}: {{ n }})
|
||||
</span>
|
||||
across <strong>{{ resetPreview.applications }}</strong> image
|
||||
application(s).
|
||||
</p>
|
||||
<SampleNameGrid
|
||||
v-if="resetPreview.sample_names?.length"
|
||||
:names="resetPreview.sample_names" class="mb-3"
|
||||
/>
|
||||
<v-btn
|
||||
color="error" variant="flat" rounded="pill"
|
||||
prepend-icon="mdi-delete-alert"
|
||||
:disabled="!resetPreview.count"
|
||||
:loading="resetCommitting"
|
||||
@click="onResetCommit"
|
||||
>Delete {{ resetPreview.count }} content tag(s) +
|
||||
{{ resetPreview.applications }} application(s)</v-btn>
|
||||
</div>
|
||||
</MaintenanceTile>
|
||||
|
||||
<MaintenanceTile
|
||||
icon="mdi-format-letter-case"
|
||||
title="Standardize tag casing"
|
||||
@@ -169,16 +119,6 @@ const {
|
||||
emptyPreview: (r) => ({ count: 0, sample_names: r.sample_names || [] }),
|
||||
})
|
||||
|
||||
// Reset content tagging (general + character).
|
||||
const {
|
||||
previewData: resetPreview, previewing: loadingResetPreview,
|
||||
committing: resetCommitting, runPreview: onResetPreview, runCommit: onResetCommit,
|
||||
} = usePreviewCommit({
|
||||
preview: () => store.resetContentTagging({ dryRun: true }),
|
||||
commit: () => store.resetContentTagging({ dryRun: false }),
|
||||
emptyPreview: { count: 0, by_kind: {}, applications: 0, sample_names: [] },
|
||||
})
|
||||
|
||||
// Standardize casing. The apply DISPATCHES a self-resuming background task (no
|
||||
// poll-until-done — that would falsely report complete after the first chunk),
|
||||
// so there's no emptyPreview: leave the projection up; a truthy normResult means
|
||||
|
||||
@@ -101,8 +101,10 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Destructive: deletes ALL general + character tags so the operator can
|
||||
// re-tag from scratch via auto-suggest. fandom + series preserved.
|
||||
// Destructive whole-instance reset: deletes ALL general + character tags AND
|
||||
// their applications (the heads' training data included) — fandom + series
|
||||
// preserved. dry-run returns a `confirm` token; the apply must pass it back
|
||||
// ({ dryRun: false, confirm }) or the server rejects it.
|
||||
function resetContentTagging(opts = {}) {
|
||||
return _dryRunPost('/api/admin/tags/reset-content', opts)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,17 @@
|
||||
<TagMaintenanceCard />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="fc-section fc-danger">
|
||||
<h3 class="fc-section__title fc-danger__title">Danger zone</h3>
|
||||
<p class="fc-section__hint">
|
||||
Whole-instance resets. Each needs a fresh preview and a typed
|
||||
confirmation code.
|
||||
</p>
|
||||
<div class="fc-tile-grid">
|
||||
<DangerZoneCard />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,6 +61,7 @@ import PostMaintenanceCard from '../components/settings/PostMaintenanceCard.vue'
|
||||
import VideoDedupCard from '../components/settings/VideoDedupCard.vue'
|
||||
import GatedPurgeCard from '../components/settings/GatedPurgeCard.vue'
|
||||
import TagMaintenanceCard from '../components/settings/TagMaintenanceCard.vue'
|
||||
import DangerZoneCard from '../components/settings/DangerZoneCard.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -75,4 +87,12 @@ import TagMaintenanceCard from '../components/settings/TagMaintenanceCard.vue'
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
/* Danger zone: visually fenced off from routine cleanup so the reset can't
|
||||
be mistaken for maintenance. */
|
||||
.fc-danger {
|
||||
border-top: 1px solid rgba(var(--v-theme-error), 0.45);
|
||||
padding-top: 18px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
.fc-danger__title { color: rgb(var(--v-theme-error)); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user