# Дев-портал — методичка-слепок для самостоятельной сборки

> **Что это.** Один самодостаточный файл, по которому ассистент Claude шаг за
> шагом проведёт тебя через сборку собственного «дев-портала» — корпоративной
> панели для команды разработчиков: вход по паролю, управление проектами, чат с
> Claude, статистика активности, единый дизайн.
>
> **Как пользоваться.** Открой новый чат с Claude (лучше — Claude Code в
> терминале на твоём сервере), вставь этот файл целиком первым сообщением и
> напиши: «Веди меня по этой методичке». Дальше просто отвечай на вопросы
> ассистента и выполняй его шаги.
>
> **Чего здесь нет (намеренно).** Готового кода `server.js`, чужих IP-адресов,
> ключей, паролей, шаблонов проектов и любых данных реальной компании. Всё это
> ты создашь сам под себя — методичка описывает **структуру и логику**, а код
> ассистент напишет вместе с тобой с нуля. Точными значениями даны только
> дизайн-токены (чтобы внешний вид совпал).

---

# §0. Инструкция для Claude-наставника  ⚠️ ЧИТАЙ ПЕРВЫМ

Ты — **наставник**. Этот файл — твоя методичка. Перед тобой ученик, который хочет
собрать свой дев-портал. Возможно, он начинающий («вайб-кодер») — не предполагай
глубоких знаний DevOps. Твоя задача — довести его до рабочего портала, объясняя
по пути, что и зачем.

### Главные правила работы

1. **Говори по-русски.** Технические идентификаторы (имена файлов, команды,
   переменные) — латиницей, как принято.
2. **Веди строго по фазам §3, по одной за раз.** Не прыгай вперёд. В начале
   каждой фазы скажи: цель фазы, что построим, зачем это нужно. В конце —
   прогони чек-лист фазы и спроси разрешения идти дальше.
3. **Строй инкрементально.** Никогда не вываливай готовый большой файл целиком.
   Пиши код небольшими кусками, объясняя каждый. Сначала — минимально рабочая
   версия, потом наращивай.
4. **Проверяй каждый шаг конкретной командой.** После любого изменения дай
   ученику команду, которая докажет, что шаг сработал (`curl`, `systemctl
   status`, открыть URL в браузере). Не говори «готово», пока не увидел
   подтверждение. Сломалось — сначала диагностируй (логи), потом чини.
5. **Объясняй ЗАЧЕМ, а не только КАК.** Ученик должен понимать систему, а не
   копировать вслепую.
6. **Никогда не выдумывай секреты и реальные адреса.** Все чувствительные
   значения ученик вводит сам — см. §6 «Плейсхолдеры». Если значения ещё нет —
   останови фазу и попроси его получить/придумать.
7. **Коммить после каждой фазы.** Локальный `git add . && git commit -m "..."` в
   рабочей папке портала. Это и история, и основа бэкапа (Ф8). В дифф **не
   должны попадать** секреты, `node_modules/`, логи, базы и сессии.
8. **Безопасность — постоянно.** Секреты только на диске, вне git (см. §7).
   Заметил утечку ключа в истории — скажи ученику ротировать ключ.

### Что сделать в самом начале (Фаза 0)

- Поприветствуй, объясни в двух словах, что построим (см. §1) и сколько фаз.
- Собери у ученика его значения и запиши их в **таблицу-конфиг** (см. §6). Без
  них дальше не двигайся — это его домен, сервер, доступ к Claude и (если нужно)
  прокси.
- Заведи рабочую папку проекта и git-репозиторий в ней.

### Если что-то не из методички

Ученик может попросить лишнего (шаблоны проектов, интеграции, боты). Это вне
ядра — мягко предложи сначала довести базовый портал, а расширения добавить
потом. Держи фокус на ядре §3.

### Темп

Это работа на несколько сессий. Если ученик устал — подведи итог, напомни, на
какой фазе остановились, и предложи продолжить позже. Каждая фаза оставляет
портал в рабочем (пусть и неполном) состоянии.

---

# §1. Что мы строим

**Дев-портал** — единая веб-точка входа для команды разработчиков. Сотрудник
заходит по своему паролю и видит:

- **свои проекты** — список рабочих папок со статусами и владельцами, может
  создать новый, открыть, заглянуть в файлы;
- **чат с Claude** — ассистент прямо в панели помогает по работе;
- **общий дизайн** — тёмная/светлая тема, единый стиль, синхронный для всех
  страниц;
- **статистику** (для руководителя) — кто сколько активен, дашборд по команде.

Администратор дополнительно управляет пользователями (приглашения, роли).

### Опыт пользователя (целевой)

1. Заходит на `https://<DOMAIN>/` → редирект на `/panel/`.
2. Видит JS-модал входа (никакого браузерного basic-auth диалога). Вводит логин
   и пароль → попадает в панель.
3. Видит свои проекты, переключает тему, общается с Claude.
4. Админ открывает «Настройки» → приглашает нового сотрудника по одноразовой
   ссылке.

### Обобщённая схема (без реальных адресов)

