@cartanova/qgrid-cli 2.6.1 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +2 -0
- package/README.md +2 -0
- package/bundle/dist/application/qgrid/oauth.js +21 -3
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +40 -6
- package/bundle/dist/application/qgrid/qgrid.frame.js +47 -19
- package/bundle/dist/application/qgrid/qgrid.types.js +13 -3
- package/bundle/dist/application/qgrid/token-death.js +77 -0
- package/bundle/dist/application/queries.generated.js +3 -2
- package/bundle/dist/application/request-log/request-log.model.js +105 -31
- package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
- package/bundle/dist/application/sonamu.generated.js +60 -5
- package/bundle/dist/application/sonamu.generated.sso.js +18 -2
- package/bundle/dist/application/token/token.model.js +53 -2
- package/bundle/dist/i18n/en.js +3 -1
- package/bundle/dist/i18n/ko.js +3 -1
- package/bundle/dist/i18n/sd.generated.js +18 -1
- package/bundle/dist/migrations/20260807133553_manual__backfill_request_log_cost.js +49 -0
- package/bundle/dist/migrations/20260807145144_alter_request_logs_alter1.js +13 -0
- package/bundle/dist/migrations/20260807164057_alter_request_logs_alter1.js +13 -0
- package/bundle/dist/server-error-handler.js +5 -3
- package/bundle/dist/sonamu.config.js +5 -1
- package/bundle/dist/utils/providers/openai/openai-refresh.js +12 -1
- package/bundle/dist/utils/slack-notify.js +65 -0
- package/bundle/src/application/qgrid/oauth.ts +20 -1
- package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +15 -3
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +30 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +44 -6
- package/bundle/src/application/qgrid/qgrid.frame.ts +67 -47
- package/bundle/src/application/qgrid/qgrid.types.ts +10 -0
- package/bundle/src/application/qgrid/token-death.test.ts +150 -0
- package/bundle/src/application/qgrid/token-death.ts +96 -0
- package/bundle/src/application/queries.generated.ts +5 -2
- package/bundle/src/application/request-log/request-log.entity.json +46 -7
- package/bundle/src/application/request-log/request-log.model.test.ts +153 -42
- package/bundle/src/application/request-log/request-log.model.ts +136 -50
- package/bundle/src/application/sonamu.generated.http +19 -1
- package/bundle/src/application/sonamu.generated.sso.ts +16 -0
- package/bundle/src/application/sonamu.generated.ts +58 -3
- package/bundle/src/application/token/token.model.test.ts +49 -0
- package/bundle/src/application/token/token.model.ts +75 -0
- package/bundle/src/i18n/en.ts +3 -0
- package/bundle/src/i18n/ko.ts +3 -0
- package/bundle/src/i18n/sd.generated.ts +17 -0
- package/bundle/src/migrations/20260807133553_manual__backfill_request_log_cost.ts +110 -0
- package/bundle/src/migrations/20260807145144_alter_request_logs_alter1.ts +13 -0
- package/bundle/src/migrations/20260807164057_alter_request_logs_alter1.ts +13 -0
- package/bundle/src/server-error-handler.test.ts +58 -0
- package/bundle/src/server-error-handler.ts +16 -5
- package/bundle/src/sonamu.config.ts +6 -0
- package/bundle/src/utils/providers/openai/openai-refresh.ts +17 -0
- package/bundle/src/utils/slack-notify.test.ts +97 -0
- package/bundle/src/utils/slack-notify.ts +74 -0
- package/bundle/web-dist/client/assets/index-BGQSGMcK.css +1 -0
- package/bundle/web-dist/client/assets/index-CnNT0mdq.js +78 -0
- package/bundle/web-dist/client/assets/logs-DCY81UWb.js +1 -0
- package/bundle/web-dist/client/assets/{monit-CyqMmy1G.js → monit-D5OoAday.js} +1 -1
- package/bundle/web-dist/client/assets/routes-B4D9C86u.js +5 -0
- package/bundle/web-dist/client/assets/{sd.generated-CjW3rfJ8.js → sd.generated-DYdwSB2I.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-BuT6Gp8V.js → services.generated-BsAvotjG.js} +1 -1
- package/bundle/web-dist/client/assets/{show-C5WxfoAt.js → show-BSX8ii7Z.js} +3 -3
- package/bundle/web-dist/client/assets/tokens-Gapk41cz.js +1 -0
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DBkxL29Y.js → use-update-token-mutation-DqEpK7It.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-PRxXWW3t.js → lazyRouteComponent-BaZaveNc.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-D0z1Ghj1.js → logs-DMOw3SjD.js} +91 -42
- package/bundle/web-dist/server/assets/{monit-dBXxnAyI.js → monit-UmcqHroX.js} +2 -2
- package/bundle/web-dist/server/assets/{routes-B3p7fuwk.js → routes-Sv_4FjtH.js} +52 -46
- package/bundle/web-dist/server/assets/{sd.generated-ek-tz-7H.js → sd.generated-B0xqnHEo.js} +21 -0
- package/bundle/web-dist/server/assets/{services.generated-BNuxWs7K.js → services.generated-DqAKskAS.js} +23 -8
- package/bundle/web-dist/server/assets/{show-0iNioJ2I.js → show-C-oSdzHX.js} +31 -25
- package/bundle/web-dist/server/assets/{tokens-CvytQ9Xy.js → tokens-h-2A3wfD.js} +292 -191
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BBCC2GKM.js → use-update-token-mutation-DHCaa7Ku.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +70 -29
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +8 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +11 -0
- package/bundle/web-dist/client/assets/index-BGGBFmzE.js +0 -78
- package/bundle/web-dist/client/assets/index-GA9b-zy4.css +0 -1
- package/bundle/web-dist/client/assets/logs-Dh19u6Tk.js +0 -1
- package/bundle/web-dist/client/assets/routes-Cy0gCq8c.js +0 -5
- package/bundle/web-dist/client/assets/tokens-Ciolqw4l.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as functionalUpdate, B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, E as onlineManager, F as matchQuery, I as noop$1, J as __commonJSMin, K as focusManager, L as partialMatchKey, M as hashQueryKeyByOptions, P as matchMutation, U as skipToken, X as __require, Z as __toESM, _ as require_jsx_runtime, c as _enum, d as string, h as QueryClientProvider, j as hashKey, l as number, m as require_shim, n as SUPPORTED_LOCALES, p as require_react_dom, q as Subscribable, r as setLocale, t as SD, u as object, v as require_react, w as Query, y as Mutation } from "./assets/sd.generated-
|
|
2
|
-
import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-
|
|
1
|
+
import { A as functionalUpdate, B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, E as onlineManager, F as matchQuery, I as noop$1, J as __commonJSMin, K as focusManager, L as partialMatchKey, M as hashQueryKeyByOptions, P as matchMutation, U as skipToken, X as __require, Z as __toESM, _ as require_jsx_runtime, c as _enum, d as string, h as QueryClientProvider, j as hashKey, l as number, m as require_shim, n as SUPPORTED_LOCALES, p as require_react_dom, q as Subscribable, r as setLocale, t as SD, u as object, v as require_react, w as Query, y as Mutation } from "./assets/sd.generated-B0xqnHEo.js";
|
|
2
|
+
import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-BaZaveNc.js";
|
|
3
3
|
//#region ../../node_modules/.pnpm/@tanstack+query-core@5.95.2/node_modules/@tanstack/query-core/build/modern/hydration.js
|
|
4
4
|
function defaultTransformerFn(data) {
|
|
5
5
|
return data;
|
|
@@ -22675,8 +22675,9 @@ var lucideTerminal = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg
|
|
|
22675
22675
|
})
|
|
22676
22676
|
});
|
|
22677
22677
|
//#endregion
|
|
22678
|
-
//#region src/components/
|
|
22679
|
-
|
|
22678
|
+
//#region src/components/nav-items.ts
|
|
22679
|
+
/** 사이드바(데스크톱)와 하단 탭 바(모바일)가 공유하는 단일 정의. */
|
|
22680
|
+
var NAV_SECTIONS = [{
|
|
22680
22681
|
label: "Overview",
|
|
22681
22682
|
items: [{
|
|
22682
22683
|
title: "Dashboard",
|
|
@@ -22693,23 +22694,57 @@ var sections = [{
|
|
|
22693
22694
|
},
|
|
22694
22695
|
{
|
|
22695
22696
|
title: "Request Logs",
|
|
22697
|
+
shortTitle: "Logs",
|
|
22696
22698
|
path: "/logs",
|
|
22697
22699
|
icon: lucideFileText
|
|
22698
22700
|
},
|
|
22699
22701
|
{
|
|
22700
22702
|
title: "Server Logs",
|
|
22703
|
+
shortTitle: "Server",
|
|
22701
22704
|
path: "/monit",
|
|
22702
22705
|
icon: lucideTerminal
|
|
22703
22706
|
}
|
|
22704
22707
|
]
|
|
22705
22708
|
}];
|
|
22706
|
-
var
|
|
22709
|
+
var NAV_ITEMS = NAV_SECTIONS.flatMap((section) => section.items);
|
|
22710
|
+
function isNavItemActive(path, pathname) {
|
|
22711
|
+
if (path === "/") return pathname === "/" || pathname === "";
|
|
22712
|
+
return pathname.startsWith(path);
|
|
22713
|
+
}
|
|
22714
|
+
//#endregion
|
|
22715
|
+
//#region src/components/BottomNav.tsx
|
|
22716
|
+
/**
|
|
22717
|
+
* 좁은 화면 전용 하단 탭 바.
|
|
22718
|
+
*
|
|
22719
|
+
* 사이드바는 `md` 미만에서 숨겨지는데, 그러면 URL 을 직접 치지 않는 한 페이지 이동이
|
|
22720
|
+
* 불가능해진다. 항목이 4개뿐이라 햄버거 메뉴보다 탭 바가 맞다 — 한 번의 탭으로 이동하고
|
|
22721
|
+
* 현재 위치도 항상 보인다.
|
|
22722
|
+
*/
|
|
22723
|
+
function BottomNav() {
|
|
22724
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
22725
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", {
|
|
22726
|
+
className: "md:hidden shrink-0 border-t border-sand-200/60 bg-sand-50/95 backdrop-blur-sm pb-[env(safe-area-inset-bottom)]",
|
|
22727
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
22728
|
+
className: "flex",
|
|
22729
|
+
children: NAV_ITEMS.map((item) => {
|
|
22730
|
+
const active = isNavItemActive(item.path, pathname);
|
|
22731
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Link$1, {
|
|
22732
|
+
to: item.path,
|
|
22733
|
+
className: `flex-1 flex flex-col items-center gap-0.5 py-2 no-underline transition-colors duration-150 ${active ? "text-sienna-600" : "text-sand-500"}`,
|
|
22734
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(item.icon, { className: `size-5 ${active ? "text-sienna-500" : "text-sand-400"}` }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
22735
|
+
className: `text-[10px] ${active ? "font-medium" : ""}`,
|
|
22736
|
+
children: item.shortTitle ?? item.title
|
|
22737
|
+
})]
|
|
22738
|
+
}, item.path);
|
|
22739
|
+
})
|
|
22740
|
+
})
|
|
22741
|
+
});
|
|
22742
|
+
}
|
|
22743
|
+
//#endregion
|
|
22744
|
+
//#region src/components/Sidebar.tsx
|
|
22745
|
+
var qgridVersion = "2.6.4";
|
|
22707
22746
|
function Sidebar({ className }) {
|
|
22708
22747
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
22709
|
-
const isActive = (path) => {
|
|
22710
|
-
if (path === "/") return pathname === "/" || pathname === "";
|
|
22711
|
-
return pathname.startsWith(path);
|
|
22712
|
-
};
|
|
22713
22748
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
|
|
22714
22749
|
className: `hidden md:flex w-[200px] bg-sand-50 flex-col shrink-0 border-r border-sand-200/40 ${className ?? ""}`,
|
|
22715
22750
|
children: [
|
|
@@ -22722,7 +22757,7 @@ function Sidebar({ className }) {
|
|
|
22722
22757
|
}),
|
|
22723
22758
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", {
|
|
22724
22759
|
className: "flex-1 px-2.5 py-3 space-y-4 overflow-y-auto",
|
|
22725
|
-
children:
|
|
22760
|
+
children: NAV_SECTIONS.map((section) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
22726
22761
|
className: "px-2 pb-1.5",
|
|
22727
22762
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
22728
22763
|
className: "text-[10px] uppercase tracking-[0.08em] text-sand-400 font-medium",
|
|
@@ -22731,7 +22766,7 @@ function Sidebar({ className }) {
|
|
|
22731
22766
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
22732
22767
|
className: "space-y-0.5",
|
|
22733
22768
|
children: section.items.map((item) => {
|
|
22734
|
-
const active =
|
|
22769
|
+
const active = isNavItemActive(item.path, pathname);
|
|
22735
22770
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Link$1, {
|
|
22736
22771
|
to: item.path,
|
|
22737
22772
|
className: `flex items-center gap-2 px-2 py-[6px] rounded-[7px] text-[13px] no-underline transition-all duration-150 ${active ? "bg-sand-200/60 text-sand-900 font-medium" : "text-sand-500 hover:bg-sand-100 hover:text-sand-700"}`,
|
|
@@ -22803,20 +22838,24 @@ function App({ children }) {
|
|
|
22803
22838
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
22804
22839
|
className: "flex h-screen w-full bg-sand-100 overflow-hidden font-sans",
|
|
22805
22840
|
children: [showSidebar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sidebar, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
22806
|
-
className: "flex-1 flex flex-col overflow-hidden",
|
|
22807
|
-
children: [
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
className: "
|
|
22811
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22841
|
+
className: "flex-1 flex flex-col overflow-hidden min-w-0",
|
|
22842
|
+
children: [
|
|
22843
|
+
showSidebar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Toolbar, {}),
|
|
22844
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("main", {
|
|
22845
|
+
className: "flex-1 overflow-y-auto main-scroll content-inset",
|
|
22846
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
22847
|
+
className: "px-3 py-4 sm:px-6 sm:py-5 md:px-8",
|
|
22848
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Suspense, {
|
|
22849
|
+
fallback: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
22850
|
+
className: "text-sand-400 text-center py-8 text-sm",
|
|
22851
|
+
children: "로딩 중..."
|
|
22852
|
+
}),
|
|
22853
|
+
children
|
|
22854
|
+
})
|
|
22817
22855
|
})
|
|
22818
|
-
})
|
|
22819
|
-
|
|
22856
|
+
}),
|
|
22857
|
+
showSidebar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BottomNav, {})
|
|
22858
|
+
]
|
|
22820
22859
|
})]
|
|
22821
22860
|
});
|
|
22822
22861
|
}
|
|
@@ -44584,11 +44623,11 @@ function RootComponent() {
|
|
|
44584
44623
|
}
|
|
44585
44624
|
//#endregion
|
|
44586
44625
|
//#region src/routes/tokens.tsx
|
|
44587
|
-
var $$splitComponentImporter$4 = () => import("./assets/tokens-
|
|
44626
|
+
var $$splitComponentImporter$4 = () => import("./assets/tokens-h-2A3wfD.js");
|
|
44588
44627
|
var Route$4 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
|
|
44589
44628
|
//#endregion
|
|
44590
44629
|
//#region src/routes/monit.tsx
|
|
44591
|
-
var $$splitComponentImporter$3 = () => import("./assets/monit-
|
|
44630
|
+
var $$splitComponentImporter$3 = () => import("./assets/monit-UmcqHroX.js");
|
|
44592
44631
|
var monitSearchSchema = object({ level: _enum([
|
|
44593
44632
|
"all",
|
|
44594
44633
|
"scaling",
|
|
@@ -44601,10 +44640,12 @@ var Route$3 = createFileRoute("/monit")({
|
|
|
44601
44640
|
});
|
|
44602
44641
|
//#endregion
|
|
44603
44642
|
//#region src/routes/logs.tsx
|
|
44604
|
-
var $$splitComponentImporter$2 = () => import("./assets/logs-
|
|
44643
|
+
var $$splitComponentImporter$2 = () => import("./assets/logs-DMOw3SjD.js");
|
|
44605
44644
|
var logsSearchSchema = object({
|
|
44606
44645
|
token: string().optional(),
|
|
44607
44646
|
project: string().optional(),
|
|
44647
|
+
model: string().optional(),
|
|
44648
|
+
sort: string().optional(),
|
|
44608
44649
|
page: number().optional().default(1)
|
|
44609
44650
|
});
|
|
44610
44651
|
var Route$2 = createFileRoute("/logs")({
|
|
@@ -44613,11 +44654,11 @@ var Route$2 = createFileRoute("/logs")({
|
|
|
44613
44654
|
});
|
|
44614
44655
|
//#endregion
|
|
44615
44656
|
//#region src/routes/index.tsx
|
|
44616
|
-
var $$splitComponentImporter$1 = () => import("./assets/routes-
|
|
44657
|
+
var $$splitComponentImporter$1 = () => import("./assets/routes-Sv_4FjtH.js");
|
|
44617
44658
|
var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
|
|
44618
44659
|
//#endregion
|
|
44619
44660
|
//#region src/routes/requests/show.tsx
|
|
44620
|
-
var $$splitComponentImporter = () => import("./assets/show-
|
|
44661
|
+
var $$splitComponentImporter = () => import("./assets/show-C-oSdzHX.js");
|
|
44621
44662
|
var showSearchSchema = object({ id: number() });
|
|
44622
44663
|
var Route = createFileRoute("/requests/show")({
|
|
44623
44664
|
validateSearch: showSearchSchema,
|
package/package.json
CHANGED
|
@@ -88,6 +88,14 @@ When helping a user set up qgrid locally, check for `QGRID_PROJECT_NAME` or conf
|
|
|
88
88
|
|
|
89
89
|
On shutdown, it stops provider dispatchers and the token subscriber.
|
|
90
90
|
|
|
91
|
+
### Readiness during startup
|
|
92
|
+
|
|
93
|
+
HTTP listening opens before the dispatchers finish starting, so requests can arrive while a provider is still unavailable. On dev0 that window is 1–2 minutes: 25 OpenAI workers spawn at `SPAWN_INTERVAL_MS` apart, plus codex process init.
|
|
94
|
+
|
|
95
|
+
- `QgridDispatcher.startupState` tracks each provider as `starting` → `ready` or `failed`. A request for an unready provider throws `ServiceUnavailableException` (503, with a `Retry-After` header) while starting, and `InternalServerErrorException` (500) once initialization has failed — retrying helps in the first case and never in the second. Both used to be `QuotaError`, which told callers their tokens were exhausted and gave them nothing to act on.
|
|
96
|
+
- `GET /api/qgrid/health` reports `ready` plus per-provider state. A 200 alone does not mean requests will succeed; read `providers` for that.
|
|
97
|
+
- `handleServerError` trusts `statusCode` only on `instanceof SoException`. Sonamu's own `isSoException` is a `statusCode !== undefined` duck-type check, which would pass upstream fetch errors straight through and echo an upstream 401 as if the caller were unauthorized.
|
|
98
|
+
|
|
91
99
|
## OAuth callbacks
|
|
92
100
|
|
|
93
101
|
- Anthropic OAuth: loopback dashboards use `/callback` on the qgrid server (base derived per request from `Origin`, falling back to `X-Forwarded-Host`/`Host`). Remote dashboards cannot use a public callback (client redirect-URI allowlist), so `oauthStart` returns `mode: "code"` with the console callback and the user pastes the shown `code#state` into the dashboard (`oauthComplete`). No env configuration either way; direct non-HTTP calls fall back to localhost and `PORT`.
|
|
@@ -201,6 +201,17 @@ OpenAI token changes can create, update, deactivate, or kill persistent Codex wo
|
|
|
201
201
|
|
|
202
202
|
Anthropic token changes only affect the in-memory token pool because every request fresh-spawns Claude Code.
|
|
203
203
|
|
|
204
|
+
## Auth-Dead Detection And Token Registration
|
|
205
|
+
|
|
206
|
+
When a refresh fails in a way that only re-login can fix, qgrid removes the token from routing instead of letting every subsequent request 401.
|
|
207
|
+
|
|
208
|
+
- Detection lives at the existing refresh failure points, not a background scanner. OpenAI classifies on the error code (`refresh_token_expired`, `refresh_token_reused`, `refresh_token_invalidated`) regardless of HTTP status; Anthropic treats refresh 400/401 as auth-dead.
|
|
209
|
+
- Before deactivating, qgrid re-reads the token and confirms the refresh token used by the failed attempt still matches the stored one. Anthropic refresh tokens rotate, so a late retry carrying an already-rotated token is a normal race, not a death — those are logged and skipped. `QgridFrame.refreshToken` also dedups concurrent refreshes per token.
|
|
210
|
+
- Deactivation is one conditional `active=true → false` update. The process whose update affects a row is the only one that sends the Slack death notification, so a shared database produces exactly one alert per event.
|
|
211
|
+
- The provider's last active token is never auto-deactivated. A systemic failure (client_id revocation, OAuth contract change) would otherwise empty the pool; qgrid logs an error, keeps the token, and sends its own Slack alert. That path leaves no state change behind, so every following request would re-alert — a per-provider 30-minute cooldown holds it to one. The cooldown is in-process, so N instances can each emit once per window.
|
|
212
|
+
- The other notification is registration: every completed login sends one alert, whether the account is new or replacing an existing row through the account dedup. Manually toggling a token back to active is not a registration and sends nothing.
|
|
213
|
+
- Notifications are fail-open: unset `SLACK_BOT_TOKEN`/`SLACK_CHANNEL_ID` is a silent no-op, and send failures (including HTTP 200 with `ok:false`) only warn. Payloads carry an internal reason code such as `anthropic:400`, never the raw provider response body.
|
|
214
|
+
|
|
204
215
|
## Quota Threshold Semantics
|
|
205
216
|
|
|
206
217
|
`quota_threshold` is a routing gate, not a hard external-provider guarantee.
|