fix: drop migration backup-gate (FC-3h supersedes) + modal Escape via document-level listener so video focus can't swallow it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,12 @@ _VALID_KINDS = frozenset({
|
|||||||
"ml_queue", "verify", "rollback", "cleanup",
|
"ml_queue", "verify", "rollback", "cleanup",
|
||||||
})
|
})
|
||||||
_INGEST_KINDS = frozenset({"gs_ingest", "ir_ingest"})
|
_INGEST_KINDS = frozenset({"gs_ingest", "ir_ingest"})
|
||||||
_APPLY_KINDS = frozenset({"gs_ingest", "ir_ingest", "tag_apply", "rollback", "cleanup"})
|
# Backup gate retired 2026-05-24 — operator-flagged the speculative-safety
|
||||||
|
# requirement was actively blocking the UI ingest path (backup itself is
|
||||||
|
# unreliable on large NFS-backed image libraries) and FC-3h will rewrite
|
||||||
|
# the backup surface as a first-class feature with its own scheduling
|
||||||
|
# + recovery. Leaving the constant for historical grep.
|
||||||
|
_APPLY_KINDS: frozenset[str] = frozenset()
|
||||||
|
|
||||||
|
|
||||||
def _bad(error: str, *, status: int = 400, **extra):
|
def _bad(error: str, *, status: int = 400, **extra):
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
class="fc-viewer" role="dialog" aria-modal="true"
|
class="fc-viewer" role="dialog" aria-modal="true"
|
||||||
@keydown.esc="$emit('close')"
|
|
||||||
@keydown.left="onArrowLeft"
|
|
||||||
@keydown.right="onArrowRight"
|
|
||||||
tabindex="-1" ref="rootEl"
|
tabindex="-1" ref="rootEl"
|
||||||
>
|
>
|
||||||
<button class="fc-viewer__close" @click="$emit('close')" aria-label="Close">
|
<button class="fc-viewer__close" @click="$emit('close')" aria-label="Close">
|
||||||
@@ -68,7 +65,7 @@ import VideoCanvas from './VideoCanvas.vue'
|
|||||||
import TagPanel from './TagPanel.vue'
|
import TagPanel from './TagPanel.vue'
|
||||||
import ProvenancePanel from './ProvenancePanel.vue'
|
import ProvenancePanel from './ProvenancePanel.vue'
|
||||||
|
|
||||||
defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
const modal = useModalStore()
|
const modal = useModalStore()
|
||||||
const rootEl = ref(null)
|
const rootEl = ref(null)
|
||||||
@@ -86,14 +83,39 @@ const integrityBadge = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let prevBodyOverflow = null
|
let prevBodyOverflow = null
|
||||||
|
|
||||||
|
// Document-level keyboard handler. Bound on document (not on the modal
|
||||||
|
// root) because <video> elements capture focus when the user interacts
|
||||||
|
// with their controls — when video has focus, Escape and arrow keys
|
||||||
|
// don't bubble reliably to ancestors. Listening on document side-steps
|
||||||
|
// that. Filtered via isTextEntry so tag/comment inputs still get their
|
||||||
|
// own keystrokes.
|
||||||
|
function onKeyDown(ev) {
|
||||||
|
if (ev.key === 'Escape') {
|
||||||
|
if (isTextEntry(ev.target)) return
|
||||||
|
ev.preventDefault()
|
||||||
|
emit('close')
|
||||||
|
} else if (ev.key === 'ArrowLeft') {
|
||||||
|
if (isTextEntry(ev.target)) return
|
||||||
|
ev.preventDefault()
|
||||||
|
modal.goPrev()
|
||||||
|
} else if (ev.key === 'ArrowRight') {
|
||||||
|
if (isTextEntry(ev.target)) return
|
||||||
|
ev.preventDefault()
|
||||||
|
modal.goNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
prevBodyOverflow = document.body.style.overflow
|
prevBodyOverflow = document.body.style.overflow
|
||||||
document.body.style.overflow = 'hidden'
|
document.body.style.overflow = 'hidden'
|
||||||
|
document.addEventListener('keydown', onKeyDown, true)
|
||||||
await nextFrame()
|
await nextFrame()
|
||||||
rootEl.value?.focus()
|
rootEl.value?.focus()
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.body.style.overflow = prevBodyOverflow
|
document.body.style.overflow = prevBodyOverflow
|
||||||
|
document.removeEventListener('keydown', onKeyDown, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => modal.currentImageId, async () => {
|
watch(() => modal.currentImageId, async () => {
|
||||||
@@ -105,16 +127,6 @@ function nextFrame() {
|
|||||||
return new Promise(resolve => requestAnimationFrame(resolve))
|
return new Promise(resolve => requestAnimationFrame(resolve))
|
||||||
}
|
}
|
||||||
|
|
||||||
function onArrowLeft(ev) {
|
|
||||||
if (isTextEntry(ev.target)) return
|
|
||||||
ev.preventDefault()
|
|
||||||
modal.goPrev()
|
|
||||||
}
|
|
||||||
function onArrowRight(ev) {
|
|
||||||
if (isTextEntry(ev.target)) return
|
|
||||||
ev.preventDefault()
|
|
||||||
modal.goNext()
|
|
||||||
}
|
|
||||||
function isTextEntry(el) {
|
function isTextEntry(el) {
|
||||||
if (!el) return false
|
if (!el) return false
|
||||||
const tag = el.tagName
|
const tag = el.tagName
|
||||||
|
|||||||
Reference in New Issue
Block a user