```
Сотрудник → https://<DOMAIN> → nginx (TLS, Let's Encrypt)
    ├── /             → 302 /panel/
    ├── /panel/       → 127.0.0.1:3010 (Express, публично; /panel/api/ → /api/)
    ├── /code/        → 127.0.0.1:<CODE_PORT> (браузерный VS Code, опц.; auth_request)
    └── /health       → 127.0.0.1:3010 (публично, для проверок)

Исходящие запросы к Claude (если регион заблокирован — см. §3 Ф5):
    Сервер → HTTP-прокси <PROXY_HOST:PROXY_PORT> → api.anthropic.com
```

Портал — **самостоятельное приложение**: одна Express-служба + статика + nginx +
SQLite. Никаких внешних продуктов и Docker-зависимостей для ядра не нужно.

---

# §2. Технологический стек и принципы

| Слой | Чем делаем | Почему так |
|------|-----------|-----------|
| Веб-сервер/прокси | **nginx** | TLS, маршрутизация, `auth_request` для защиты страниц |
| Бэкенд | **Node 20+ / Express** | просто, один процесс, легко читать |
| Фронтенд | **vanilla JS SPA** (без фреймворка и сборки) | ученику видно «как всё работает», нет build-шага |
| Хранилище данных портала | **файловая система** (папки проектов + JSON-метаданные) | прозрачно, легко бэкапить |
| Статистика | **SQLite** (режим WAL) | один файл, без отдельной СУБД |
| Сессии | **session-file-store** (файлы на диске) | переживают рестарт, без Redis |
| Пароли | **htpasswd (bcrypt)** + `users.json` для ролей | стандартно, проверяется одной командой |
| Процессы | **systemd** | автозапуск, рестарт, логи в journald |
| Бэкап | **git** (+ rsync-снапшот) | история и аварийное восстановление |

### Принципы дизайна (детали — §4)

- Палитра **Nord** (тёмная Polar Night / светлая Snow Storm), HSL-токены.
- Шрифт **JetBrains Mono** (моноширинный, «инженерный» вид).
- **`border-radius: 0`** везде — строгая прямоугольная эстетика.
- Тёмная/светлая тема, **синхронизируется между всеми страницами** через
  `localStorage` + событие `storage`.
- **Единый navbar** генерируется из одного файла и подключается на всех
  страницах. Любая правка меню — только в этом файле, появляется везде.

### Принцип изоляции модулей

Каждый модуль — одна понятная задача, общается через явный интерфейс, тестируется
отдельно. Если файл разрастается — это сигнал, что он делает слишком много;
дроби. Для каждого модуля держи в голове: что делает, как им пользоваться, от чего
зависит.

---

# §3. Фазовый план (ядро методички)

> Наставник: идёшь по фазам сверху вниз. Каждая фаза = цель → постройка →
> проверка → чек-лист → коммит → разрешение идти дальше.

## Фаза 0 — Подготовка и сбор данных

**Цель.** Убедиться, что у ученика есть всё нужное, и собрать его значения.

**Что нужно от ученика (проверь наличие, занеси в таблицу §6):**

- **Сервер** — VPS/выделенный с Ubuntu 22.04/24.04, root или sudo, публичный
  IP (`<SERVER_IP>`).
- **Домен** — `<DOMAIN>` с A-записью на `<SERVER_IP>` (DNS уже распространился).
- **Доступ к Claude** — один из:
  - подписка **Claude Max** (OAuth-доступ, тот же, что у Claude Code), **или**
  - **API-ключ Anthropic** (`<ANTHROPIC_ACCESS>`).
- **Прокси для исходящих** — нужен, **только если** из региона ученика
  `api.anthropic.com` недоступен напрямую (напр. РФ). Тогда — внешний
  HTTP/HTTPS-прокси `<PROXY_HOST>:<PROXY_PORT>` (+ `<PROXY_USER>:<PROXY_PASS>`).
  Проверка доступности — в Ф5.
- **Git-удалёнка для бэкапа** — приватный репозиторий `<BACKUP_REMOTE>` (GitHub/
  GitLab/свой), куда позже будем пушить снапшот (Ф8). Необязательно прямо сейчас.

**Постройка.**

1. Заведи рабочую папку портала, например `/opt/dev-portal/` (далее `<PORTAL_DIR>`).
2. `git init` в ней, первый коммит (пустой `README.md` + `.gitignore`).
3. Сразу заполни `.gitignore`: `node_modules/`, `*.log`, `*.sqlite*`, `*.db`,
   `.env*`, `sessions/`, `*.portal-meta.json`, `data/`.

**Проверка / чек-лист Ф0.**
- [ ] `ping <DOMAIN>` резолвится в `<SERVER_IP>`.
- [ ] есть SSH-доступ к серверу.
- [ ] определён способ доступа к Claude.
- [ ] таблица плейсхолдеров §6 заполнена (или явно помечено, что добудем позже).
- [ ] `<PORTAL_DIR>` создана, git-репо инициализирован.

---

