Files
MyIP/frontend-server.js
T
Claude 96525d2fc7 Chore: trim the branch — leftovers, redundancy, comments
A pass over every file the branch touched, after the iOS animation hunt
left tries and reverts behind it. Removed what nothing reaches: the
view-transition rule for a transition no code starts, an unreachable
guard in the transition hook, a no-op KeepAlive key branch, a sign-in
guard the menu already enforces, usage-dialog wrappers that only
forwarded to the store, a duplicated modifier-click check and card
filter (isPlainClick / listedTools), a test that hand-rolled
listedTools, a drawer-era fixture parameter. auth-flow.js folded into
auth-redirect.js. Comments cut to what the code cannot say itself —
file headers of a few lines, the measured iOS reasons, non-obvious
orderings — and stale ones fixed (Nav under KeepAlive, Home.vue
reusing DEFAULT_META, "from Nav", enabledCards). AGENTS.md sections
tightened to the code as it is now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
2026-10-05 16:50:16 +00:00

105 lines
5.1 KiB
JavaScript

// frontend-server.js
import dotenv from 'dotenv';
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { createProxyMiddleware } from 'http-proxy-middleware';
import logger from './server/logger.js';
import { FIREBASE_AUTH_PROXY_PATH, firebaseAuthProxyTarget } from './common/firebase-auth-proxy.js';
dotenv.config({ quiet: true });
const frontendApp = express();
const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10);
const frontEndPort = parseInt(process.env.FRONTEND_PORT || 18966, 10);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// API Proxy to backend server
frontendApp.use('/api', createProxyMiddleware({
target: `http://localhost:${backEndPort}/api`,
changeOrigin: true
}));
// Firebase's auth handler served first-party (api/AGENTS.md "Firebase Auth
// handler proxy"); ahead of the static layer and SPA fallback so it never
// resolves to index.html.
const firebaseAuthTarget = firebaseAuthProxyTarget(process.env.VITE_FIREBASE_PROJECT_ID);
if (firebaseAuthTarget) {
frontendApp.use(FIREBASE_AUTH_PROXY_PATH, createProxyMiddleware({
target: `${firebaseAuthTarget}${FIREBASE_AUTH_PROXY_PATH}`,
changeOrigin: true
}));
logger.info(`🛡️ Firebase Auth handler proxied: ${FIREBASE_AUTH_PROXY_PATH}/* → ${firebaseAuthTarget}`);
}
// Set static file directory.
// Cache-Control is set per-asset class so the static layer behaves well
// even when no CDN sits in front of it (CF in production sets its own
// Browser TTL on top of these, so the longer values are upper bounds):
// - dist/assets/** Vite-hashed JS/CSS/images — content-addressed → 1y immutable
// - dist/fonts/** non-hashed but essentially never change → 1y immutable
// - favicons/** connectivity-target icons → 30d; not hashed,
// but a changed icon ships under a new member id
// (= new filename), so long caching is safe
// - non-hashed images favicon / logos / achievements / … → 7d
// (any depth; not content-hashed, so changing one
// of these images requires renaming the file)
// - HTML + manifest + 24h at the edge (s-maxage), zero in browsers — the
// SEO surfaces build's postbuild purge evicts them on deploy, so
// the TTL only caps drift between deploys and no
// browser copy outlives a purge; manifest references
// only stable /logos/* paths, so a stale copy never
// points at dead assets
// - everything else (ads.txt, …) → 1h
const distDir = path.join(__dirname, './dist');
// Root-level SEO / AI-engine surfaces
const SEO_SURFACES = new Set(['llms.txt', 'llms-full.txt', 'sitemap.xml', 'robots.txt']);
function setStaticHeaders(res, filePath) {
const rel = path.relative(distDir, filePath).replaceAll(path.sep, '/');
if (rel.startsWith('assets/') || rel.startsWith('fonts/')) {
res.setHeader('Cache-Control', `public, max-age=${24 * 60 * 60 * 365}, immutable`);
} else if (rel.startsWith('favicons/')) {
res.setHeader('Cache-Control', `public, max-age=${30 * 24 * 60 * 60}`);
} else if (/\.(png|jpg|jpeg|webp|svg|ico)$/i.test(rel)) {
res.setHeader('Cache-Control', `public, max-age=${7 * 24 * 60 * 60}`);
} else if (rel.endsWith('.html') || rel === 'manifest.webmanifest' || SEO_SURFACES.has(rel)) {
res.setHeader('Cache-Control', `public, max-age=0, s-maxage=${24 * 60 * 60}, must-revalidate`);
} else {
res.setHeader('Cache-Control', `public, max-age=${60 * 60}`);
}
}
frontendApp.use(express.static(distDir, { setHeaders: setStaticHeaders }));
// SPA history fallback. The app uses HTML5 history routing (e.g. /tools/whois),
// so a hard load / new-tab of a client route reaches this server as a real
// path with no matching file. Serve index.html for navigation requests and let
// vue-router resolve the route. Mounted after the /api proxy and the static
// layer, so real assets and API calls are untouched.
// - GET only; other methods fall through.
// - `req.accepts('html')` gates on navigations: browsers asking for a page
// send `Accept: text/html`.
// - A path whose final segment has a file extension (e.g. /assets/x.js) is an
// asset request, not a client route — let it 404 rather than return an HTML
// body, which would break a missing JS/CSS chunk's importer after a deploy
// (`*/*` requests otherwise match `accepts('html')`).
// - Client-route loads stay uncached (unlike / and /index.html on the
// static layer): only those two URLs live at the edge, so a deploy-time
// purge stays a two-URL operation.
frontendApp.use((req, res, next) => {
if (req.method !== 'GET' || !req.accepts('html')) return next();
if (req.path.split('/').pop().includes('.')) return next();
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.sendFile(path.join(distDir, 'index.html'));
});
// Start static file server
frontendApp.listen(frontEndPort, () => {
logger.info(`🚀 Static file server ready on http://localhost:${frontEndPort}`);
});