init: server build project template

This commit is contained in:
2026-10-10 06:27:51 +05:00
commit a6167fccfa
15 changed files with 304 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
name: build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install pyyaml
- run: python scripts/build.py
- run: |
git config user.email ci@gitea && git config user.name ci
git add docs/estimate.md web/data.json
git diff --staged --quiet || git commit -m "ci: rebuild estimate [skip ci]"
git push
+5
View File
@@ -0,0 +1,5 @@
generated/
__pycache__/
*.pyc
.venv/
.env
+17
View File
@@ -0,0 +1,17 @@
.PHONY: build prices serve clean install
install:
pip install --user pyyaml jinja2 requests beautifulsoup4
build:
python3 scripts/build.py
prices:
python3 scripts/fetch_prices.py
serve:
@echo "http://localhost:8000/web/index.html"
python3 -m http.server 8000
clean:
rm -rf generated/*
+26
View File
@@ -0,0 +1,26 @@
# Server Build Project
Проект сборки серверной инфраструктуры: требования, комплектующие, ссылки, смета.
## 📚 Документация
- [Общая идея](docs/idea.md)
- [Структура](docs/structure.md)
- [Требования к узлам](docs/requirements.md)
- [Смета](docs/estimate.md)
## 📊 Данные
- Комплектующие: [`data/components.yaml`](data/components.yaml)
- Узлы: [`data/nodes.yaml`](data/nodes.yaml)
- Источники цен: [`data/prices.yaml`](data/prices.yaml)
## 🛠 Сборка
```bash
make build # пересобрать docs/estimate.md и web/index.html
make prices # обновить цены из источников (заглушка)
make serve # локальный предпросмотр HTML-дашборда
\
/
end
```
+38
View File
@@ -0,0 +1,38 @@
# Каталог комплектующих. Одна позиция = один компонент.
# links: можно указать несколько ссылок на разные магазины.
items:
- id: cpu-epyc-7443p
category: CPU
name: "AMD EPYC 7443P"
price: 130000
links:
- { title: "DNS", url: "https://www.dns-shop.ru/" }
- { title: "Citilink", url: "https://www.citilink.ru/" }
- id: ram-ddr4-ecc-32
category: RAM
name: "Samsung DDR4 ECC 32GB"
price: 18000
links:
- { title: "DNS", url: "https://www.dns-shop.ru/" }
- id: mb-romed8-2t
category: Motherboard
name: "ASRock Rack ROMED8-2T"
price: 65000
links:
- { title: "ServerFlow", url: "https://serverflow.ru/" }
- id: case-4u
category: Case
name: "4U Rackmount 24-bay"
price: 35000
links:
- { title: "AliExpress", url: "https://aliexpress.ru/" }
- id: psu-1200w
category: PSU
name: "Seasonic 1200W Platinum"
price: 25000
links:
- { title: "DNS", url: "https://www.dns-shop.ru/" }
+18
View File
@@ -0,0 +1,18 @@
# Узлы и их состав. qty — количество, price — опционально переопределить цену из каталога.
nodes:
- id: node-1
name: "Hypervisor"
role: "Виртуализация"
items:
- { component_id: cpu-epyc-7443p, qty: 1 }
- { component_id: ram-ddr4-ecc-32, qty: 8 }
- { component_id: mb-romed8-2t, qty: 1 }
- { component_id: case-4u, qty: 1 }
- { component_id: psu-1200w, qty: 2 }
- id: node-2
name: "NAS"
role: "Хранилище"
items:
- { component_id: case-4u, qty: 1 }
- { component_id: psu-1200w, qty: 1 }
+9
View File
@@ -0,0 +1,9 @@
# Источники цен. fetch_prices.py может ходить на эти URL.
currency: RUB
sources:
- { id: dns, name: "DNS", base: "https://www.dns-shop.ru/" }
- { id: citilink, name: "Citilink", base: "https://www.citilink.ru/" }
- { id: serverflow,name: "ServerFlow",base: "https://serverflow.ru/" }
# Таблица переопределений (если хочешь фиксировать цену вручную):
overrides:
# cpu-epyc-7443p: 125000
+5
View File
@@ -0,0 +1,5 @@
Смета
Файл генерируется автоматически: make build. Правки вручную будут перезаписаны.
(запусти make build)
+19
View File
@@ -0,0 +1,19 @@
Общая идея
Цель
Собрать отказоустойчивую серверную инфраструктуру для <задача>.
Задачи
□ Определить узлы и роли
□ Согласовать бюджет
□ Выбрать комплектующие
□ Закупить и собрать
Ограничения
Бюджет: ~ X ₽
Сроки: Q1 2025
Энергопотребление / шум / стойка
Критерии успеха
...
+19
View File
@@ -0,0 +1,19 @@
Технические требования к узлам
Узел 1: Hypervisor
CPU: ≥16 ядер
RAM: ≥128 ГБ
Диск: NVMe ≥2 ТБ (mirror)
Сеть: 2×10GbE
Узел 2: NAS
CPU: ≥4 ядра
RAM: ≥16 ГБ
Диск: 4×8 ТБ HDD (RAID10)
Сеть: 1×10GbE
+16
View File
@@ -0,0 +1,16 @@
Структура
Схема узлов
graph TD
Net[Сеть / Роутер] --> FW[Firewall]
FW --> HV[Hypervisor]
HV --> VM1[VM: Web]
HV --> VM2[VM: DB]
HV --> VM3[VM: Backup]
HV --> NAS[(NAS / Хранилище)]
Роли узлов
Узел Роль Критичность
Hypervisor Виртуализация Высокая
NAS Хранилище бэкапов Средняя
EOF
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Генерирует docs/estimate.md (с mermaid-графиками) и web/index.html."""
from pathlib import Path
import json, yaml
ROOT = Path(__file__).resolve().parent.parent
DATA, GEN, WEB = ROOT/"data", ROOT/"generated", ROOT/"web"
GEN.mkdir(exist_ok=True); WEB.mkdir(exist_ok=True)
def load(p): return yaml.safe_load(p.read_text())
def main():
comps = {c["id"]: c for c in load(DATA/"components.yaml")["items"]}
nodes = load(DATA/"nodes.yaml")["nodes"]
overrides = load(DATA/"prices.yaml").get("overrides") or {}
rows, per_node, per_cat, total = [], {}, {}, 0
for n in nodes:
nsum = 0
for it in n["items"]:
c = comps[it["component_id"]]
price = overrides.get(c["id"], it.get("price", c.get("price", 0)))
qty = it.get("qty", 1)
sub = price * qty
nsum += sub; total += sub
per_cat[c["category"]] = per_cat.get(c["category"], 0) + sub
rows.append((n["name"], c["category"], c["name"], qty, price, sub, c.get("links", [])))
per_node[n["name"]] = nsum
# ---------- markdown ----------
md = ["# Смета\n", f"**Итого: {total:,.0f} ₽**\n".replace(",", " "), "## Таблица\n"]
md.append("| Узел | Категория | Наименование | Кол-во | Цена, ₽ | Сумма, ₽ | Ссылки |")
md.append("|------|-----------|--------------|:------:|--------:|---------:|--------|")
for r in rows:
links = " · ".join(f"[{l['title']}]({l['url']})" for l in r[6])
md.append(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]} | {r[4]:,} | {r[5]:,} | {links} |".replace(",", " "))
md += ["\n## Распределение по узлам\n", "```mermaid", "pie showData", ' title "Бюджет по узлам"']
for k, v in per_node.items(): md.append(f' "{k}" : {v}')
md.append("```\n")
md += ["## Распределение по категориям\n", "```mermaid", "pie showData", ' title "Бюджет по категориям"']
for k, v in per_cat.items(): md.append(f' "{k}" : {v}')
md.append("```")
(ROOT/"docs"/"estimate.md").write_text("\n".join(md))
# ---------- HTML-дашборд ----------
payload = {
"total": total,
"per_node": per_node,
"per_cat": per_cat,
"rows": [{"node":r[0],"cat":r[1],"name":r[2],"qty":r[3],"price":r[4],"sub":r[5],
"links":r[6]} for r in rows],
}
(WEB/"data.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2))
print(f"[OK] Итого: {total:,.0f} ₽".replace(",", " "))
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""Заглушка: обходит sources из data/prices.yaml и обновляет цены.
Здесь оставлены TODO — реализация парсеров зависит от сайта."""
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
prices = yaml.safe_load((ROOT/"data"/"prices.yaml").read_text())
print("Sources:", [s["name"] for s in prices["sources"]])
# TODO: requests + BeautifulSoup для каждой позиции из components.yaml
# TODO: сохранить результат в data/prices.yaml -> overrides
+24
View File
@@ -0,0 +1,24 @@
const fmt = n => n.toLocaleString('ru-RU') + ' ₽';
fetch('data.json').then(r => r.json()).then(d => {
document.getElementById('total').textContent = fmt(d.total);
new Chart(document.getElementById('chNodes'), {
type: 'pie',
data: { labels: Object.keys(d.per_node), datasets: [{ data: Object.values(d.per_node) }] },
options: { plugins: { title: { display: true, text: 'Бюджет по узлам' } } }
});
new Chart(document.getElementById('chCats'), {
type: 'doughnut',
data: { labels: Object.keys(d.per_cat), datasets: [{ data: Object.values(d.per_cat) }] },
options: { plugins: { title: { display: true, text: 'Бюджет по категориям' } } }
});
const tb = document.querySelector('#tbl tbody');
d.rows.forEach(r => {
const links = r.links.map(l => `<a href="${l.url}" target="_blank">${l.title}</a>`).join(' · ');
tb.insertAdjacentHTML('beforeend',
`<tr><td>${r.node}</td><td>${r.cat}</td><td>${r.name}</td><td>${r.qty}</td>
<td class="num">${r.price.toLocaleString('ru-RU')}</td>
<td class="num">${r.sub.toLocaleString('ru-RU')}</td><td>${links}</td></tr>`);
});
});
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="ru"><head>
<meta charset="utf-8"><title>Server Build — Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body{font:14px/1.5 system-ui,sans-serif;max-width:1100px;margin:2rem auto;padding:0 1rem;color:#222}
h1{margin-bottom:.2rem} .total{font-size:2rem;font-weight:600;color:#1f7a1f}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin:1.5rem 0}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:.45rem .6rem;border-bottom:1px solid #eee;text-align:left}
th{background:#f6f8fa} td.num{text-align:right;font-variant-numeric:tabular-nums}
a{color:#0969da;text-decoration:none} a:hover{text-decoration:underline}
</style></head><body>
<h1>Server Build — Dashboard</h1>
<div class="total" id="total">— ₽</div>
<div class="grid">
<canvas id="chNodes"></canvas>
<canvas id="chCats"></canvas>
</div>
<table id="tbl"><thead><tr>
<th>Узел</th><th>Категория</th><th>Наименование</th><th>Кол-во</th>
<th class="num">Цена</th><th class="num">Сумма</th><th>Ссылки</th>
</tr></thead><tbody></tbody></table>
<script src="app.js"></script>
</body></html>