## Фаза 1 — Базовый сервер + nginx + TLS

**Цель.** Сервер отвечает на `https://<DOMAIN>/health`.

**Постройка.**
1. Установи Node 20+ (через nodesource или nvm), nginx, certbot.
2. Создай nginx-сайт для `<DOMAIN>`: пока — отдаёт заглушку и `/health`.
3. Выпусти TLS: `certbot --nginx -d <DOMAIN>` (автообновление включится само).
4. Проверь, что HTTP редиректит на HTTPS.

**Логика nginx (опиши и собери вместе с учеником):**
- `server` слушает 443 с сертификатом Let's Encrypt, 80 → редирект на 443.
- `location /health` — публичный, отдаёт 200 (позже проксируем на панель).
- остальные `location` добавим в следующих фазах.

**Проверка / чек-лист Ф1.**
- [ ] `curl -I https://<DOMAIN>/` → 200/302, валидный TLS.
- [ ] `curl https://<DOMAIN>/health` → 200.
- [ ] `nginx -t` без ошибок.
- [ ] коммит.

---

## Фаза 2 — Express-панель + статика SPA

**Цель.** На `:3010` работает Express, nginx проксирует `/panel/`, открывается
пустая оболочка панели.

**Постройка.**
1. В `<PORTAL_DIR>/panel/` инициализируй Node-проект (`npm init`), поставь
   `express`. Структуру держи как в §5 (`server.js`, `public/`, `lib/`).
2. `server.js`: слушает `127.0.0.1:3010`, отдаёт статику из `public/`,
   эндпоинт-заглушку `GET /api/me` → `{username:null,isAdmin:false}`.
3. `public/index.html` — минимальная оболочка SPA (пустой каркас, подключённый
   `js/app.js` и токены §4 подключим в Ф6).
4. Заверни в **systemd-юнит** `dev-portal-panel.service` (WD = `panel/`, рестарт
   при падении, логи в journald, `EnvironmentFile` для будущих переменных).
5. nginx: `location /panel/` → `proxy_pass http://127.0.0.1:3010/`, при этом
   `/panel/api/...` должен приходить в Express как `/api/...` (срез префикса).
   `location = /` → `return 302 /panel/`. `/health` → проксируй на панель.

**Логика (опиши ученику):**
- Панель публична на уровне nginx (вход проверяет само приложение в Ф3), кроме
  защищённых страниц, которые добавим через `auth_request`.
- Префикс `/panel/` снаружи ↔ корень внутри приложения — чтобы код не зависел от
  внешнего пути.

**Проверка / чек-лист Ф2.**
- [ ] `systemctl status dev-portal-panel` → active (running).
- [ ] `curl http://127.0.0.1:3010/api/me` → JSON.
- [ ] в браузере `https://<DOMAIN>/` → редирект на `/panel/`, видна оболочка.
- [ ] коммит.

---

## Фаза 3 — Авторизация (сессии, роли, приглашения)

**Цель.** Вход по паролю через JS-модал, роли admin/user, защищённые страницы,
приглашение новых сотрудников.

**Модель данных (создай файлы вне git, см. §7):**
- `<CONFIG_DIR>/auth/.htpasswd` — логины/пароли (bcrypt). Управляется утилитой
  `htpasswd`.
- `<CONFIG_DIR>/auth/users.json` — роли (`admin` / `user`) и активные
  приглашения.
- `<CONFIG_DIR>/sessions/` — файлы сессий (`session-file-store`).

**Логика входа (опиши и реализуй):**
1. Фронтенд: модал логина (свой HTML+JS, **не** браузерный basic-auth). Отправляет
   `POST /panel/api/login {username, password}`.
2. Бэкенд `POST /api/login`: проверяет пару через `htpasswd -vb` (или bcrypt-
   сравнение), сверяет роль в `users.json`, создаёт сессию → ставит httpOnly-
   куку. TTL ~30 дней, сессии переживают рестарт.
3. `GET /api/me` → `{username, isAdmin}` из сессии (или 401).
4. `POST /api/logout` — уничтожает сессию.

**Защита страниц через nginx `auth_request`:**
- Заведи внутренний `location = /panel-auth-check` (проксирует на `/api/me`).
- Защищённые `location` (напр. `/code/`, приватные страницы) делают
  `auth_request /panel-auth-check;`. При 401 — редирект на `/panel/` (форма
  входа). `auth_basic` в nginx **не используем нигде**.

**Роли:**
- **admin** — управление пользователями, настройки. Понизить другого admin
  нельзя (защита в API). **user** — обычный доступ.
- Добавь защиту эндпоинтов: middleware «требуется сессия» и «требуется admin».

**Приглашения (одноразовые):**
- Admin создаёт приглашение → одноразовый токен → ссылка
  `https://<DOMAIN>/panel/invite.html?token=…`.
- Страница `invite.html`: пользователь задаёт пароль → токен гасится, запись в
  `.htpasswd` + `users.json`, автологин. Токен отзываемый, одноразовый.

