@cartanova/qgrid-cli 2.10.3 → 2.10.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/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +63 -140
- package/bundle/dist/application/qgrid/qgrid.frame.js +2 -5
- package/bundle/dist/application/setting/setting.model.js +2 -64
- package/bundle/dist/application/setting/setting.store.js +3 -4
- package/bundle/dist/application/setting/setting.types.js +2 -12
- package/bundle/dist/i18n/en.js +1 -3
- package/bundle/dist/i18n/ko.js +1 -3
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +0 -111
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +82 -178
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
- package/bundle/src/application/qgrid/qgrid.frame.ts +0 -4
- package/bundle/src/application/setting/setting.model.test.ts +1 -101
- package/bundle/src/application/setting/setting.model.ts +0 -73
- package/bundle/src/application/setting/setting.store.ts +2 -3
- package/bundle/src/application/setting/setting.types.ts +0 -12
- package/bundle/src/application/sonamu.generated.http +2 -7
- package/bundle/src/i18n/en.ts +0 -3
- package/bundle/src/i18n/ko.ts +0 -3
- package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
- package/bundle/web-dist/client/assets/{index-BIC1sQxw.js → index-reW8VvJF.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-zvdujZWn.js → logs-DDIt97D1.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-CraVV3MQ.js → monit-DkABv7PG.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-B3ZBoJ60.js → routes-BRmQ4M3-.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
- package/bundle/web-dist/client/assets/settings-BHvtycEZ.js +1 -0
- package/bundle/web-dist/client/assets/{show-ItTExdAh.js → show-qeTIzgDZ.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-BynKx8Vn.js → tokens-5Bep_G-w.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-8xw6on5P.js → use-oauth-login-flow-Cx-QO25M.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-D-Y8OySK.js → use-update-token-mutation-DQCwCYfR.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CagNGTBj.js} +3 -3
- package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-DSxs4Jnl.js} +3 -3
- package/bundle/web-dist/server/assets/{plus-CmvbWuDf.js → plus-Bvdj8Pwx.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BjR1Iaz8.js → react-dom-CyuShs9K.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-B8U9BL9-.js → refresh-cw-3QAiSWBd.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-C7lB2Q85.js → routes-i-sTqXMX.js} +4 -4
- package/bundle/web-dist/server/assets/{services.generated-BM4X3Ecj.js → services.generated-FPwsG9p3.js} +116 -131
- package/bundle/web-dist/server/assets/{settings-BFVuldch.js → settings-DeHNwoMv.js} +40 -251
- package/bundle/web-dist/server/assets/{show-Cswh_tqs.js → show-C4jk1Hrp.js} +2 -2
- package/bundle/web-dist/server/assets/{tokens-DiTUwUwr.js → tokens-odgS2-9q.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DYKnG4A2.js → use-oauth-login-flow-DTQKdYBO.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-DqGMopII.js → use-update-token-mutation-xvXbPrCI.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
- package/bundle/dist/utils/process-supervisor.js +0 -22
- package/bundle/dist/utils/server-restart.js +0 -61
- package/bundle/src/utils/process-supervisor.test.ts +0 -24
- package/bundle/src/utils/process-supervisor.ts +0 -17
- package/bundle/src/utils/server-restart.test.ts +0 -76
- package/bundle/src/utils/server-restart.ts +0 -68
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
- package/bundle/web-dist/client/assets/settings-DwbzT5jo.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as require_react, O as useQueryClient, X as __toESM, a as SettingService, k as require_jsx_runtime, o as TokenService } from "./services.generated-FPwsG9p3.js";
|
|
2
2
|
import { l as lucideRotateCcw } from "../entry-server.generated.js";
|
|
3
|
-
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-
|
|
4
|
-
import { t as lucidePlus } from "./plus-
|
|
3
|
+
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-xvXbPrCI.js";
|
|
4
|
+
import { t as lucidePlus } from "./plus-Bvdj8Pwx.js";
|
|
5
5
|
//#region ~icons/lucide/eye.jsx
|
|
6
6
|
var import_react = /* @__PURE__ */ __toESM(require_react());
|
|
7
7
|
var import_jsx_runtime = require_jsx_runtime();
|
|
@@ -62,7 +62,6 @@ var GROUP_LABELS = {
|
|
|
62
62
|
slack: "Slack 알림",
|
|
63
63
|
slackConnection: "Slack 연결"
|
|
64
64
|
};
|
|
65
|
-
var RESTART_READY_TIMEOUT_MS = 180 * 1e3;
|
|
66
65
|
function maskValue(value) {
|
|
67
66
|
if (!value) return "";
|
|
68
67
|
if (value.length <= 12) return "•".repeat(value.length);
|
|
@@ -357,212 +356,6 @@ function TriggerReminderButton() {
|
|
|
357
356
|
})]
|
|
358
357
|
});
|
|
359
358
|
}
|
|
360
|
-
/**
|
|
361
|
-
* 재시작은 스스로 종료하고 감독자(pm2)가 다시 띄우는 것에 맡긴다. 되돌릴 수 없고 1~2분간
|
|
362
|
-
* 요청이 실패하므로 확인 단계를 둔다 — 무엇이 일어나는지 모달이 먼저 설명한다.
|
|
363
|
-
*/
|
|
364
|
-
function RestartPanel({ supervisor }) {
|
|
365
|
-
const [confirming, setConfirming] = (0, import_react.useState)(false);
|
|
366
|
-
const [restartRequested, setRestartRequested] = (0, import_react.useState)(false);
|
|
367
|
-
const [restartStartedAt, setRestartStartedAt] = (0, import_react.useState)(null);
|
|
368
|
-
const [sawUnavailable, setSawUnavailable] = (0, import_react.useState)(false);
|
|
369
|
-
const [completing, setCompleting] = (0, import_react.useState)(false);
|
|
370
|
-
const [error, setError] = (0, import_react.useState)(null);
|
|
371
|
-
const completingRef = (0, import_react.useRef)(false);
|
|
372
|
-
const restartAttemptRef = (0, import_react.useRef)(0);
|
|
373
|
-
const queryClient = useQueryClient();
|
|
374
|
-
const mutation = SettingService.useRestartServerMutation();
|
|
375
|
-
const resetMutation = mutation.reset;
|
|
376
|
-
const healthQuery = useQuery({
|
|
377
|
-
...QgridService.healthQueryOptions(),
|
|
378
|
-
enabled: restartRequested && !completing,
|
|
379
|
-
refetchInterval: restartRequested && !completing ? 1e3 : false,
|
|
380
|
-
refetchIntervalInBackground: true,
|
|
381
|
-
retry: false
|
|
382
|
-
});
|
|
383
|
-
(0, import_react.useEffect)(() => {
|
|
384
|
-
if (!restartRequested || restartStartedAt === null || completing) return;
|
|
385
|
-
const remaining = Math.max(0, RESTART_READY_TIMEOUT_MS - (Date.now() - restartStartedAt));
|
|
386
|
-
const timeout = window.setTimeout(() => {
|
|
387
|
-
restartAttemptRef.current += 1;
|
|
388
|
-
resetMutation();
|
|
389
|
-
setConfirming(false);
|
|
390
|
-
setRestartRequested(false);
|
|
391
|
-
setRestartStartedAt(null);
|
|
392
|
-
setSawUnavailable(false);
|
|
393
|
-
setError("재시작 후 3분 안에 서버 준비를 확인하지 못했습니다. 상태를 확인한 뒤 다시 시도하세요.");
|
|
394
|
-
}, remaining);
|
|
395
|
-
return () => window.clearTimeout(timeout);
|
|
396
|
-
}, [
|
|
397
|
-
completing,
|
|
398
|
-
resetMutation,
|
|
399
|
-
restartRequested,
|
|
400
|
-
restartStartedAt
|
|
401
|
-
]);
|
|
402
|
-
(0, import_react.useEffect)(() => {
|
|
403
|
-
if (!restartRequested || restartStartedAt === null) return;
|
|
404
|
-
const currentAttemptError = healthQuery.isError && healthQuery.errorUpdatedAt >= restartStartedAt;
|
|
405
|
-
const currentAttemptData = healthQuery.isSuccess && healthQuery.dataUpdatedAt >= restartStartedAt;
|
|
406
|
-
if (currentAttemptError || currentAttemptData && !healthQuery.data.ready) {
|
|
407
|
-
setSawUnavailable(true);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (!sawUnavailable || !currentAttemptData || !healthQuery.data.ready || completingRef.current) return;
|
|
411
|
-
completingRef.current = true;
|
|
412
|
-
setCompleting(true);
|
|
413
|
-
queryClient.invalidateQueries({ queryKey: SettingService.getSettingListQueryOptions().queryKey }).then(() => {
|
|
414
|
-
restartAttemptRef.current += 1;
|
|
415
|
-
resetMutation();
|
|
416
|
-
setConfirming(false);
|
|
417
|
-
setRestartRequested(false);
|
|
418
|
-
setRestartStartedAt(null);
|
|
419
|
-
setSawUnavailable(false);
|
|
420
|
-
}).catch((cause) => {
|
|
421
|
-
restartAttemptRef.current += 1;
|
|
422
|
-
resetMutation();
|
|
423
|
-
setConfirming(false);
|
|
424
|
-
setRestartRequested(false);
|
|
425
|
-
setRestartStartedAt(null);
|
|
426
|
-
setSawUnavailable(false);
|
|
427
|
-
setError(`서버는 준비됐지만 설정을 새로고침하지 못했습니다: ${cause.message}`);
|
|
428
|
-
}).finally(() => {
|
|
429
|
-
completingRef.current = false;
|
|
430
|
-
setCompleting(false);
|
|
431
|
-
});
|
|
432
|
-
}, [
|
|
433
|
-
healthQuery.dataUpdatedAt,
|
|
434
|
-
healthQuery.data?.ready,
|
|
435
|
-
healthQuery.errorUpdatedAt,
|
|
436
|
-
healthQuery.isError,
|
|
437
|
-
healthQuery.isSuccess,
|
|
438
|
-
queryClient,
|
|
439
|
-
resetMutation,
|
|
440
|
-
restartStartedAt,
|
|
441
|
-
restartRequested,
|
|
442
|
-
sawUnavailable
|
|
443
|
-
]);
|
|
444
|
-
const restart = async () => {
|
|
445
|
-
const attempt = restartAttemptRef.current + 1;
|
|
446
|
-
restartAttemptRef.current = attempt;
|
|
447
|
-
setError(null);
|
|
448
|
-
setSawUnavailable(false);
|
|
449
|
-
setCompleting(false);
|
|
450
|
-
completingRef.current = false;
|
|
451
|
-
setRestartRequested(true);
|
|
452
|
-
setRestartStartedAt(Date.now());
|
|
453
|
-
try {
|
|
454
|
-
await mutation.mutateAsync();
|
|
455
|
-
} catch (e) {
|
|
456
|
-
if (restartAttemptRef.current !== attempt) return;
|
|
457
|
-
if (isSonamuError(e) ? e.code >= 400 && e.code < 500 : isAxiosError(e) && e.response !== void 0 && e.response.status >= 400 && e.response.status < 500) {
|
|
458
|
-
restartAttemptRef.current += 1;
|
|
459
|
-
resetMutation();
|
|
460
|
-
setConfirming(false);
|
|
461
|
-
setRestartRequested(false);
|
|
462
|
-
setRestartStartedAt(null);
|
|
463
|
-
setError(e.message || "재시작 요청이 거부되었습니다.");
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
const closeConfirmation = () => {
|
|
468
|
-
if (restartRequested) return;
|
|
469
|
-
setConfirming(false);
|
|
470
|
-
setError(null);
|
|
471
|
-
};
|
|
472
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
473
|
-
className: "panel overflow-hidden",
|
|
474
|
-
children: [
|
|
475
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
476
|
-
className: "panel-header px-4 py-2.5 sm:px-5",
|
|
477
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
478
|
-
className: "text-[13px] font-medium text-sand-700",
|
|
479
|
-
children: "서버 제어"
|
|
480
|
-
})
|
|
481
|
-
}),
|
|
482
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
483
|
-
className: "px-4 py-3 sm:px-5 flex items-start justify-between gap-4",
|
|
484
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
485
|
-
className: "min-w-0",
|
|
486
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
487
|
-
className: "text-[13px] text-sand-800",
|
|
488
|
-
children: "qgrid 재시작"
|
|
489
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
490
|
-
className: "mt-0.5 text-[11px] text-sand-500",
|
|
491
|
-
children: supervisor ? "재시작 필요 설정을 반영하고 qgrid·Codex·Claude CLI를 확인합니다" : "로컬 실행 환경에서는 재시작할 수 없습니다"
|
|
492
|
-
})]
|
|
493
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
494
|
-
type: "button",
|
|
495
|
-
disabled: !supervisor || restartRequested || mutation.isPending,
|
|
496
|
-
onClick: () => setConfirming(true),
|
|
497
|
-
className: "shrink-0 px-2.5 py-1.5 text-[11px] font-medium rounded-lg border border-red-200 text-red-600 hover:bg-red-50 disabled:opacity-40 disabled:hover:bg-transparent transition-colors duration-150",
|
|
498
|
-
children: "재시작"
|
|
499
|
-
})]
|
|
500
|
-
}),
|
|
501
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
502
|
-
className: "px-4 pb-3 sm:px-5 text-[11px] text-red-500",
|
|
503
|
-
children: error
|
|
504
|
-
}),
|
|
505
|
-
confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
506
|
-
className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
|
|
507
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
508
|
-
className: "absolute inset-0 bg-sand-900/8 backdrop-blur-sm",
|
|
509
|
-
onClick: closeConfirmation,
|
|
510
|
-
onKeyDown: () => {}
|
|
511
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
512
|
-
className: "relative panel shadow-xl w-full max-w-sm mx-4",
|
|
513
|
-
children: [
|
|
514
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
515
|
-
className: "px-5 py-4 border-b border-sand-100/60",
|
|
516
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
|
|
517
|
-
className: "text-base font-medium text-sand-900",
|
|
518
|
-
children: restartRequested ? mutation.isPending ? "재시작 요청 중" : "서버 재연결 중" : "qgrid 재시작"
|
|
519
|
-
})
|
|
520
|
-
}),
|
|
521
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
522
|
-
className: "px-5 py-4 space-y-2",
|
|
523
|
-
children: [
|
|
524
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
525
|
-
className: "text-[13px] text-sand-700",
|
|
526
|
-
children: [
|
|
527
|
-
"워커가 다시 뜰 때까지 ",
|
|
528
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", {
|
|
529
|
-
className: "text-sand-900",
|
|
530
|
-
children: "1~2분간 요청이 실패"
|
|
531
|
-
}),
|
|
532
|
-
"합니다."
|
|
533
|
-
]
|
|
534
|
-
}),
|
|
535
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
536
|
-
className: "text-[12px] text-sand-500",
|
|
537
|
-
children: "서버를 종료하면 모니터링 도구가 다시 띄웁니다. 이때 재시작 필요 설정을 반영하고, qgrid와 Codex CLI·Claude Code의 최신 버전을 확인합니다."
|
|
538
|
-
}),
|
|
539
|
-
restartRequested && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
540
|
-
className: "text-[12px] font-medium text-sienna-600",
|
|
541
|
-
children: mutation.isPending ? "종료 요청을 처리하고 있습니다…" : sawUnavailable ? "새 서버가 준비될 때까지 연결을 확인하고 있습니다…" : "서버 종료를 기다리고 있습니다…"
|
|
542
|
-
})
|
|
543
|
-
]
|
|
544
|
-
}),
|
|
545
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
546
|
-
className: "px-5 py-3 border-t border-sand-100/60 flex justify-end gap-2",
|
|
547
|
-
children: [!restartRequested && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
548
|
-
type: "button",
|
|
549
|
-
onClick: closeConfirmation,
|
|
550
|
-
className: "px-3 py-1.5 text-[12px] rounded-lg text-sand-600 hover:bg-sand-100 transition-colors duration-150",
|
|
551
|
-
children: "취소"
|
|
552
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
553
|
-
type: "button",
|
|
554
|
-
disabled: restartRequested || mutation.isPending,
|
|
555
|
-
onClick: () => void restart(),
|
|
556
|
-
className: "px-3 py-1.5 text-[12px] font-medium rounded-lg bg-red-500 text-white hover:bg-red-600 disabled:opacity-50 transition-colors duration-150",
|
|
557
|
-
children: restartRequested ? mutation.isPending ? "요청 중" : "재연결 중" : "재시작"
|
|
558
|
-
})]
|
|
559
|
-
})
|
|
560
|
-
]
|
|
561
|
-
})]
|
|
562
|
-
})
|
|
563
|
-
]
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
359
|
function SettingsPanel() {
|
|
567
360
|
const settingsQuery = SettingService.useSettingList();
|
|
568
361
|
const { data, isLoading } = settingsQuery;
|
|
@@ -611,53 +404,49 @@ function SettingsPanel() {
|
|
|
611
404
|
const groups = [...new Set(settings.map((s) => s.group))];
|
|
612
405
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
613
406
|
className: "space-y-4",
|
|
614
|
-
children: [
|
|
615
|
-
|
|
407
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
408
|
+
className: "panel overflow-hidden",
|
|
409
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
410
|
+
className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
|
|
411
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
412
|
+
className: "text-[13px] font-medium text-sand-700",
|
|
413
|
+
children: "환경"
|
|
414
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
415
|
+
className: "text-[11px] text-sand-400",
|
|
416
|
+
children: "기동 시점 값"
|
|
417
|
+
})]
|
|
418
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
|
|
419
|
+
className: "px-4 py-3 sm:px-5 grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-1.5",
|
|
420
|
+
children: runtime.map((info) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
421
|
+
className: "flex items-baseline justify-between gap-3 min-w-0",
|
|
422
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
|
|
423
|
+
className: "text-[11px] text-sand-500 shrink-0",
|
|
424
|
+
children: info.label
|
|
425
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
|
|
426
|
+
className: "text-[12px] font-mono text-sand-700 truncate",
|
|
427
|
+
children: info.value
|
|
428
|
+
})]
|
|
429
|
+
}, info.label))
|
|
430
|
+
})]
|
|
431
|
+
}), groups.map((group) => {
|
|
432
|
+
const items = settings.filter((s) => s.group === group);
|
|
433
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
616
434
|
className: "panel overflow-hidden",
|
|
617
435
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
618
436
|
className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
|
|
619
437
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
620
438
|
className: "text-[13px] font-medium text-sand-700",
|
|
621
|
-
children:
|
|
622
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
className: "flex items-baseline justify-between gap-3 min-w-0",
|
|
630
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
|
|
631
|
-
className: "text-[11px] text-sand-500 shrink-0",
|
|
632
|
-
children: info.label
|
|
633
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
|
|
634
|
-
className: "text-[12px] font-mono text-sand-700 truncate",
|
|
635
|
-
children: info.value
|
|
636
|
-
})]
|
|
637
|
-
}, info.label))
|
|
439
|
+
children: GROUP_LABELS[group] ?? group
|
|
440
|
+
}), group === "slack" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriggerReminderButton, {})]
|
|
441
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
442
|
+
className: "divide-y divide-sand-100/80",
|
|
443
|
+
children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingRow, {
|
|
444
|
+
item,
|
|
445
|
+
disabled: item.key === "qgrid.tokenWindowKeepaliveEnabled" && !data?.keepaliveRunnerEnabled
|
|
446
|
+
}), item.key === "qgrid.tokenWindowKeepaliveEnabled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeepaliveTokenSettings, { runnerEnabled: data?.keepaliveRunnerEnabled ?? false })] }, item.key))
|
|
638
447
|
})]
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
const items = settings.filter((s) => s.group === group);
|
|
642
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
643
|
-
className: "panel overflow-hidden",
|
|
644
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
645
|
-
className: "panel-header flex items-center justify-between px-4 py-2.5 sm:px-5",
|
|
646
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
647
|
-
className: "text-[13px] font-medium text-sand-700",
|
|
648
|
-
children: GROUP_LABELS[group] ?? group
|
|
649
|
-
}), group === "slack" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriggerReminderButton, {})]
|
|
650
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
651
|
-
className: "divide-y divide-sand-100/80",
|
|
652
|
-
children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingRow, {
|
|
653
|
-
item,
|
|
654
|
-
disabled: item.key === "qgrid.tokenWindowKeepaliveEnabled" && !data?.keepaliveRunnerEnabled
|
|
655
|
-
}), item.key === "qgrid.tokenWindowKeepaliveEnabled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeepaliveTokenSettings, { runnerEnabled: data?.keepaliveRunnerEnabled ?? false })] }, item.key))
|
|
656
|
-
})]
|
|
657
|
-
}, group);
|
|
658
|
-
}),
|
|
659
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RestartPanel, { supervisor: data?.supervisor ?? null })
|
|
660
|
-
]
|
|
448
|
+
}, group);
|
|
449
|
+
})]
|
|
661
450
|
});
|
|
662
451
|
}
|
|
663
452
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as Link } from "./lazyRouteComponent-
|
|
1
|
+
import { A as require_react, E as useQuery, X as __toESM, i as RequestLogStepService, k as require_jsx_runtime, l as isSonamuError, q as __commonJSMin, r as RequestLogService } from "./services.generated-FPwsG9p3.js";
|
|
2
|
+
import { a as Link } from "./lazyRouteComponent-CUR-kWzI.js";
|
|
3
3
|
import { c as lucideX, d as Markdown, t as Route, u as remarkGfm } from "../entry-server.generated.js";
|
|
4
4
|
import { n as formatMicroUsd, t as cacheHitRate } from "./cost-CSH_t17I.js";
|
|
5
5
|
import { t as requestModelDisplay } from "./request-model-CnCeaNLE.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as require_react, O as useQueryClient, X as __toESM, k as require_jsx_runtime, n as QgridService, o as TokenService } from "./services.generated-FPwsG9p3.js";
|
|
2
2
|
import { f as lucideKeyRound, o as Q } from "../entry-server.generated.js";
|
|
3
|
-
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-
|
|
4
|
-
import { n as OAuthCodeEntry, t as useOAuthLoginFlow } from "./use-oauth-login-flow-
|
|
5
|
-
import { t as lucidePlus } from "./plus-
|
|
3
|
+
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-xvXbPrCI.js";
|
|
4
|
+
import { n as OAuthCodeEntry, t as useOAuthLoginFlow } from "./use-oauth-login-flow-DTQKdYBO.js";
|
|
5
|
+
import { t as lucidePlus } from "./plus-Bvdj8Pwx.js";
|
|
6
6
|
//#region src/components/qgrid/AddTokenModal.tsx
|
|
7
7
|
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
8
8
|
var import_jsx_runtime = require_jsx_runtime();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as require_react, O as useQueryClient, X as __toESM, k as require_jsx_runtime, n as QgridService, o as TokenService } from "./services.generated-FPwsG9p3.js";
|
|
2
2
|
import { o as Q } from "../entry-server.generated.js";
|
|
3
3
|
//#region src/components/qgrid/OAuthCodeEntry.tsx
|
|
4
4
|
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { T as useMutation, n as QgridService } from "./services.generated-FPwsG9p3.js";
|
|
2
2
|
//#region src/services/token/use-update-token-mutation.ts
|
|
3
3
|
function useUpdateTokenMutation() {
|
|
4
4
|
return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight, keepaliveEnabled }) => QgridService.updateToken(id, name, quotaThreshold, weight, keepaliveEnabled) });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
3
|
-
import { n as require_shim, t as require_react_dom } from "./assets/react-dom-
|
|
1
|
+
import { A as require_react, B as matchQuery, C as union, D as QueryClientProvider, F as notifyManager, G as focusManager, H as partialMatchKey, I as functionalUpdate, J as __exportAll, K as Subscribable, L as hashKey, M as infiniteQueryBehavior, N as Query, P as onlineManager, R as hashQueryKeyByOptions, S as string$2, U as resolveStaleTime, V as noop$1, W as skipToken, X as __toESM, Y as __require, _ as custom, b as number$1, c as fetch$1, d as SD, f as SUPPORTED_LOCALES, g as boolean$1, h as array, j as Mutation, k as require_jsx_runtime, m as _enum, n as QgridService, o as TokenService, p as setLocale, q as __commonJSMin, s as SonamuQueryMode, u as zArrayable, v as date, w as unknown, x as object, y as int, z as matchMutation } from "./assets/services.generated-FPwsG9p3.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-CUR-kWzI.js";
|
|
3
|
+
import { n as require_shim, t as require_react_dom } from "./assets/react-dom-CyuShs9K.js";
|
|
4
4
|
import minpath from "node:path";
|
|
5
5
|
import minproc from "node:process";
|
|
6
6
|
import { fileURLToPath as urlToPath } from "node:url";
|
|
@@ -46874,7 +46874,7 @@ function ChatWidget() {
|
|
|
46874
46874
|
}
|
|
46875
46875
|
//#endregion
|
|
46876
46876
|
//#region src/components/Sidebar.tsx
|
|
46877
|
-
var qgridVersion = "2.10.
|
|
46877
|
+
var qgridVersion = "2.10.4";
|
|
46878
46878
|
function Sidebar({ className }) {
|
|
46879
46879
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
46880
46880
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
|
|
@@ -68752,15 +68752,15 @@ function RootComponent() {
|
|
|
68752
68752
|
}
|
|
68753
68753
|
//#endregion
|
|
68754
68754
|
//#region src/routes/tokens.tsx
|
|
68755
|
-
var $$splitComponentImporter$5 = () => import("./assets/tokens-
|
|
68755
|
+
var $$splitComponentImporter$5 = () => import("./assets/tokens-odgS2-9q.js");
|
|
68756
68756
|
var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
|
|
68757
68757
|
//#endregion
|
|
68758
68758
|
//#region src/routes/settings.tsx
|
|
68759
|
-
var $$splitComponentImporter$4 = () => import("./assets/settings-
|
|
68759
|
+
var $$splitComponentImporter$4 = () => import("./assets/settings-DeHNwoMv.js");
|
|
68760
68760
|
var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
|
|
68761
68761
|
//#endregion
|
|
68762
68762
|
//#region src/routes/monit.tsx
|
|
68763
|
-
var $$splitComponentImporter$3 = () => import("./assets/monit-
|
|
68763
|
+
var $$splitComponentImporter$3 = () => import("./assets/monit-DSxs4Jnl.js");
|
|
68764
68764
|
var monitSearchSchema = object({ level: _enum([
|
|
68765
68765
|
"all",
|
|
68766
68766
|
"warn",
|
|
@@ -69137,7 +69137,7 @@ object({
|
|
|
69137
69137
|
_enum(["A"]);
|
|
69138
69138
|
//#endregion
|
|
69139
69139
|
//#region src/routes/logs.tsx
|
|
69140
|
-
var $$splitComponentImporter$2 = () => import("./assets/logs-
|
|
69140
|
+
var $$splitComponentImporter$2 = () => import("./assets/logs-CagNGTBj.js");
|
|
69141
69141
|
var logsSearchSchema = object({
|
|
69142
69142
|
token: string$2().optional(),
|
|
69143
69143
|
project: string$2().optional(),
|
|
@@ -69151,11 +69151,11 @@ var Route$2 = createFileRoute("/logs")({
|
|
|
69151
69151
|
});
|
|
69152
69152
|
//#endregion
|
|
69153
69153
|
//#region src/routes/index.tsx
|
|
69154
|
-
var $$splitComponentImporter$1 = () => import("./assets/routes-
|
|
69154
|
+
var $$splitComponentImporter$1 = () => import("./assets/routes-i-sTqXMX.js");
|
|
69155
69155
|
var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
|
|
69156
69156
|
//#endregion
|
|
69157
69157
|
//#region src/routes/requests/show.tsx
|
|
69158
|
-
var $$splitComponentImporter = () => import("./assets/show-
|
|
69158
|
+
var $$splitComponentImporter = () => import("./assets/show-C4jk1Hrp.js");
|
|
69159
69159
|
var showSearchSchema = object({ id: number$1() });
|
|
69160
69160
|
var Route = createFileRoute("/requests/show")({
|
|
69161
69161
|
validateSearch: showSearchSchema,
|
package/package.json
CHANGED
|
@@ -92,17 +92,20 @@ Some env values are editable at runtime through the dashboard's Settings page, b
|
|
|
92
92
|
- Editable: Slack delivery control (master switch, reminder on/off, reminder interval, quiet-hours window, weekend behaviour), and the Slack channel/user-map/bot-token. `SETTING_DEFS` in `setting.constant.ts` is the single definition — key, env name, type, bounds, and whether the change applies immediately or needs a restart. The `settings` table itself (`setting.entity.json`) only stores `(key, value)` strings; everything the UI needs to render and validate a key lives in that constant, not in the schema.
|
|
93
93
|
- Not editable: anything needed before the server can read its own database (`QGRID_DB_*`, `HOST`, `PORT`, `NODE_ENV`). These are exposed read-only on the same page so an operator can confirm which environment they are looking at; the DB password is masked.
|
|
94
94
|
|
|
95
|
-
###
|
|
95
|
+
### Process lifecycle
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
qgrid does not detect a process supervisor or expose a restart API or dashboard
|
|
98
|
+
control. Restarting, replacing, and recovering the process belong to the
|
|
99
|
+
operator's deployment tooling. Settings marked `restart` still take effect on
|
|
100
|
+
the next process start; the label does not imply an in-app restart action.
|
|
101
|
+
|
|
102
|
+
There is no SIGTERM drain handler. Replacing the process during traffic can
|
|
103
|
+
interrupt active responses, so deployment tooling must account for startup
|
|
104
|
+
readiness and interruption. Ordinary request success/error/abort accounting and
|
|
105
|
+
stale tool-wait cleanup remain independent of the deployment mechanism.
|
|
106
|
+
|
|
107
|
+
### Live notification settings
|
|
98
108
|
|
|
99
|
-
- `detectSupervisor` (`utils/process-supervisor.ts`) accepts only `pm_id` (pm2). `INVOCATION_ID` proves systemd launched a process but does not prove the unit has a restart policy, so it is not sufficient. Without pm2, `restartServer` returns 400 and the button is disabled: exiting with nothing to relaunch the process is a shutdown, not a restart. Presence is the signal, not the value — pm2 sets `pm_id=0` for the first process.
|
|
100
|
-
- Browser requests must be same-host: when `Origin` is present and its host differs from `X-Forwarded-Host`/`Host`, `restartServer` returns 403. Origin-less operational clients such as `curl` remain supported. This is CSRF protection, not a substitute for authenticating the management API at Caddy.
|
|
101
|
-
- The first restart request latches `restartPending`, blocks new native query/stream dispatch with 503, and marks only process-local active native request-log IDs as `error: server restarted`. Tool-result-waiting runs and externally owned logger runs are untouched. This is state reconciliation, not request draining.
|
|
102
|
-
- Exit is process-wide and one-shot. It runs after the restart response emits `finish`; a response `close` or a five-second fallback covers disconnects. Concurrent restart calls do not schedule multiple exits.
|
|
103
|
-
- The supervisor relaunches from `ecosystem.config.cjs`, not from the dying process's argv. That file carries no `--skip-update`, so each restart also runs the CLI self-update — a restart is a deploy. The confirmation dialog says so; dropping the update means adding `--skip-update` there.
|
|
104
|
-
- The relaunch reuses the environment pm2 captured when the app was first started. Neither the dashboard restart nor `pm2 restart qgrid` re-reads `ecosystem.config.cjs`, so an env edit in that file (for example `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED`) stays invisible until `pm2 restart ecosystem.config.cjs --update-env` (or `pm2 delete` + `pm2 start`) followed by `pm2 save`. Verify with `GET /api/setting/listSettings`, which reports the env the process actually sees. dev0 ran for a week with the keepalive env in the file but not in the process because of this (2026-09-02).
|
|
105
|
-
- In-flight provider responses are not drained. There is no SIGTERM handler, so a restart during traffic cuts active responses, and the OpenAI pool needs 1–2 minutes before requests succeed again (`QgridDispatcher.startupState` answers 503 in that window). The dashboard stays locked after confirmation, observes an unavailable/not-ready health state, then waits for a later `ready: true` before refreshing settings.
|
|
106
109
|
- Slack channel, bot-token, and user-map changes are read on the next notification. Changing the expiry-reminder interval or toggling the reminder replaces the current timer immediately without sending a notification merely because the setting changed; process boot still performs the intended immediate reminder run.
|
|
107
110
|
- `slack.enabled` is the holiday switch: an operator turns it off for periods no rule can express. It suppresses ordinary notifications but not `urgent` ones, so a provider losing its last token still reports during a long break. `slack.remindersEnabled` is narrower — it stops only the repeat timer and preserves the chosen interval so re-enabling restores it.
|
|
108
111
|
- Quiet hours are `slack.quietFromHour`/`slack.quietUntilHour` (0–23, Asia/Seoul) plus `slack.notifyOnWeekends`. From > until reads as an overnight window (20→8); from < until reads as a same-day window, which night-shift teams may want. Equal values mean no quiet window at all. Out-of-range values fall back to 20/8 at runtime, since env can bypass the stored-value validation.
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/utils/process-supervisor.ts
|
|
2
|
-
/**
|
|
3
|
-
* 프로세스 모니터링 도구 감지.
|
|
4
|
-
*
|
|
5
|
-
* 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
|
|
6
|
-
* 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
|
|
7
|
-
* 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
|
|
8
|
-
*
|
|
9
|
-
* 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
|
|
10
|
-
* unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
|
|
11
|
-
*
|
|
12
|
-
* 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
|
|
13
|
-
* 판별하면 놓친다.
|
|
14
|
-
*/
|
|
15
|
-
function detectSupervisor(env = process.env) {
|
|
16
|
-
if (env.pm_id !== void 0) return "pm2";
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
export { detectSupervisor };
|
|
21
|
-
|
|
22
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvY2Vzcy1zdXBlcnZpc29yLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9wcm9jZXNzLXN1cGVydmlzb3IudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDtlITroZzshLjsiqQg66qo64uI7YSw66eBIOuPhOq1rCDqsJDsp4AuXG4gKlxuICog7ZmU66m07J2YIFwi7J6s7Iuc7J6RXCLsnYAg7Iqk7Iqk66Gc66W8IOyiheujjO2VmOuKlCDqsoPsnbTri6Qg4oCUIHBtMiDqsJnsnYAg64+E6rWs6rCAIOyjveydgCDtlITroZzshLjsiqTrpbwg64uk7IucXG4gKiDrnYTsm4zso7zripQg6rKD7J20IOqzpyDsnqzsi5zsnpHsnbTri6QuIOq3uOuemOyEnCDqt7jrn7Ag64+E6rWsIOyXhuydtCDsooXro4ztlZjrqbQg6re464OlIOyEnOuyhOqwgCDrgrTroKTqsITri6QuXG4gKiDroZzsu6wg6rCc67Cc7JeQ7IScIOuyhO2KvOydhCDriITrpbTrqbQg7JWE66y064+EIOyCtOugpOyjvOyngCDslYrsnLzrr4DroZwsIOyiheujjCDsoITsl5Ag67CY65Oc7IucIO2ZleyduO2VnOuLpC5cbiAqXG4gKiDtmITsnqwg67Cw7Y+sIOqzhOyVveydgCBwbTIg66eMIOyerOyLnOyekeydhCDrs7TsnqXtlZzri6QuIHN5c3RlbWQg7J2YIGBJTlZPQ0FUSU9OX0lEYCDripQg7ZW064u5XG4gKiB1bml0IOydtCBgUmVzdGFydD1gIOulvCDsk7Tri6TripQg65y77J20IOyVhOuLiOuvgOuhnCDslYjsoITtlZwg6rCQ64+F7J6QIOyLoO2YuOuhnCDsgqzsmqntlaAg7IiYIOyXhuuLpC5cbiAqXG4gKiDqsJLsnbQg7JWE64uI6528IOyhtOyerCDsl6zrtoDqsIAg7Iug7Zi464ukLiBwbTIg64qUIOyyqyDtlITroZzshLjsiqTsl5AgYHBtX2lkPTBgIOydhCDso7zrr4DroZwgdHJ1dGh5IOqygOyCrOuhnFxuICog7YyQ67OE7ZWY66m0IOuGk+y5nOuLpC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGRldGVjdFN1cGVydmlzb3IoZW52OiBOb2RlSlMuUHJvY2Vzc0VudiA9IHByb2Nlc3MuZW52KTogXCJwbTJcIiB8IG51bGwge1xuICBpZiAoZW52LnBtX2lkICE9PSB1bmRlZmluZWQpIHJldHVybiBcInBtMlwiO1xuICByZXR1cm4gbnVsbDtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7QUFhQSxTQUFnQixpQkFBaUIsTUFBeUIsUUFBUSxLQUFtQjtDQUNuRixJQUFJLElBQUksVUFBVSxRQUFXLE9BQU87Q0FDcEMsT0FBTztBQUNUIn0=
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
//#region src/utils/server-restart.ts
|
|
2
|
-
const RESTART_EXIT_FALLBACK_MS = 5e3;
|
|
3
|
-
let restartPending = false;
|
|
4
|
-
let exitTriggered = false;
|
|
5
|
-
let exitArmed = false;
|
|
6
|
-
let fallbackTimer;
|
|
7
|
-
let armedReplyRaw;
|
|
8
|
-
let armedExit;
|
|
9
|
-
/** 재시작 요청 뒤에는 새 native provider 실행을 받지 않는다. */
|
|
10
|
-
function isRestartPending() {
|
|
11
|
-
return restartPending;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* 프로세스 전체에서 최초 요청만 재시작을 시작한다. 종료 신호는 active run 정합성 처리가
|
|
15
|
-
* 끝난 뒤 별도로 arm 한다. 그래야 이미 닫힌 연결이나 fallback 타이머가 DB 정리보다 앞서
|
|
16
|
-
* 프로세스를 끝내지 않는다.
|
|
17
|
-
*/
|
|
18
|
-
function beginServerRestart() {
|
|
19
|
-
if (restartPending) return false;
|
|
20
|
-
restartPending = true;
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
function clearExitArm() {
|
|
24
|
-
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
25
|
-
fallbackTimer = void 0;
|
|
26
|
-
if (armedReplyRaw && armedExit) {
|
|
27
|
-
armedReplyRaw.removeListener("finish", armedExit);
|
|
28
|
-
armedReplyRaw.removeListener("close", armedExit);
|
|
29
|
-
}
|
|
30
|
-
armedReplyRaw = void 0;
|
|
31
|
-
armedExit = void 0;
|
|
32
|
-
}
|
|
33
|
-
/** 정합성 처리가 끝난 뒤 HTTP 응답 flush/연결 종료/유한 fallback 중 먼저 온 신호로 종료한다. */
|
|
34
|
-
function armServerRestartExit(replyRaw) {
|
|
35
|
-
if (!restartPending || exitArmed) return;
|
|
36
|
-
exitArmed = true;
|
|
37
|
-
const exitOnce = () => {
|
|
38
|
-
if (exitTriggered) return;
|
|
39
|
-
exitTriggered = true;
|
|
40
|
-
clearExitArm();
|
|
41
|
-
process.exit(0);
|
|
42
|
-
};
|
|
43
|
-
armedReplyRaw = replyRaw;
|
|
44
|
-
armedExit = exitOnce;
|
|
45
|
-
replyRaw.once("finish", exitOnce);
|
|
46
|
-
replyRaw.once("close", exitOnce);
|
|
47
|
-
fallbackTimer = setTimeout(exitOnce, RESTART_EXIT_FALLBACK_MS);
|
|
48
|
-
fallbackTimer.unref();
|
|
49
|
-
if (replyRaw.destroyed && !replyRaw.writableEnded) exitOnce();
|
|
50
|
-
}
|
|
51
|
-
/** 모듈 전역 상태를 격리해야 하는 단위 테스트 전용. */
|
|
52
|
-
function resetServerRestartForTests() {
|
|
53
|
-
clearExitArm();
|
|
54
|
-
restartPending = false;
|
|
55
|
-
exitTriggered = false;
|
|
56
|
-
exitArmed = false;
|
|
57
|
-
}
|
|
58
|
-
//#endregion
|
|
59
|
-
export { armServerRestartExit, beginServerRestart, isRestartPending, resetServerRestartForTests };
|
|
60
|
-
|
|
61
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmVyLXJlc3RhcnQuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3NlcnZlci1yZXN0YXJ0LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHR5cGUgU2VydmVyUmVzcG9uc2UgfSBmcm9tIFwibm9kZTpodHRwXCI7XG5cbmNvbnN0IFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyA9IDVfMDAwO1xuXG5sZXQgcmVzdGFydFBlbmRpbmcgPSBmYWxzZTtcbmxldCBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG5sZXQgZXhpdEFybWVkID0gZmFsc2U7XG5sZXQgZmFsbGJhY2tUaW1lcjogTm9kZUpTLlRpbWVvdXQgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRSZXBseVJhdzogUmVwbHlSYXcgfCB1bmRlZmluZWQ7XG5sZXQgYXJtZWRFeGl0OiAoKCkgPT4gdm9pZCkgfCB1bmRlZmluZWQ7XG5cbnR5cGUgUmVwbHlSYXcgPSBQaWNrPFNlcnZlclJlc3BvbnNlLCBcIm9uY2VcIiB8IFwicmVtb3ZlTGlzdGVuZXJcIiB8IFwiZGVzdHJveWVkXCIgfCBcIndyaXRhYmxlRW5kZWRcIj47XG5cbi8qKiDsnqzsi5zsnpEg7JqU7LKtIOuSpOyXkOuKlCDsg4ggbmF0aXZlIHByb3ZpZGVyIOyLpO2WieydhCDrsJvsp4Ag7JWK64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUmVzdGFydFBlbmRpbmcoKTogYm9vbGVhbiB7XG4gIHJldHVybiByZXN0YXJ0UGVuZGluZztcbn1cblxuLyoqXG4gKiDtlITroZzshLjsiqQg7KCE7LK07JeQ7IScIOy1nOy0iCDsmpTssq3rp4wg7J6s7Iuc7J6R7J2EIOyLnOyeke2VnOuLpC4g7KKF66OMIOyLoO2YuOuKlCBhY3RpdmUgcnVuIOygle2VqeyEsSDsspjrpqzqsIBcbiAqIOuBneuCnCDrkqQg67OE64+E66GcIGFybSDtlZzri6QuIOq3uOuemOyVvCDsnbTrr7gg64ur7Z6MIOyXsOqysOydtOuCmCBmYWxsYmFjayDtg4DsnbTrqLjqsIAgREIg7KCV66as67O064ukIOyVnuyEnFxuICog7ZSE66Gc7IS47Iqk66W8IOuBneuCtOyngCDslYrripTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBiZWdpblNlcnZlclJlc3RhcnQoKTogYm9vbGVhbiB7XG4gIGlmIChyZXN0YXJ0UGVuZGluZykgcmV0dXJuIGZhbHNlO1xuICByZXN0YXJ0UGVuZGluZyA9IHRydWU7XG4gIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBjbGVhckV4aXRBcm0oKTogdm9pZCB7XG4gIGlmIChmYWxsYmFja1RpbWVyKSBjbGVhclRpbWVvdXQoZmFsbGJhY2tUaW1lcik7XG4gIGZhbGxiYWNrVGltZXIgPSB1bmRlZmluZWQ7XG4gIGlmIChhcm1lZFJlcGx5UmF3ICYmIGFybWVkRXhpdCkge1xuICAgIGFybWVkUmVwbHlSYXcucmVtb3ZlTGlzdGVuZXIoXCJmaW5pc2hcIiwgYXJtZWRFeGl0KTtcbiAgICBhcm1lZFJlcGx5UmF3LnJlbW92ZUxpc3RlbmVyKFwiY2xvc2VcIiwgYXJtZWRFeGl0KTtcbiAgfVxuICBhcm1lZFJlcGx5UmF3ID0gdW5kZWZpbmVkO1xuICBhcm1lZEV4aXQgPSB1bmRlZmluZWQ7XG59XG5cbi8qKiDsoJXtlanshLEg7LKY66as6rCAIOuBneuCnCDrkqQgSFRUUCDsnZHri7UgZmx1c2gv7Jew6rKwIOyiheujjC/snKDtlZwgZmFsbGJhY2sg7KSRIOuovOyggCDsmKgg7Iug7Zi466GcIOyiheujjO2VnOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhcm1TZXJ2ZXJSZXN0YXJ0RXhpdChyZXBseVJhdzogUmVwbHlSYXcpOiB2b2lkIHtcbiAgaWYgKCFyZXN0YXJ0UGVuZGluZyB8fCBleGl0QXJtZWQpIHJldHVybjtcbiAgZXhpdEFybWVkID0gdHJ1ZTtcblxuICBjb25zdCBleGl0T25jZSA9ICgpID0+IHtcbiAgICBpZiAoZXhpdFRyaWdnZXJlZCkgcmV0dXJuO1xuICAgIGV4aXRUcmlnZ2VyZWQgPSB0cnVlO1xuICAgIGNsZWFyRXhpdEFybSgpO1xuICAgIHByb2Nlc3MuZXhpdCgwKTtcbiAgfTtcblxuICBhcm1lZFJlcGx5UmF3ID0gcmVwbHlSYXc7XG4gIGFybWVkRXhpdCA9IGV4aXRPbmNlO1xuICByZXBseVJhdy5vbmNlKFwiZmluaXNoXCIsIGV4aXRPbmNlKTtcbiAgcmVwbHlSYXcub25jZShcImNsb3NlXCIsIGV4aXRPbmNlKTtcbiAgZmFsbGJhY2tUaW1lciA9IHNldFRpbWVvdXQoZXhpdE9uY2UsIFJFU1RBUlRfRVhJVF9GQUxMQkFDS19NUyk7XG4gIGZhbGxiYWNrVGltZXIudW5yZWYoKTtcbiAgaWYgKHJlcGx5UmF3LmRlc3Ryb3llZCAmJiAhcmVwbHlSYXcud3JpdGFibGVFbmRlZCkgZXhpdE9uY2UoKTtcbn1cblxuLyoqIOuqqOuTiCDsoITsl60g7IOB7YOc66W8IOqyqeumrO2VtOyVvCDtlZjripQg64uo7JyEIO2FjOyKpO2KuCDsoITsmqkuICovXG5leHBvcnQgZnVuY3Rpb24gcmVzZXRTZXJ2ZXJSZXN0YXJ0Rm9yVGVzdHMoKTogdm9pZCB7XG4gIGNsZWFyRXhpdEFybSgpO1xuICByZXN0YXJ0UGVuZGluZyA9IGZhbHNlO1xuICBleGl0VHJpZ2dlcmVkID0gZmFsc2U7XG4gIGV4aXRBcm1lZCA9IGZhbHNlO1xufVxuIl0sIm1hcHBpbmdzIjoiO0FBRUEsTUFBTSwyQkFBMkI7QUFFakMsSUFBSSxpQkFBaUI7QUFDckIsSUFBSSxnQkFBZ0I7QUFDcEIsSUFBSSxZQUFZO0FBQ2hCLElBQUk7QUFDSixJQUFJO0FBQ0osSUFBSTs7QUFLSixTQUFnQixtQkFBNEI7Q0FDMUMsT0FBTztBQUNUOzs7Ozs7QUFPQSxTQUFnQixxQkFBOEI7Q0FDNUMsSUFBSSxnQkFBZ0IsT0FBTztDQUMzQixpQkFBaUI7Q0FDakIsT0FBTztBQUNUO0FBRUEsU0FBUyxlQUFxQjtDQUM1QixJQUFJLGVBQWUsYUFBYSxhQUFhO0NBQzdDLGdCQUFnQjtDQUNoQixJQUFJLGlCQUFpQixXQUFXO0VBQzlCLGNBQWMsZUFBZSxVQUFVLFNBQVM7RUFDaEQsY0FBYyxlQUFlLFNBQVMsU0FBUztDQUNqRDtDQUNBLGdCQUFnQjtDQUNoQixZQUFZO0FBQ2Q7O0FBR0EsU0FBZ0IscUJBQXFCLFVBQTBCO0NBQzdELElBQUksQ0FBQyxrQkFBa0IsV0FBVztDQUNsQyxZQUFZO0NBRVosTUFBTSxpQkFBaUI7RUFDckIsSUFBSSxlQUFlO0VBQ25CLGdCQUFnQjtFQUNoQixhQUFhO0VBQ2IsUUFBUSxLQUFLLENBQUM7Q0FDaEI7Q0FFQSxnQkFBZ0I7Q0FDaEIsWUFBWTtDQUNaLFNBQVMsS0FBSyxVQUFVLFFBQVE7Q0FDaEMsU0FBUyxLQUFLLFNBQVMsUUFBUTtDQUMvQixnQkFBZ0IsV0FBVyxVQUFVLHdCQUF3QjtDQUM3RCxjQUFjLE1BQU07Q0FDcEIsSUFBSSxTQUFTLGFBQWEsQ0FBQyxTQUFTLGVBQWUsU0FBUztBQUM5RDs7QUFHQSxTQUFnQiw2QkFBbUM7Q0FDakQsYUFBYTtDQUNiLGlCQUFpQjtDQUNqQixnQkFBZ0I7Q0FDaEIsWUFBWTtBQUNkIn0=
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { detectSupervisor } from "./process-supervisor";
|
|
4
|
-
|
|
5
|
-
describe("detectSupervisor", () => {
|
|
6
|
-
it("pm2 아래에서는 pm2 로 판별한다", () => {
|
|
7
|
-
// pm2 는 자식에게 pm_id 를 넣는다. "0" 이 흔한 값이라 falsy 검사로는 놓친다.
|
|
8
|
-
expect(detectSupervisor({ pm_id: "0" })).toBe("pm2");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("systemd 환경변수만으로는 재시작 보장을 가정하지 않는다", () => {
|
|
12
|
-
expect(detectSupervisor({ INVOCATION_ID: "abc123" })).toBeNull();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("로컬 실행에서는 null 이다", () => {
|
|
16
|
-
// 종료해도 다시 띄워줄 주체가 없어 재시작을 막아야 한다.
|
|
17
|
-
expect(detectSupervisor({})).toBeNull();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("빈 문자열도 pm2 로 인정한다", () => {
|
|
21
|
-
// 값이 아니라 존재 여부가 신호다. pm_id 가 빈 문자열로 오는 환경이 있어도 pm2 다.
|
|
22
|
-
expect(detectSupervisor({ pm_id: "" })).toBe("pm2");
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 프로세스 모니터링 도구 감지.
|
|
3
|
-
*
|
|
4
|
-
* 화면의 "재시작"은 스스로를 종료하는 것이다 — pm2 같은 도구가 죽은 프로세스를 다시
|
|
5
|
-
* 띄워주는 것이 곧 재시작이다. 그래서 그런 도구 없이 종료하면 그냥 서버가 내려간다.
|
|
6
|
-
* 로컬 개발에서 버튼을 누르면 아무도 살려주지 않으므로, 종료 전에 반드시 확인한다.
|
|
7
|
-
*
|
|
8
|
-
* 현재 배포 계약은 pm2 만 재시작을 보장한다. systemd 의 `INVOCATION_ID` 는 해당
|
|
9
|
-
* unit 이 `Restart=` 를 쓴다는 뜻이 아니므로 안전한 감독자 신호로 사용할 수 없다.
|
|
10
|
-
*
|
|
11
|
-
* 값이 아니라 존재 여부가 신호다. pm2 는 첫 프로세스에 `pm_id=0` 을 주므로 truthy 검사로
|
|
12
|
-
* 판별하면 놓친다.
|
|
13
|
-
*/
|
|
14
|
-
export function detectSupervisor(env: NodeJS.ProcessEnv = process.env): "pm2" | null {
|
|
15
|
-
if (env.pm_id !== undefined) return "pm2";
|
|
16
|
-
return null;
|
|
17
|
-
}
|