Compare commits

..

2 Commits

3 changed files with 50 additions and 146 deletions

View File

@ -1,4 +1,4 @@
<!-- vibe42-agents-version: v39-skills-2026-09-11 -->
<!-- vibe42-agents-version: v43-apps-publish-2026-09-15 -->
# Vibe42 — учебная песочница: сайты, боты и первые приложения
Workspace юзера `tore`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение.
@ -21,14 +21,15 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
| Что хочет юзер | Стек (ЖЁСТКО) | Навык / рецепт — загрузить ПЕРЕД работой |
|----------------|---------------|--------|
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** в проекте УЖЕ лежит стартовый `index.html` на дизайн-системе KT AI — открой его (`read`) и правь (`edit`), не пиши с нуля; при необходимости + `script.js`. Ванильный JS, БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap/Google Fonts и без своих hex-цветов — только классы `kt-ai-*` и токены `var(--kt-ai-…)`. | навык `landing-templates` (шаблоны секций A–E) + `design.md` → правка готового `index.html`, публикация в `pages` |
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist (форма на нём — только если ответы никуда сохранять не надо; иначе строка «Простое приложение с сервером») | **Статика:** в проекте УЖЕ лежит стартовый `index.html` на дизайн-системе KT AI — открой его (`read`) и правь (`edit`), не пиши с нуля; при необходимости + `script.js`. Ванильный JS, БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap/Google Fonts и без своих hex-цветов — только классы `kt-ai-*` и токены `var(--kt-ai-…)`. | навык `landing-templates` (шаблоны секций A–E) + `design.md` → правка готового `index.html`, публикация в `pages` |
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | навык `backend-run` → Рецепт T |
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навык `backend-run` → Рецепт B |
| Простое приложение с сервером: форма регистрации/заявки/опроса, ответы которой надо **сохранять**, API, счётчик, запись на время (лендинг с рабочей формой — тоже сюда) | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навыки `kt-design-system` (архетип экрана) → `backend-run` → Рецепт B |
| **Полноценное приложение:** кабинет, панель, CRM-lite, несколько экранов, живые данные — юзер явно просит «приложение/систему», а не страницу | **Express + Vite/React (фронт собирается в `dist/`, сервер его раздаёт с одного порта) + `data.json`.** Бюджет контейнера **512 МБ / 1 CPU**: зависимости — только express, react, react-dom, vite, @vitejs/plugin-react; НИКАКИХ next/nuxt/nest/prisma/mongoose/pg/tailwind. Сборку и установку делает `run` (scripts.build + scripts.start), сам в шелле не собирай | навыки `kt-design-system` (архетип экрана) → `backend-run` → Рецепт A |
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | навык `backend-run` → блок «ИИ» |
| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | навык `backend-run` → раздел «Агент Alem» |
| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | навык `presentations` — бери оттуда скелет целиком |
| Разобрать документы юзера (Excel / Word / PDF): свод, отчёт, выжимка | Предустановленные `xlsx` / `mammoth` / `pdf-parse` | навык `office-files` |
| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | навыки `backend-run` (Рецепт B) + `accounting-system` |
| **Учёт и трекер:** склад, заявки, журнал, задачи, СИЗ, путевые листы, сбор статистики, отчётность | **Node.js + `node:http` + `data.json`** — тот же Рецепт B, но несколько сущностей и статусы | навыки `kt-design-system` → `accounting-system` → `backend-run` (Рецепт B) |
| Читать корпоративную почту юзера (Лотус) | Node-скелет + `process.env.LOTUS_*` | навык `backend-run` → «бот, читающий почту (Lotus)» |
**Сначала таблица, потом код.** Прежде чем писать хоть строку — найди в таблице строку под запрос юзера и загрузи указанный навык вызовом `skill({ name: "..." })`: рецепты и скелеты лежат там, а не здесь. Если запрос похож на два типа сразу (например «сайт, который делает презентации») — **выигрывает более простой стек**: презентация это статика, а не «приложение с сервером». Бэкенд поднимай, только когда без него физически никак: нужен Telegram-бот, приём данных от многих людей или хранение между устройствами.
@ -39,6 +40,7 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле.
2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой.
3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле.
4. **Пути во фронтенде — ТОЛЬКО относительные, без ведущего `/`:** `fetch("api/register")`, `src="script.js"`, `href="list.html"`. Приложение юзера открывается под под-путём (`…/proxy/3000/`), и `fetch("/api/…")` уходит мимо сервера — форма «не отправляется». В сервере наоборот сравнивай `url` с `/api/...` как обычно.
---
## 🍳 Готовые рецепты кода → навык `backend-run`
@ -100,7 +102,7 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
Файл памяти: `/srv/opencode/workspaces/users/tore/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`.
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/tore/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/tore/.mimo/memory.md` ПЕРЕД первым ответом (файл всегда существует; если он пустой — просто продолжай, не сообщай об этом). Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь:
- как обращаться к юзеру (имя/ник);
@ -185,6 +187,8 @@ Workspace юзера `tore`. Это **учебная среда**, где обы
---
## 🚀 ПУБЛИКАЦИЯ В ИНТЕРНЕТ — ТОЛЬКО ПО КНОПКЕ «Опубликовать»
**Проект с сервером (server.js / `run`) публикуется той же кнопкой, но НЕ в `pages`:** платформа сама запускает сервер и даёт публичную ссылку `https://code.vibe42.kz/apps/tore/<repo>/` (открывается без входа, формы и `api/…` работают, уснувшее приложение просыпается по первому заходу). Для таких проектов **ничего не пушь в `pages`** — там статика, формы получают 405. Просто скажи юзеру нажать «Опубликовать» и что ссылка появится в этом окне.
**Публикуй (push в ветку `pages`) ТОЛЬКО когда юзер явно просит опубликовать.** Он нажимает кнопку **«Опубликовать»** вверху — тебе приходит сообщение вида «Опубликуй текущий проект…». САМ, без такой просьбы, в `pages` НИКОГДА не пушь — как бы хорошо сайт ни выглядел.
Когда юзер попросил опубликовать:
@ -273,22 +277,23 @@ git push origin HEAD:pages
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
Публикация в Pages — статика, `server.js` там НЕ работает. Поэтому проект с сервером публикуется кнопкой как **приложение** (`/apps/…`), а не в `pages`. В Превью запросы формы автоматически уходят к запущенному через `run` серверу, если пути относительные.
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
- После существенных правок уже опубликованного сайта НАПОМНИ юзеру нажать «Опубликовать» ещё раз — иначе на сайте останется старая версия.
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- **Публикация — только кнопкой «Опубликовать» в интерфейсе; API git-хостинга (Gitea/GitLab Pages) не вызывай** — публикацией управляет Pages-шлюз платформы, из проекта достаточно push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ `npm install` руками в шелле и тяжёлые зависимости (mongoose/pg/prisma/next/nuxt/nest) — контейнер юзера 512 МБ
- ❌ Поднимать бэкенд там, где хватает статики (лендинг, визитка, презентация) — сверься с таблицей стека
- ❌ Ставить прод-фреймворки (express / nest / next / django) — сервер только на `node:http`
- ❌ Ставить тяжёлые фреймворки (nest / next / nuxt / django) — сервер на `node:http`, а для полноценного приложения (Рецепт A) — express
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
@ -304,7 +309,7 @@ git push origin HEAD:pages
---
## 🎨 design.md + дизайн-система KT AI
Рядом лежит `design.md` — **прочитай его перед первой строкой вёрстки**. В нём каркас `index.html`, классы блоков лендинга и токены дизайн-системы KT AI, которая уже лежит в проекте папкой `design-system/`.
Рядом лежит `design.md` — **прочитай его перед первой строкой вёрстки**. `design.md` — про сайты и лендинги. Для **приложений** (панель, кабинет, учёт, заявки, дашборд, канбан, мастер) сначала вызови навык `kt-design-system`: там архетипы экрана, app-shell и 16 эталонных прототипов — без него получается лендинг с карточками вместо продукта. В нём каркас `index.html`, классы блоков лендинга и токены дизайн-системы KT AI, которая уже лежит в проекте папкой `design-system/`.
**Порядок для сайта/страницы — ровно такой:** 1) `read design.md`, 2) `read index.html` — стартовый каркас на ДС уже лежит в проекте, 3) `edit index.html` под задачу юзера. Отдельный `style.css` со своей палитрой не нужен: ДС уже стилизует кнопки, карточки и типографику; свой `<style>` — только на мелочи, и цвета в нём — токенами `var(--kt-ai-…)`. Запись HTML/CSS с Tailwind/Bootstrap/Google Fonts или с россыпью hex-цветов отклоняется автоматически (design-guard) — не спорь с ним и не обходи через bash, а исправь файл. Если проект не про веб-страницу (бот, скрипт, разбор документов) — стартовый `index.html` просто удали.

