remote-coder 0.4.1__py3-none-any.whl

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.
Files changed (78) hide show
  1. app/__init__.py +3 -0
  2. app/admin/__init__.py +0 -0
  3. app/admin/advanced_settings.py +88 -0
  4. app/admin/database_browser.py +301 -0
  5. app/admin/router.py +528 -0
  6. app/admin/static/i18n.js +401 -0
  7. app/admin/static/icons/advanced.svg +8 -0
  8. app/admin/static/icons/database.svg +5 -0
  9. app/admin/static/icons/download.svg +3 -0
  10. app/admin/static/icons/home.svg +4 -0
  11. app/admin/static/icons/logs.svg +3 -0
  12. app/admin/static/icons/projects.svg +5 -0
  13. app/admin/static/summary.js +73 -0
  14. app/admin/templates/admin.html +511 -0
  15. app/admin/templates/advanced.html +635 -0
  16. app/admin/templates/database.html +880 -0
  17. app/admin/templates/logs.html +686 -0
  18. app/admin/templates/projects.html +878 -0
  19. app/ai/__init__.py +0 -0
  20. app/ai/base.py +129 -0
  21. app/ai/claude.py +20 -0
  22. app/ai/codex.py +34 -0
  23. app/ai/factory.py +27 -0
  24. app/ai/gemini.py +20 -0
  25. app/ai/model_catalog.py +47 -0
  26. app/ai/usage.py +134 -0
  27. app/cli.py +238 -0
  28. app/config.py +130 -0
  29. app/git/__init__.py +0 -0
  30. app/git/ai_commit.py +88 -0
  31. app/git/branch_naming.py +21 -0
  32. app/git/commit_message.py +279 -0
  33. app/git/service.py +669 -0
  34. app/jobs/__init__.py +0 -0
  35. app/jobs/manager.py +770 -0
  36. app/jobs/schemas.py +116 -0
  37. app/jobs/store.py +334 -0
  38. app/main.py +265 -0
  39. app/models.py +20 -0
  40. app/monitoring/__init__.py +10 -0
  41. app/monitoring/code.py +161 -0
  42. app/monitoring/events.py +33 -0
  43. app/monitoring/git.py +103 -0
  44. app/monitoring/log_buffer.py +245 -0
  45. app/monitoring/memory.py +19 -0
  46. app/monitoring/model.py +598 -0
  47. app/projects/__init__.py +19 -0
  48. app/projects/registry.py +384 -0
  49. app/security/__init__.py +0 -0
  50. app/security/auth.py +19 -0
  51. app/system_startup.py +34 -0
  52. app/telegram/__init__.py +0 -0
  53. app/telegram/bot_instances.py +67 -0
  54. app/telegram/commands/__init__.py +64 -0
  55. app/telegram/commands/base.py +222 -0
  56. app/telegram/commands/branch.py +366 -0
  57. app/telegram/commands/clear_stop.py +221 -0
  58. app/telegram/commands/fix.py +219 -0
  59. app/telegram/commands/model.py +93 -0
  60. app/telegram/commands/monitor.py +185 -0
  61. app/telegram/commands/registry.py +110 -0
  62. app/telegram/commands/status.py +243 -0
  63. app/telegram/commands/system.py +201 -0
  64. app/telegram/confirmations.py +36 -0
  65. app/telegram/conversation.py +789 -0
  66. app/telegram/i18n.py +742 -0
  67. app/telegram/model_preferences.py +53 -0
  68. app/telegram/notifier.py +387 -0
  69. app/telegram/parser.py +267 -0
  70. app/telegram/webhook.py +988 -0
  71. app/telegram/webhook_registration.py +172 -0
  72. app/tunnel.py +104 -0
  73. remote_coder-0.4.1.dist-info/METADATA +520 -0
  74. remote_coder-0.4.1.dist-info/RECORD +78 -0
  75. remote_coder-0.4.1.dist-info/WHEEL +5 -0
  76. remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
  77. remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
  78. remote_coder-0.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,401 @@
