- Updated markAccountUnavailable function to accept resetsAtMs for precise cooldown management.

- Added email backfill functionality for Codex OAuth connections to improve account information accuracy.
This commit is contained in:
decolua
2026-04-24 11:36:16 +07:00
parent fd8163e26e
commit 030fb34f88
15 changed files with 628 additions and 132 deletions
+175 -27
View File
@@ -43,10 +43,12 @@ export default function Sidebar({ onClose }) {
const [updateInfo, setUpdateInfo] = useState(null);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [updateStatus, setUpdateStatus] = useState(null);
const [enableTranslator, setEnableTranslator] = useState(false);
const { copied, copy } = useCopyToClipboard(2000);
const INSTALL_CMD = UPDATER_CONFIG.installCmd;
const STATUS_URL = `http://localhost:${UPDATER_CONFIG.statusPort}/update/status`;
useEffect(() => {
fetch("/api/settings")
@@ -81,14 +83,30 @@ export default function Sidebar({ onClose }) {
setIsUpdating(false);
return;
}
// Server will exit shortly; show disconnected overlay
setIsDisconnected(true);
} catch (e) {
// Expected once the server exits; treat as disconnected
setIsDisconnected(true);
}
};
// Poll updater status server while updating (Next server is dead, updater.js is alive)
useEffect(() => {
if (!isUpdating || !isDisconnected) return;
let stopped = false;
const tick = async () => {
try {
const res = await fetch(STATUS_URL, { cache: "no-store" });
if (res.ok) {
const data = await res.json();
if (!stopped) setUpdateStatus(data);
}
} catch { /* updater not ready yet or finished */ }
};
tick();
const id = setInterval(tick, UPDATER_CONFIG.statusPollIntervalMs);
return () => { stopped = true; clearInterval(id); };
}, [isUpdating, isDisconnected, STATUS_URL]);
const handleShutdown = async () => {
setIsShuttingDown(true);
try {
@@ -338,31 +356,27 @@ export default function Sidebar({ onClose }) {
{/* Disconnected Overlay */}
{isDisconnected && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="text-center p-8">
{isUpdating ? (
<>
<div className="flex items-center justify-center size-16 rounded-full bg-green-500/20 text-green-500 mx-auto mb-4">
<span className="material-symbols-outlined text-[32px]">download</span>
</div>
<h2 className="text-xl font-semibold text-white mb-2">Updating 9Router</h2>
<p className="text-text-muted mb-6">
A new terminal window is installing the update. Once finished, run <code className="text-green-400">9router</code> again.
</p>
</>
) : (
<>
<div className="flex items-center justify-center size-16 rounded-full bg-red-500/20 text-red-500 mx-auto mb-4">
<span className="material-symbols-outlined text-[32px]">power_off</span>
</div>
<h2 className="text-xl font-semibold text-white mb-2">Server Disconnected</h2>
<p className="text-text-muted mb-6">The proxy server has been stopped.</p>
<Button variant="secondary" onClick={() => globalThis.location.reload()}>
Reload Page
</Button>
</>
)}
</div>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-6">
{isUpdating ? (
<UpdateProgress
status={updateStatus}
latestVersion={updateInfo?.latestVersion}
installCmd={INSTALL_CMD}
copied={copied}
onCopy={() => copy(INSTALL_CMD)}
/>
) : (
<div className="text-center p-8">
<div className="flex items-center justify-center size-16 rounded-full bg-red-500/20 text-red-500 mx-auto mb-4">
<span className="material-symbols-outlined text-[32px]">power_off</span>
</div>
<h2 className="text-xl font-semibold text-white mb-2">Server Disconnected</h2>
<p className="text-text-muted mb-6">The proxy server has been stopped.</p>
<Button variant="secondary" onClick={() => globalThis.location.reload()}>
Reload Page
</Button>
</div>
)}
</div>
)}
</>
@@ -372,3 +386,137 @@ export default function Sidebar({ onClose }) {
Sidebar.propTypes = {
onClose: PropTypes.func,
};
function UpdateProgress({ status, latestVersion, installCmd, copied, onCopy }) {
const phase = status?.phase || "connecting";
const done = status?.done === true;
const success = status?.success === true;
const attempt = status?.attempt || 0;
const maxRetries = status?.maxRetries || 0;
const logTail = status?.logTail || [];
const errorMsg = status?.error;
const steps = [
{ key: "stopped", label: "Stopped 9Router server", state: "done" },
{
key: "launched",
label: "Launched background installer",
state: status ? "done" : "active",
},
{
key: "waiting",
label: "Waiting for app processes to exit",
state: phase === "waitingForExit" ? "active" :
(status && phase !== "starting" ? "done" : "pending"),
},
{
key: "installing",
label: attempt > 1 ? `Installing v${latestVersion || "latest"} (attempt ${attempt}/${maxRetries})` : `Installing v${latestVersion || "latest"}`,
state: done ? (success ? "done" : "error") : (phase === "installing" ? "active" : "pending"),
},
{
key: "finished",
label: done && success ? "Installed — ready to restart" : "Waiting to finish",
state: done && success ? "done" : (done && !success ? "error" : "pending"),
},
];
return (
<div className="w-full max-w-lg rounded-xl bg-neutral-900/95 border border-white/10 p-6 text-white">
<div className="flex items-center gap-3 mb-4">
<div className={cn(
"flex items-center justify-center size-11 rounded-full",
done && success ? "bg-green-500/20 text-green-400" :
done && !success ? "bg-red-500/20 text-red-400" :
"bg-blue-500/20 text-blue-400"
)}>
<span className={cn(
"material-symbols-outlined text-[24px]",
!done && "animate-spin"
)}>
{done && success ? "check_circle" : done && !success ? "error" : "progress_activity"}
</span>
</div>
<div>
<h2 className="text-lg font-semibold">
{done && success ? "Update Completed" : done && !success ? "Update Failed" : "Updating 9Router"}
</h2>
<p className="text-xs text-white/60">
{done && success
? `Installed v${latestVersion || "latest"} successfully`
: done && !success
? (errorMsg || "Installation failed")
: `Installing v${latestVersion || "latest"} from npm...`}
</p>
</div>
</div>
{/* Timeline */}
<ul className="space-y-2 mb-4">
{steps.map((s) => (
<li key={s.key} className="flex items-center gap-3 text-sm">
<span className={cn(
"material-symbols-outlined text-[18px] shrink-0",
s.state === "done" && "text-green-400",
s.state === "active" && "text-blue-400 animate-pulse",
s.state === "error" && "text-red-400",
s.state === "pending" && "text-white/30"
)}>
{s.state === "done" ? "check_circle" :
s.state === "error" ? "cancel" :
s.state === "active" ? "radio_button_checked" : "radio_button_unchecked"}
</span>
<span className={cn(
s.state === "pending" ? "text-white/40" : "text-white/90"
)}>{s.label}</span>
</li>
))}
</ul>
{/* Log tail */}
{logTail.length > 0 && (
<div className="rounded-md bg-black/50 border border-white/5 p-3 mb-4 max-h-40 overflow-auto">
<pre className="text-[11px] font-mono text-white/70 whitespace-pre-wrap break-all">
{logTail.join("\n")}
</pre>
</div>
)}
{/* Actions */}
{done && success ? (
<div className="space-y-2">
<p className="text-sm text-white/80">
Run <code className="px-1.5 py-0.5 rounded bg-white/10 text-green-400">9router</code> in your terminal to start the new version.
</p>
<Button variant="secondary" fullWidth onClick={() => globalThis.location.reload()}>
Reload Page
</Button>
</div>
) : done && !success ? (
<div className="space-y-2">
<p className="text-sm text-white/80">Run the install command manually:</p>
<button
onClick={onCopy}
className="w-full text-left px-3 py-2 rounded bg-white/5 hover:bg-white/10 transition-colors"
>
<code className="text-xs font-mono text-amber-400">
{copied ? "✓ copied!" : installCmd}
</code>
</button>
</div>
) : (
<p className="text-xs text-white/50 text-center">
This may take 30-60 seconds. Please don't close this window.
</p>
)}
</div>
);
}
UpdateProgress.propTypes = {
status: PropTypes.object,
latestVersion: PropTypes.string,
installCmd: PropTypes.string.isRequired,
copied: PropTypes.bool,
onCopy: PropTypes.func.isRequired,
};
+10
View File
@@ -17,6 +17,16 @@ export const UPDATER_CONFIG = {
npmPackageName: "9router",
installCmd: "npm i -g 9router",
exitDelayMs: 500,
statusPort: 20129,
statusPollIntervalMs: 1000,
statusLogTailLines: 8,
installRetries: 3,
installRetryDelayMs: 5000,
lingerAfterDoneMs: 30000,
waitForExitMinMs: 3000,
waitForExitMaxMs: 15000,
waitForExitCheckMs: 500,
appPort: 20128,
};
// Theme configuration