diff --git a/frontend/src/assets/fonts/pixel/PICO-8.ttf b/frontend/src/assets/fonts/pixel/PICO-8.ttf deleted file mode 100644 index 7085f45..0000000 Binary files a/frontend/src/assets/fonts/pixel/PICO-8.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/pixel/Silkscreen-Regular.woff2 b/frontend/src/assets/fonts/pixel/Silkscreen-Regular.woff2 deleted file mode 100644 index 285a17a..0000000 Binary files a/frontend/src/assets/fonts/pixel/Silkscreen-Regular.woff2 and /dev/null differ diff --git a/frontend/src/assets/fonts/pixel/TomThumb.ttf b/frontend/src/assets/fonts/pixel/TomThumb.ttf deleted file mode 100644 index 1e85fd9..0000000 Binary files a/frontend/src/assets/fonts/pixel/TomThumb.ttf and /dev/null differ diff --git a/frontend/src/router.js b/frontend/src/router.js index 9198495..c0dc882 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -56,13 +56,16 @@ const EXPANDED_ROUTE_BUILDERS = { itemDetailRoute(store.getters.identityHandleById[owner_identity_id], item_local_id), group_item: ({owner_group_id, item_local_id}) => itemDetailRoute(store.getters.groupHandleById[owner_group_id], item_local_id), - group: ({group_id}) => `/groups/${group_id}`, + group: ({group_id}) => { + const handle = store.getters.groupHandleById[group_id]; + return handle ? `/groups/${encodeHandleForUrl(handle)}` : null; + }, storage_location: ({storage_location_id}) => `/storage-locations/${storage_location_id}`, workflow: ({workflow_id}) => `/workflows/${workflow_id}`, }; // Kinds whose route needs identityHandleById/groupHandleById (from state.idmap); ShortId.vue only waits on an idmap fetch for these. -export const NEEDS_IDMAP = new Set(['item', 'group_item']); +export const NEEDS_IDMAP = new Set(['item', 'group_item', 'group']); export function expandedRoute({kind, ...fields}) { const buildRoute = EXPANDED_ROUTE_BUILDERS[kind]; @@ -117,7 +120,7 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, { component: GroupNew, meta: {requiresAuth: true} }, { - path: '/groups/:id', + path: '/groups/:handle', component: GroupDetail, meta: {requiresAuth: true}, props: true diff --git a/frontend/src/views/GroupDetail.vue b/frontend/src/views/GroupDetail.vue index 181b089..448f639 100644 --- a/frontend/src/views/GroupDetail.vue +++ b/frontend/src/views/GroupDetail.vue @@ -134,7 +134,7 @@ import {mapActions, mapGetters, mapState} from "vuex"; import * as BIcons from "bootstrap-icons-vue"; import BaseLayout from "@/components/BaseLayout.vue"; import UserNameTag from "@/components/UserNameTag.vue"; -import {encodeHandleForUrl} from "@/router"; +import {decodeHandleFromUrl, encodeHandleForUrl} from "@/router"; export default { name: 'GroupDetail', @@ -144,8 +144,8 @@ export default { ...BIcons }, props: { - id: { - type: [String, Number], + handle: { + type: String, required: true } }, @@ -159,14 +159,17 @@ export default { }, computed: { ...mapState(['user']), + id() { + return this.groupIdByHandle[decodeHandleFromUrl(this.handle)] + }, items() { return this.groupInventoryItems(this.id) }, - ...mapGetters(['groupInventoryItems']) + ...mapGetters(['groupInventoryItems', 'groupIdByHandle']) }, methods: { ...mapActions(['fetchGroup', 'removeGroupMember', 'inviteToGroup', 'fetchGroupInventoryItems', - 'deleteInventoryItem']), + 'deleteInventoryItem', 'fetchIdMap']), refresh() { this.fetchGroup({id: this.id}).then((group) => { this.group = group @@ -201,8 +204,15 @@ export default { } }, mounted() { - this.refresh() - this.fetchItems() + if (this.id === undefined) { + this.fetchIdMap().then(() => { + this.refresh() + this.fetchItems() + }) + } else { + this.refresh() + this.fetchItems() + } } } diff --git a/frontend/src/views/GroupNew.vue b/frontend/src/views/GroupNew.vue index 3a3be87..86415d4 100644 --- a/frontend/src/views/GroupNew.vue +++ b/frontend/src/views/GroupNew.vue @@ -25,6 +25,7 @@ diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 6d7fd0f..e74ebf9 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,6 +5,30 @@ import {existsSync} from 'node:fs' import {defineConfig} from 'vite' import vue from '@vitejs/plugin-vue' +// Vite's dev-server SPA fallback only rewrites a request to index.html when the last path +// segment has no dot (it sniffs for a file extension). A route whose only/last segment is a +// URL-encoded handle (e.g. /groups/+user@a.localhost, see encodeHandleForUrl in src/router.js) +// legitimately contains a dot from the domain part, so a direct load/refresh 404s there unless +// we pre-empt that check ourselves. Gating on both a text/html Accept header and an '@' in the +// last segment keeps this from ever matching real asset requests or vite's own internal +// @fs/@id/@vite module paths (which never carry an html Accept header). +function handleUrlFallback() { + return { + name: 'handle-url-fallback', + configureServer(server) { + server.middlewares.use((req, res, next) => { + const accept = req.headers.accept || '' + const pathname = (req.url || '').split('?')[0] + const lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1) + if (req.method === 'GET' && accept.includes('text/html') && lastSegment.includes('@')) { + req.url = '/index.html' + } + next() + }) + } + } +} + function gitCommit() { // deploy/dev/docker-compose.yml bind-mounts the repo's real .git dir at // /git (separate from /app, which only has the frontend/ subtree) - @@ -23,7 +47,7 @@ function gitCommit() { } export default defineConfig({ - plugins: [vue()], + plugins: [vue(), handleUrlFallback()], define: { __GIT_COMMIT__: JSON.stringify(gitCommit()) }, @@ -44,6 +68,10 @@ export default defineConfig({ //'Upgrade-Insecure-Requests': '1', 'Content-Security-Policy': 'default-src \'self\';' + ' script-src \'self\' \'wasm-unsafe-eval\' \'unsafe-eval\' \'unsafe-inline\';' + // Without this, worker-src falls back to script-src, which has no blob: source - + // CameraScanner.vue's decode Worker (built from a Blob URL) would be silently + // blocked by CSP rather than the app's own capability checks. + + ' worker-src \'self\' blob:;' + ' style-src \'self\' \'unsafe-inline\';' + ' img-src \'self\' * data: blob:;' + ' connect-src * data:',