**Проверка / чек-лист Ф3.**
- [ ] неавторизованный `GET /panel/api/me` → 401.
- [ ] вход через модал ставит куку, `/api/me` отдаёт `{username,isAdmin}`.
- [ ] защищённый `location` без сессии → редирект на вход.
- [ ] admin видит признак `isAdmin:true`, обычный юзер — нет.
- [ ] приглашение создаётся, по ссылке заводится пользователь, токен гаснет.
- [ ] коммит.

---

## Фаза 4 — Проекты

**Цель.** Сотрудник видит свои проекты, создаёт/архивирует, меняет статус и
владельца, заглядывает в файлы.

**Модель данных:**
- `<DATA_DIR>/projects/<name>/` — папка проекта (имя вида `{компания}-{приложение}`
  или любое kebab-case).
- В каждой — `.portal-meta.json` (владелец, статус, дата создания). **Вне git и
  вне бэкапа.**
- `<DATA_DIR>/deleted/` — архив удалённых (не стираем безвозвратно).

**API (реализуй, см. §5):**
- `GET /api/projects` — список проектов с метаданными (только для авторизованных).
- `POST /api/projects` — создать (имя + начальные метаданные; папка + `mkdir -p`).
- `PATCH /api/projects/:name/status` — статус (напр. `НОВЫЙ / В РАБОТЕ / БОЕВОЙ`).
- `PATCH /api/projects/:name/owner` — сменить владельца.
- `DELETE /api/projects/:name` — переместить в `deleted/` (архив, не rm).
- `GET /api/explore/tree` и `/api/explore/file` — дерево и чтение файла (лимит
  размера, напр. ≤200 КБ; запрет выхода за пределы папки проекта — проверяй
  путь!).

**Фронтенд:**
- Экран «Проекты»: карточки (имя, владелец, статус-бейдж, дата). Inline-смена
  статуса. Кнопка «создать». Простой файловый эксплорер (дерево + просмотр).

**Безопасность (важно!):** при чтении файлов и дереве — строго валидируй путь
(никаких `..`), отдавай только из папки проекта, ограничивай размер.

> **Шаблоны проектов** (готовые заготовки приложений) — это расширение **вне
> ядра**. В базовой версии «создать проект» = создать пустую папку с
> метаданными. Скажи ученику: библиотеку шаблонов можно добавить позже.

**Проверка / чек-лист Ф4.**
- [ ] `GET /panel/api/projects` (с сессией) → список.
- [ ] создание проекта появляется в списке и на диске.
- [ ] смена статуса/владельца сохраняется в `.portal-meta.json`.
- [ ] удаление переносит в `deleted/`, из списка пропадает.
- [ ] эксплорер показывает дерево и файл; путь с `..` отбивается.
- [ ] коммит.

---

## Фаза 5 — Чат с Claude + виджеты хедера

**Цель.** В панели работает чат с Claude; в шапке — индикаторы лимита и нагрузки.

**Доступ к Claude из заблокированного региона (если нужно):**
- Если из сервера `api.anthropic.com` **недоступен напрямую** (напр. РФ) — весь
  исходящий трафик к Anthropic идёт через **внешний HTTP/HTTPS-прокси**
  `<PROXY_HOST>:<PROXY_PORT>`.
- Где прописать прокси:
  - **systemd**: `EnvironmentFile=<CONFIG_DIR>/env/proxy.env` в юните панели
    (переменные `HTTPS_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY_URL`, `NO_PROXY`).
  - **код панели** читает `process.env.HTTPS_PROXY_URL || process.env.HTTPS_PROXY`.
- Файл `proxy.env` — `chmod 600`, **вне git и бэкапа** (см. §7).
- Проверка доступа: `curl -s -o /dev/null -w "%{http_code}\n" --proxy
  "$HTTPS_PROXY" https://api.anthropic.com/v1/messages` → ожидаем `401`
  (значит, до API достучались, не хватает только авторизации — это норма).
- Если регион **не** блокирует Anthropic — прокси не нужен, переменные не ставим.

**Логика чата (опиши и реализуй):**
- `POST /api/chat`: бэкенд запускает Claude CLI как дочерний процесс — примерно:
  `claude -p "<prompt>" --system-prompt "<sys>" --model sonnet --allowed-tools ""
  --output-format text`, в окружении пробрасывает `HTTPS_PROXY` и `HOME`.
- `--allowed-tools ""` — только текст, без инструментов (для чата-ассистента
  этого достаточно и безопаснее).
- В prompt подмешивай **историю** (например, последние ~10 сообщений).
- **Таймаут** ~90 с (холодный старт CLI занимает 20–40 с) — по таймауту аккуратно
  убей процесс и верни понятную ошибку.
- Каждый запрос **логируй в статистику** (таблица `chat_events`, см. Ф7).
- *(Альтернатива CLI — прямой вызов Anthropic API из Node через SDK с
  `ANTHROPIC_API_KEY`. Если у ученика API-ключ, а не Max — идите этим путём,
  логика та же.)*

