V1.12.3: live updates behind any proxy, fewer requests

- Live updates start on plain requests and move up to a websocket when
  one gets through. The site used to ask for the websocket first, with
  no way back: behind a proxy that does not pass the upgrade, nothing
  live arrived at all.
- A browser asks the API's leave before a request, the site and the API
  being on different hosts. It now remembers the answer for two hours,
  where it used to ask before nearly every request.
- A built file is kept by the browser for good: its name changes when
  its contents do. Each used to be checked again on every page load.
- For a real websocket through nginx, the blocks that pass requests to
  the API need "proxy_http_version 1.1", "proxy_set_header Upgrade
  $http_upgrade" and "proxy_set_header Connection "upgrade"".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 21:04:45 +02:00
co-authored by Claude Opus 5.5
parent f04e3497f4
commit 7b3d121151
7 changed files with 33 additions and 14 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "1.12.2",
"version": "1.12.3",
"type": "module",
"scripts": {
"dev": "vite",
+4 -1
View File
@@ -8,7 +8,10 @@ let sharedSocket: Socket | null = null;
function getSocket(token: string): Socket {
if (!sharedSocket) {
sharedSocket = io(API_URL, { auth: { token }, transports: ['websocket', 'polling'] });
// Starts on plain requests and moves up to a websocket when one gets
// through. Asking for the websocket first leaves no way back: behind a
// proxy that does not pass the upgrade, nothing live arrives at all.
sharedSocket = io(API_URL, { auth: { token } });
}
return sharedSocket;
}
+18 -5
View File
@@ -1,20 +1,33 @@
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import react from '@vitejs/plugin-react'
import compression from 'compression'
import { defineConfig, type Connect, type Plugin } from 'vite'
// `vite preview` is what serves the built site, and nothing in front of it
// compresses (frp only tunnels), so the files are gzipped here.
const gzipPreview: Plugin = {
name: 'gzip-preview',
// `vite preview` is what serves the built site, so the two things a web
// server would be set up to do are done here.
const servePreview: Plugin = {
name: 'serve-preview',
configurePreviewServer(server) {
const assets = join(server.config.root, server.config.build.outDir)
// Typed for Express, and at home in any Node server.
server.middlewares.use(compression() as Connect.NextHandleFunction)
// A built file carries a hash of its contents in its name, so a browser
// can keep it for good and never ask again. Only for files that are
// there: a name from an older build must not be remembered as missing.
server.middlewares.use((req, res, next) => {
const path = (req.url ?? '').split('?')[0]
if (path.startsWith('/assets/') && !path.includes('..') && existsSync(join(assets, path))) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
next()
})
},
}
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), gzipPreview],
plugins: [react(), servePreview],
preview: {
host: true,