mirror of
https://github.com/lbr77/SideImpactor.git
synced 2026-05-06 11:14:01 -04:00
fix: show 2FA errors in modal, fix stale device check, add dark mode
- Keep TwoFactorModal open after code submit; display server-side errors inline instead of silently closing the modal - handlePair now returns PairedDeviceInfo so handleInstall uses the fresh UDID instead of stale closure state - Add explicit /wisp/ location in nginx.conf returning 502 to prevent SPA fallback from swallowing websocket upgrades - Add prefers-color-scheme dark mode via CSS custom property overrides; replace hardcoded rgba/hex with color-mix() for automatic adaptation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ pnpm-debug.log*
|
|||||||
# Misc
|
# Misc
|
||||||
coverage/
|
coverage/
|
||||||
.wrangler/
|
.wrangler/
|
||||||
|
repomix-output.xml
|
||||||
|
|||||||
2
dependencies/webmuxd/package.json
vendored
2
dependencies/webmuxd/package.json
vendored
@@ -28,7 +28,7 @@
|
|||||||
"build": "bun run build:openssl-wasm && tsc && bun run copy:openssl-wasm",
|
"build": "bun run build:openssl-wasm && tsc && bun run copy:openssl-wasm",
|
||||||
"build:openssl-wasm": "cd ../../wasm/openssl && bun run build",
|
"build:openssl-wasm": "cd ../../wasm/openssl && bun run build",
|
||||||
"copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs",
|
"copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs",
|
||||||
"prepare": "bun run build",
|
"prepack": "bun run build",
|
||||||
"prepublishOnly": "bun run test && bun run lint",
|
"prepublishOnly": "bun run test && bun run lint",
|
||||||
"preversion": "bun run lint",
|
"preversion": "bun run lint",
|
||||||
"version": "bun run format && git add -A src",
|
"version": "bun run format && git add -A src",
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ export function App() {
|
|||||||
const [twoFactor, setTwoFactor] = useState<{
|
const [twoFactor, setTwoFactor] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
submit: ((code: string) => void) | null;
|
submit: ((code: string) => void) | null;
|
||||||
}>({ open: false, submit: null });
|
error: string | null;
|
||||||
|
}>({ open: false, submit: null, error: null });
|
||||||
|
|
||||||
// Derived
|
// Derived
|
||||||
const isWebUsbSupported = useMemo(() => {
|
const isWebUsbSupported = useMemo(() => {
|
||||||
@@ -258,8 +259,8 @@ export function App() {
|
|||||||
}, [knownUdids, selectedTargetUdid]);
|
}, [knownUdids, selectedTargetUdid]);
|
||||||
|
|
||||||
// ---- pair flow ----
|
// ---- pair flow ----
|
||||||
const handlePair = useCallback(async () => {
|
const handlePair = useCallback(async (): Promise<PairedDeviceInfo | null> => {
|
||||||
if (busyRef.current.pair) return;
|
if (busyRef.current.pair) return null;
|
||||||
setBusy((prev) => ({ ...prev, pair: true }));
|
setBusy((prev) => ({ ...prev, pair: true }));
|
||||||
setTrustOpen(true);
|
setTrustOpen(true);
|
||||||
addLog('pair: please continue on your device');
|
addLog('pair: please continue on your device');
|
||||||
@@ -279,12 +280,14 @@ export function App() {
|
|||||||
saveText(SELECTED_DEVICE_UDID_STORAGE_KEY, info.udid);
|
saveText(SELECTED_DEVICE_UDID_STORAGE_KEY, info.udid);
|
||||||
setPairRecordsVersion((v) => v + 1);
|
setPairRecordsVersion((v) => v + 1);
|
||||||
setTrustOpen(false);
|
setTrustOpen(false);
|
||||||
|
return info;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isPairingDialogPendingError(error)) {
|
if (isPairingDialogPendingError(error)) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
setTrustOpen(false);
|
setTrustOpen(false);
|
||||||
addLog(`pair failed: ${formatError(error)}`);
|
addLog(`pair failed: ${formatError(error)}`);
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy((prev) => ({ ...prev, pair: false }));
|
setBusy((prev) => ({ ...prev, pair: false }));
|
||||||
}
|
}
|
||||||
@@ -300,6 +303,8 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBusy((prev) => ({ ...prev, loginSign: true }));
|
setBusy((prev) => ({ ...prev, loginSign: true }));
|
||||||
|
let twoFactorOpened = false;
|
||||||
|
let twoFactorErrorShown = false;
|
||||||
try {
|
try {
|
||||||
saveText(APPLE_ID_STORAGE_KEY, trimmedAppleId);
|
saveText(APPLE_ID_STORAGE_KEY, trimmedAppleId);
|
||||||
addLog('login: initializing anisette...');
|
addLog('login: initializing anisette...');
|
||||||
@@ -315,11 +320,13 @@ export function App() {
|
|||||||
anisetteData: nextAnisette,
|
anisetteData: nextAnisette,
|
||||||
log: addLog,
|
log: addLog,
|
||||||
onTwoFactorRequired: (submit) => {
|
onTwoFactorRequired: (submit) => {
|
||||||
setTwoFactor({ open: true, submit });
|
twoFactorOpened = true;
|
||||||
|
setTwoFactor({ open: true, submit, error: null });
|
||||||
addLog('login: 2FA required, opening verification dialog');
|
addLog('login: 2FA required, opening verification dialog');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTwoFactor({ open: false, submit: null, error: null });
|
||||||
addLog(`login: authenticated as ${context.appleId} / ${context.team.identifier}`);
|
addLog(`login: authenticated as ${context.appleId} / ${context.team.identifier}`);
|
||||||
const key = accountKey(context.appleId, context.team.identifier);
|
const key = accountKey(context.appleId, context.team.identifier);
|
||||||
accountContextMapRef.current.set(key, context);
|
accountContextMapRef.current.set(key, context);
|
||||||
@@ -335,9 +342,16 @@ export function App() {
|
|||||||
addLog('login: done, navigating to sign page');
|
addLog('login: done, navigating to sign page');
|
||||||
navigateToPage('sign');
|
navigateToPage('sign');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addLog(`login failed: ${formatError(error)}`);
|
const msg = formatError(error);
|
||||||
|
addLog(`login failed: ${msg}`);
|
||||||
|
if (twoFactorOpened) {
|
||||||
|
setTwoFactor((prev) => ({ ...prev, error: msg }));
|
||||||
|
twoFactorErrorShown = true;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setTwoFactor({ open: false, submit: null });
|
if (!twoFactorErrorShown) {
|
||||||
|
setTwoFactor({ open: false, submit: null, error: null });
|
||||||
|
}
|
||||||
setBusy((prev) => ({ ...prev, loginSign: false }));
|
setBusy((prev) => ({ ...prev, loginSign: false }));
|
||||||
}
|
}
|
||||||
}, [addLog, anisetteData, appleId, clearPrepared, navigateToPage, password]);
|
}, [addLog, anisetteData, appleId, clearPrepared, navigateToPage, password]);
|
||||||
@@ -345,7 +359,6 @@ export function App() {
|
|||||||
const handleTwoFactorSubmit = useCallback(
|
const handleTwoFactorSubmit = useCallback(
|
||||||
(code: string) => {
|
(code: string) => {
|
||||||
const submit = twoFactor.submit;
|
const submit = twoFactor.submit;
|
||||||
setTwoFactor({ open: false, submit: null });
|
|
||||||
if (submit) submit(code);
|
if (submit) submit(code);
|
||||||
},
|
},
|
||||||
[twoFactor.submit],
|
[twoFactor.submit],
|
||||||
@@ -353,7 +366,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleTwoFactorCancel = useCallback(() => {
|
const handleTwoFactorCancel = useCallback(() => {
|
||||||
const submit = twoFactor.submit;
|
const submit = twoFactor.submit;
|
||||||
setTwoFactor({ open: false, submit: null });
|
setTwoFactor({ open: false, submit: null, error: null });
|
||||||
if (submit) {
|
if (submit) {
|
||||||
addLog('login: 2FA canceled');
|
addLog('login: 2FA canceled');
|
||||||
submit('__CANCELLED__');
|
submit('__CANCELLED__');
|
||||||
@@ -422,10 +435,12 @@ export function App() {
|
|||||||
onStateChange: () => {},
|
onStateChange: () => {},
|
||||||
onTrustPending: () => setTrustOpen(true),
|
onTrustPending: () => setTrustOpen(true),
|
||||||
});
|
});
|
||||||
|
let currentDeviceUdid = pairedDeviceInfo?.udid ?? null;
|
||||||
if (!client.isSessionStarted) {
|
if (!client.isSessionStarted) {
|
||||||
await handlePair();
|
const freshInfo = await handlePair();
|
||||||
|
currentDeviceUdid = freshInfo?.udid ?? null;
|
||||||
}
|
}
|
||||||
if (pairedDeviceInfo?.udid !== targetUdid) {
|
if (currentDeviceUdid !== targetUdid) {
|
||||||
throw new Error('connected device udid does not match selected target');
|
throw new Error('connected device udid does not match selected target');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +621,12 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TrustModal open={trustOpen} onClose={() => setTrustOpen(false)} pairing={busy.pair} />
|
<TrustModal open={trustOpen} onClose={() => setTrustOpen(false)} pairing={busy.pair} />
|
||||||
<TwoFactorModal open={twoFactor.open} onSubmit={handleTwoFactorSubmit} onCancel={handleTwoFactorCancel} />
|
<TwoFactorModal
|
||||||
|
open={twoFactor.open}
|
||||||
|
onSubmit={handleTwoFactorSubmit}
|
||||||
|
onCancel={handleTwoFactorCancel}
|
||||||
|
serverError={twoFactor.error}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ interface TwoFactorModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onSubmit: (code: string) => void;
|
onSubmit: (code: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
/** Server-side error (e.g. wrong code). Shown when the login flow rejects after 2FA submit. */
|
||||||
|
serverError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TwoFactorModal({ open, onSubmit, onCancel }: TwoFactorModalProps) {
|
export function TwoFactorModal({ open, onSubmit, onCancel, serverError }: TwoFactorModalProps) {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const displayError = serverError || error;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -67,13 +70,13 @@ export function TwoFactorModal({ open, onSubmit, onCancel }: TwoFactorModalProps
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 min-h-[18px] text-[12px] text-[var(--color-danger)]">{error ?? ''}</p>
|
<p className="mt-2 min-h-[18px] text-[12px] text-[var(--color-danger)]">{displayError ?? ''}</p>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||||
<Button variant="ghost" onClick={onCancel}>
|
<Button variant="ghost" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleSubmit}>
|
<Button variant="primary" onClick={handleSubmit} disabled={!!serverError}>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
--color-danger: #dc2626;
|
--color-danger: #dc2626;
|
||||||
--color-danger-soft: #fef2f2;
|
--color-danger-soft: #fef2f2;
|
||||||
|
|
||||||
|
--color-on-ink: #ffffff;
|
||||||
|
--color-ink-hover: #1f1f1f;
|
||||||
|
--color-accent-hover: #1d4ed8;
|
||||||
|
|
||||||
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif;
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,30 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--color-bg: #0a0a0a;
|
||||||
|
--color-surface: #141414;
|
||||||
|
--color-elevated: #1a1a1a;
|
||||||
|
--color-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--color-border-strong: rgba(255, 255, 255, 0.14);
|
||||||
|
--color-ink: #ededed;
|
||||||
|
--color-muted: #a1a1aa;
|
||||||
|
--color-subtle: #71717a;
|
||||||
|
--color-accent: #3b82f6;
|
||||||
|
--color-accent-soft: rgba(59, 130, 246, 0.12);
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-success-soft: rgba(34, 197, 94, 0.12);
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-danger-soft: rgba(239, 68, 68, 0.12);
|
||||||
|
--color-on-ink: #0a0a0a;
|
||||||
|
--color-ink-hover: #d4d4d4;
|
||||||
|
--color-accent-hover: #60a5fa;
|
||||||
|
--color-log-bg: #111111;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -48,7 +76,7 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(37, 99, 235, 0.18);
|
background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- form primitives ----- */
|
/* ----- form primitives ----- */
|
||||||
@@ -78,7 +106,7 @@ body.modal-open {
|
|||||||
|
|
||||||
.field-input:focus {
|
.field-input:focus {
|
||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-input:disabled {
|
.field-input:disabled {
|
||||||
@@ -129,11 +157,11 @@ body.modal-open {
|
|||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--color-ink);
|
background: var(--color-ink);
|
||||||
color: #ffffff;
|
color: var(--color-on-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
background: #1f1f1f;
|
background: var(--color-ink-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-accent {
|
.btn-accent {
|
||||||
@@ -142,7 +170,7 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-accent:hover:not(:disabled) {
|
.btn-accent:hover:not(:disabled) {
|
||||||
background: #1d4ed8;
|
background: var(--color-accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
@@ -192,9 +220,9 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seg-btn[data-active='true'] {
|
.seg-btn[data-active='true'] {
|
||||||
background: var(--color-bg);
|
background: var(--color-elevated);
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- card ----- */
|
/* ----- card ----- */
|
||||||
@@ -252,9 +280,9 @@ body.modal-open {
|
|||||||
|
|
||||||
.stepper-step[data-state='active'] .stepper-marker {
|
.stepper-step[data-state='active'] .stepper-marker {
|
||||||
background: var(--color-ink);
|
background: var(--color-ink);
|
||||||
color: #ffffff;
|
color: var(--color-on-ink);
|
||||||
border-color: var(--color-ink);
|
border-color: var(--color-ink);
|
||||||
box-shadow: 0 0 0 4px rgba(10, 10, 10, 0.06);
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-ink) 6%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepper-step[data-state='active'] .stepper-marker::after {
|
.stepper-step[data-state='active'] .stepper-marker::after {
|
||||||
@@ -262,7 +290,7 @@ body.modal-open {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -6px;
|
inset: -6px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid rgba(10, 10, 10, 0.18);
|
border: 1px solid color-mix(in srgb, var(--color-ink) 18%, transparent);
|
||||||
animation: pulse-ring 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: pulse-ring 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -270,7 +298,7 @@ body.modal-open {
|
|||||||
.stepper-step[data-state='done'] .stepper-marker {
|
.stepper-step[data-state='done'] .stepper-marker {
|
||||||
background: var(--color-success-soft);
|
background: var(--color-success-soft);
|
||||||
color: var(--color-success);
|
color: var(--color-success);
|
||||||
border-color: rgba(22, 163, 74, 0.28);
|
border-color: color-mix(in srgb, var(--color-success) 28%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepper-label {
|
.stepper-label {
|
||||||
@@ -386,7 +414,7 @@ pre.log {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #0a0a0a;
|
background: var(--color-log-bg, #0a0a0a);
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -437,10 +465,10 @@ pre.log::-webkit-scrollbar-track {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border-strong);
|
||||||
background: var(--color-bg);
|
background: var(--color-elevated);
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
box-shadow: 0 20px 50px -20px rgba(10, 10, 10, 0.25), 0 4px 12px -4px rgba(10, 10, 10, 0.12);
|
box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.2);
|
||||||
transform: translateY(8px) scale(0.98);
|
transform: translateY(8px) scale(0.98);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease;
|
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease;
|
||||||
@@ -470,13 +498,13 @@ pre.log::-webkit-scrollbar-track {
|
|||||||
|
|
||||||
.chip[data-tone='success'] {
|
.chip[data-tone='success'] {
|
||||||
background: var(--color-success-soft);
|
background: var(--color-success-soft);
|
||||||
border-color: rgba(22, 163, 74, 0.22);
|
border-color: color-mix(in srgb, var(--color-success) 22%, transparent);
|
||||||
color: var(--color-success);
|
color: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip[data-tone='accent'] {
|
.chip[data-tone='accent'] {
|
||||||
background: var(--color-accent-soft);
|
background: var(--color-accent-soft);
|
||||||
border-color: rgba(37, 99, 235, 0.22);
|
border-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,3 +571,15 @@ pre.log::-webkit-scrollbar-track {
|
|||||||
.anim-in {
|
.anim-in {
|
||||||
animation: fade-in-up 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
|
animation: fade-in-up 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----- dark mode component overrides ----- */
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.field-select {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%23a1a1aa' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ server {
|
|||||||
add_header Cache-Control "no-cache" always;
|
add_header Cache-Control "no-cache" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# WISP websocket — must be proxied by an outer reverse proxy or sidecar.
|
||||||
|
# Return 502 so the frontend gets a clear signal rather than the SPA shell.
|
||||||
|
location /wisp/ {
|
||||||
|
return 502;
|
||||||
|
}
|
||||||
|
|
||||||
# Hash routing (no server-side routes needed) — fall back to index.html.
|
# Hash routing (no server-side routes needed) — fall back to index.html.
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
Reference in New Issue
Block a user