**Виджеты хедера (опиши; это «датчики» нагрузки):**
- **Лимит сессии Claude** — раз в ~120 с минимальный запрос к API (1 токен,
  модель haiku) с OAuth/ключом; из заголовков ответа читаем остаток лимита
  (`anthropic-ratelimit-*`). Показываем процент и время сброса.
  *(Тонкость: при работе через прокси нативный fetch может конфликтовать с
  прокси-агентом — используй проверенный HTTP-клиент с поддержкой прокси.)*
- **Использование 5h-блока** — периодический расчёт расхода/прогноза (можно
  утилитой `ccusage` или своим счётчиком токенов из `chat_events`).
- **CPU/MEM сервера** — раз в ~30 с из `/proc/stat` и `/proc/meminfo`, копим
  историю (напр. сутки) для мини-графика.

**Проверка / чек-лист Ф5.**
- [ ] (если РФ) `curl --proxy "$HTTPS_PROXY" .../v1/messages` → 401.
- [ ] чат отвечает на сообщение за разумное время.
- [ ] запись появилась в `chat_events`.
- [ ] виджеты в шапке показывают значения (лимит, CPU/MEM).
- [ ] коммит.

---

## Фаза 6 — Дизайн-система

**Цель.** Портал выглядит как «слепок»: Nord-палитра, JetBrains Mono,
`radius:0`, тёмная/светлая тема, единый navbar.

**Постройка (точные значения — §4):**
1. `public/css/tokens.css` — CSS-переменные палитры Nord (light по умолчанию +
   `.dark`), шрифт, радиус. **Значения возьми точно из §4.**
2. `public/shared-header.js` — **единый navbar** для всех страниц: виджеты
   (лимит Claude / нагрузка / сессии), навигация, переключатель темы, settings
   (только admin), logout. Подключается на каждой странице. Любая правка меню —
   только здесь.
3. Переключение темы: класс `.dark` на `<html>`/`<body>`, выбор хранится в
   `localStorage` (ключ напр. `aif-theme`). Слушай событие `storage`, чтобы тема
   менялась синхронно во всех вкладках/страницах.
4. Компоненты в общем `panel.css`: карточки проектов, модалки, кнопки, таблицы,
   статус-бейджи, поля ввода — все в единой стилистике (§4).

**Проверка / чек-лист Ф6.**
- [ ] токены применились, шрифт JetBrains Mono грузится, углы прямые.
- [ ] переключатель темы работает; смена темы на одной странице меняет её и на
      другой открытой странице портала.
- [ ] navbar одинаков на всех страницах и правится из одного файла.
- [ ] коммит.

---

## Фаза 7 — Статистика и дашборд

**Цель.** Портал собирает активность в SQLite; руководитель видит дашборд
`/statistika/`.

**Хранилище:** `<CONFIG_DIR>/stats.db` (SQLite, режим **WAL**, индексы
`(user, ts)`). Таблицы — см. §5:
- `heartbeats` — пульс активной вкладки;
- `chat_events` — каждый запрос в чат (токены, модель, длительность, ok);
- `activity` — клики/нажатия за минуту;
- `vscode_sessions` — сессии в браузерном VS Code;
- `meta` — служебное (позиция в логе, момент старта сбора).

**Сбор данных (опиши и реализуй):**
1. **Heartbeat** — клиентский `js/heartbeat.js` (подключён единым хедером везде).
   Слушает `click/keydown/scroll/mousemove/touchstart` (throttled), раз в ~60 с
   шлёт `POST /panel/api/stats/heartbeat {page, active, clicks, keys}`. На уходе
   со страницы — `navigator.sendBeacon`. Идемпотентен (один экземпляр).
2. **API статистики** — `POST /api/stats/heartbeat`, `GET /api/stats/aggregate`
   (агрегаты за период), helper-модуль `lib/stats.js` (запись/чтение, оценка
   токенов, агрегирование диапазона).
3. **Часы в браузерном VS Code** (если ставишь code-server):
   - nginx в `location /code/` пишет отдельный лог-формат, включающий
     `sid=$cookie_<session_cookie>`.
   - Парсер (`scripts/parse-vscode-log.js`) инкрементально читает лог (позиция в
     `meta`, учёт ротации по inode), резолвит `sid` → пользователь через файл
     сессии, группирует обращения в сессии (разрыв > 5 мин = новая) → пишет в
     `vscode_sessions`.
   - Запускается по таймеру `systemd` раз в ~5 минут.

**Дашборд `/statistika/`:** вкладки (Команда / Вовлечённость / Результативность),
карточка сотрудника, выбор периода и сравнение с прошлым. Доступ — по сессии;
расширенный режим — для admin.

**Проверка / чек-лист Ф7.**
- [ ] `stats.db` создаётся, таблицы есть, режим WAL.
- [ ] активность на странице порождает строки в `heartbeats`/`activity`.
- [ ] чат пишет в `chat_events`.
- [ ] (если code-server) таймер-парсер наполняет `vscode_sessions`.
- [ ] дашборд `/statistika/` рисует агрегаты.
- [ ] коммит.

---

## Фаза 8 — Бэкап и эксплуатация