View File

@ -1,4 +1,4 @@
<!-- vibe42-design-version: v4-ds-scaffold-2026-09-11 -->
<!-- vibe42-design-version: v5-ktds-apps-2026-09-15 -->
# Дизайн-система KT AI — по ней собирается весь UI
В папке проекта лежит `design-system/` — это **дизайн-система KT AI**
@ -152,6 +152,10 @@ cp -r /srv/opencode/design-system/kt-ai design-system
## Не лендинг, а приложение (дашборд, таблица, очередь заявок, чат)
> Для приложений есть отдельный слой дизайн-системы: архетипы экрана, app-shell, KPI, таблицы, drawer и 16 эталонных прототипов.
> Вызови навык `skill({ name: "kt-design-system" })` — он важнее этого раздела.
Тогда экран собирается **не вёрсткой руками, а конфигом**:
1. Возьми `design-system/templates/kt-ai-app-shell.html` — самодостаточный HTML-каркас приложения

View File

@ -41,30 +41,7 @@
color: var(--kt-ai-fg-muted);
margin-top: var(--kt-ai-space-2);
}
.registrations-list {
margin-top: var(--kt-ai-space-8);
}
.reg-item {
background: var(--kt-ai-card-bg);
border: 1px solid var(--kt-ai-card-border);
border-radius: var(--kt-ai-radius-xl);
padding: var(--kt-ai-space-5);
margin-bottom: var(--kt-ai-space-4);
display: grid;
grid-template-columns: 1fr auto;
gap: var(--kt-ai-space-3);
align-items: center;
}
.reg-name {
font-weight: var(--kt-ai-font-semibold);
}
.reg-meta {
color: var(--kt-ai-fg-muted);
font-size: var(--kt-ai-text-sm);
}
.track-badge {
justify-self: end;
}
.success-message {
background: var(--kt-ai-bg-soft);
border: 2px solid var(--kt-ai-primary);
@ -73,17 +50,34 @@
text-align: center;
margin-top: var(--kt-ai-space-8);
}
.empty-state {
text-align: center;
padding: var(--kt-ai-space-12);
color: var(--kt-ai-fg-muted);
}
.filter-bar {
display: flex;
gap: var(--kt-ai-space-4);
margin-bottom: var(--kt-ai-space-6);
flex-wrap: wrap;
align-items: center;
@media (max-width: 768px) {
.kt-ai-wrap {
padding-left: var(--kt-ai-space-4);
padding-right: var(--kt-ai-space-4);
}
.hero-gradient {
padding: var(--kt-ai-space-6);
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--kt-ai-space-4);
}
.kt-ai-grid {
grid-template-columns: 1fr !important;
}
.kt-ai-hero-actions {
flex-direction: column;
width: 100%;
}
.kt-ai-hero-actions .kt-ai-btn {
width: 100%;
}
.kt-ai-pagehead-in {
flex-wrap: wrap;
}
.kt-ai-pagehead-in .kt-ai-btn {
margin-left: var(--kt-ai-space-2);
}
}
</style>
</head>
@ -311,48 +305,7 @@
</div>
</section>
<section id="participants" class="kt-ai-section" data-divider="true">
<div class="kt-ai-wrap" style="max-width:900px">
<div class="kt-ai-section-head">
<h2>Зарегистрированные участники</h2>
<p>Посмотри, кто уже присоединился к хакатону</p>
</div>
<div class="filter-bar">
<select id="filter-track" class="kt-ai-select" data-size="sm">
<option value="all">Все направления</option>
<option value="AI / Machine Learning">AI / Machine Learning</option>
<option value="IoT & Smart Devices">IoT & Smart Devices</option>
<option value="Cybersecurity">Cybersecurity</option>
<option value="Big Data & Analytics">Big Data & Analytics</option>
<option value="Cloud & DevOps">Cloud & DevOps</option>
<option value="Telecom Solutions">Telecom Solutions</option>
</select>
<select id="filter-experience" class="kt-ai-select" data-size="sm">
<option value="all">Весь опыт</option>
<option value="Junior (до 1 года)">Junior</option>
<option value="Middle (1-3 года)">Middle</option>
<option value="Senior (3+ лет)">Senior</option>
<option value="Student">Студент</option>
</select>
<button id="clear-filters" class="kt-ai-btn" data-variant="ghost" data-size="sm">Сбросить фильтры</button>
<span class="kt-ai-spacer"></span>
<span id="count-display" class="kt-ai-chip">0 участников</span>
</div>
<div id="registrations-list" class="registrations-list">
<!-- Сюда JavaScript вставит список -->
</div>
<div id="empty-state" class="empty-state" style="display:none">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--kt-ai-fg-faint)" stroke-width="1.5" style="margin-bottom:var(--kt-ai-space-4)">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<p>Пока нет участников с такими фильтрами</p>
<p style="font-size:var(--kt-ai-text-sm);margin-top:var(--kt-ai-space-2)">Стань первым!</p>
</div>
</div>
</section>
<footer class="kt-ai-wrap kt-ai-pagefoot">
<span>© 2026 Казахтелеком. КТ Опен Хакатон</span>
@ -395,46 +348,7 @@ function saveRegistration(reg) {
return regs;
}
// === Рендеринг списка участников ===
function renderRegistrations(filters = { track: 'all', experience: 'all' }) {
const regs = getRegistrations();
const container = document.getElementById('registrations-list');
const emptyState = document.getElementById('empty-state');
const countDisplay = document.getElementById('count-display');
let filtered = regs.filter(r => {
const trackMatch = filters.track === 'all' || r.track === filters.track;
const expMatch = filters.experience === 'all' || r.experience === filters.experience;
return trackMatch && expMatch;
});
countDisplay.textContent = filtered.length + (filtered.length === 1 ? ' участник' : ' участников');
if (filtered.length === 0) {
container.style.display = 'none';
emptyState.style.display = 'block';
return;
}
container.style.display = 'block';
emptyState.style.display = 'none';
container.innerHTML = filtered.map(r => `
<div class="reg-item">
<div>
<div class="reg-name">${escapeHtml(r.fullname)}</div>
<div class="reg-meta">
${r.team ? 'Команда: ' + escapeHtml(r.team) + ' • ' : ''}
${r.experience || 'Опыт не указан'}
</div>
<div class="reg-meta" style="margin-top:var(--kt-ai-space-1)">
Зарегистрирован: ${formatDate(r.registeredAt)}
</div>
</div>
<span class="kt-ai-chip track-badge">${escapeHtml(r.track)}</span>
</div>
`).join('');
}
// === Обновление статистики ===
function updateStats() {
@ -503,29 +417,10 @@ document.getElementById('new-registration').addEventListener('click', function()
document.getElementById('registration-form').scrollIntoView({ behavior: 'smooth' });
});
document.getElementById('filter-track').addEventListener('change', function() {
renderRegistrations({
track: this.value,
experience: document.getElementById('filter-experience').value
});
});
document.getElementById('filter-experience').addEventListener('change', function() {
renderRegistrations({
track: document.getElementById('filter-track').value,
experience: this.value
});
});
document.getElementById('clear-filters').addEventListener('click', function() {
document.getElementById('filter-track').value = 'all';
document.getElementById('filter-experience').value = 'all';
renderRegistrations({ track: 'all', experience: 'all' });
});
// === Запуск при загрузке ===
updateStats();
renderRegistrations();
// Плавный скролл для якорных ссылок
document.querySelectorAll('a[href^="#"]').forEach(anchor => {