prelöoad experiment
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
certs/
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
|
||||||
<meta name="theme-color" content="#3b5bdb" />
|
<meta name="theme-color" content="#3b5bdb" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>Dashfront</title>
|
<title>Dashfront</title>
|
||||||
|
|||||||
5352
package-lock.json
generated
5352
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -4,7 +4,11 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node scripts/dev.mjs",
|
||||||
|
"dev:https": "VITE_HTTPS=1 node scripts/dev.mjs",
|
||||||
|
"dev:hmr": "vite",
|
||||||
|
"dev:hmr:https": "VITE_HTTPS=1 vite",
|
||||||
|
"cert": "./scripts/generate-cert.sh",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
@@ -21,6 +25,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"vite": "^8.0.16"
|
"qrcode": "^1.5.4",
|
||||||
|
"vite": "^8.0.16",
|
||||||
|
"vite-plugin-pwa": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "/",
|
|
||||||
"name": "Dashfront",
|
|
||||||
"short_name": "Dashfront",
|
|
||||||
"start_url": "/",
|
|
||||||
"scope": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#dceeea",
|
|
||||||
"theme_color": "#3b5bdb",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
45
scripts/dev.mjs
Normal file
45
scripts/dev.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Dev with full PWA precache: production build --watch + preview.
|
||||||
|
* Vite HMR cannot precache in-memory modules; use npm run dev:hmr for that.
|
||||||
|
*/
|
||||||
|
import { build, preview } from 'vite';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const configFile = path.resolve(root, '../vite.config.js');
|
||||||
|
|
||||||
|
const watcher = await build({
|
||||||
|
configFile,
|
||||||
|
build: { watch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
let previewServer;
|
||||||
|
|
||||||
|
async function startPreview() {
|
||||||
|
if (previewServer) return;
|
||||||
|
previewServer = await preview({
|
||||||
|
configFile,
|
||||||
|
preview: { host: true },
|
||||||
|
});
|
||||||
|
previewServer.printUrls();
|
||||||
|
previewServer.bindCLIShortcuts({ print: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.on('event', (event) => {
|
||||||
|
if (event.code === 'BUNDLE_END') {
|
||||||
|
void startPreview();
|
||||||
|
}
|
||||||
|
if (event.code === 'ERROR') {
|
||||||
|
console.error(event.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function shutdown() {
|
||||||
|
await previewServer?.close();
|
||||||
|
await watcher.close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
75
scripts/generate-cert.sh
Executable file
75
scripts/generate-cert.sh
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate a self-signed TLS cert for local HTTPS (Vite on :4308).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CERT_DIR="${CERT_DIR:-$ROOT/certs}"
|
||||||
|
DAYS="${CERT_DAYS:-825}"
|
||||||
|
KEY="$CERT_DIR/key.pem"
|
||||||
|
CERT="$CERT_DIR/cert.pem"
|
||||||
|
CNF="$CERT_DIR/openssl.cnf"
|
||||||
|
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Extra hostnames / IPs: ./scripts/generate-cert.sh 192.168.188.22 myhost.local
|
||||||
|
EXTRA=("$@")
|
||||||
|
|
||||||
|
SAN="DNS:localhost,IP:127.0.0.1"
|
||||||
|
for entry in "${EXTRA[@]}"; do
|
||||||
|
if [[ "$entry" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
SAN+=",IP:$entry"
|
||||||
|
else
|
||||||
|
SAN+=",DNS:$entry"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Include non-loopback IPv4 addresses so LAN / WebView origins work.
|
||||||
|
while read -r ip; do
|
||||||
|
[[ -n "$ip" ]] || continue
|
||||||
|
[[ "$SAN" == *",$ip"* || "$SAN" == *"IP:$ip"* ]] && continue
|
||||||
|
SAN+=",IP:$ip"
|
||||||
|
done < <(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
|
cat >"$CNF" <<EOF
|
||||||
|
[req]
|
||||||
|
default_bits = 2048
|
||||||
|
prompt = no
|
||||||
|
default_md = sha256
|
||||||
|
distinguished_name = dn
|
||||||
|
x509_extensions = v3_req
|
||||||
|
|
||||||
|
[dn]
|
||||||
|
CN = localhost
|
||||||
|
|
||||||
|
[v3_req]
|
||||||
|
subjectAltName = $SAN
|
||||||
|
keyUsage = digitalSignature, keyEncipherment
|
||||||
|
extendedKeyUsage = serverAuth
|
||||||
|
EOF
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:2048 -sha256 -days "$DAYS" -nodes \
|
||||||
|
-keyout "$KEY" \
|
||||||
|
-out "$CERT" \
|
||||||
|
-config "$CNF"
|
||||||
|
|
||||||
|
chmod 600 "$KEY"
|
||||||
|
rm -f "$CNF"
|
||||||
|
|
||||||
|
# Base64(DER) — preferred QR payload (plain text, no JSON/URL/PEM wrappers).
|
||||||
|
DER_B64="$(openssl x509 -in "$CERT" -outform DER | base64 -w0)"
|
||||||
|
printf '%s\n' "$DER_B64" >"$CERT_DIR/cert.der.b64"
|
||||||
|
|
||||||
|
echo "Wrote $CERT"
|
||||||
|
echo "Wrote $KEY"
|
||||||
|
echo "Wrote $CERT_DIR/cert.der.b64"
|
||||||
|
echo "SAN: $SAN"
|
||||||
|
echo "Valid for $DAYS days."
|
||||||
|
echo
|
||||||
|
echo "=== certificate ==="
|
||||||
|
openssl x509 -in "$CERT" -noout -subject -issuer -dates -fingerprint -sha256 -ext subjectAltName
|
||||||
|
echo
|
||||||
|
echo "=== QR (base64 DER, raw payload) ==="
|
||||||
|
printf '%s' "$DER_B64" | node "$ROOT/scripts/qr-cert.mjs"
|
||||||
|
echo
|
||||||
|
echo "Start HTTPS: npm run dev:https"
|
||||||
|
echo "URL: https://localhost:4308/"
|
||||||
20
scripts/qr-cert.mjs
Normal file
20
scripts/qr-cert.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/** Print a terminal QR for stdin (or argv[2]) — raw payload only. */
|
||||||
|
import qrcode from 'qrcode';
|
||||||
|
|
||||||
|
const fromArg = process.argv[2];
|
||||||
|
const payload = fromArg
|
||||||
|
? fromArg
|
||||||
|
: (await new Response(process.stdin).text()).trim();
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
console.error('usage: qr-cert.mjs <base64-der> or pipe payload on stdin');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = await qrcode.toString(payload, {
|
||||||
|
type: 'terminal',
|
||||||
|
errorCorrectionLevel: 'L',
|
||||||
|
small: true,
|
||||||
|
});
|
||||||
|
process.stdout.write(out);
|
||||||
@@ -2,11 +2,14 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import { SnackbarProvider } from 'notistack';
|
import { SnackbarProvider } from 'notistack';
|
||||||
|
import { registerSW } from 'virtual:pwa-register';
|
||||||
import AuthProvider from './auth/AuthProvider';
|
import AuthProvider from './auth/AuthProvider';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
registerSW({ immediate: true });
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
export default defineConfig({
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
plugins: [react()],
|
const useHttps = process.env.VITE_HTTPS === '1';
|
||||||
server: {
|
const certDir = path.resolve(__dirname, 'certs');
|
||||||
port: 4209,
|
const keyPath = path.join(certDir, 'key.pem');
|
||||||
|
const certPath = path.join(certDir, 'cert.pem');
|
||||||
|
|
||||||
|
function httpsOptions() {
|
||||||
|
if (!useHttps) return false;
|
||||||
|
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
||||||
|
throw new Error(
|
||||||
|
'Missing TLS certs. Run: npm run cert (or ./scripts/generate-cert.sh)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: fs.readFileSync(keyPath),
|
||||||
|
cert: fs.readFileSync(certPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedServer = {
|
||||||
|
// HTTP :4209 — HTTPS :4308 (self-signed; see npm run cert / dev:https)
|
||||||
|
port: useHttps ? 4308 : 4209,
|
||||||
|
https: httpsOptions(),
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
@@ -21,18 +44,61 @@ export default defineConfig({
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Backend sets Secure; WebView on http://LAN-IP will refuse it.
|
let cookies = Array.isArray(raw) ? raw : [raw];
|
||||||
const rewritten = (Array.isArray(raw) ? raw : [raw]).map((c) =>
|
// Backend sets Secure; browsers/WebViews on plain HTTP refuse it.
|
||||||
c.replace(/;\s*Secure/gi, ''),
|
if (!useHttps) {
|
||||||
);
|
cookies = cookies.map((c) => c.replace(/;\s*Secure/gi, ''));
|
||||||
proxyRes.headers['set-cookie'] = rewritten;
|
proxyRes.headers['set-cookie'] = cookies;
|
||||||
|
}
|
||||||
console.log('[vite-proxy]', req.method, req.url, {
|
console.log('[vite-proxy]', req.method, req.url, {
|
||||||
status: proxyRes.statusCode,
|
status: proxyRes.statusCode,
|
||||||
setCookie: rewritten,
|
setCookie: cookies,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
manifest: {
|
||||||
|
id: '/',
|
||||||
|
name: 'Dashfront',
|
||||||
|
short_name: 'Dashfront',
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#dceeea',
|
||||||
|
theme_color: '#3b5bdb',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/icons/icon-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/icons/icon-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
||||||
|
navigateFallback: 'index.html',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
server: sharedServer,
|
||||||
|
preview: sharedServer,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user