**Цель.** Сервер бэкапится автоматически; есть гигиена памяти и финальная
приёмка.

**Бэкап:**
1. Приватный git-репозиторий `<BACKUP_REMOTE>` (отдельный SSH-ключ для пушей).
2. Скрипт `scripts/backup-all.sh`: rsync рабочих файлов в рабочую копию репо →
   `git add/commit/push`. **Исключай из снапшота:** `node_modules/`, вложенные
   `.git/`, `.env*`, `*.log`, `*.sqlite*`/`*.db`, `__pycache__`, сессии,
   `.portal-meta.json`, `data/`, `dist`/`build`.
3. Автозапуск: `systemd`-таймер (напр. каждые 4 ч) + ручной запуск. Защита от
   параллельного запуска через `flock`.
4. В бэкап-репо можно класть: код портала, конфиги nginx, твои systemd-юниты и
   обезличенный список пользователей (только `username`+роль, **без** хэшей и
   секретов).

**Гигиена памяти (рекомендуется на слабом сервере):** небольшой watchdog по
таймеру (каждые ~15 мин): убивает зависшие/«осиротевшие» процессы Claude (не
менявшие CPU-время много часов), долгие вспомогательные процессы, при RAM ≥ ~85 %
логирует топ потребителей и гасит самый жадный незащищённый процесс. **Защити**
список критичных (nginx, панель, sshd, systemd, сам watchdog).

**Откат из бэкапа (покажи ученику):** `git log` в рабочей копии репо,
`git show <hash>:<path>` для точечного восстановления файла.

**Проверка / финальный чек-лист §8 (приёмка всего портала):**
- [ ] `https://<DOMAIN>/` открывается, редирект на `/panel/`.
- [ ] вход/выход работают; роли (admin/user) разграничены; приглашение заводит
      пользователя.
- [ ] проекты: список/создание/статус/владелец/архив/эксплорер.
- [ ] чат с Claude отвечает; (если РФ) идёт через прокси.
- [ ] тема переключается и синхронизируется между страницами.
- [ ] статистика пишется, дашборд показывает данные.
- [ ] бэкап-скрипт делает коммит/пуш; таймер активен.
- [ ] секретов нет в git (проверь `git log -p | grep` по ключевым словам).
- [ ] финальный коммит.

🎉 Портал собран.

---

# §4. Дизайн-референс (точные значения)

> Это единственное место с точными значениями — чтобы внешний вид совпал.
> Палитра **Nord**, HSL-триплеты (формат CSS-переменной без `hsl()`-обёртки,
> применяется как `hsl(var(--token))`).

**Шрифт:** `JetBrains Mono` (подключается с Google Fonts; начертания 300–700 +
italic 400). Стек: `'JetBrains Mono', ui-monospace, SFMono-Regular, monospace`.
**Радиус:** `--radius: 0rem` (везде прямые углы).

### Светлая тема — `:root` (Nord Snow Storm)

```
--background:            218 27% 94%;   /* #eceff4 */
--foreground:            220 16% 22%;   /* #2e3440 */
--card:                  218 27% 89%;   /* #e5e9f0 */
--card-foreground:       220 16% 22%;
--popover:               218 27% 89%;
--popover-foreground:    220 16% 22%;
--primary:               213 32% 40%;   /* frost, WCAG-adjusted */
--primary-foreground:    0 0% 100%;
--secondary:             219 28% 84%;   /* #d8dee9 */
--secondary-foreground:  220 16% 22%;
--muted:                 219 28% 84%;
--muted-foreground:      220 17% 28%;   /* #4c566a */
--accent:                213 20% 86%;
--accent-foreground:     213 32% 40%;
--destructive:           355 55% 45%;
--destructive-foreground:0 0% 100%;
--border:                219 18% 72%;
--input:                 219 18% 72%;
--ring:                  213 32% 40%;
--radius:                0rem;

/* Frost (для светлого фона затемнены) */
--frost-1: 176 30% 35%;  --frost-2: 193 40% 38%;
--frost-3: 210 34% 40%;  --frost-4: 213 32% 36%;
/* Aurora (затемнены) */
--red: 355 55% 44%;  --orange: 14 55% 44%;  --yellow: 40 70% 34%;
--green: 92 35% 34%; --purple: 311 28% 40%;
/* Поверхности */
--surface-0: 218 27% 94%; --surface-1: 218 27% 89%;
--surface-2: 219 28% 84%; --surface-3: 219 20% 76%;
```

### Тёмная тема — `.dark` (Nord Polar Night)

