mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- 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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user