@cartanova/qgrid-cli 2.6.8 → 2.6.10
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/expired-token-reminder.js +20 -5
- package/bundle/dist/application/setting/setting.constant.js +226 -0
- package/bundle/dist/application/setting/setting.model.js +20 -2
- package/bundle/dist/application/setting/setting.types.js +5 -2
- package/bundle/dist/sonamu.config.js +2 -2
- package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +2 -2
- package/bundle/dist/utils/quiet-hours.js +30 -26
- package/bundle/dist/utils/slack-notify.js +6 -2
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +43 -0
- package/bundle/src/application/qgrid/expired-token-reminder.ts +22 -2
- package/bundle/src/application/setting/{setting.schema.test.ts → setting.constant.test.ts} +30 -1
- package/bundle/src/application/setting/{setting.schema.ts → setting.constant.ts} +86 -10
- package/bundle/src/application/setting/setting.model.ts +15 -1
- package/bundle/src/application/setting/setting.types.ts +3 -1
- package/bundle/src/application/sonamu.generated.http +7 -0
- package/bundle/src/sonamu.config.ts +3 -1
- package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +1 -1
- package/bundle/src/utils/quiet-hours.ts +56 -17
- package/bundle/src/utils/slack-notify.test.ts +96 -13
- package/bundle/src/utils/slack-notify.ts +8 -1
- package/bundle/web-dist/client/assets/{index-DrsXE13r.js → index-BRRNJRgu.js} +3 -3
- package/bundle/web-dist/client/assets/index-rdbxJinT.css +1 -0
- package/bundle/web-dist/client/assets/{logs-e4-6aYnO.js → logs-BI5K8oe4.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-eOAlYTK9.js → monit-4Y-Razet.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-Qie4ogtJ.js → routes-CkBXB-V4.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +1 -0
- package/bundle/web-dist/client/assets/settings-B62_r1i4.js +1 -0
- package/bundle/web-dist/client/assets/{show-60iOOM-e.js → show-CGRchJj7.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-Bx5mQnoJ.js +1 -0
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-0ZGq74bZ.js → use-oauth-login-flow-Bz87RV5v.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{logs-DmkzGxGU.js → logs-C1rxLh8H.js} +1 -1
- package/bundle/web-dist/server/assets/{monit-DyKLJX4s.js → monit-C8gx9Sar.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-g62E9xmC.js → routes-ZW_Ih5Em.js} +8 -5
- package/bundle/web-dist/server/assets/{services.generated-DADr9OGH.js → services.generated-CZIChgHV.js} +8 -0
- package/bundle/web-dist/server/assets/{settings-CMJKayff.js → settings--aoatERH.js} +72 -22
- package/bundle/web-dist/server/assets/{show-BGAiCPGq.js → show-BY27w9oi.js} +1 -1
- package/bundle/web-dist/server/assets/{tokens-DmhIfVct.js → tokens-DIObtTuZ.js} +13 -53
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-B6MY65IN.js → use-oauth-login-flow-V3k8PSqQ.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +7 -7
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +5 -2
- package/skills/qgrid/references/decision-rationale.md +7 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -1
- package/skills/qgrid/references/tool-calling-and-multiturn.md +21 -0
- package/bundle/dist/application/setting/setting.schema.js +0 -157
- package/bundle/web-dist/client/assets/index-7SZyiqAx.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-T4ctVcia.js +0 -1
- package/bundle/web-dist/client/assets/settings-CTVZpAAg.js +0 -1
- package/bundle/web-dist/client/assets/tokens-DdDccGlX.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Y as __toESM, g as require_react, h as require_jsx_runtime, m as useQueryClient } from "./sd.generated-LO-h1_16.js";
|
|
2
|
-
import { a as SettingService } from "./services.generated-
|
|
2
|
+
import { a as SettingService } from "./services.generated-CZIChgHV.js";
|
|
3
3
|
import { t as lucidePlus } from "./plus-BISXNqRU.js";
|
|
4
4
|
//#region ~icons/lucide/eye.jsx
|
|
5
5
|
var import_react = /* @__PURE__ */ __toESM(require_react());
|
|
@@ -74,7 +74,8 @@ var lucideRotateCcw = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("sv
|
|
|
74
74
|
//#region src/components/qgrid/SettingsPanel.tsx
|
|
75
75
|
var GROUP_LABELS = {
|
|
76
76
|
openai: "OpenAI 워커",
|
|
77
|
-
slack: "Slack 알림"
|
|
77
|
+
slack: "Slack 알림",
|
|
78
|
+
slackConnection: "Slack 연결"
|
|
78
79
|
};
|
|
79
80
|
function maskValue(value) {
|
|
80
81
|
if (!value) return "";
|
|
@@ -120,6 +121,28 @@ function Stepper({ value, min, max, step, disabled, onChange }) {
|
|
|
120
121
|
]
|
|
121
122
|
});
|
|
122
123
|
}
|
|
124
|
+
/** 고정 선택지. 자유 입력보다 고르는 편이 빠르고, 범위를 벗어난 값이 생기지 않는다. */
|
|
125
|
+
function PresetGroup({ value, presets, disabled, onChange }) {
|
|
126
|
+
const current = Number(value);
|
|
127
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
128
|
+
className: "segmented-control inline-flex",
|
|
129
|
+
children: presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
130
|
+
type: "button",
|
|
131
|
+
disabled,
|
|
132
|
+
"data-active": current === preset,
|
|
133
|
+
onClick: () => onChange(String(preset)),
|
|
134
|
+
className: "rounded-[6px] px-2.5 py-1 text-[11px] font-medium tabular-nums text-sand-500 transition-colors hover:text-sand-800 disabled:opacity-50 data-[active=true]:text-sand-900",
|
|
135
|
+
children: preset >= 60 ? `${preset / 60}시간` : `${preset}분`
|
|
136
|
+
}, preset))
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** 값이 어디서 왔는지. 되돌림 버튼이 무엇을 지우는지 이 배지가 설명한다. */
|
|
140
|
+
function SourceBadge({ source }) {
|
|
141
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
142
|
+
className: `text-[10px] px-1.5 py-0.5 rounded ${source === "db" ? "bg-sienna-50 text-sienna-600" : "bg-sand-100 text-sand-500"}`,
|
|
143
|
+
children: source === "db" ? "저장값" : source === "env" ? "환경변수" : "기본값"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
123
146
|
function Toggle({ on, disabled, onChange }) {
|
|
124
147
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
125
148
|
type: "button",
|
|
@@ -165,8 +188,6 @@ function SettingRow({ item }) {
|
|
|
165
188
|
setError(e.message);
|
|
166
189
|
}
|
|
167
190
|
};
|
|
168
|
-
const isToggle = item.kind === "boolean";
|
|
169
|
-
const isNumeric = item.kind === "integer" || item.kind === "number";
|
|
170
191
|
const isSecret = item.kind === "secret";
|
|
171
192
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
172
193
|
className: "px-4 py-3 sm:px-5",
|
|
@@ -176,14 +197,18 @@ function SettingRow({ item }) {
|
|
|
176
197
|
className: "min-w-0 flex-1",
|
|
177
198
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
178
199
|
className: "flex items-center gap-2",
|
|
179
|
-
children: [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
202
|
+
className: "text-[13px] text-sand-800",
|
|
203
|
+
children: item.label
|
|
204
|
+
}),
|
|
205
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SourceBadge, { source: item.source }),
|
|
206
|
+
item.applies === "restart" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
207
|
+
className: "text-[10px] text-caution-500",
|
|
208
|
+
title: "저장해도 다음 재시작부터 반영됩니다",
|
|
209
|
+
children: "재시작 필요"
|
|
210
|
+
})
|
|
211
|
+
]
|
|
187
212
|
}), item.help && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
188
213
|
className: "mt-0.5 text-[11px] text-sand-500",
|
|
189
214
|
children: item.help
|
|
@@ -191,12 +216,18 @@ function SettingRow({ item }) {
|
|
|
191
216
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
192
217
|
className: "flex items-center gap-1.5 shrink-0",
|
|
193
218
|
children: [
|
|
194
|
-
|
|
219
|
+
item.kind === "boolean" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Toggle, {
|
|
195
220
|
on: value === "true",
|
|
196
221
|
disabled: pending,
|
|
197
222
|
onChange: (next) => void commit(String(next))
|
|
198
223
|
}),
|
|
199
|
-
|
|
224
|
+
item.kind === "preset" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PresetGroup, {
|
|
225
|
+
value,
|
|
226
|
+
presets: item.presets,
|
|
227
|
+
disabled: pending,
|
|
228
|
+
onChange: (next) => void commit(next)
|
|
229
|
+
}),
|
|
230
|
+
(item.kind === "integer" || item.kind === "number") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stepper, {
|
|
200
231
|
value,
|
|
201
232
|
min: item.min,
|
|
202
233
|
max: item.max,
|
|
@@ -229,7 +260,7 @@ function SettingRow({ item }) {
|
|
|
229
260
|
}),
|
|
230
261
|
!dirty && item.source === "db" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
231
262
|
type: "button",
|
|
232
|
-
title: "저장값을
|
|
263
|
+
title: "저장값을 지웁니다 — 환경변수 또는 기본값으로 돌아갑니다",
|
|
233
264
|
onClick: () => void reset(),
|
|
234
265
|
disabled: pending,
|
|
235
266
|
className: "size-7 grid place-items-center rounded-lg text-sand-400 hover:text-sand-600 hover:bg-sand-100 disabled:opacity-50 transition-colors duration-150",
|
|
@@ -243,12 +274,31 @@ function SettingRow({ item }) {
|
|
|
243
274
|
})]
|
|
244
275
|
});
|
|
245
276
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
277
|
+
/** 만료 알림을 주기를 기다리지 않고 지금 보낸다. 설정이 제대로 붙었는지 확인하는 용도. */
|
|
278
|
+
function TriggerReminderButton() {
|
|
279
|
+
const [result, setResult] = (0, import_react.useState)(null);
|
|
280
|
+
const mutation = SettingService.useTriggerExpiryReminderMutation();
|
|
281
|
+
const trigger = async () => {
|
|
282
|
+
setResult(null);
|
|
283
|
+
try {
|
|
284
|
+
const { sent } = await mutation.mutateAsync();
|
|
285
|
+
setResult(sent === 0 ? "재로그인이 필요한 토큰이 없습니다" : `${sent}건 발송`);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
setResult(e.message);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
291
|
+
className: "flex items-center gap-2",
|
|
292
|
+
children: [result && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
293
|
+
className: "text-[11px] text-sand-500",
|
|
294
|
+
children: result
|
|
295
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
296
|
+
type: "button",
|
|
297
|
+
onClick: () => void trigger(),
|
|
298
|
+
disabled: mutation.isPending,
|
|
299
|
+
className: "px-2 py-1 text-[11px] rounded-lg border border-sand-200/80 text-sand-600 hover:bg-sand-100 disabled:opacity-50 transition-colors duration-150",
|
|
300
|
+
children: mutation.isPending ? "보내는 중" : "지금 보내기"
|
|
301
|
+
})]
|
|
252
302
|
});
|
|
253
303
|
}
|
|
254
304
|
function SettingsPanel() {
|
|
@@ -311,7 +361,7 @@ function SettingsPanel() {
|
|
|
311
361
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
312
362
|
className: "text-[13px] font-medium text-sand-700",
|
|
313
363
|
children: GROUP_LABELS[group] ?? group
|
|
314
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
364
|
+
}), group === "slack" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriggerReminderButton, {})]
|
|
315
365
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
316
366
|
className: "divide-y divide-sand-100/80",
|
|
317
367
|
children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingRow, { item }, item.key))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { K as __commonJSMin, Y as __toESM, g as require_react, h as require_jsx_runtime, q as __exportAll } from "./sd.generated-LO-h1_16.js";
|
|
2
|
-
import { i as RequestLogStepService, r as RequestLogService } from "./services.generated-
|
|
2
|
+
import { i as RequestLogStepService, r as RequestLogService } from "./services.generated-CZIChgHV.js";
|
|
3
3
|
import { a as Link } from "./lazyRouteComponent-Ws7I6fL-.js";
|
|
4
4
|
import { t as Route } from "../entry-server.generated.js";
|
|
5
5
|
import { n as formatMicroUsd, t as cacheHitRate } from "./cost-DzjfVtUr.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Y as __toESM, g as require_react, h as require_jsx_runtime, m as useQueryClient } from "./sd.generated-LO-h1_16.js";
|
|
2
|
-
import { n as QgridService, o as TokenService } from "./services.generated-
|
|
2
|
+
import { n as QgridService, o as TokenService } from "./services.generated-CZIChgHV.js";
|
|
3
3
|
import { i as Q, o as lucideKeyRound } from "../entry-server.generated.js";
|
|
4
|
-
import { n as OAuthCodeEntry, r as useUpdateTokenMutation, t as useOAuthLoginFlow } from "./use-oauth-login-flow-
|
|
4
|
+
import { n as OAuthCodeEntry, r as useUpdateTokenMutation, t as useOAuthLoginFlow } from "./use-oauth-login-flow-V3k8PSqQ.js";
|
|
5
5
|
import { t as lucidePlus } from "./plus-BISXNqRU.js";
|
|
6
6
|
//#region src/components/qgrid/AddTokenModal.tsx
|
|
7
7
|
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
@@ -180,7 +180,6 @@ function TokenTable({ data, isLoading }) {
|
|
|
180
180
|
const removeMutation = QgridService.useRemoveTokenMutation();
|
|
181
181
|
const updateMutation = useUpdateTokenMutation();
|
|
182
182
|
const toggleMutation = QgridService.useToggleTokenMutation();
|
|
183
|
-
const oauth = useOAuthLoginFlow();
|
|
184
183
|
const invalidate = () => Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
|
|
185
184
|
const handleToggle = async (token) => {
|
|
186
185
|
await toggleMutation.mutateAsync({ id: token.id });
|
|
@@ -192,9 +191,6 @@ function TokenTable({ data, isLoading }) {
|
|
|
192
191
|
await invalidate();
|
|
193
192
|
setDeleteTarget(null);
|
|
194
193
|
};
|
|
195
|
-
const submitReloginCode = async (pastedCode) => {
|
|
196
|
-
if (await oauth.submitCode(pastedCode)) oauth.reset();
|
|
197
|
-
};
|
|
198
194
|
const openEdit = (token) => {
|
|
199
195
|
setEditTarget(token);
|
|
200
196
|
setEditName(token.name ?? "");
|
|
@@ -308,28 +304,17 @@ function TokenTable({ data, isLoading }) {
|
|
|
308
304
|
className: "px-3 py-3",
|
|
309
305
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
310
306
|
className: "flex items-center gap-1",
|
|
311
|
-
children: [
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
321
|
-
|
|
322
|
-
className: "p-1 rounded text-sand-400 hover:text-sienna-500 transition-colors duration-150",
|
|
323
|
-
onClick: () => openEdit(token),
|
|
324
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucidePencil, { className: "size-4" })
|
|
325
|
-
}),
|
|
326
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
327
|
-
type: "button",
|
|
328
|
-
className: "p-1 rounded text-sand-400 hover:text-red-500 transition-colors duration-150",
|
|
329
|
-
onClick: () => setDeleteTarget(token),
|
|
330
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideTrash2, { className: "size-4" })
|
|
331
|
-
})
|
|
332
|
-
]
|
|
307
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
308
|
+
type: "button",
|
|
309
|
+
className: "p-1 rounded text-sand-400 hover:text-sienna-500 transition-colors duration-150",
|
|
310
|
+
onClick: () => openEdit(token),
|
|
311
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucidePencil, { className: "size-4" })
|
|
312
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
313
|
+
type: "button",
|
|
314
|
+
className: "p-1 rounded text-sand-400 hover:text-red-500 transition-colors duration-150",
|
|
315
|
+
onClick: () => setDeleteTarget(token),
|
|
316
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideTrash2, { className: "size-4" })
|
|
317
|
+
})]
|
|
333
318
|
})
|
|
334
319
|
})
|
|
335
320
|
]
|
|
@@ -339,31 +324,6 @@ function TokenTable({ data, isLoading }) {
|
|
|
339
324
|
})
|
|
340
325
|
})
|
|
341
326
|
}),
|
|
342
|
-
oauth.codeEntry && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
343
|
-
className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
|
|
344
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
345
|
-
className: "absolute inset-0 bg-sand-900/8 backdrop-blur-sm",
|
|
346
|
-
onClick: oauth.reset,
|
|
347
|
-
onKeyDown: () => {}
|
|
348
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
349
|
-
className: "relative panel shadow-xl w-full max-w-md mx-4",
|
|
350
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
351
|
-
className: "px-5 py-4 border-b border-sand-100/60",
|
|
352
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
|
|
353
|
-
className: "text-base font-medium text-sand-900",
|
|
354
|
-
children: "재로그인"
|
|
355
|
-
})
|
|
356
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
357
|
-
className: "px-5 py-4",
|
|
358
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OAuthCodeEntry, {
|
|
359
|
-
isPending: oauth.completeMutation.isPending,
|
|
360
|
-
isError: oauth.completeMutation.isError,
|
|
361
|
-
onSubmit: (code) => void submitReloginCode(code),
|
|
362
|
-
onRestart: oauth.reset
|
|
363
|
-
})
|
|
364
|
-
})]
|
|
365
|
-
})]
|
|
366
|
-
}),
|
|
367
327
|
editTarget && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
368
328
|
className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
|
|
369
329
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Y as __toESM, g as require_react, h as require_jsx_runtime, m as useQueryClient } from "./sd.generated-LO-h1_16.js";
|
|
2
|
-
import { n as QgridService, s as useMutation } from "./services.generated-
|
|
2
|
+
import { n as QgridService, s as useMutation } from "./services.generated-CZIChgHV.js";
|
|
3
3
|
import { i as Q } from "../entry-server.generated.js";
|
|
4
4
|
//#region src/services/token/use-update-token-mutation.ts
|
|
5
5
|
function useUpdateTokenMutation() {
|
|
@@ -22768,7 +22768,7 @@ function BottomNav() {
|
|
|
22768
22768
|
}
|
|
22769
22769
|
//#endregion
|
|
22770
22770
|
//#region src/components/Sidebar.tsx
|
|
22771
|
-
var qgridVersion = "2.6.
|
|
22771
|
+
var qgridVersion = "2.6.10";
|
|
22772
22772
|
function Sidebar({ className }) {
|
|
22773
22773
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
22774
22774
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
|
|
@@ -44649,15 +44649,15 @@ function RootComponent() {
|
|
|
44649
44649
|
}
|
|
44650
44650
|
//#endregion
|
|
44651
44651
|
//#region src/routes/tokens.tsx
|
|
44652
|
-
var $$splitComponentImporter$5 = () => import("./assets/tokens-
|
|
44652
|
+
var $$splitComponentImporter$5 = () => import("./assets/tokens-DIObtTuZ.js");
|
|
44653
44653
|
var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
|
|
44654
44654
|
//#endregion
|
|
44655
44655
|
//#region src/routes/settings.tsx
|
|
44656
|
-
var $$splitComponentImporter$4 = () => import("./assets/settings
|
|
44656
|
+
var $$splitComponentImporter$4 = () => import("./assets/settings--aoatERH.js");
|
|
44657
44657
|
var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
|
|
44658
44658
|
//#endregion
|
|
44659
44659
|
//#region src/routes/monit.tsx
|
|
44660
|
-
var $$splitComponentImporter$3 = () => import("./assets/monit-
|
|
44660
|
+
var $$splitComponentImporter$3 = () => import("./assets/monit-C8gx9Sar.js");
|
|
44661
44661
|
var monitSearchSchema = object({ level: _enum([
|
|
44662
44662
|
"all",
|
|
44663
44663
|
"scaling",
|
|
@@ -44670,7 +44670,7 @@ var Route$3 = createFileRoute("/monit")({
|
|
|
44670
44670
|
});
|
|
44671
44671
|
//#endregion
|
|
44672
44672
|
//#region src/routes/logs.tsx
|
|
44673
|
-
var $$splitComponentImporter$2 = () => import("./assets/logs-
|
|
44673
|
+
var $$splitComponentImporter$2 = () => import("./assets/logs-C1rxLh8H.js");
|
|
44674
44674
|
var logsSearchSchema = object({
|
|
44675
44675
|
token: string().optional(),
|
|
44676
44676
|
project: string().optional(),
|
|
@@ -44684,11 +44684,11 @@ var Route$2 = createFileRoute("/logs")({
|
|
|
44684
44684
|
});
|
|
44685
44685
|
//#endregion
|
|
44686
44686
|
//#region src/routes/index.tsx
|
|
44687
|
-
var $$splitComponentImporter$1 = () => import("./assets/routes-
|
|
44687
|
+
var $$splitComponentImporter$1 = () => import("./assets/routes-ZW_Ih5Em.js");
|
|
44688
44688
|
var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
|
|
44689
44689
|
//#endregion
|
|
44690
44690
|
//#region src/routes/requests/show.tsx
|
|
44691
|
-
var $$splitComponentImporter = () => import("./assets/show-
|
|
44691
|
+
var $$splitComponentImporter = () => import("./assets/show-BY27w9oi.js");
|
|
44692
44692
|
var showSearchSchema = object({ id: number() });
|
|
44693
44693
|
var Route = createFileRoute("/requests/show")({
|
|
44694
44694
|
validateSearch: showSearchSchema,
|
package/package.json
CHANGED
|
@@ -92,10 +92,13 @@ On shutdown, it stops provider dispatchers and the token subscriber.
|
|
|
92
92
|
|
|
93
93
|
Some env values are editable at runtime through the dashboard's Settings page, backed by the `settings` table (key-value). Resolution order is DB → env → code default: env stays as a fallback so a deploy that has not written any setting yet keeps working exactly as before. `loadSettings()` runs in `onStart` before the dispatchers are constructed, since the OpenAI pool reads its worker counts in its constructor.
|
|
94
94
|
|
|
95
|
-
- Editable: OpenAI autoscale and min/max workers per token, the worker memory guards,
|
|
95
|
+
- Editable: OpenAI autoscale and min/max workers per token, the worker memory guards, 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.
|
|
96
96
|
- 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.
|
|
97
97
|
- Worker settings are marked `restart` because `resolveOpenAIWorkerPoolConfig` is called once in the dispatcher constructor. Changing them writes to the DB but does not resize a running pool.
|
|
98
|
-
- Slack channel, bot-token, and user-map changes are read on the next notification. Changing the expiry-reminder interval replaces the current timer immediately without sending a notification merely because the setting changed; process boot still performs the intended immediate reminder run.
|
|
98
|
+
- 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.
|
|
99
|
+
- `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.
|
|
100
|
+
- 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.
|
|
101
|
+
- `POST /api/setting/triggerExpiryReminder` sends the reminder once on demand and returns `{ sent }` so the caller can distinguish "nothing to remind about" from a delivery. It is sent as `urgent`, bypassing quiet hours and the master switch, because a human pressed the button.
|
|
99
102
|
- `immediate` means the API process that handled the update. The in-memory settings cache is not propagated to sibling processes; multi-instance deployments need a separate settings-change transport before treating the label as cluster-wide.
|
|
100
103
|
- The public settings API exposes the curated list/set/reset methods only. Keep generic Sonamu `save`/`del` methods internal so callers cannot bypass key validation, cache updates, or runtime change hooks.
|
|
101
104
|
- The settings response returns the Slack bot token in full so the authenticated dashboard can support its reveal toggle. Treat that endpoint as an operator-only surface at the deployment boundary; dev0 relies on Caddy authentication for the dashboard. The database password remains masked and is never returned in full.
|
|
@@ -66,6 +66,13 @@ Key decisions:
|
|
|
66
66
|
- `runContext.requestLogId` is intentionally direct. qgrid SDK and server are in the same product boundary, so an opaque indirection was not worth the complexity.
|
|
67
67
|
- Lifecycle endpoints remain public because `createQgridLogger` records non-qgrid AI SDK calls into qgrid logs without forcing those calls through the qgrid provider.
|
|
68
68
|
- Structured-output failures should fail honestly. Especially on Claude Code, partial or invalid structured output must not be rescued into a fake success.
|
|
69
|
+
- Streaming with tools re-emits `answer` deltas by incrementally parsing the
|
|
70
|
+
envelope client-side (SDK 2.6.10, SON-527). The server keeps forwarding raw
|
|
71
|
+
envelope deltas unchanged; the SDK parser is preview-only and the strict
|
|
72
|
+
server decoder still owns the final result. Silence-on-anomaly plus the done
|
|
73
|
+
full-text fallback means the worst case degrades to the old hold-everything
|
|
74
|
+
behavior, never to leaked envelope text. See
|
|
75
|
+
`references/tool-calling-and-multiturn.md` (Streaming With Tools).
|
|
69
76
|
|
|
70
77
|
## OpenAI/Codex Runtime
|
|
71
78
|
|
|
@@ -210,7 +210,8 @@ When a refresh fails in a way that only re-login can fix, qgrid removes the toke
|
|
|
210
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
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
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
|
-
- Quiet hours suppress non-urgent Slack notifications: weekends, plus 20:00–08:00 on weekdays, in `Asia/Seoul`. The hour is computed through `Intl` with an explicit timezone rather than `getHours()` — dev0 runs in UTC, where "20:00" would land at 05:00 Korean time. Expiry alerts repeat, so one dropped in quiet hours returns on the first cycle of the next working window. The last-active-token death alert sets `urgent: true` and ignores quiet hours: it means the provider pool is empty, which cannot wait for the next working day.
|
|
213
|
+
- Quiet hours suppress non-urgent Slack notifications: weekends, plus 20:00–08:00 on weekdays, in `Asia/Seoul`. All three parts are settings (`slack.quietFromHour`, `slack.quietUntilHour`, `slack.notifyOnWeekends`), defaulting to that window. The hour is computed through `Intl` with an explicit timezone rather than `getHours()` — dev0 runs in UTC, where "20:00" would land at 05:00 Korean time. Expiry alerts repeat, so one dropped in quiet hours returns on the first cycle of the next working window. The last-active-token death alert sets `urgent: true` and ignores quiet hours: it means the provider pool is empty, which cannot wait for the next working day.
|
|
214
|
+
- `slack.enabled` exists because holidays cannot be expressed as a recurring rule — substitute holidays, one-off national days, and company closures change every year, so an operator turns notifications off directly instead. It gates the same non-urgent path as quiet hours, which keeps the escape hatch for pool-empty alerts identical in both mechanisms.
|
|
214
215
|
- 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.
|
|
215
216
|
- `expired-token-reminder.ts` repeats the session-expiry alert for tokens that are still inactive, because the one-shot alert is lost on anyone not watching the channel at that moment and re-login only happens when a person does it. It reuses the same `세션 만료` title rather than inventing a second name — there is one event, so a differently-named alert would read as a separate problem. It runs once at startup and then every `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` (unset or 0 disables), batches every inactive token into one message, and sends nothing when none are inactive. The startup run matters because a restart is exactly when someone is watching, and waiting a full interval to learn a token is already dead is backwards; the cost is one extra alert per restart — a periodic "all clear" trains people to ignore the channel.
|
|
216
217
|
- Owners are mentioned through `SLACK_USER_MAP` (`tokenName:SlackUserId`) on every token notification, not just the reminder. The map is explicit rather than resolved through `users.list`: Slack handles diverge from token names often enough (`haze`→`haze.lee`, `byeongjun`→`potados`) that auto-matching drops the mention silently, and an alert nobody is tagged in has no one to act on it. Shared accounts are left out of the map on purpose and appear by name only. Each instance sends its own reminder — unlike deactivation there is no state change to gate on, and a few duplicates a day cost less than a locking scheme.
|
|
@@ -266,6 +266,27 @@ The old tolerant decoder silently shipped degenerate envelopes as answers
|
|
|
266
266
|
Unknown tool names throw. `answerKind` is required and derived from
|
|
267
267
|
`input.jsonSchema` presence at the dispatcher.
|
|
268
268
|
|
|
269
|
+
## Streaming With Tools (Envelope Delta Re-Emission)
|
|
270
|
+
|
|
271
|
+
With tools, provider deltas are raw envelope JSON. The server forwards them
|
|
272
|
+
unmodified over SSE. The SDK (`envelope-stream-parser.ts`, wired into
|
|
273
|
+
`doStream`) incrementally parses that envelope and re-emits only the
|
|
274
|
+
`result.answer` value as `text-delta` parts once `result.action` resolves to
|
|
275
|
+
`"answer"` (SON-527, SDK-only change — the server was never the blocker).
|
|
276
|
+
|
|
277
|
+
- Undetermined or `tool_call` envelopes emit nothing, matching the pre-change
|
|
278
|
+
hold-everything behavior.
|
|
279
|
+
- `answerKind: "text"`: the answer JSON string is unescaped to plain text;
|
|
280
|
+
escape sequences split across delta boundaries are safe (char-level state
|
|
281
|
+
machine).
|
|
282
|
+
- `answerKind: "json"`: the answer value's raw JSON text is emitted verbatim so
|
|
283
|
+
AI SDK `partialOutputStream` can partial-parse it.
|
|
284
|
+
- The parser is preview-only. The server strict decoder still decides the final
|
|
285
|
+
result, and the `done` event's full-text fallback covers streams where the
|
|
286
|
+
parser emitted nothing (`deltaTextEmitted` flag).
|
|
287
|
+
- Do not re-add a client-side gate that drops deltas when tools are present;
|
|
288
|
+
that was the failure this section replaces.
|
|
289
|
+
|
|
269
290
|
## Multi-Step And Multi-Turn
|
|
270
291
|
|
|
271
292
|
AI SDK multi-step behavior is client-orchestrated:
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
//#region src/application/setting/setting.schema.ts
|
|
2
|
-
/** 64 GiB 운영 호스트에서 세 자리 오입력을 막는 설정 상한. 안전을 보장하는 값은 아니다. */
|
|
3
|
-
const MAX_OPENAI_ESTIMATED_RSS_GIB = 32;
|
|
4
|
-
const MAX_OPENAI_MIN_HOST_AVAILABLE_GIB = 64;
|
|
5
|
-
const SETTING_DEFS = [
|
|
6
|
-
{
|
|
7
|
-
key: "openai.autoscale",
|
|
8
|
-
fallback: "true",
|
|
9
|
-
group: "openai",
|
|
10
|
-
envKey: "QGRID_OPENAI_AUTOSCALE",
|
|
11
|
-
label: "OpenAI 워커 오토스케일",
|
|
12
|
-
kind: "boolean",
|
|
13
|
-
applies: "restart",
|
|
14
|
-
help: "끄면 워커 수가 최소값으로 고정됩니다"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
key: "openai.minWorkersPerToken",
|
|
18
|
-
fallback: "1",
|
|
19
|
-
group: "openai",
|
|
20
|
-
envKey: "QGRID_OPENAI_MIN_WORKERS_PER_TOKEN",
|
|
21
|
-
label: "토큰당 최소 워커",
|
|
22
|
-
kind: "integer",
|
|
23
|
-
applies: "restart",
|
|
24
|
-
min: 1,
|
|
25
|
-
max: 20
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
key: "openai.maxWorkersPerToken",
|
|
29
|
-
fallback: "3",
|
|
30
|
-
group: "openai",
|
|
31
|
-
envKey: "QGRID_OPENAI_MAX_WORKERS_PER_TOKEN",
|
|
32
|
-
label: "토큰당 최대 워커",
|
|
33
|
-
kind: "integer",
|
|
34
|
-
applies: "restart",
|
|
35
|
-
min: 1,
|
|
36
|
-
max: 20,
|
|
37
|
-
help: "오토스케일이 꺼져 있으면 무시됩니다"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
key: "openai.maxEstimatedRssGiB",
|
|
41
|
-
fallback: "16",
|
|
42
|
-
group: "openai",
|
|
43
|
-
envKey: "QGRID_OPENAI_MAX_ESTIMATED_RSS_GIB",
|
|
44
|
-
label: "워커 메모리 상한 (GiB)",
|
|
45
|
-
kind: "number",
|
|
46
|
-
applies: "restart",
|
|
47
|
-
min: 1,
|
|
48
|
-
max: 32,
|
|
49
|
-
help: "이 추정치를 넘기면 워커를 더 늘리지 않습니다. 올릴수록 OOM 위험이 커지며 상한은 오입력 방지용입니다"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: "openai.minHostAvailableGiB",
|
|
53
|
-
fallback: "20",
|
|
54
|
-
group: "openai",
|
|
55
|
-
envKey: "QGRID_OPENAI_MIN_HOST_AVAILABLE_GIB",
|
|
56
|
-
label: "호스트 여유 메모리 하한 (GiB)",
|
|
57
|
-
kind: "number",
|
|
58
|
-
applies: "restart",
|
|
59
|
-
min: 0,
|
|
60
|
-
max: 64,
|
|
61
|
-
help: "호스트 여유가 이 값 아래면 스케일업을 멈춥니다. 확장을 끄려면 오토스케일을 끄세요"
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
key: "slack.expiryReminderMinutes",
|
|
65
|
-
fallback: "0",
|
|
66
|
-
group: "slack",
|
|
67
|
-
envKey: "SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES",
|
|
68
|
-
label: "만료 알림 반복 주기 (분)",
|
|
69
|
-
kind: "integer",
|
|
70
|
-
applies: "immediate",
|
|
71
|
-
min: 0,
|
|
72
|
-
max: 1440,
|
|
73
|
-
help: "0 이면 반복 알림을 끕니다"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
key: "slack.channelId",
|
|
77
|
-
fallback: "",
|
|
78
|
-
group: "slack",
|
|
79
|
-
envKey: "SLACK_CHANNEL_ID",
|
|
80
|
-
label: "Slack 채널 ID",
|
|
81
|
-
kind: "string",
|
|
82
|
-
applies: "immediate",
|
|
83
|
-
help: "사용자 ID(U...)를 넣으면 그 사람 DM 으로 갑니다"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
key: "slack.userMap",
|
|
87
|
-
fallback: "",
|
|
88
|
-
group: "slack",
|
|
89
|
-
envKey: "SLACK_USER_MAP",
|
|
90
|
-
label: "Slack 사용자 매핑",
|
|
91
|
-
kind: "string",
|
|
92
|
-
applies: "immediate",
|
|
93
|
-
help: "토큰명:SlackUserId 를 쉼표로. 공용 계정은 넣지 않습니다"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
key: "slack.botToken",
|
|
97
|
-
fallback: "",
|
|
98
|
-
group: "slack",
|
|
99
|
-
envKey: "SLACK_BOT_TOKEN",
|
|
100
|
-
label: "Slack 봇 토큰",
|
|
101
|
-
kind: "secret",
|
|
102
|
-
applies: "immediate"
|
|
103
|
-
}
|
|
104
|
-
];
|
|
105
|
-
function findSettingDef(key) {
|
|
106
|
-
return SETTING_DEFS.find((d) => d.key === key);
|
|
107
|
-
}
|
|
108
|
-
/** 저장 전 검증. 잘못된 값이 들어가면 런타임에서 조용히 기본값으로 떨어져 원인을 찾기 어렵다. */
|
|
109
|
-
function validateSettingValue(def, raw) {
|
|
110
|
-
const trimmed = raw.trim();
|
|
111
|
-
if (def.kind === "boolean") {
|
|
112
|
-
if (!["true", "false"].includes(trimmed)) return {
|
|
113
|
-
ok: false,
|
|
114
|
-
error: "true 또는 false"
|
|
115
|
-
};
|
|
116
|
-
return {
|
|
117
|
-
ok: true,
|
|
118
|
-
value: trimmed
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (def.kind === "integer" || def.kind === "number") {
|
|
122
|
-
const parsed = Number(trimmed);
|
|
123
|
-
if (!Number.isFinite(parsed)) return {
|
|
124
|
-
ok: false,
|
|
125
|
-
error: "숫자를 입력하세요"
|
|
126
|
-
};
|
|
127
|
-
if (def.kind === "integer" && !Number.isInteger(parsed)) return {
|
|
128
|
-
ok: false,
|
|
129
|
-
error: "정수를 입력하세요"
|
|
130
|
-
};
|
|
131
|
-
if (def.min !== void 0 && parsed < def.min) return {
|
|
132
|
-
ok: false,
|
|
133
|
-
error: `${def.min} 이상이어야 합니다`
|
|
134
|
-
};
|
|
135
|
-
if (def.max !== void 0 && parsed > def.max) return {
|
|
136
|
-
ok: false,
|
|
137
|
-
error: `${def.max} 이하여야 합니다`
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
ok: true,
|
|
141
|
-
value: String(parsed)
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
ok: true,
|
|
146
|
-
value: trimmed
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
/** 화면에 그대로 내려보내면 안 되는 값. 앞뒤만 남긴다. */
|
|
150
|
-
function maskSecret(value) {
|
|
151
|
-
if (value.length <= 12) return "***";
|
|
152
|
-
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
|
153
|
-
}
|
|
154
|
-
//#endregion
|
|
155
|
-
export { MAX_OPENAI_ESTIMATED_RSS_GIB, MAX_OPENAI_MIN_HOST_AVAILABLE_GIB, SETTING_DEFS, findSettingDef, maskSecret, validateSettingValue };
|
|
156
|
-
|
|
157
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dGluZy5zY2hlbWEuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwcGxpY2F0aW9uL3NldHRpbmcvc2V0dGluZy5zY2hlbWEudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDqtIDrpqwg6rCA64ql7ZWcIOufsO2DgOyehCDshKTsoJUg66qp66GdLlxuICpcbiAqIO2CpMK3ZW52IOydtOumhMK37YOA7J6FwrfqsoDspp3snYQg7ZWcIOqzs+yXkCDrqqjslYQgQVBJIOyZgCBVSSDqsIAg6rCZ7J2AIOygleydmOulvCDrs7jri6QuIOyXrOq4sCDsl4bripQgZW52IOuKlFxuICog7ZmU66m07JeQ7IScIOuwlOq/gCDsiJgg7JeG64ukIOKAlCBEQiDsoJHsho0g7KCV67O064KYIO2PrO2KuOyymOufvCDshJzrsoTqsIAg65yo6riwIOyghOyXkCDtlYTsmpTtlZwg6rCS65Ok7J2064ukLlxuICovXG5cbmV4cG9ydCB0eXBlIFNldHRpbmdLaW5kID0gXCJib29sZWFuXCIgfCBcImludGVnZXJcIiB8IFwibnVtYmVyXCIgfCBcInN0cmluZ1wiIHwgXCJzZWNyZXRcIjtcblxuLyoqIO2ZlOuptOyXkOyEnCDrrLbslrQg67O07Jes7KSEIOuLqOychC4gOeqwnOulvCDtj4ntj4ntlZjqsowg64KY7Je07ZWY66m0IOyWtOuUlOulvCDrtJDslbwg7ZWg7KeAIOyVjCDsiJgg7JeG64ukLiAqL1xuZXhwb3J0IHR5cGUgU2V0dGluZ0dyb3VwID0gXCJvcGVuYWlcIiB8IFwic2xhY2tcIjtcblxuZXhwb3J0IHR5cGUgU2V0dGluZ0RlZiA9IHtcbiAga2V5OiBzdHJpbmc7XG4gIGdyb3VwOiBTZXR0aW5nR3JvdXA7XG4gIGVudktleTogc3RyaW5nO1xuICBsYWJlbDogc3RyaW5nO1xuICBraW5kOiBTZXR0aW5nS2luZDtcbiAgLyoqIOyggeyaqSDsi5zsoJAuIOybjOy7pCDshKTsoJXsnYAgZGlzcGF0Y2hlciDsg53shLHsnpDsl5DshJwg7ZWcIOuyiOunjCDsnb3tmIAg7J6s7Iuc7J6R7J20IO2VhOyalO2VmOuLpC4gKi9cbiAgYXBwbGllczogXCJpbW1lZGlhdGVcIiB8IFwicmVzdGFydFwiO1xuICBtaW4/OiBudW1iZXI7XG4gIG1heD86IG51bWJlcjtcbiAgLyoqXG4gICAqIOyggOyepeqwkuuPhCBlbnYg64+EIOyXhuydhCDrlYwg7Iuk7KCc66GcIOyggeyaqeuQmOuKlCDqsJIuIO2ZlOuptOydhCDruYTsm4zrkZDrqbQgXCLshKTsoJUg7JWIIOuQqFwi7Jy866GcIOydve2eiOyngOunjFxuICAgKiDsi6TsoJzroZzripQg7J20IOqwkuydtCDrj4zqs6Ag7J6I64ukIOKAlCDshozruYTsspjsnZgg6riw67O46rCS6rO8IOuwmOuTnOyLnCDqsJnslYTslbwg7ZWc64ukLlxuICAgKi9cbiAgZmFsbGJhY2s6IHN0cmluZztcbiAgaGVscD86IHN0cmluZztcbn07XG5cbi8qKiA2NCBHaUIg7Jq07JiBIO2YuOyKpO2KuOyXkOyEnCDshLgg7J6Q66asIOyYpOyeheugpeydhCDrp4nripQg7ISk7KCVIOyDge2VnC4g7JWI7KCE7J2EIOuztOyepe2VmOuKlCDqsJLsnYAg7JWE64uI64ukLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9PUEVOQUlfRVNUSU1BVEVEX1JTU19HSUIgPSAzMjtcbmV4cG9ydCBjb25zdCBNQVhfT1BFTkFJX01JTl9IT1NUX0FWQUlMQUJMRV9HSUIgPSA2NDtcblxuZXhwb3J0IGNvbnN0IFNFVFRJTkdfREVGUzogU2V0dGluZ0RlZltdID0gW1xuICB7XG4gICAga2V5OiBcIm9wZW5haS5hdXRvc2NhbGVcIixcbiAgICBmYWxsYmFjazogXCJ0cnVlXCIsXG4gICAgZ3JvdXA6IFwib3BlbmFpXCIsXG4gICAgZW52S2V5OiBcIlFHUklEX09QRU5BSV9BVVRPU0NBTEVcIixcbiAgICBsYWJlbDogXCJPcGVuQUkg7JuM7LukIOyYpO2GoOyKpOy8gOydvFwiLFxuICAgIGtpbmQ6IFwiYm9vbGVhblwiLFxuICAgIGFwcGxpZXM6IFwicmVzdGFydFwiLFxuICAgIGhlbHA6IFwi64GE66m0IOybjOy7pCDsiJjqsIAg7LWc7IaM6rCS7Jy866GcIOqzoOygleuQqeuLiOuLpFwiLFxuICB9LFxuICB7XG4gICAga2V5OiBcIm9wZW5haS5taW5Xb3JrZXJzUGVyVG9rZW5cIixcbiAgICBmYWxsYmFjazogXCIxXCIsXG4gICAgZ3JvdXA6IFwib3BlbmFpXCIsXG4gICAgZW52S2V5OiBcIlFHUklEX09QRU5BSV9NSU5fV09SS0VSU19QRVJfVE9LRU5cIixcbiAgICBsYWJlbDogXCLthqDtgbDri7kg7LWc7IaMIOybjOy7pFwiLFxuICAgIGtpbmQ6IFwiaW50ZWdlclwiLFxuICAgIGFwcGxpZXM6IFwicmVzdGFydFwiLFxuICAgIG1pbjogMSxcbiAgICBtYXg6IDIwLFxuICB9LFxuICB7XG4gICAga2V5OiBcIm9wZW5haS5tYXhXb3JrZXJzUGVyVG9rZW5cIixcbiAgICBmYWxsYmFjazogXCIzXCIsXG4gICAgZ3JvdXA6IFwib3BlbmFpXCIsXG4gICAgZW52S2V5OiBcIlFHUklEX09QRU5BSV9NQVhfV09SS0VSU19QRVJfVE9LRU5cIixcbiAgICBsYWJlbDogXCLthqDtgbDri7kg7LWc64yAIOybjOy7pFwiLFxuICAgIGtpbmQ6IFwiaW50ZWdlclwiLFxuICAgIGFwcGxpZXM6IFwicmVzdGFydFwiLFxuICAgIG1pbjogMSxcbiAgICBtYXg6IDIwLFxuICAgIGhlbHA6IFwi7Jik7Yag7Iqk7LyA7J287J20IOq6vOyguCDsnojsnLzrqbQg66y07Iuc65Cp64uI64ukXCIsXG4gIH0sXG4gIHtcbiAgICBrZXk6IFwib3BlbmFpLm1heEVzdGltYXRlZFJzc0dpQlwiLFxuICAgIGZhbGxiYWNrOiBcIjE2XCIsXG4gICAgZ3JvdXA6IFwib3BlbmFpXCIsXG4gICAgZW52S2V5OiBcIlFHUklEX09QRU5BSV9NQVhfRVNUSU1BVEVEX1JTU19HSUJcIixcbiAgICBsYWJlbDogXCLsm4zsu6Qg66mU66qo66asIOyDge2VnCAoR2lCKVwiLFxuICAgIGtpbmQ6IFwibnVtYmVyXCIsXG4gICAgYXBwbGllczogXCJyZXN0YXJ0XCIsXG4gICAgbWluOiAxLFxuICAgIG1heDogTUFYX09QRU5BSV9FU1RJTUFURURfUlNTX0dJQixcbiAgICBoZWxwOiBcIuydtCDstpTsoJXsuZjrpbwg64SY6riw66m0IOybjOy7pOulvCDrjZQg64qY66as7KeAIOyViuyKteuLiOuLpC4g7Jis66a07IiY66GdIE9PTSDsnITtl5jsnbQg7Luk7KeA66mwIOyDge2VnOydgCDsmKTsnoXroKUg67Cp7KeA7Jqp7J6F64uI64ukXCIsXG4gIH0sXG4gIHtcbiAgICBrZXk6IFwib3BlbmFpLm1pbkhvc3RBdmFpbGFibGVHaUJcIixcbiAgICBmYWxsYmFjazogXCIyMFwiLFxuICAgIGdyb3VwOiBcIm9wZW5haVwiLFxuICAgIGVudktleTogXCJRR1JJRF9PUEVOQUlfTUlOX0hPU1RfQVZBSUxBQkxFX0dJQlwiLFxuICAgIGxhYmVsOiBcIu2YuOyKpO2KuCDsl6zsnKAg66mU66qo66asIO2VmO2VnCAoR2lCKVwiLFxuICAgIGtpbmQ6IFwibnVtYmVyXCIsXG4gICAgYXBwbGllczogXCJyZXN0YXJ0XCIsXG4gICAgbWluOiAwLFxuICAgIG1heDogTUFYX09QRU5BSV9NSU5fSE9TVF9BVkFJTEFCTEVfR0lCLFxuICAgIGhlbHA6IFwi7Zi47Iqk7Yq4IOyXrOycoOqwgCDsnbQg6rCSIOyVhOuemOuptCDsiqTsvIDsnbzsl4XsnYQg66mI7Lal64uI64ukLiDtmZXsnqXsnYQg64GE66Ck66m0IOyYpO2GoOyKpOy8gOydvOydhCDrgYTshLjsmpRcIixcbiAgfSxcbiAge1xuICAgIGtleTogXCJzbGFjay5leHBpcnlSZW1pbmRlck1pbnV0ZXNcIixcbiAgICBmYWxsYmFjazogXCIwXCIsXG4gICAgZ3JvdXA6IFwic2xhY2tcIixcbiAgICBlbnZLZXk6IFwiU0xBQ0tfRVhQSVJZX1JFTUlOREVSX0lOVEVSVkFMX01JTlVURVNcIixcbiAgICBsYWJlbDogXCLrp4zro4wg7JWM66a8IOuwmOuztSDso7zquLAgKOu2hClcIixcbiAgICBraW5kOiBcImludGVnZXJcIixcbiAgICBhcHBsaWVzOiBcImltbWVkaWF0ZVwiLFxuICAgIG1pbjogMCxcbiAgICBtYXg6IDE0NDAsXG4gICAgaGVscDogXCIwIOydtOuptCDrsJjrs7Ug7JWM66a87J2EIOuBleuLiOuLpFwiLFxuICB9LFxuICB7XG4gICAga2V5OiBcInNsYWNrLmNoYW5uZWxJZFwiLFxuICAgIGZhbGxiYWNrOiBcIlwiLFxuICAgIGdyb3VwOiBcInNsYWNrXCIsXG4gICAgZW52S2V5OiBcIlNMQUNLX0NIQU5ORUxfSURcIixcbiAgICBsYWJlbDogXCJTbGFjayDssYTrhJAgSURcIixcbiAgICBraW5kOiBcInN0cmluZ1wiLFxuICAgIGFwcGxpZXM6IFwiaW1tZWRpYXRlXCIsXG4gICAgaGVscDogXCLsgqzsmqnsnpAgSUQoVS4uLinrpbwg64Sj7Jy866m0IOq3uCDsgqzrnowgRE0g7Jy866GcIOqwkeuLiOuLpFwiLFxuICB9LFxuICB7XG4gICAga2V5OiBcInNsYWNrLnVzZXJNYXBcIixcbiAgICBmYWxsYmFjazogXCJcIixcbiAgICBncm91cDogXCJzbGFja1wiLFxuICAgIGVudktleTogXCJTTEFDS19VU0VSX01BUFwiLFxuICAgIGxhYmVsOiBcIlNsYWNrIOyCrOyaqeyekCDrp6TtlZFcIixcbiAgICBraW5kOiBcInN0cmluZ1wiLFxuICAgIGFwcGxpZXM6IFwiaW1tZWRpYXRlXCIsXG4gICAgaGVscDogXCLthqDtgbDrqoU6U2xhY2tVc2VySWQg66W8IOyJvO2RnOuhnC4g6rO17JqpIOqzhOygleydgCDrhKPsp4Ag7JWK7Iq164uI64ukXCIsXG4gIH0sXG4gIHtcbiAgICBrZXk6IFwic2xhY2suYm90VG9rZW5cIixcbiAgICBmYWxsYmFjazogXCJcIixcbiAgICBncm91cDogXCJzbGFja1wiLFxuICAgIGVudktleTogXCJTTEFDS19CT1RfVE9LRU5cIixcbiAgICBsYWJlbDogXCJTbGFjayDrtIcg7Yag7YGwXCIsXG4gICAga2luZDogXCJzZWNyZXRcIixcbiAgICBhcHBsaWVzOiBcImltbWVkaWF0ZVwiLFxuICB9LFxuXTtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRTZXR0aW5nRGVmKGtleTogc3RyaW5nKTogU2V0dGluZ0RlZiB8IHVuZGVmaW5lZCB7XG4gIHJldHVybiBTRVRUSU5HX0RFRlMuZmluZCgoZCkgPT4gZC5rZXkgPT09IGtleSk7XG59XG5cbi8qKiDsoIDsnqUg7KCEIOqygOymnS4g7J6Y66q765CcIOqwkuydtCDrk6TslrTqsIDrqbQg65+w7YOA7J6E7JeQ7IScIOyhsOyaqe2eiCDquLDrs7jqsJLsnLzroZwg65ao7Ja07KC4IOybkOyduOydhCDssL7quLAg7Ja066C164ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHZhbGlkYXRlU2V0dGluZ1ZhbHVlKFxuICBkZWY6IFNldHRpbmdEZWYsXG4gIHJhdzogc3RyaW5nLFxuKTogeyBvazogdHJ1ZTsgdmFsdWU6IHN0cmluZyB9IHwgeyBvazogZmFsc2U7IGVycm9yOiBzdHJpbmcgfSB7XG4gIGNvbnN0IHRyaW1tZWQgPSByYXcudHJpbSgpO1xuXG4gIGlmIChkZWYua2luZCA9PT0gXCJib29sZWFuXCIpIHtcbiAgICBpZiAoIVtcInRydWVcIiwgXCJmYWxzZVwiXS5pbmNsdWRlcyh0cmltbWVkKSkgcmV0dXJuIHsgb2s6IGZhbHNlLCBlcnJvcjogXCJ0cnVlIOuYkOuKlCBmYWxzZVwiIH07XG4gICAgcmV0dXJuIHsgb2s6IHRydWUsIHZhbHVlOiB0cmltbWVkIH07XG4gIH1cblxuICBpZiAoZGVmLmtpbmQgPT09IFwiaW50ZWdlclwiIHx8IGRlZi5raW5kID09PSBcIm51bWJlclwiKSB7XG4gICAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHRyaW1tZWQpO1xuICAgIGlmICghTnVtYmVyLmlzRmluaXRlKHBhcnNlZCkpIHJldHVybiB7IG9rOiBmYWxzZSwgZXJyb3I6IFwi7Iir7J6Q66W8IOyeheugpe2VmOyEuOyalFwiIH07XG4gICAgaWYgKGRlZi5raW5kID09PSBcImludGVnZXJcIiAmJiAhTnVtYmVyLmlzSW50ZWdlcihwYXJzZWQpKSB7XG4gICAgICByZXR1cm4geyBvazogZmFsc2UsIGVycm9yOiBcIuygleyImOulvCDsnoXroKXtlZjshLjsmpRcIiB9O1xuICAgIH1cbiAgICBpZiAoZGVmLm1pbiAhPT0gdW5kZWZpbmVkICYmIHBhcnNlZCA8IGRlZi5taW4pIHtcbiAgICAgIHJldHVybiB7IG9rOiBmYWxzZSwgZXJyb3I6IGAke2RlZi5taW59IOydtOyDgeydtOyWtOyVvCDtlanri4jri6RgIH07XG4gICAgfVxuICAgIGlmIChkZWYubWF4ICE9PSB1bmRlZmluZWQgJiYgcGFyc2VkID4gZGVmLm1heCkge1xuICAgICAgcmV0dXJuIHsgb2s6IGZhbHNlLCBlcnJvcjogYCR7ZGVmLm1heH0g7J207ZWY7Jes7JW8IO2VqeuLiOuLpGAgfTtcbiAgICB9XG4gICAgcmV0dXJuIHsgb2s6IHRydWUsIHZhbHVlOiBTdHJpbmcocGFyc2VkKSB9O1xuICB9XG5cbiAgcmV0dXJuIHsgb2s6IHRydWUsIHZhbHVlOiB0cmltbWVkIH07XG59XG5cbi8qKiDtmZTrqbTsl5Ag6re464yA66GcIOuCtOugpOuztOuCtOuptCDslYgg65CY64qUIOqwki4g7JWe65Kk66eMIOuCqOq4tOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtYXNrU2VjcmV0KHZhbHVlOiBzdHJpbmcpOiBzdHJpbmcge1xuICBpZiAodmFsdWUubGVuZ3RoIDw9IDEyKSByZXR1cm4gXCIqKipcIjtcbiAgcmV0dXJuIGAke3ZhbHVlLnNsaWNlKDAsIDgpfS4uLiR7dmFsdWUuc2xpY2UoLTQpfWA7XG59XG4iXSwibWFwcGluZ3MiOiI7O0FBK0JBLE1BQWEsK0JBQStCO0FBQzVDLE1BQWEsb0NBQW9DO0FBRWpELE1BQWEsZUFBNkI7Q0FDeEM7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0VBQ1QsTUFBTTtDQUNSO0NBQ0E7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0VBQ1QsS0FBSztFQUNMLEtBQUs7Q0FDUDtDQUNBO0VBQ0UsS0FBSztFQUNMLFVBQVU7RUFDVixPQUFPO0VBQ1AsUUFBUTtFQUNSLE9BQU87RUFDUCxNQUFNO0VBQ04sU0FBUztFQUNULEtBQUs7RUFDTCxLQUFLO0VBQ0wsTUFBTTtDQUNSO0NBQ0E7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0VBQ1QsS0FBSztFQUNMLEtBQUE7RUFDQSxNQUFNO0NBQ1I7Q0FDQTtFQUNFLEtBQUs7RUFDTCxVQUFVO0VBQ1YsT0FBTztFQUNQLFFBQVE7RUFDUixPQUFPO0VBQ1AsTUFBTTtFQUNOLFNBQVM7RUFDVCxLQUFLO0VBQ0wsS0FBQTtFQUNBLE1BQU07Q0FDUjtDQUNBO0VBQ0UsS0FBSztFQUNMLFVBQVU7RUFDVixPQUFPO0VBQ1AsUUFBUTtFQUNSLE9BQU87RUFDUCxNQUFNO0VBQ04sU0FBUztFQUNULEtBQUs7RUFDTCxLQUFLO0VBQ0wsTUFBTTtDQUNSO0NBQ0E7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0VBQ1QsTUFBTTtDQUNSO0NBQ0E7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0VBQ1QsTUFBTTtDQUNSO0NBQ0E7RUFDRSxLQUFLO0VBQ0wsVUFBVTtFQUNWLE9BQU87RUFDUCxRQUFRO0VBQ1IsT0FBTztFQUNQLE1BQU07RUFDTixTQUFTO0NBQ1g7QUFDRjtBQUVBLFNBQWdCLGVBQWUsS0FBcUM7Q0FDbEUsT0FBTyxhQUFhLE1BQU0sTUFBTSxFQUFFLFFBQVEsR0FBRztBQUMvQzs7QUFHQSxTQUFnQixxQkFDZCxLQUNBLEtBQzREO0NBQzVELE1BQU0sVUFBVSxJQUFJLEtBQUs7Q0FFekIsSUFBSSxJQUFJLFNBQVMsV0FBVztFQUMxQixJQUFJLENBQUMsQ0FBQyxRQUFRLE9BQU8sQ0FBQyxDQUFDLFNBQVMsT0FBTyxHQUFHLE9BQU87R0FBRSxJQUFJO0dBQU8sT0FBTztFQUFnQjtFQUNyRixPQUFPO0dBQUUsSUFBSTtHQUFNLE9BQU87RUFBUTtDQUNwQztDQUVBLElBQUksSUFBSSxTQUFTLGFBQWEsSUFBSSxTQUFTLFVBQVU7RUFDbkQsTUFBTSxTQUFTLE9BQU8sT0FBTztFQUM3QixJQUFJLENBQUMsT0FBTyxTQUFTLE1BQU0sR0FBRyxPQUFPO0dBQUUsSUFBSTtHQUFPLE9BQU87RUFBWTtFQUNyRSxJQUFJLElBQUksU0FBUyxhQUFhLENBQUMsT0FBTyxVQUFVLE1BQU0sR0FDcEQsT0FBTztHQUFFLElBQUk7R0FBTyxPQUFPO0VBQVk7RUFFekMsSUFBSSxJQUFJLFFBQVEsVUFBYSxTQUFTLElBQUksS0FDeEMsT0FBTztHQUFFLElBQUk7R0FBTyxPQUFPLEdBQUcsSUFBSSxJQUFJO0VBQVk7RUFFcEQsSUFBSSxJQUFJLFFBQVEsVUFBYSxTQUFTLElBQUksS0FDeEMsT0FBTztHQUFFLElBQUk7R0FBTyxPQUFPLEdBQUcsSUFBSSxJQUFJO0VBQVc7RUFFbkQsT0FBTztHQUFFLElBQUk7R0FBTSxPQUFPLE9BQU8sTUFBTTtFQUFFO0NBQzNDO0NBRUEsT0FBTztFQUFFLElBQUk7RUFBTSxPQUFPO0NBQVE7QUFDcEM7O0FBR0EsU0FBZ0IsV0FBVyxPQUF1QjtDQUNoRCxJQUFJLE1BQU0sVUFBVSxJQUFJLE9BQU87Q0FDL0IsT0FBTyxHQUFHLE1BQU0sTUFBTSxHQUFHLENBQUMsRUFBRSxLQUFLLE1BQU0sTUFBTSxFQUFFO0FBQ2pEIn0=
|