```
--background:            213 16% 12%;   /* #1a1e24 */
--foreground:            213 27% 88%;   /* #d8dee9 */
--card:                  217 16% 15.5%; /* #21262e */
--card-foreground:       213 27% 88%;
--popover:               217 16% 15.5%;
--popover-foreground:    213 27% 88%;
--primary:               193 44% 67%;   /* #88c0d0 frost */
--primary-foreground:    213 16% 12%;
--secondary:             216 15% 19%;   /* #282e37 */
--secondary-foreground:  213 27% 88%;
--muted:                 216 15% 19%;
--muted-foreground:      213 14% 65%;   /* #8e99a8 */
--accent:                193 10% 16%;
--accent-foreground:     193 44% 67%;
--destructive:           355 52% 65%;
--destructive-foreground:219 28% 94%;
--border:                217 17% 28%;   /* #3b4252 */
--input:                 217 17% 28%;
--ring:                  193 44% 67%;
--radius:                0rem;

/* Frost (оригинальный Nord) */
--frost-1: 176 25% 65%;  --frost-2: 193 44% 67%;
--frost-3: 210 34% 63%;  --frost-4: 213 32% 52%;
/* Aurora (оригинальный Nord) */
--red: 355 52% 64%;  --orange: 14 51% 63%;  --yellow: 40 71% 73%;
--green: 92 28% 65%; --purple: 311 24% 63%;
/* Поверхности */
--surface-0: 213 16% 12%; --surface-1: 217 16% 15.5%;
--surface-2: 216 15% 19%; --surface-3: 215 14% 22%;
```

### Инвентарь компонентов (стилизуй единообразно)

- **Navbar** (из `shared-header.js`): слева бренд/лого, по центру навигация,
  справа — виджеты (лимит Claude, нагрузка/сессии, CPU/MEM), переключатель темы,
  «Настройки» (только admin), «Выход».
- **Карточка проекта**: имя, владелец, статус-бейдж (цвет из Aurora), дата;
  inline-смена статуса.
- **Модалки**: вход, создание проекта, приглашение, настройки. Затемнённый фон,
  прямоугольная рамка `--border`.
- **Кнопки**: primary (заливка `--primary`), secondary, destructive.
- **Таблицы/списки**: статистика, пользователи.
- **Поля ввода**: рамка `--input`, фокус-кольцо `--ring`.
- **Бейджи статусов**: фон из палитры Aurora под смысл статуса.

Общая эстетика: моноширинный шрифт, прямые углы, спокойный сине-серый Nord,
чёткие границы вместо теней.

---

# §5. Справочник API и данных

### Эндпоинты (подмножество «ядро + статистика»)

| Метод | Путь | Доступ | Назначение |
|-------|------|--------|-----------|
| GET | `/api/me` | — | `{username, isAdmin}` из сессии (или 401) |
| POST | `/api/login` | — | Вход (htpasswd/bcrypt → сессия) |
| POST | `/api/logout` | — | Выход |
| GET | `/api/projects` | user | Список проектов + метаданные |
| POST | `/api/projects` | user | Создать проект |
| PATCH | `/api/projects/:name/status` | user | Сменить статус |
| PATCH | `/api/projects/:name/owner` | user | Сменить владельца |
| DELETE | `/api/projects/:name` | user | Архивировать в `deleted/` |
| GET | `/api/explore/tree` | user | Дерево файлов проекта |
| GET | `/api/explore/file` | user | Чтение файла (лимит размера, защита пути) |
| GET/POST/DELETE | `/api/users[/:u/role]` | admin | Управление пользователями |
| POST/GET/DELETE | `/api/invites[/:token[/accept]]` | mixed | Приглашения (`:token`/`accept` публичны) |
| POST/GET/DELETE | `/api/chat[/history]` | user | Чат с Claude (+ лог в `chat_events`) |
| GET | `/api/claude-status` | user | Лимит сессии Claude (процент, сброс) |
| GET | `/api/system-stats` | user | CPU/MEM за период |
| GET/POST | `/api/stats/{heartbeat,aggregate}` | user | Статистика активности |

> Снаружи всё под префиксом `/panel/api/...`; nginx срезает `/panel` → в Express
> приходит `/api/...`.

### Файловая раскладка (обобщённо)

```
<PORTAL_DIR>/                  код портала (в git)
  ├── panel/
  │   ├── server.js
  │   ├── lib/stats.js         helper статистики (SQLite)
  │   ├── scripts/parse-vscode-log.js
  │   └── public/
  │       ├── index.html       SPA панели
  │       ├── statistika.html  дашборд
  │       ├── invite.html
  │       ├── shared-header.js  ЕДИНЫЙ navbar
  │       ├── css/{tokens,navbar,panel}.css
  │       └── js/{app,heartbeat,statistika}.js
  ├── scripts/                 backup-all.sh и пр.
  └── docs/                    эта методичка и заметки

<DATA_DIR>/                    данные (ВНЕ git)
  ├── projects/<name>/         проекты (+ .portal-meta.json)
  └── deleted/                 архив

<CONFIG_DIR>/                  конфиги и секреты (ВНЕ git, chmod 600)
  ├── auth/{.htpasswd, users.json}
  ├── sessions/                файлы сессий
  ├── stats.db                 SQLite (+ -wal, -shm)
  └── env/proxy.env            прокси-креды (если РФ)
```

### Схема SQLite (`stats.db`, WAL, индексы `(user, ts)`)

