ecac6c4bda
Closes the last two findings from the 2026-06-02 audit (G5.1 + G5.4). G5.1 — Centroid version no longer drifts: CentroidService now reads MLSettings.embedder_model_version (the DB row tag_and_embed already writes from) for both the centroid model- version stamp and the drift-detection comparison. Previously the centroid sites imported MODEL_VERSION from env, so the version stamped on centroids could disagree with the version stamped on the embeddings they were built from. By construction those now match, so list_drifted won't silently miss the env-vs-DB drift case. embedder.py keeps MODEL_VERSION as an env-driven constant for the actual model loader — that's a different concern (which weights are loaded) from the version-stamp that gets persisted alongside data. G5.4 — Modal is a Pinia-only overlay: The previous URL↔modal sync in GalleryView and ArtistGalleryTab leaked the modal across route changes (RouterLink to /artist/<slug> left the modal mounted on top of the new route) and re-opened it on history back/forward with stale ?image=N entries. Now: openImage() just calls modal.open(id) — no URL push. GalleryView's dead closeImage helper is deleted. A route.name watcher in App.vue closes the modal whenever the route changes, which auto-fixes RouterLink-in-modal and back/forward. Backward-compat: ?image=N is still honored on initial mount as a one-shot deep-link opener, then router.replace strips the query so the URL doesn't re-trigger and no extra history entry is added. Existing bookmarks / shared URLs keep working; new opens stay Pinia-only.
97 lines
3.2 KiB
Vue
97 lines
3.2 KiB
Vue
<template>
|
|
<v-container fluid class="pt-2 pb-6">
|
|
<Teleport to="#fc-nav-actions">
|
|
<v-btn
|
|
:color="sel.isSelectMode ? 'accent' : undefined"
|
|
:variant="sel.isSelectMode ? 'flat' : 'tonal'"
|
|
size="small"
|
|
@click="sel.isSelectMode ? sel.exitSelectMode() : sel.enterSelectMode()"
|
|
>{{ sel.isSelectMode ? 'Done' : 'Select' }}</v-btn>
|
|
</Teleport>
|
|
|
|
<div class="fc-gallery-layout">
|
|
<div class="fc-gallery-layout__main">
|
|
<PostInfoHeader />
|
|
<EmptyState v-if="store.isEmpty" />
|
|
<GalleryGrid v-else @open="openImage" />
|
|
</div>
|
|
<TimelineSidebar v-if="store.images.length > 0" class="fc-gallery-layout__sidebar" />
|
|
</div>
|
|
|
|
<BulkEditorPanel />
|
|
</v-container>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useGalleryStore } from '../stores/gallery.js'
|
|
import { useModalStore } from '../stores/modal.js'
|
|
import GalleryGrid from '../components/gallery/GalleryGrid.vue'
|
|
import TimelineSidebar from '../components/gallery/TimelineSidebar.vue'
|
|
import EmptyState from '../components/gallery/EmptyState.vue'
|
|
import PostInfoHeader from '../components/gallery/PostInfoHeader.vue'
|
|
import BulkEditorPanel from '../components/gallery/BulkEditorPanel.vue'
|
|
import { useGallerySelectionStore } from '../stores/gallerySelection.js'
|
|
|
|
const store = useGalleryStore()
|
|
const modal = useModalStore()
|
|
const sel = useGallerySelectionStore()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
onMounted(async () => {
|
|
const postId = parseInt(route.query.post_id, 10)
|
|
const tagId = parseInt(route.query.tag_id, 10)
|
|
if (!isNaN(postId)) store.setPostFilter(postId)
|
|
else if (!isNaN(tagId)) store.setTagFilter(tagId)
|
|
await store.loadInitial()
|
|
await store.loadTimeline()
|
|
// Audit 2026-06-02: modal is now a Pinia-only overlay (see
|
|
// G5.4 design). The previous URL↔modal sync leaked the modal
|
|
// across route changes and re-opened it on history back/forward.
|
|
// We still honor `?image=N` as a one-shot deep-link opener so
|
|
// existing bookmarks / shared URLs keep working; we strip the
|
|
// query immediately via router.replace so the URL doesn't
|
|
// re-trigger and no history entry is added.
|
|
const initial = parseInt(route.query.image, 10)
|
|
if (!isNaN(initial)) {
|
|
modal.open(initial)
|
|
const q = { ...route.query }
|
|
delete q.image
|
|
router.replace({ query: q })
|
|
}
|
|
})
|
|
|
|
watch(() => route.query.tag_id, (q) => {
|
|
sel.clear() // result set changed — selected ids are no longer valid
|
|
const tagId = parseInt(q, 10)
|
|
store.setTagFilter(isNaN(tagId) ? null : tagId)
|
|
})
|
|
|
|
watch(() => route.query.post_id, (q) => {
|
|
sel.clear() // result set changed — selected ids are no longer valid
|
|
const postId = parseInt(q, 10)
|
|
store.setPostFilter(isNaN(postId) ? null : postId)
|
|
})
|
|
|
|
function openImage(id) {
|
|
modal.open(id)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-gallery-layout {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: flex-start;
|
|
}
|
|
.fc-gallery-layout__main { flex: 1; min-width: 0; }
|
|
.fc-gallery-layout__sidebar { flex-shrink: 0; }
|
|
|
|
@media (max-width: 900px) {
|
|
.fc-gallery-layout { flex-direction: column-reverse; }
|
|
.fc-gallery-layout__sidebar { width: 100%; max-height: 200px; }
|
|
}
|
|
</style>
|