Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .helm/templates/admin/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ spec:
- sh
- -c
- |
mkdir -p /data/output /data/logs /data/posts
mkdir -p /data/output /data/logs /data/posts /data/prompts
test -f /data/mkdocs.yml || cp /app/mkdocs-default.yml /data/mkdocs.yml
cp -n /app/prompts/*.txt /data/prompts/ 2>/dev/null || true
volumeMounts:
- name: data
mountPath: /data
Expand Down
5 changes: 3 additions & 2 deletions admin/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/generator \
CONFIG_PATH=/data/config.yml \
OUTPUT_DIR=/data/output \
PROMPTS_DIR=/app/prompts
PROMPTS_DIR=/data/prompts

RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/* \
Expand All @@ -30,4 +30,5 @@ EXPOSE 8501
CMD ["streamlit", "run", "admin/admin_app.py", \
"--server.port=8501", \
"--server.address=0.0.0.0", \
"--server.headless=true"]
"--server.headless=true", \
"--client.toolbarMode=minimal"]
14 changes: 8 additions & 6 deletions admin/admin_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@

pages = {
"Генерация": [
st.Page("pages/generation.py", title="Генерация", icon=":material/play_circle:")
st.Page("pages/generation.py", title="Запуск", icon=":material/play_circle:"),
st.Page("pages/schedule.py", title="Расписание", icon=":material/schedule:"),
st.Page("pages/logs.py", title="Логи", icon=":material/terminal:"),
],
"Контент": [
st.Page("pages/posts.py", title="Посты", icon=":material/article:"),
st.Page("pages/settings.py", title="Настройки", icon=":material/tune:"),
st.Page("pages/config_editor.py", title="Конфиг YAML", icon=":material/code:"),
st.Page("pages/mkdocs_editor.py", title="Блог", icon=":material/web:"),
],
"Система": [
st.Page("pages/schedule.py", title="Расписание", icon=":material/schedule:"),
st.Page("pages/logs.py", title="Логи", icon=":material/terminal:"),
"Настройки": [
st.Page("pages/settings.py", title="Основные", icon=":material/tune:"),
st.Page("pages/repos.py", title="Репозитории", icon=":material/folder_copy:"),
st.Page("pages/prompts.py", title="Промпты", icon=":material/edit_note:"),
st.Page("pages/config_editor.py", title="Конфиг YAML", icon=":material/code:"),
],
}

Expand Down
65 changes: 65 additions & 0 deletions admin/pages/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Страница редактирования промптов для LLM."""

from pathlib import Path

import streamlit as st

from scheduler import DATA_DIR

# Промпты на PVC (редактируемые), фолбэк — встроенные в образ
PROMPTS_DIR = DATA_DIR / "prompts"
DEFAULT_PROMPTS_DIR = Path("/app/prompts")

PROMPT_FILES = {
"analyze_repo.txt": "Анализ репозитория",
"summarize_file.txt": "Суммаризация большого файла",
"synthesize.txt": "Синтез фич из нескольких репозиториев",
"generate_post.txt": "Генерация поста",
"review_post.txt": "Ревью поста",
}

st.header("Промпты")
st.caption(
"Шаблоны запросов к LLM."
" Переменные в `{фигурных скобках}` подставляются автоматически."
)


def _read_prompt(filename: str) -> str:
pvc_path = PROMPTS_DIR / filename
if pvc_path.exists():
return pvc_path.read_text(encoding="utf-8")
default_path = DEFAULT_PROMPTS_DIR / filename
if default_path.exists():
return default_path.read_text(encoding="utf-8")
return ""


def _save_prompt(filename: str, content: str) -> None:
PROMPTS_DIR.mkdir(parents=True, exist_ok=True)
(PROMPTS_DIR / filename).write_text(content, encoding="utf-8")


tabs = st.tabs(list(PROMPT_FILES.values()))

for tab, (filename, label) in zip(tabs, PROMPT_FILES.items()):
with tab:
current = _read_prompt(filename)
edited = st.text_area(
filename, value=current, height=300, key=f"prompt_{filename}"
)

col1, col2 = st.columns([1, 4])
with col1:
if st.button("Сохранить", key=f"save_{filename}", type="primary"):
_save_prompt(filename, edited)
st.success("Сохранено!")
with col2:
if st.button("Сбросить к дефолту", key=f"reset_{filename}"):
default = DEFAULT_PROMPTS_DIR / filename
if default.exists():
_save_prompt(filename, default.read_text(encoding="utf-8"))
st.success("Сброшено к значению по умолчанию!")
st.rerun()
else:
st.warning("Дефолтный промпт не найден")
193 changes: 193 additions & 0 deletions admin/pages/repos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Страница управления репозиториями."""

import streamlit as st
import yaml

from scheduler import DATA_DIR

CONFIG_PATH = DATA_DIR / "config.yml"

st.header("Репозитории")


def _load_config() -> dict:
if CONFIG_PATH.exists():
return yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8")) or {}
return {}


def _save_config(data: dict) -> None:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(
yaml.dump(data, allow_unicode=True, default_flow_style=False),
encoding="utf-8",
)


def _lines_to_list(text: str) -> list[str]:
return [line.strip() for line in text.strip().splitlines() if line.strip()]


def _render_repo(repo: dict, idx: int) -> dict | None:
"""Рендерит форму репозитория. Возвращает None если удалён."""
name = repo.get("name") or repo.get("url", f"repo-{idx}")
with st.expander(f"**{name}** — {repo.get('url', '')}", expanded=False):
col1, col2 = st.columns([4, 1])
with col1:
url = st.text_input(
"URL *",
value=repo.get("url", ""),
key=f"r{idx}_url",
help="HTTPS-ссылка на git-репозиторий",
)
with col2:
if st.button("Удалить", key=f"r{idx}_del", type="secondary"):
return None

col1, col2, col3 = st.columns(3)
with col1:
rname = st.text_input(
"Имя",
value=repo.get("name", ""),
key=f"r{idx}_n",
help="Отображаемое имя. Если пусто — берётся из URL",
)
with col2:
branch = st.text_input(
"Ветка", value=repo.get("branch", "main"), key=f"r{idx}_br"
)
with col3:
token = st.text_input(
"Токен",
value=repo.get("token", ""),
type="password",
key=f"r{idx}_tok",
help="Персональный токен для этого репозитория. "
"Перекрывает глобальный Git Token",
)

col1, col2 = st.columns(2)
with col1:
modes = ["period", "tag"]
mode_idx = modes.index(repo.get("diff_mode", "period"))
diff_mode = st.selectbox(
"Режим diff",
options=modes,
index=mode_idx,
key=f"r{idx}_dm",
help="period — diff за N дней. "
"tag — diff между git-тегами (релизами)",
)
with col2:
lookback = st.number_input(
"Глубина поиска тега (периодов)",
value=repo.get("tag_lookback_periods", 1),
min_value=1,
max_value=10,
key=f"r{idx}_lb",
disabled=diff_mode != "tag",
help="Сколько периодов назад искать базовый тег. "
"Увеличьте, если между релизами большие паузы",
)

col1, col2, col3 = st.columns(3)
with col1:
r_noise = st.text_area(
"Шум",
value="\n".join(repo.get("noise_patterns", [])),
height=100,
key=f"r{idx}_np",
help="Дополнительные паттерны к глобальным. " "По одному на строку",
)
with col2:
r_prio = st.text_area(
"Приоритетные",
value="\n".join(repo.get("priority_patterns", [])),
height=100,
key=f"r{idx}_pp",
)
with col3:
r_sec = st.text_area(
"Вторичные",
value="\n".join(repo.get("secondary_patterns", [])),
height=100,
key=f"r{idx}_sp",
)

if not url:
st.warning("URL — обязательное поле")

result: dict[str, object] = {
"url": url,
"branch": branch,
"diff_mode": diff_mode,
}
if rname:
result["name"] = rname
if token:
result["token"] = token
if diff_mode == "tag":
result["tag_lookback_periods"] = lookback
np = _lines_to_list(r_noise)
pp = _lines_to_list(r_prio)
sp = _lines_to_list(r_sec)
if np:
result["noise_patterns"] = np
if pp:
result["priority_patterns"] = pp
if sp:
result["secondary_patterns"] = sp
return result


config = _load_config()
repos: list[dict] = config.get("repos", [])

updated: list[dict] = []
for i, repo in enumerate(repos):
result = _render_repo(repo, i)
if result is not None:
updated.append(result)

# --- Add new ---
st.divider()
with st.expander("Добавить репозиторий", icon=":material/add:"):
new_url = st.text_input(
"URL *", key="new_url", placeholder="https://github.com/..."
)
col1, col2, col3 = st.columns(3)
with col1:
new_name = st.text_input("Имя", key="new_name")
with col2:
new_branch = st.text_input("Ветка", value="main", key="new_branch")
with col3:
new_token = st.text_input("Токен", type="password", key="new_token")

if st.button("Добавить", type="primary"):
if not new_url:
st.error("URL — обязательное поле")
else:
new_repo: dict[str, object] = {
"url": new_url,
"branch": new_branch,
}
if new_name:
new_repo["name"] = new_name
if new_token:
new_repo["token"] = new_token
updated.append(new_repo)
config["repos"] = updated
_save_config(config)
st.success("Репозиторий добавлен!")
st.rerun()

# --- Save all ---
st.divider()
if st.button("Сохранить изменения", type="primary", use_container_width=True):
invalid = [r for r in updated if not r.get("url")]
if invalid:
st.error("У всех репозиториев должен быть указан URL")
else:
config["repos"] = updated
_save_config(config)
st.success("Репозитории сохранены!")
Loading