| Таблица | Ключевые поля | Назначение |
|---------|---------------|-----------|
| `heartbeats` | `user, page, active, ts` | Пульс активной вкладки |
| `chat_events` | `user, ts, prompt_tokens, response_tokens, model, duration_ms, ok` | Каждый запрос в чат |
| `activity` | `user, page, clicks, keys, ts` | Клики/нажатия за минуту |
| `vscode_sessions` | `user, start_ts, end_ts, duration_sec` | Сессии в браузерном VS Code |
| `meta` | `k, v` | Служебное (позиция в логе, старт сбора) |

---

# §6. Плейсхолдеры (значения ученика)

> Наставник: заполни эту таблицу в Фазе 0. Это **единственные** места, где
> ученик подставляет свои данные. **Никогда не коммить реальные значения** —
> держи их только на диске (см. §7).

| Плейсхолдер | Что это | Где берётся |
|-------------|---------|-------------|
| `<DOMAIN>` | домен портала | у регистратора, A-запись на сервер |
| `<SERVER_IP>` | IP сервера | у хостера |
| `<PORTAL_DIR>` | папка кода (напр. `/opt/dev-portal`) | выбирает ученик |
| `<DATA_DIR>` | папка данных (напр. `/data`) | выбирает ученик |
| `<CONFIG_DIR>` | папка конфигов/секретов (напр. `/data/config`) | выбирает ученик |
| `<CODE_PORT>` | порт code-server (если ставит) | напр. 8181 |
| `<ANTHROPIC_ACCESS>` | API-ключ **или** OAuth Claude Max | в консоли Anthropic / подписка |
| `<PROXY_HOST>:<PROXY_PORT>` | внешний прокси (если регион блокирует) | свой/арендованный прокси |
| `<PROXY_USER>:<PROXY_PASS>` | креды прокси | у провайдера прокси |
| `<BACKUP_REMOTE>` | git-удалёнка для бэкапа | приватный репо GitHub/GitLab/свой |
| `<ADMIN_USER>` / `<ADMIN_PASS>` | первый администратор | придумывает ученик |

---

# §7. Безопасность и эксплуатация

- **Секреты только на диске, вне git.** `.htpasswd`, `users.json`, `proxy.env`,
  сессии, `stats.db`, `.portal-meta.json` — в `.gitignore` и в исключениях
  бэкапа. `proxy.env` и файлы с кредами — `chmod 600`.
- **В диффе не должно быть** секретов, `node_modules/`, логов, баз и сессий.
  Перед коммитом — взгляни на `git status`/`git diff`.
- **Утечка ключа** в истории — ротируй ключ у провайдера и убери значение из
  актуального состояния. Старые коммиты с секретом — повод перевыпустить ключ.
- **Сессии** httpOnly, разумный TTL, secure-кука на HTTPS.
- **Эксплорер файлов** — строгая проверка пути (без `..`), лимит размера, только
  внутри папки проекта.
- **Прокси-креды** держи в одном файле, подключай через `EnvironmentFile`, не
  хардкодь в коде и юнитах.

### Важные команды (шпаргалка)

```bash
# Панель
systemctl {status,restart} dev-portal-panel
journalctl -u dev-portal-panel -f

# nginx
nginx -t && nginx -s reload

# Пользователи
htpasswd -b  <CONFIG_DIR>/auth/.htpasswd <user> <pass>   # добавить/сменить
htpasswd -vb <CONFIG_DIR>/auth/.htpasswd <user> <pass>   # проверить
htpasswd -D  <CONFIG_DIR>/auth/.htpasswd <user>          # удалить

# Проверка доступа к Claude (если через прокси)
set -a; . <CONFIG_DIR>/env/proxy.env; set +a
curl -s -o /dev/null -w "%{http_code}\n" --proxy "$HTTPS_PROXY" \
     https://api.anthropic.com/v1/messages       # ждём 401

# Панель без сессии → 401
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3010/api/me
```

---

# §8. Финальный чек-лист готовности

Собери всё воедино (дублирует приёмку Ф8):

- [ ] `https://<DOMAIN>/` → `/panel/`, TLS валиден.
- [ ] вход/выход; роли admin/user; приглашение работает.
- [ ] проекты: список/создание/статус/владелец/архив/эксплорер; путь защищён.
- [ ] чат с Claude отвечает; (если РФ) через прокси, `curl` даёт 401.
- [ ] дизайн: Nord, JetBrains Mono, radius 0; тема синхронна между страницами;
      navbar из одного файла.
- [ ] статистика пишется (heartbeats/chat_events/activity[/vscode_sessions]),
      дашборд `/statistika/` показывает данные.
- [ ] бэкап делает коммит/пуш по таймеру; секретов в git нет.
- [ ] всё закоммичено.

---

> **Наставнику на финал.** Поздравь ученика, покажи, что и где лежит (§5),
> напомни про гигиену секретов (§7) и предложи следующие шаги вне ядра, если
> захочет: библиотека шаблонов проектов, лендинг-витрина, база знаний/wiki,
> боты-ассистенты в мессенджерах. Это отдельные расширения поверх готового ядра.
