izteamslots 1.2.0 → 1.3.0
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/CHANGELOG.md +7 -0
- package/backend/rpc_server.py +64 -0
- package/package.json +1 -1
- package/ui/src/menus/mainMenus.ts +16 -1
- package/ui/src/menus/types.ts +9 -0
- package/ui/src/screens/MainScreen.ts +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.2.0...v1.3.0) (2026-03-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add Settings menu for API keys and mail providers ([e026db7](https://github.com/izzzzzi/izTeamSlots/commit/e026db7dd01db0544ad7993dfb6ab020490f2ae8))
|
|
7
|
+
|
|
1
8
|
# [1.2.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.6...v1.2.0) (2026-03-06)
|
|
2
9
|
|
|
3
10
|
|
package/backend/rpc_server.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
import traceback
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
from typing import Any
|
|
8
10
|
|
|
9
11
|
from .file_logger import FileLogger
|
|
@@ -51,6 +53,59 @@ class RPCServer:
|
|
|
51
53
|
def _run_job(self, title: str, fn):
|
|
52
54
|
return self.jobs.start(title, fn)
|
|
53
55
|
|
|
56
|
+
_SETTINGS_KEYS = [
|
|
57
|
+
("BOOMLIFY_API_KEY", "Boomlify API ключ"),
|
|
58
|
+
("BOOMLIFY_DOMAIN", "Домен временной почты"),
|
|
59
|
+
("BOOMLIFY_TIME", "Время жизни ящика"),
|
|
60
|
+
("SLOT_MAIL_PROVIDER", "Провайдер почты для слотов"),
|
|
61
|
+
("MAIL_PROVIDER", "Провайдер почты для админов"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _settings_path() -> Path:
|
|
66
|
+
return Path.home() / ".izteamslots" / ".env"
|
|
67
|
+
|
|
68
|
+
def _get_settings(self) -> dict[str, Any]:
|
|
69
|
+
items = []
|
|
70
|
+
for key, label in self._SETTINGS_KEYS:
|
|
71
|
+
value = os.environ.get(key, "")
|
|
72
|
+
masked = ""
|
|
73
|
+
if value:
|
|
74
|
+
masked = value[:4] + "***" + value[-4:] if len(value) > 12 else "***"
|
|
75
|
+
items.append({"key": key, "label": label, "value": value, "masked": masked})
|
|
76
|
+
return {"items": items, "path": str(self._settings_path())}
|
|
77
|
+
|
|
78
|
+
def _set_setting(self, key: str, value: str) -> None:
|
|
79
|
+
allowed = {k for k, _ in self._SETTINGS_KEYS}
|
|
80
|
+
if key not in allowed:
|
|
81
|
+
raise RPCError(-32602, "Unknown setting", {"key": key})
|
|
82
|
+
|
|
83
|
+
env_path = self._settings_path()
|
|
84
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# Read existing lines
|
|
87
|
+
lines: list[str] = []
|
|
88
|
+
if env_path.is_file():
|
|
89
|
+
lines = env_path.read_text(encoding="utf-8").splitlines()
|
|
90
|
+
|
|
91
|
+
# Update or append
|
|
92
|
+
found = False
|
|
93
|
+
for i, line in enumerate(lines):
|
|
94
|
+
if line.startswith(f"{key}=") or line.startswith(f"{key} ="):
|
|
95
|
+
lines[i] = f"{key}={value}"
|
|
96
|
+
found = True
|
|
97
|
+
break
|
|
98
|
+
if not found:
|
|
99
|
+
lines.append(f"{key}={value}")
|
|
100
|
+
|
|
101
|
+
env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
# Update runtime env
|
|
104
|
+
if value:
|
|
105
|
+
os.environ[key] = value
|
|
106
|
+
elif key in os.environ:
|
|
107
|
+
del os.environ[key]
|
|
108
|
+
|
|
54
109
|
def _handle_request(self, req: RPCRequest) -> dict[str, Any]:
|
|
55
110
|
m = req.method
|
|
56
111
|
p = req.params
|
|
@@ -168,6 +223,15 @@ class RPCServer:
|
|
|
168
223
|
)
|
|
169
224
|
return make_success_response(req.request_id, {"job_id": job_id})
|
|
170
225
|
|
|
226
|
+
if m == "settings.get":
|
|
227
|
+
return make_success_response(req.request_id, self._get_settings())
|
|
228
|
+
|
|
229
|
+
if m == "settings.set":
|
|
230
|
+
key = self._as_str_param(p, "key")
|
|
231
|
+
value = self._as_str_param(p, "value")
|
|
232
|
+
self._set_setting(key, value)
|
|
233
|
+
return make_success_response(req.request_id, {"ok": True})
|
|
234
|
+
|
|
171
235
|
if m == "shutdown":
|
|
172
236
|
self.facade.shutdown()
|
|
173
237
|
return make_success_response(req.request_id, {"ok": True})
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
|
|
|
6
6
|
{ id: "menu_admins", label: "Админы", hint: "Доступы, токены, браузерные профили" },
|
|
7
7
|
{ id: "menu_slots", label: "Слоты", hint: "Создание, перелогин и обслуживание" },
|
|
8
8
|
{ id: "menu_mail", label: "Почта", hint: "Ящики и входящие письма" },
|
|
9
|
+
{ id: "menu_settings", label: "Настройки", hint: "API-ключи и провайдеры почты" },
|
|
9
10
|
{ id: "menu_exit", label: "Выход", hint: "Закрыть приложение" },
|
|
10
11
|
]
|
|
11
12
|
}
|
|
@@ -62,6 +63,16 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
|
|
|
62
63
|
}))
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (menuName === "settings") {
|
|
67
|
+
return [
|
|
68
|
+
...(state.settings ?? []).map((s: { key: string; label: string; masked: string }) => ({
|
|
69
|
+
id: `setting:${s.key}`,
|
|
70
|
+
label: s.label,
|
|
71
|
+
hint: s.masked || "не задан",
|
|
72
|
+
})),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
if (menuName === "confirm") {
|
|
66
77
|
return [
|
|
67
78
|
{ id: "confirm_yes", label: "Да, удалить", hint: "Подтвердить необратимое действие", destructive: true },
|
|
@@ -110,6 +121,7 @@ export function getScreenTitle(menuName: MenuName, ctx: MenuContext): string {
|
|
|
110
121
|
if (menuName === "admins") return "Администраторы"
|
|
111
122
|
if (menuName === "slots") return "Слоты"
|
|
112
123
|
if (menuName === "mail") return "Почтовые ящики"
|
|
124
|
+
if (menuName === "settings") return "Настройки"
|
|
113
125
|
if (menuName === "pick_admin") return ctx.title ?? "Выбор администратора"
|
|
114
126
|
if (menuName === "pick_worker") return ctx.title ?? "Выбор слота"
|
|
115
127
|
if (menuName === "pick_account") return ctx.title ?? "Выбор аккаунта"
|
|
@@ -140,6 +152,8 @@ export function getScreenDescription(menuName: MenuName, state: AppState, ctx: M
|
|
|
140
152
|
: "Выберите аккаунт слева, чтобы забрать входящие письма."
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
if (menuName === "settings") return "Настройте API-ключи и провайдеры почты. Enter для редактирования."
|
|
156
|
+
|
|
143
157
|
if (menuName === "pick_admin") return ctx.title ?? "Выберите администратора для следующего действия."
|
|
144
158
|
if (menuName === "pick_worker") return ctx.title ?? "Выберите слот для следующего действия."
|
|
145
159
|
if (menuName === "pick_account") return ctx.title ?? "Выберите аккаунт."
|
|
@@ -200,7 +214,7 @@ export function getTable(menuName: MenuName, state: AppState, ctx: MenuContext):
|
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
export function getHint(menuName: MenuName, _ctx: MenuContext): string {
|
|
203
|
-
if (menuName === "main") return "↑↓ или 1-
|
|
217
|
+
if (menuName === "main") return "↑↓ или 1-5: выбор Enter: открыть r: обновить q: выход"
|
|
204
218
|
if (menuName === "confirm") return "Enter: подтвердить Esc: отмена"
|
|
205
219
|
return "↑↓ или 1-9: выбор Enter: действие Esc: назад r: обновить y: копировать лог"
|
|
206
220
|
}
|
|
@@ -212,6 +226,7 @@ export function parentMenu(menuName: MenuName, ctx: MenuContext): MenuName {
|
|
|
212
226
|
admins: "main",
|
|
213
227
|
slots: "main",
|
|
214
228
|
mail: "main",
|
|
229
|
+
settings: "main",
|
|
215
230
|
pick_admin: "admins",
|
|
216
231
|
pick_worker: "slots",
|
|
217
232
|
pick_account: "mail",
|
package/ui/src/menus/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type MenuName =
|
|
|
3
3
|
| "admins"
|
|
4
4
|
| "slots"
|
|
5
5
|
| "mail"
|
|
6
|
+
| "settings"
|
|
6
7
|
| "pick_admin"
|
|
7
8
|
| "pick_worker"
|
|
8
9
|
| "pick_account"
|
|
@@ -46,10 +47,18 @@ export interface MailAccountRow {
|
|
|
46
47
|
email: string
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export interface SettingItem {
|
|
51
|
+
key: string
|
|
52
|
+
label: string
|
|
53
|
+
value: string
|
|
54
|
+
masked: string
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export interface AppState {
|
|
50
58
|
admins: AdminRow[]
|
|
51
59
|
workers: WorkerRow[]
|
|
52
60
|
accounts: MailAccountRow[]
|
|
61
|
+
settings?: SettingItem[]
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export interface MenuContext {
|
|
@@ -127,6 +127,7 @@ function getMenuOptionDescription(option: MenuOption | undefined): string {
|
|
|
127
127
|
if (optionId === "menu_admins") return "Добавление, логин и обслуживание администраторов."
|
|
128
128
|
if (optionId === "menu_slots") return "Создание слотов, перелогин и работа с профилями."
|
|
129
129
|
if (optionId === "menu_mail") return "Проверка входящих писем админов и слотов."
|
|
130
|
+
if (optionId === "menu_settings") return "API-ключи и настройки провайдеров почты."
|
|
130
131
|
if (optionId === "menu_exit") return "Аккуратно завершить интерфейс."
|
|
131
132
|
if (optionId === "adm_add") return "Создать нового админа: в авто-режиме ввод в TUI, в ручном режиме вход сразу в браузере."
|
|
132
133
|
if (optionId === "adm_relogin") return "Выбрать админа и режим перелогина: автоматический или ручной."
|
|
@@ -372,6 +373,17 @@ export class MainScreen {
|
|
|
372
373
|
this.render()
|
|
373
374
|
}
|
|
374
375
|
|
|
376
|
+
private async loadSettings() {
|
|
377
|
+
try {
|
|
378
|
+
const result = await this.rpc.request<{ items: Array<{ key: string; label: string; value: string; masked: string }> }>("settings.get")
|
|
379
|
+
this.state = { ...this.state, settings: result.items }
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.pushLog(`Ошибка загрузки настроек: ${String(error)}`)
|
|
382
|
+
}
|
|
383
|
+
this.rebuildMenuOptions()
|
|
384
|
+
this.render()
|
|
385
|
+
}
|
|
386
|
+
|
|
375
387
|
private rebuildMenuOptions() {
|
|
376
388
|
this.menuOptions = getMenuOptions(this.menuName, this.state)
|
|
377
389
|
if (this.selectedIndex >= this.menuOptions.length) {
|
|
@@ -833,6 +845,10 @@ export class MainScreen {
|
|
|
833
845
|
if (optionId === "menu_admins") this.menuName = "admins"
|
|
834
846
|
else if (optionId === "menu_slots") this.menuName = "slots"
|
|
835
847
|
else if (optionId === "menu_mail") this.menuName = "mail"
|
|
848
|
+
else if (optionId === "menu_settings") {
|
|
849
|
+
this.menuName = "settings"
|
|
850
|
+
await this.loadSettings()
|
|
851
|
+
}
|
|
836
852
|
else if (optionId === "menu_exit") { await this.exit(); return }
|
|
837
853
|
this.menuCtx = {}
|
|
838
854
|
this.selectedIndex = 0
|
|
@@ -902,6 +918,22 @@ export class MainScreen {
|
|
|
902
918
|
}
|
|
903
919
|
}
|
|
904
920
|
|
|
921
|
+
if (this.menuName === "settings" && optionId.startsWith("setting:")) {
|
|
922
|
+
const key = optionId.split(":", 2)[1]
|
|
923
|
+
const current = this.state.settings?.find(s => s.key === key)
|
|
924
|
+
const newValue = await this.promptInput(`${current?.label ?? key}`, key.includes("KEY"))
|
|
925
|
+
if (newValue === null) return
|
|
926
|
+
try {
|
|
927
|
+
await this.rpc.request("settings.set", { key, value: newValue })
|
|
928
|
+
this.pushLog(`Настройка ${key} обновлена`)
|
|
929
|
+
} catch (error) {
|
|
930
|
+
this.pushLog(`Ошибка: ${String(error)}`)
|
|
931
|
+
}
|
|
932
|
+
await this.loadSettings()
|
|
933
|
+
this.render()
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
905
937
|
if (this.menuName === "mail" && optionId.startsWith("mail_pick:")) {
|
|
906
938
|
const index = Number(optionId.split(":", 2)[1])
|
|
907
939
|
if (!Number.isInteger(index) || index < 0 || index >= this.state.accounts.length) return
|