CI / ruff · mypy · pytest (pull_request) Successful in 1h3m54s
- invites.max_uses/uses: обычное приглашение — 1 вход, демо-QR — до DEMO_INVITE_MAX_USES (50), живёт DEMO_INVITE_TTL_HOURS (3 ч); захват атомарный, UPDATE … WHERE uses < max_uses - users.guest: вошедшие по демо-QR; «🧹 Отключить гостей демо» отзывает всех и гасит демо-QR - «👥 Участники»: кнопка «🎤 Демо-QR», гости в списке — 🎟 - миграция 0003 (ADD COLUMN без batch — SQLite не пересоздаёт users); проверена на Postgres - SPEC A-3a, ADR-017, DEMO.md, .env.example Refs #55 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
5.0 KiB
Python
267 lines
5.0 KiB
Python
"""UI texts from `config/texts.yaml`, validated at startup so a missing key fails fast instead of mid-dialog."""
|
|
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
|
|
from app.db.models import AlertKind
|
|
|
|
|
|
class _Strict(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
|
|
class MenuTexts(_Strict):
|
|
commit: str
|
|
status: str
|
|
logs: str
|
|
alerts: str
|
|
members: str
|
|
back: str
|
|
cancel: str
|
|
placeholder: str
|
|
|
|
|
|
class StartTexts(_Strict):
|
|
welcome: str
|
|
no_access: str
|
|
invite_rejected: str
|
|
|
|
|
|
class HelpTexts(_Strict):
|
|
text: str
|
|
|
|
|
|
class SectionTexts(_Strict):
|
|
cancelled: str
|
|
|
|
|
|
class MembersTexts(_Strict):
|
|
title: str
|
|
line: str
|
|
never: str
|
|
invite: str
|
|
revoke: str
|
|
confirm: str
|
|
confirm_yes: str
|
|
confirm_no: str
|
|
revoked: str
|
|
not_revoked: str
|
|
invite_caption: str
|
|
demo_invite: str
|
|
demo_invite_caption: str
|
|
revoke_guests: str
|
|
confirm_guests: str
|
|
guests_revoked: str
|
|
|
|
|
|
class CommitTexts(_Strict):
|
|
title: str
|
|
empty: str
|
|
page: str
|
|
up: str
|
|
prev: str
|
|
next: str
|
|
dir_button: str
|
|
file_button: str
|
|
new_file: str
|
|
new_dir: str
|
|
file_card: str
|
|
last_commit: str
|
|
no_commits: str
|
|
view: str
|
|
replace: str
|
|
delete: str
|
|
to_dir: str
|
|
soon: str
|
|
unavailable: str
|
|
sent_as_file: str
|
|
ask_file_name: str
|
|
ask_dir_name: str
|
|
bad_name: str
|
|
exists: str
|
|
protected: str
|
|
ask_content: str
|
|
too_big: str
|
|
binary: str
|
|
encoding: str
|
|
syntax: str
|
|
fix: str
|
|
force: str
|
|
added: str
|
|
modified: str
|
|
deleted: str
|
|
draft_button: str
|
|
draft_title: str
|
|
draft_empty: str
|
|
unstage: str
|
|
draft_full: str
|
|
in_draft_new: str
|
|
in_draft_modified: str
|
|
in_draft_deleted: str
|
|
mark_new: str
|
|
mark_modified: str
|
|
mark_deleted: str
|
|
op_add: str
|
|
op_modify: str
|
|
op_delete: str
|
|
message_button: str
|
|
message_current: str
|
|
message_missing: str
|
|
ask_message: str
|
|
bad_message: str
|
|
push_button: str
|
|
commit_button: str
|
|
edit_message: str
|
|
push_confirm: str
|
|
push_go: str
|
|
pushing: str
|
|
pushed: str
|
|
conflict: str
|
|
overwrite: str
|
|
push_empty: str
|
|
push_limit: str
|
|
push_failed: str
|
|
|
|
|
|
class PipelineTexts(_Strict):
|
|
ci_pending: str
|
|
ci_running: str
|
|
ci_success: str
|
|
ci_failed: str
|
|
ci_timeout: str
|
|
cd_waiting: str
|
|
cd_running: str
|
|
cd_success: str
|
|
cd_failed: str
|
|
cd_not_run: str
|
|
cd_superseded: str
|
|
notify_ci_ok: str
|
|
notify_ci_failed: str
|
|
notify_ci_timeout: str
|
|
notify_cd_ok: str
|
|
deploy_failed: str
|
|
removed: str
|
|
log_file: str
|
|
|
|
|
|
class StatusTexts(_Strict):
|
|
title: str
|
|
line: str
|
|
empty: str
|
|
unavailable: str
|
|
card: str
|
|
none: str
|
|
no_limit: str
|
|
check_button: str
|
|
refresh: str
|
|
to_list: str
|
|
to_card: str
|
|
checking: str
|
|
report_title: str
|
|
report_container: str
|
|
report_line: str
|
|
check_running: str
|
|
check_health: str
|
|
check_restarts: str
|
|
check_cpu: str
|
|
check_mem: str
|
|
check_log_errors: str
|
|
check_http: str
|
|
days: str
|
|
hours: str
|
|
minutes: str
|
|
seconds: str
|
|
restart: str
|
|
restarted: str
|
|
restart_failed: str
|
|
|
|
|
|
class LogsTexts(_Strict):
|
|
title: str
|
|
empty: str
|
|
unavailable: str
|
|
container: str
|
|
tail: str
|
|
since: str
|
|
minutes: str
|
|
hours: str
|
|
grep_button: str
|
|
agent_button: str
|
|
agent_ask: str
|
|
agent_off: str
|
|
agent_thinking: str
|
|
agent_refused: str
|
|
agent_quota: str
|
|
agent_unavailable: str
|
|
agent_not_understood: str
|
|
head_agent: str
|
|
to_list: str
|
|
to_container: str
|
|
ask_grep: str
|
|
head_tail: str
|
|
head_grep: str
|
|
nothing: str
|
|
truncated: str
|
|
partial: str
|
|
skipped: str
|
|
sent_as_file: str
|
|
err_empty: str
|
|
err_quotes: str
|
|
err_flag: str
|
|
err_context: str
|
|
err_extra: str
|
|
err_too_long: str
|
|
err_regex: str
|
|
|
|
|
|
class AlertsTexts(_Strict):
|
|
kinds: dict[AlertKind, str]
|
|
details: dict[AlertKind, str]
|
|
message: str
|
|
repeats: str
|
|
resolved: str
|
|
logs_button: str
|
|
status_button: str
|
|
list_button: str
|
|
list_title: str
|
|
list_empty: str
|
|
list_line: str
|
|
active_mark: str
|
|
resolved_mark: str
|
|
page: str
|
|
prev: str
|
|
next: str
|
|
card: str
|
|
ongoing: str
|
|
to_list: str
|
|
host: str
|
|
gone: str
|
|
|
|
@field_validator("kinds", "details")
|
|
@classmethod
|
|
def _every_kind(cls, value: dict[AlertKind, str]) -> dict[AlertKind, str]:
|
|
missing = set(AlertKind) - set(value)
|
|
if missing:
|
|
raise ValueError(f"no text for {sorted(missing)}")
|
|
return value
|
|
|
|
|
|
class Texts(_Strict):
|
|
menu: MenuTexts
|
|
start: StartTexts
|
|
help: HelpTexts
|
|
section: SectionTexts
|
|
members: MembersTexts
|
|
commit: CommitTexts
|
|
pipeline: PipelineTexts
|
|
status: StatusTexts
|
|
logs: LogsTexts
|
|
alerts: AlertsTexts
|
|
|
|
|
|
def load_texts(config_dir: Path) -> Texts:
|
|
with (config_dir / "texts.yaml").open(encoding="utf-8") as f:
|
|
return Texts.model_validate(yaml.safe_load(f))
|