1
+ (function (window) {
2
+ // NOTE: English is the canonical text in the templates. This catalog overlays
3
+ // Korean when window.__UI_LANG__ === "ko" (set server-side from ui_language).
4
+ // t(key) localizes keyed strings; tv(value) localizes backend-supplied values
5
+ // by reverse English lookup; apply() rewrites [data-i18n*] elements on load.
6
+ var CATALOG = {
7
+ // shared: navigation + page chrome
8
+ "nav.aria": { en: "Page navigation", ko: "페이지 이동" },
9
+ "nav.home": { en: "Admin home", ko: "관리 홈" },
10
+ "nav.projects": { en: "Projects", ko: "프로젝트 등록" },
11
+ "nav.advanced": { en: "Advanced settings", ko: "고급 설정" },
12
+ "nav.logs": { en: "Server logs", ko: "서버 로그" },
13
+ "nav.database": { en: "Data browser", ko: "데이터 조회" },
14
+ "common.localOnly": { en: "Local only", ko: "로컬 전용" },
15
+ "common.localhostNote": {
16
+ en: "This page is only available from <strong>127.0.0.1</strong>.",
17
+ ko: "이 페이지는 <strong>127.0.0.1</strong>에서만 열립니다.",
18
+ },
19
+ "common.summaryAria": { en: "Summary", ko: "요약" },
20
+
21
+ // shared: small labels reused across pages
22
+ "common.none": { en: "(none)", ko: "(없음)" },
23
+ "common.all": { en: "All", ko: "전체" },
24
+ "common.off": { en: "Off", ko: "끔" },
25
+ "common.set": { en: "Set", ko: "설정됨" },
26
+ "common.unset": { en: "Not set", ko: "미설정" },
27
+ "common.notSet": { en: "(not set)", ko: "(설정 안 됨)" },
28
+ "common.name": { en: "Name", ko: "이름" },
29
+ "common.status": { en: "Status", ko: "상태" },
30
+ "common.model": { en: "Model", ko: "모델" },
31
+ "common.actions": { en: "Actions", ko: "동작" },
32
+ "common.root": { en: "Root", ko: "루트" },
33
+ "common.worktree": { en: "Worktree", ko: "워크트리" },
34
+ "common.defaultModel": { en: "Default model", ko: "기본 모델" },
35
+ "common.enabled": { en: "Enabled", ko: "활성" },
36
+ "common.allowedChatIds": { en: "Allowed Chat IDs", ko: "허용 Chat ID" },
37
+ "common.allowedUserIds": { en: "Allowed User IDs", ko: "허용 User ID" },
38
+ "common.edit": { en: "Edit", ko: "편집" },
39
+ "common.delete": { en: "Delete", ko: "삭제" },
40
+ "common.search": { en: "Search", ko: "검색" },
41
+ "common.prev": { en: "Prev", ko: "이전" },
42
+ "common.next": { en: "Next", ko: "다음" },
43
+ "common.close": { en: "Close", ko: "닫기" },
44
+ "common.fallbackDefault": { en: "Fallback default", ko: "폴백 기본" },
45
+ "common.secondsSuffix": { en: "s", ko: "초" },
46
+
47
+ // summary cards (summary.js)
48
+ "summary.registered": { en: "Registered projects", ko: "등록 프로젝트" },
49
+ "summary.active": { en: "Active", ko: "활성" },
50
+ "summary.envModel": { en: "Env default model", ko: "환경 기본 모델" },
51
+ "summary.envTimeout": { en: "Env timeout", ko: "환경 타임아웃" },
52
+ "summary.botToken": { en: "Bot token", ko: "봇 토큰" },
53
+
54
+ // page titles
55
+ "title.admin": { en: "Remote AI Coder - Admin", ko: "Remote AI Coder - 관리" },
56
+ "title.projects": { en: "Remote AI Coder - Projects", ko: "Remote AI Coder - 프로젝트 등록" },
57
+ "title.advanced": { en: "Remote AI Coder - Advanced Settings", ko: "Remote AI Coder - 고급 설정" },
58
+ "title.logs": { en: "Remote AI Coder - Server Logs", ko: "Remote AI Coder - 서버 로그" },
59
+ "title.database": { en: "Remote AI Coder - Data Browser", ko: "Remote AI Coder - 데이터 조회" },
60
+
61
+ // admin.html (hub)
62
+ "admin.tagline": { en: "Local admin UI", ko: "로컬 관리 UI" },
63
+ "admin.activeProjects": { en: "Active projects", ko: "활성 프로젝트" },
64
+ "admin.manageLink": { en: "Manage", ko: "등록·편집" },
65
+ "admin.activeLead": {
66
+ en: "Only enabled projects are shown. Add, disable, or delete them on the Projects page.",
67
+ ko: "활성화된 프로젝트만 표시합니다. 추가·비활성·삭제는 프로젝트 등록 화면에서 할 수 있습니다.",
68
+ },
69
+ "admin.envSummary": { en: "Environment summary", ko: "환경 요약" },
70
+ "admin.envLead": {
71
+ en: "Registry file path and Telegram-related settings (masked). Webhooks are registered via script.",
72
+ ko: "등록 파일 경로와 텔레그램 관련 설정(마스킹)입니다. Webhook은 스크립트로 등록합니다.",
73
+ },
74
+ "admin.noActiveHtml": {
75
+ en: 'No active projects. Add or enable one on the <a href="/projects" class="link-manage">Projects</a> page.',
76
+ ko: '활성화된 프로젝트가 없습니다. <a href="/projects" class="link-manage">프로젝트 등록</a>에서 추가·활성화할 수 있습니다.',
77
+ },
78
+ "admin.projectsConfigFile": { en: "Projects config file", ko: "프로젝트 설정 파일" },
79
+ "admin.botTokenMasked": { en: "Bot token (masked)", ko: "봇 토큰 (마스킹)" },
80
+ "admin.webhookSecret": { en: "Webhook secret", ko: "Webhook 시크릿" },
81
+ "admin.webhookHintLabel": { en: "Webhook guide", ko: "Webhook 안내" },
82
+
83
+ // projects.html
84
+ "projects.h1": { en: "Project registration", ko: "프로젝트 등록" },
85
+ "projects.tagline": {
86
+ en: "Each entry is bound to <strong>one Telegram bot</strong> and a fixed repository.",
87
+ ko: "각 등록 항목은 <strong>하나의 Telegram 봇</strong>과 고정된 저장소에 연결됩니다.",
88
+ },
89
+ "projects.listHeading": { en: "Registered projects", ko: "등록 목록" },
90
+ "projects.listLead": {
91
+ en: "Add a project or pick the fallback default. Saving is applied immediately to the running server's <code>BotInstanceManager</code>.",
92
+ ko: "프로젝트를 추가하거나 기본 폴백 프로젝트를 지정합니다. 저장 시 실행 중인 서버의 <code>BotInstanceManager</code>에 즉시 반영됩니다.",
93
+ },
94
+ "projects.colGitRoot": { en: "Git root", ko: "Git 루트" },
95
+ "projects.formHeading": { en: "Add / edit", ko: "추가 / 수정" },
96
+ "projects.addNew": { en: "Add new project", ko: "새 프로젝트 추가" },
97
+ "projects.editingPrefix": { en: "Editing: ", ko: "편집 중: " },
98
+ "projects.nameHint": {
99
+ en: "Start with a letter or digit; <code>.</code> <code>_</code> <code>-</code> allowed",
100
+ ko: "영문·숫자로 시작, <code>.</code> <code>_</code> <code>-</code> 허용",
101
+ },
102
+ "projects.gitRootPath": { en: "Git root path", ko: "Git 루트 경로" },
103
+ "projects.gitRootHint": {
104
+ en: "Absolute path. The directory must exist to save.",
105
+ ko: "절대 경로. 디렉터리가 존재해야 저장됩니다.",
106
+ },
107
+ "projects.worktreeBaseDir": { en: "Worktree base directory", ko: "워크트리 베이스 디렉터리" },
108
+ "projects.worktreeHint": { en: "Created if missing.", ko: "없으면 생성됩니다." },
109
+ "projects.botToken": { en: "Bot token", ko: "봇 토큰" },
110
+ "projects.botTokenHint": {
111
+ en: "API token from Telegram BotFather after <code>/newbot</code>. The plaintext is stored only in the registry file; lists and APIs show a masked value.",
112
+ ko: "Telegram의 BotFather에서 <code>/newbot</code> 후 받은 API 토큰. 평문은 레지스트리 파일에만 저장되며, 목록·API에는 마스킹된 값만 표시됩니다.",
113
+ },
114
+ "projects.chatIdsHint": {
115
+ en: "Comma- or space-separated. At least one.",
116
+ ko: "쉼표 또는 공백으로 구분. 최소 1개.",
117
+ },
118
+ "projects.optional": { en: "Optional fields", ko: "선택 항목" },
119
+ "projects.webhookSecret": { en: "Webhook secret (optional)", ko: "웹훅 시크릿 (선택)" },
120
+ "projects.webhookSecretHint": {
121
+ en: "Telegram <code>secret_token</code>. Leave blank when editing to keep the existing value.",
122
+ ko: "Telegram <code>secret_token</code>. 편집 시 비우면 기존 값 유지.",
123
+ },
124
+ "projects.allowedUserIdsOptional": { en: "Allowed User IDs (optional)", ko: "허용 User ID (선택)" },
125
+ "projects.userIdsHint": {
126
+ en: "Blank allows by chat ID only.",
127
+ ko: "비우면 채팅 ID만으로 허용합니다.",
128
+ },
129
+ "projects.btnAdd": { en: "Add", ko: "추가" },
130
+ "projects.btnSave": { en: "Save (PUT)", ko: "저장 (PUT)" },
131
+ "projects.btnClear": { en: "Clear form", ko: "폼 비우기" },
132
+ "projects.captionDefault": { en: "Registry fallback default: ", ko: "등록 폴백 기본값: " },
133
+ "projects.emptyRow": {
134
+ en: "No projects registered. Add one with the form below.",
135
+ ko: "등록된 프로젝트가 없습니다. 아래 폼에서 추가하세요.",
136
+ },
137
+ "projects.badgeDefault": { en: "Default", ko: "기본" },
138
+ "projects.badgeOn": { en: "Enabled", ko: "활성" },
139
+ "projects.badgeOff": { en: "Disabled", ko: "비활성" },
140
+ "projects.secretBadge": { en: "Secret", ko: "시크릿" },
141
+ "projects.secretSetTitle": { en: "Webhook secret set", ko: "웹훅 시크릿 설정됨" },
142
+ "projects.maskedTokenTitle": { en: "Masked bot token", ko: "마스킹된 봇 토큰" },
143
+ "projects.btnMakeDefault": { en: "Make default", ko: "기본으로" },
144
+ "projects.setDefaultOk": {
145
+ en: 'Set default project to "{name}".',
146
+ ko: '기본 프로젝트를 "{name}"(으)로 설정했습니다.',
147
+ },
148
+ "projects.confirmDelete": {
149
+ en: 'Delete project "{name}"? This cannot be undone.',
150
+ ko: '프로젝트 "{name}"을(를) 삭제할까요? 이 작업은 되돌릴 수 없습니다.',
151
+ },
152
+ "projects.deletedOk": { en: "Deleted.", ko: "삭제했습니다." },
153
+ "projects.editInfo": {
154
+ en: "Name cannot be changed. Leave the bot token and webhook secret blank to keep existing values.",
155
+ ko: "이름은 변경할 수 없습니다. 봇 토큰·웹훅 시크릿은 비워 두면 기존 값이 유지됩니다.",
156
+ },
157
+ "projects.errChatIds": { en: "Enter at least one allowed Chat ID.", ko: "허용 Chat ID를 하나 이상 입력하세요." },
158
+ "projects.errBotToken": { en: "Enter the bot token.", ko: "봇 토큰을 입력하세요." },
159
+ "projects.updatedOk": { en: "Updated.", ko: "수정했습니다." },
160
+ "projects.createdOk": { en: "Added.", ko: "추가했습니다." },
161
+
162
+ // advanced.html
163
+ "advanced.h1": { en: "Advanced Settings", ko: "고급 설정" },
164
+ "advanced.tagline": {
165
+ en: "Options that can affect repositories and conversation memory.",
166
+ ko: "저장소와 대화 기억에 영향을 줄 수 있는 옵션입니다.",
167
+ },
168
+ "advanced.secTelegram": { en: "Telegram Notifications", ko: "Telegram 알림" },
169
+ "advanced.secTelegramLead": {
170
+ en: "Configure message display and server event notifications.",
171
+ ko: "메시지 표시와 서버 이벤트 알림을 설정합니다.",
172
+ },
173
+ "advanced.uiLangLabel": { en: "Interface language", ko: "인터페이스 언어" },
174
+ "advanced.uiLangHint": {
175
+ en: "Default: English. Choose Korean here for the Telegram bot and this admin UI.",
176
+ ko: "기본값: English. 여기서 한국어를 선택하면 Telegram 봇과 이 관리 UI에 적용됩니다.",
177
+ },
178
+ "advanced.statusLimitLabel": { en: "Recent jobs shown by /status", ko: "/status가 보여주는 최근 작업 수" },
179
+ "advanced.statusLimitHint": {
180
+ en: "Maximum number of recent jobs selectable with inline buttons in /status. Default: 10",
181
+ ko: "/status에서 인라인 버튼으로 고를 수 있는 최근 작업 최대 개수. 기본값: 10",
182
+ },
183
+ "advanced.phStatus": { en: "Example: 10", ko: "예: 10" },
184
+ "advanced.phTimeout": { en: "Example: 3600", ko: "예: 3600" },
185
+ "advanced.phRows": { en: "Example: 5000", ko: "예: 5000" },
186
+ "advanced.phBytes": { en: "Example: 10485760", ko: "예: 10485760" },
187
+ "advanced.naturalConfirmLabel": {
188
+ en: "Use inline buttons instead of <code>y</code>/<code>Y</code> for natural-language job confirmations",
189
+ ko: "자연어 작업 확인에 <code>y</code>/<code>Y</code> 대신 인라인 버튼 사용",
190
+ },
191
+ "advanced.naturalConfirmHint": {
192
+ en: "Default: off. When enabled, job confirmation messages show <strong>Yes</strong>/<strong>No</strong> buttons.",
193
+ ko: "기본값: 꺼짐. 켜면 작업 확인 메시지에 <strong>Yes</strong>/<strong>No</strong> 버튼이 표시됩니다.",
194
+ },
195
+ "advanced.lifecycleLabel": {
196
+ en: "Send Telegram notifications when the server starts or stops",
197
+ ko: "서버 시작·중지 시 Telegram 알림 전송",
198
+ },
199
+ "advanced.lifecycleHint": {
200
+ en: "Default: on. When disabled, restarts do not send server start/stop messages.",
201
+ ko: "기본값: 켜짐. 끄면 재시작 시 서버 시작·중지 메시지를 보내지 않습니다.",
202
+ },
203
+ "advanced.secJob": { en: "Job Execution", ko: "작업 실행" },
204
+ "advanced.secJobLead": { en: "Configure AI job execution limits.", ko: "AI 작업 실행 제한을 설정합니다." },
205
+ "advanced.jobTimeoutLabel": {
206
+ en: "AI job timeout (seconds, blank uses environment default)",
207
+ ko: "AI 작업 타임아웃 (초, 비우면 환경 기본값 사용)",
208
+ },
209
+ "advanced.jobTimeoutHint": {
210
+ en: "If a Claude/Codex/Gemini runner does not finish within this time, the job fails.",
211
+ ko: "Claude/Codex/Gemini 러너가 이 시간 내에 끝나지 않으면 작업이 실패합니다.",
212
+ },
213
+ "advanced.secGit": { en: "Git Integration", ko: "Git 통합" },
214
+ "advanced.secGitLead": {
215
+ en: "These options can broadly affect repositories. Use them only when you understand the impact.",
216
+ ko: "이 옵션들은 저장소에 광범위한 영향을 줄 수 있습니다. 영향을 이해한 경우에만 사용하세요.",
217
+ },
218
+ "advanced.startupPullLabel": {
219
+ en: "Run <code>git pull</code> for registered active project repositories on server startup/restart",
220
+ ko: "서버 시작·재시작 시 등록된 활성 프로젝트 저장소에 <code>git pull</code> 실행",
221
+ },
222
+ "advanced.startupPullHint": {
223
+ en: "Default: off. Pulls from the remote based on each active project's checked-out branch. Network errors, conflicts, or local changes can fail; the server still starts.",
224
+ ko: "기본값: 꺼짐. 각 활성 프로젝트의 체크아웃된 브랜치 기준으로 원격에서 pull합니다. 네트워크 오류·충돌·로컬 변경이 있으면 실패할 수 있으나 서버는 그대로 시작됩니다.",
225
+ },
226
+ "advanced.mergeMainLabel": {
227
+ en: "Apply job results immediately to <code>main</code>/<code>master</code>, then push",
228
+ ko: "작업 결과를 <code>main</code>/<code>master</code>에 즉시 반영한 뒤 push",
229
+ },
230
+ "advanced.mergeMainHint": {
231
+ en: "When disabled, jobs only commit and push to their work branch. When enabled, successful jobs run an integration similar to <code>/rebase</code> (rebase -> main fast-forward merge -> push).",
232
+ ko: "끄면 작업은 자신의 작업 브랜치에만 커밋·push합니다. 켜면 성공한 작업은 <code>/rebase</code>와 유사한 통합(rebase -> main fast-forward merge -> push)을 수행합니다.",
233
+ },
234
+ "advanced.deleteRebasedLabel": {
235
+ en: "Delete the rebased branch locally and remotely after <code>/rebase</code>",
236
+ ko: "<code>/rebase</code> 후 리베이스한 브랜치를 로컬·원격에서 삭제",
237
+ },
238
+ "advanced.deleteRebasedHint": {
239
+ en: "Default: on. When disabled, <code>/rebase</code> only merges into main/master and pushes; the target branch remains.",
240
+ ko: "기본값: 켜짐. 끄면 <code>/rebase</code>는 main/master에 병합·push만 하고 대상 브랜치는 남깁니다.",
241
+ },
242
+ "advanced.secMemory": { en: "Conversation Memory (SQLite)", ko: "대화 기억 (SQLite)" },
243
+ "advanced.secMemoryLead": {
244
+ en: "These options can broadly affect the conversation memory SQLite database. Enable only when needed.",
245
+ ko: "이 옵션들은 대화 기억 SQLite 데이터베이스에 광범위한 영향을 줄 수 있습니다. 필요할 때만 켜세요.",
246
+ },
247
+ "advanced.memoryEnabledLabel": {
248
+ en: "Limit SQLite conversation memory storage",
249
+ ko: "SQLite 대화 기억 저장 제한",
250
+ },
251
+ "advanced.memoryEnabledHint": {
252
+ en: "When enabled, old <code>conversation_entries</code> rows are deleted <strong>globally</strong>. Set at least one positive row-count or DB-size limit.",
253
+ ko: "켜면 오래된 <code>conversation_entries</code> 행이 <strong>전역적으로</strong> 삭제됩니다. 행 수 또는 DB 크기 제한 중 최소 하나를 양수로 설정하세요.",
254
+ },
255
+ "advanced.maxRowsLabel": { en: "Maximum rows (blank disables)", ko: "최대 행 수 (비우면 비활성)" },
256
+ "advanced.maxBytesLabel": { en: "Maximum DB size (bytes, blank disables)", ko: "최대 DB 크기 (바이트, 비우면 비활성)" },
257
+ "advanced.btnSave": { en: "Save", ko: "저장" },
258
+ "advanced.btnReload": { en: "Reload", ko: "다시 불러오기" },
259
+ "advanced.loadedMsg": { en: "Summary and advanced settings loaded.", ko: "요약과 고급 설정을 불러왔습니다." },
260
+ "advanced.savedMsg": { en: "Advanced settings saved.", ko: "고급 설정을 저장했습니다." },
261
+
262
+ // logs.html
263
+ "logs.h1": { en: "Server logs", ko: "서버 로그" },
264
+ "logs.tagline": {
265
+ en: "Recent logs collected by the <code>app</code> package logger. Auto-refresh shows them near real time.",
266
+ ko: "<code>app</code> 패키지 로거에 쌓인 최근 로그입니다. 자동 새로고침으로 실시간에 가깝게 볼 수 있습니다.",
267
+ },
268
+ "logs.console": { en: "Console", ko: "콘솔" },
269
+ "logs.lead": {
270
+ en: "Level is a <strong>minimum</strong>. Search partially matches message and exception text. Click a badge to fill that filter.",
271
+ ko: "레벨은 <strong>최소</strong> 기준입니다. 검색어는 메시지·예외 텍스트에 부분 일치합니다. 배지를 클릭하면 해당 필터가 채워집니다.",
272
+ },
273
+ "logs.level": { en: "Level", ko: "레벨" },
274
+ "logs.levelAria": { en: "Minimum log level", ko: "최소 로그 레벨" },
275
+ "logs.category": { en: "Category", ko: "카테고리" },
276
+ "logs.categoryAria": { en: "Event category", ko: "이벤트 카테고리" },
277
+ "logs.phChatId": { en: "e.g. 123", ko: "예: 123" },
278
+ "logs.phUserId": { en: "e.g. 456", ko: "예: 456" },
279
+ "logs.phJobId": { en: "e.g. job_…", ko: "예: job_…" },
280
+ "logs.phProject": { en: "Project name", ko: "프로젝트 이름" },
281
+ "logs.logger": { en: "Logger (partial match)", ko: "로거 (부분 일치)" },
282
+ "logs.phLogger": { en: "e.g. app.telegram", ko: "예: app.telegram" },
283
+ "logs.phSearch": { en: "Message/exception", ko: "메시지·예외" },
284
+ "logs.refresh": { en: "Refresh (s)", ko: "새로고침(초)" },
285
+ "logs.refreshAria": { en: "Auto-refresh interval", ko: "자동 새로고침 간격" },
286
+ "logs.applyFilter": { en: "Apply filters", ko: "필터 적용" },
287
+ "logs.clearView": { en: "Clear view", ko: "화면 비우기" },
288
+ "logs.stickBottom": { en: "Scroll to bottom", ko: "맨 아래로 스크롤" },
289
+ "logs.loadWaiting": { en: "Waiting to load", ko: "로드 대기" },
290
+ "logs.shownLines": { en: "Lines shown: {n}", ko: "표시 줄 수: {n}" },
291
+
292
+ // database.html
293
+ "database.h1": { en: "Data browser", ko: "데이터 조회" },
294
+ "database.tagline": {
295
+ en: "Read-only view of the conversation memory SQLite. Arbitrary SQL is never run.",
296
+ ko: "대화 기억 SQLite를 읽기 전용으로 조회합니다. 임의 SQL은 실행되지 않습니다.",
297
+ },
298
+ "database.tablesHeading": { en: "Tables", ko: "표" },
299
+ "database.lead": {
300
+ en: "Tables and sort columns are restricted to a server whitelist.",
301
+ ko: "테이블과 정렬 컬럼은 서버 화이트리스트로만 허용됩니다.",
302
+ },
303
+ "database.table": { en: "Table", ko: "테이블" },
304
+ "database.tableAria": { en: "Table", ko: "테이블" },
305
+ "database.sort": { en: "Sort", ko: "정렬" },
306
+ "database.sortAria": { en: "Sort column", ko: "정렬 컬럼" },
307
+ "database.order": { en: "Order", ko: "순서" },
308
+ "database.orderAria": { en: "Sort order", ko: "정렬 순서" },
309
+ "database.desc": { en: "Descending", ko: "내림차순" },
310
+ "database.asc": { en: "Ascending", ko: "오름차순" },
311
+ "database.pageSize": { en: "Page size", ko: "페이지 크기" },
312
+ "database.pageSizeAria": { en: "Page size", ko: "페이지 크기" },
313
+ "database.projectAria": { en: "Project filter", ko: "프로젝트 필터" },
314
+ "database.roleAria": { en: "Role filter", ko: "역할 필터" },
315
+ "database.searchPartial": { en: "Search (partial match)", ko: "검색 (부분 일치)" },
316
+ "database.btnLoad": { en: "Load", ko: "조회" },
317
+ "database.csvTitle": {
318
+ en: "Download CSV (current filters/sort, up to 50,000 rows)",
319
+ ko: "CSV 다운로드 (현재 필터·정렬, 최대 5만행)",
320
+ },
321
+ "database.csvAria": { en: "Download CSV", ko: "CSV 다운로드" },
322
+ "database.textContent": { en: "Text content", ko: "text 내용" },
323
+ "database.dbPrefix": { en: "DB: {path}", ko: "DB: {path}" },
324
+ "database.dbMissing": { en: "DB file missing: {path}", ko: "DB 파일 없음: {path}" },
325
+ "database.noTableMeta": { en: "No registered table metadata.", ko: "등록된 테이블 메타가 없습니다." },
326
+ "database.noRows": { en: "No rows.", ko: "행이 없습니다." },
327
+ "database.viewDetail": { en: "View", ko: "상세보기" },
328
+ "database.pagerSummary": {
329
+ en: "Total {total} rows · showing {from}–{to} (limit {limit}, offset {offset})",
330
+ ko: "총 {total}행 · 표시 {from}–{to} (limit {limit}, offset {offset})",
331
+ },
332
+
333
+ // backend-supplied values (resolved via tv())
334
+ "db.label.conversation_entries": { en: "Conversation & job history", ko: "대화·작업 기록" },
335
+ "db.label.message_branch_links": { en: "Message–branch links", ko: "메시지–브랜치 연결" },
336
+ "settings.webhookHint": {
337
+ en: "Each project (bot) has its own webhook_path and token_hash_prefix. The full URL is the public Base joined with webhook_path. While ./run.sh is running, registration/edits refresh it automatically. Manual registration: python scripts/set_webhook.py <Base URL>",
338
+ ko: "각 프로젝트(봇)마다 webhook_path·token_hash_prefix가 다릅니다. 전체 URL은 공개 Base에 webhook_path를 이어붙입니다. ./run.sh 실행 중에는 등록·수정 시 자동 갱신됩니다. 수동 등록: python scripts/set_webhook.py <Base URL>",
339
+ },
340
+ };
341
+
342
+ var REVERSE = {};
343
+ for (var k in CATALOG) {
344
+ if (Object.prototype.hasOwnProperty.call(CATALOG, k)) {
345
+ REVERSE[CATALOG[k].en] = k;
346
+ }
347
+ }
348
+
349
+ function resolve(key, lang) {
350
+ var e = CATALOG[key];
351
+ if (!e) return key;
352
+ return e[lang] || e.en || key;
353
+ }
354
+
355
+ var i18n = {
356
+ lang: window.__UI_LANG__ === "ko" ? "ko" : "en",
357
+ t: function (key, vars) {
358
+ var s = resolve(key, this.lang);
359
+ if (vars) {
360
+ for (var v in vars) {
361
+ if (Object.prototype.hasOwnProperty.call(vars, v)) {
362
+ s = s.split("{" + v + "}").join(String(vars[v]));
363
+ }
364
+ }
365
+ }
366
+ return s;
367
+ },
368
+ tv: function (value) {
369
+ if (value == null) return value;
370
+ var key = REVERSE[value];
371
+ return key ? resolve(key, this.lang) : value;
372
+ },
373
+ apply: function (root) {
374
+ if (this.lang === "en") return;
375
+ var self = this;
376
+ var scope = root || document;
377
+ scope.querySelectorAll("[data-i18n]").forEach(function (el) {
378
+ el.textContent = self.t(el.getAttribute("data-i18n"));
379
+ });
380
+ scope.querySelectorAll("[data-i18n-html]").forEach(function (el) {
381
+ el.innerHTML = self.t(el.getAttribute("data-i18n-html"));
382
+ });
383
+ scope.querySelectorAll("[data-i18n-title]").forEach(function (el) {
384
+ el.setAttribute("title", self.t(el.getAttribute("data-i18n-title")));
385
+ });
386
+ scope.querySelectorAll("[data-i18n-placeholder]").forEach(function (el) {
387
+ el.setAttribute("placeholder", self.t(el.getAttribute("data-i18n-placeholder")));
388
+ });
389
+ scope.querySelectorAll("[data-i18n-aria-label]").forEach(function (el) {
390
+ el.setAttribute("aria-label", self.t(el.getAttribute("data-i18n-aria-label")));
391
+ });
392
+ },
393
+ };
394
+
395
+ document.addEventListener("DOMContentLoaded", function () {
396
+ document.documentElement.lang = i18n.lang;
397
+ i18n.apply(document);
398
+ });
399
+
400
+ window.i18n = i18n;
401
+ })(window);
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <line x1="4" y1="7" x2="20" y2="7" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
3
+ <circle cx="9" cy="7" r="2.5" stroke="#b8c9dc" stroke-width="1.75" fill="none"/>
4
+ <line x1="4" y1="12" x2="20" y2="12" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
5
+ <circle cx="15" cy="12" r="2.5" stroke="#b8c9dc" stroke-width="1.75" fill="none"/>
6
+ <line x1="4" y1="17" x2="20" y2="17" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
7
+ <circle cx="11" cy="17" r="2.5" stroke="#b8c9dc" stroke-width="1.75" fill="none"/>
8
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <ellipse cx="12" cy="6" rx="7" ry="3" stroke="#b8c9dc" stroke-width="1.75"/>
3
+ <path d="M5 6v4c0 1.7 3.1 3 7 3s7-1.3 7-3V6" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
4
+ <path d="M5 14v4c0 1.7 3.1 3 7 3s7-1.3 7-3v-4" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
5
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <path d="M12 3v12m0 0 4-4m-4 4-4-4M5 19h14" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1v-9.5Z"
3
+ stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <path d="M6 7h12M6 12h12M6 17h8" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
2
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v11Z"
3
+ stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M8 13h8M8 17h5" stroke="#b8c9dc" stroke-width="1.75" stroke-linecap="round"/>
5
+ </svg>
@@ -0,0 +1,73 @@
1
+ (function (window) {
2
+ function _escapeHtml(s) {
3
+ return String(s).replace(/[&<>"']/g, function (c) {
4
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
5
+ });
6
+ }
7
+
8
+ async function _parseApiError(r, rawText) {
9
+ var ct = (r.headers.get("content-type") || "").toLowerCase();
10
+ if (ct.includes("application/json")) {
11
+ try {
12
+ var j = JSON.parse(rawText);
13
+ if (j && typeof j.detail === "string") return j.detail;
14
+ if (Array.isArray(j.detail)) {
15
+ return j.detail.map(function (d) {
16
+ return typeof d.msg === "string" ? d.msg : JSON.stringify(d);
17
+ }).join("\n");
18
+ }
19
+ } catch (_) { /* ignore */ }
20
+ }
21
+ return rawText || "Request failed (" + r.status + ")";
22
+ }
23
+
24
+ function renderSummaryGrid(settings, projectsPayload) {
25
+ var grid = document.getElementById("summary-grid");
26
+ if (!grid) return;
27
+ var list = projectsPayload.projects || [];
28
+ var enabled = list.filter(function (p) { return p.enabled; }).length;
29
+ var defName = projectsPayload.default_project || window.i18n.t("common.none");
30
+ var envModel = settings.default_model_env || "—";
31
+ var timeout = settings.job_timeout_seconds_env != null
32
+ ? String(settings.job_timeout_seconds_env) + window.i18n.t("common.secondsSuffix")
33
+ : "—";
34
+ var tokenOk = settings.telegram_bot_token_masked &&
35
+ settings.telegram_bot_token_masked !== "(not set)";
36
+
37
+ grid.innerHTML =
38
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("summary.registered")) + '</p><p class="value">' + list.length + "</p></div>" +
39
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("summary.active")) + '</p><p class="value">' + enabled + "</p></div>" +
40
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("common.fallbackDefault")) + '</p><p class="value" style="font-size:1rem">' + _escapeHtml(defName) + "</p></div>" +
41
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("summary.envModel")) + '</p><p class="value" style="font-size:1rem">' + _escapeHtml(envModel) + "</p></div>" +
42
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("summary.envTimeout")) + '</p><p class="value" style="font-size:1rem">' + _escapeHtml(timeout) + "</p></div>" +
43
+ '<div class="stat-card"><p class="label">' + _escapeHtml(window.i18n.t("summary.botToken")) + '</p><p class="value" style="font-size:0.95rem">' +
44
+ (tokenOk ? _escapeHtml(window.i18n.t("common.set")) : _escapeHtml(window.i18n.t("common.unset"))) +
45
+ '</p><p class="sub">' + _escapeHtml(window.i18n.tv(settings.telegram_bot_token_masked || "")) + "</p></div>";
46
+ }
47
+
48
+ async function loadSummaryGrid(onError) {
49
+ try {
50
+ var rs = await fetch("/api/settings");
51
+ var rp = await fetch("/api/projects");
52
+ var settingsText = await rs.text();
53
+ var projectsText = await rp.text();
54
+ if (!rs.ok) {
55
+ if (onError) onError(await _parseApiError(rs, settingsText));
56
+ return { ok: false };
57
+ }
58
+ if (!rp.ok) {
59
+ if (onError) onError(await _parseApiError(rp, projectsText));
60
+ return { ok: false };
61
+ }
62
+ var settings = JSON.parse(settingsText);
63
+ var projectsPayload = JSON.parse(projectsText);
64
+ renderSummaryGrid(settings, projectsPayload);
65
+ return { ok: true, settings: settings, projectsPayload: projectsPayload };
66
+ } catch (e) {
67
+ if (onError) onError(String(e));
68
+ return { ok: false };
69
+ }
70
+ }
71
+
72
+ window.adminSummary = { renderSummaryGrid: renderSummaryGrid, loadSummaryGrid: loadSummaryGrid };
73
+ })(window);