Make brand identity reusable for redeploying to other clients

Groundwork for reusing this codebase per-client (own VPS/DB/bot per
cafe): the operational config (bot token, DB URL, secrets, Checkbox PRRO
creds) was already .env/Setting-driven, but ~13 spots still hardcoded the
literal string "Bober BBQ" instead of reading the existing cafe_name
Setting, and the webapp's static shell (index.html title, PWA manifest)
had no templating at all since Settings only load after JS boots.

- Route the remaining hardcoded strings through Setting.get("cafe_name",
  "Bober BBQ") — same pattern already used correctly in bot/handlers/
  start.py. Every fallback stays "Bober BBQ", so production is byte-
  identical with no cafe_name override.
- Template webapp/index.html via Vite's native %VITE_CAFE_NAME% HTML
  replacement, backed by a committed webapp/.env (default "Bober BBQ",
  no secrets) with a per-client override via gitignored .env.local.
- Add webapp/scripts/gen-manifest.mjs to generate manifest.json from a
  new manifest.template.json the same way, since Vite doesn't process
  public/ assets — wired into the build script.
- Parameterize deploy.sh's systemd unit names and the admin log-viewer's
  log paths via optional SERVICE_WEB/SERVICE_BOT/LOG_FILE_WEB/
  LOG_FILE_BOT env vars, defaulting to today's literal values.
- Rewrite docs/DEPLOYMENT.md into a repeatable new-client runbook (fixed
  an existing /opt/bober-bbq vs /opt/bober-bbq-bot inconsistency, added
  a rebranding checklist).

Verified via a smoke test that every changed string still renders
"Bober BBQ" with no overrides present (matching prod's actual .env/DB
state), and that webapp builds both with and without a VITE_CAFE_NAME
override produce the expected output — including a no-override rebuild
confirming byte-identical output to before this change.
This commit is contained in:
byrsapty 2026-08-25 17:54:29 +03:00
parent 23e931d777
commit 922d9c5172
21 changed files with 179 additions and 45 deletions

View file

@ -34,3 +34,11 @@ MONOBANK_WEBHOOK_URL=https://example.com/api/payments/monobank/webhook
# === Misc ===
TIMEZONE=Europe/Kyiv
# === Deployment identity (only needed if this instance's systemd units/logs
# aren't named bober-bbq-web/bober-bbq-bot — e.g. a second client on the
# same codebase). Leave unset to use those defaults. ===
# SERVICE_WEB=bober-bbq-web
# SERVICE_BOT=bober-bbq-bot
# LOG_FILE_WEB=/var/log/bober-bbq-web.log
# LOG_FILE_BOT=/var/log/bober-bbq-bot.log

6
.gitignore vendored
View file

@ -1,4 +1,10 @@
.env
# webapp/.env holds only build-time branding (no secrets) and is committed
# with the current defaults so Vite's %VAR% HTML replacement always has a
# value to substitute; a per-client override goes in webapp/.env.local
# instead (higher precedence, never touches the tracked defaults).
!webapp/.env
webapp/.env.local
__pycache__/
*.pyc
instance/*.db

View file

@ -17,7 +17,10 @@ from bober_bbq.utils.gdrive_backup import backup_now, gdrive_enabled, test_conne
APP_VERSION = "1.0.0"
BACKUP_DIR = BASE_DIR / "backups"
LOG_FILES = ["/var/log/bober-bbq-web.log", "/var/log/bober-bbq-bot.log"]
LOG_FILES = [
os.getenv("LOG_FILE_WEB", "/var/log/bober-bbq-web.log"),
os.getenv("LOG_FILE_BOT", "/var/log/bober-bbq-bot.log"),
]
_tz = ZoneInfo(config.TIMEZONE)

View file

@ -170,7 +170,8 @@ def create_app() -> Flask:
@app.route("/")
def index():
return jsonify({"service": "Bober BBQ backend", "admin": "/admin", "webapp": "/webapp/", "api": "/api"})
cafe_name = Setting.get("cafe_name", "Bober BBQ")
return jsonify({"service": f"{cafe_name} backend", "admin": "/admin", "webapp": "/webapp/", "api": "/api"})
@app.route("/health")
def health():

View file

@ -526,7 +526,7 @@ DEFAULT_SETTINGS = {
"monobank_test_token": "",
"monobank_test_mode": "0",
"monobank_show_basket": "1",
"payment_destination_template": "Оплата замовлення {number} Bober BBQ",
"payment_destination_template": "Оплата замовлення {number}",
"gdrive_backup_enabled": "0",
"gdrive_service_account_json": "",
"gdrive_folder_id": "",

View file

@ -86,13 +86,14 @@ def create_invoice(order: Order) -> dict:
"""Create a payment invoice for the given order and return
{"invoiceId": ..., "pageUrl": ...}. Raises MonobankNotConfigured /
MonobankError on failure."""
cafe_name = Setting.get("cafe_name", "Bober BBQ")
destination_template = Setting.get(
"payment_destination_template", "Оплата замовлення {number} Bober BBQ"
"payment_destination_template", "Оплата замовлення {number} " + cafe_name
)
try:
destination = destination_template.format(number=order.display_number())
except (KeyError, IndexError):
destination = f"Оплата замовлення {order.display_number()} Bober BBQ"
destination = f"Оплата замовлення {order.display_number()} {cafe_name}"
merchant_paym_info = {
"reference": order.display_number(),

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Bober BBQ — Адмін{% endblock %}</title>
<title>{% block title %}{{ cafe_name }} — Адмін{% endblock %}</title>
<link rel="icon" type="image/png" href="{{ url_for('webapp_icon', size=32) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='admin/css/admin.css') }}?v={{ asset_version('admin/css/admin.css') }}">
{% if admin_theme %}
@ -30,7 +30,7 @@
<span class="brand-emoji">🦫</span>
{% endif %}
<div>
<div class="brand-name">Bober BBQ</div>
<div class="brand-name">{{ cafe_name }}</div>
<div class="brand-sub">Admin Panel</div>
</div>
</div>

View file

@ -195,7 +195,7 @@
<div class="tg-item">
<div class="tg-icon">📨</div>
<div>
<div class="tg-name">Bober BBQ — Доставка</div>
<div class="tg-name">{{ cafe_name }} — Доставка</div>
<div class="tg-sub">{{ stats.today.delivery }} нових замовлень сьогодні{% if not delivery_chat_configured %} · чат не підключено{% endif %}</div>
</div>
<a class="btn secondary small" href="{{ url_for('admin.telegram_chats') }}">Відкрити</a>
@ -203,7 +203,7 @@
<div class="tg-item">
<div class="tg-icon">📦</div>
<div>
<div class="tg-name">Bober BBQ — Самовивіз</div>
<div class="tg-name">{{ cafe_name }} — Самовивіз</div>
<div class="tg-sub">{{ stats.today.pickup }} нових замовлень сьогодні{% if not pickup_chat_configured %} · чат не підключено{% endif %}</div>
</div>
<a class="btn secondary small" href="{{ url_for('admin.telegram_chats') }}">Відкрити</a>
@ -211,7 +211,7 @@
<div class="tg-item">
<div class="tg-icon">🍽️</div>
<div>
<div class="tg-name">Bober BBQУ закладі</div>
<div class="tg-name">{{ cafe_name }}У закладі</div>
<div class="tg-sub">{{ stats.today.dine_in }} нових замовлень сьогодні{% if not dine_in_chat_configured %} · чат не підключено{% endif %}</div>
</div>
<a class="btn secondary small" href="{{ url_for('admin.telegram_chats') }}">Відкрити</a>

View file

@ -149,7 +149,7 @@
</p>
<label>Опис платежу (показується клієнту на сторінці оплати)</label>
<input type="text" name="payment_destination_template" value="{{ values.payment_destination_template }}" placeholder="Оплата замовлення {number} Bober BBQ">
<input type="text" name="payment_destination_template" value="{{ values.payment_destination_template }}" placeholder="Оплата замовлення {number} {{ cafe_name }}">
<p class="muted field-hint">{number} буде замінено на номер замовлення, напр. №1005.</p>
<label style="display:flex;align-items:center;gap:8px;">

View file

@ -9,7 +9,7 @@
<div class="grid-2-even">
<div class="card">
<div class="card-head"><h3>🛵 Bober BBQ — Доставка</h3></div>
<div class="card-head"><h3>🛵 {{ cafe_name }} — Доставка</h3></div>
<table>
<tr><td class="muted">Статус</td><td>
{% if delivery_chat_id %}<span class="status-dot ok"></span>Підключено{% else %}<span class="status-dot bad"></span>Не підключено{% endif %}
@ -24,7 +24,7 @@
</div>
<div class="card">
<div class="card-head"><h3>{{ pickup_icon(18) }} Bober BBQ — Самовивіз</h3></div>
<div class="card-head"><h3>{{ pickup_icon(18) }} {{ cafe_name }} — Самовивіз</h3></div>
<table>
<tr><td class="muted">Статус</td><td>
{% if pickup_chat_id %}<span class="status-dot ok"></span>Підключено{% else %}<span class="status-dot bad"></span>Не підключено{% endif %}
@ -39,7 +39,7 @@
</div>
<div class="card">
<div class="card-head"><h3>🍽️ Bober BBQУ закладі</h3></div>
<div class="card-head"><h3>🍽️ {{ cafe_name }}У закладі</h3></div>
<table>
<tr><td class="muted">Статус</td><td>
{% if dine_in_chat_id %}<span class="status-dot ok"></span>Підключено{% else %}<span class="status-dot bad"></span>Не підключено (сповіщення йдуть власнику){% endif %}

View file

@ -12,7 +12,7 @@ STATUS_MESSAGES = {
"cooking": "👨‍🍳 <b>Замовлення {number} готується</b>\n\nЗовсім скоро буде готове!",
"ready": "🎉 <b>Замовлення {number} готове!</b>",
"courier": "🛵 <b>Замовлення {number} в дорозі!</b>\n\nКур'єр вже прямує до вас.",
"completed": "🦫 <b>Дякуємо, що обрали Bober BBQ!</b>\n\nЗамовлення {number} виконано. Смачного!",
"completed": "🦫 <b>Дякуємо, що обрали {cafe_name}!</b>\n\nЗамовлення {number} виконано. Смачного!",
"cancelled": "❌ <b>Замовлення {number} скасовано</b>\n\nЗа потреби менеджер зв'яжеться з вами.",
}
@ -23,7 +23,7 @@ def notify_customer_status_change(order: Order) -> None:
template = STATUS_MESSAGES.get(order.status)
if not template:
return
text = template.format(number=order.display_number())
text = template.format(number=order.display_number(), cafe_name=Setting.get("cafe_name", "Bober BBQ"))
if order.status == "cancelled" and order.cancel_reason:
text += f"\n\n<i>Причина: {html.escape(order.cancel_reason)}</i>"
if order.order_type == "pickup" and order.status == "ready":

View file

@ -53,8 +53,9 @@ def closed_message() -> str:
custom = Setting.get("force_closed_message", "").strip()
return custom or "Наразі заклад тимчасово не приймає замовлення. Перепрошуємо за незручності!"
start, end = working_hours()
cafe_name = Setting.get("cafe_name", "Bober BBQ")
return (
f"Наразі Bober BBQ не приймає замовлення.\n"
f"Наразі {cafe_name} не приймає замовлення.\n"
f"Графік роботи: щодня з {start} до {end}.\n"
f"Ви можете переглянути меню зараз, а оформити замовлення під час роботи закладу."
)

View file

@ -10,6 +10,7 @@ from bober_bbq.app import create_app
from bober_bbq.config import config
from bober_bbq.extensions import db
from bober_bbq.migrate import migrate
from bober_bbq.models import Setting
from bober_bbq.seed import seed
from bot.handlers import contacts, menu, rating, start
@ -39,7 +40,7 @@ async def main():
dp.include_router(rating.router)
await bot.delete_webhook(drop_pending_updates=True)
logger.info("Bober BBQ bot starting (long polling)...")
logger.info("%s bot starting (long polling)...", Setting.get("cafe_name", "Bober BBQ"))
await dp.start_polling(bot)

View file

@ -1,6 +1,15 @@
#!/bin/bash
set -e
cd /opt/bober-bbq-bot
cd "$(dirname "${BASH_SOURCE[0]}")"
# SERVICE_WEB/SERVICE_BOT let a differently-named deployment (e.g. a second
# client on the same codebase) override the systemd unit names via .env
# without editing this script. Read directly instead of sourcing the whole
# .env, so unrelated values (secrets, tokens) never end up in this shell.
SERVICE_WEB=$(grep -E '^SERVICE_WEB=' .env 2>/dev/null | cut -d= -f2-)
SERVICE_BOT=$(grep -E '^SERVICE_BOT=' .env 2>/dev/null | cut -d= -f2-)
SERVICE_WEB="${SERVICE_WEB:-bober-bbq-web}"
SERVICE_BOT="${SERVICE_BOT:-bober-bbq-bot}"
echo "==> git pull"
git pull
@ -15,8 +24,8 @@ npm run build
cd ..
echo "==> restart services"
sudo systemctl restart bober-bbq-web bober-bbq-bot
sudo systemctl restart "$SERVICE_WEB" "$SERVICE_BOT"
sleep 2
sudo systemctl is-active bober-bbq-web bober-bbq-bot
sudo systemctl is-active "$SERVICE_WEB" "$SERVICE_BOT"
echo "==> deploy done"

View file

@ -4,6 +4,13 @@
термінатор TLS, два systemd-сервіси (backend + bot), SQLite (або Postgres,
якщо очікується більше навантаження).
Цей документ описує розгортання **нового, окремого** інстансу (напр. для
іншого клієнта на тому самому коді). Він не стосується і не повинен
застосовуватись до вже працюючого продакшн-сервера Bober BBQ.
Далі `$INSTALL_DIR` — шлях, куди клонується репозиторій (напр.
`/opt/<client-slug>-bot`); підставляйте свій на кожному кроці.
## 1. Підготовка сервера
```bash
@ -13,8 +20,8 @@ sudo apt update && sudo apt install -y python3.11 python3.11-venv nginx certbot
## 2. Код і залежності
```bash
git clone <репозиторій> /opt/bober-bbq
cd /opt/bober-bbq
git clone <репозиторій> $INSTALL_DIR
cd $INSTALL_DIR
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
@ -33,6 +40,10 @@ cd webapp && npm install && npm run build && cd ..
- `MONOBANK_TOKEN` — токен еквайрингу від замовника (розділ 22.8 ТЗ)
- `DATABASE_URL` — для Postgres: `postgresql+psycopg2://user:pass@localhost/bober_bbq`
- `SECRET_KEY`, `FLASK_ADMIN_USERNAME`, `FLASK_ADMIN_PASSWORD` — змінити на бойові значення
- `SERVICE_WEB`, `SERVICE_BOT`, `LOG_FILE_WEB`, `LOG_FILE_BOT` — лише якщо
systemd-юніти цього інстансу названі не `bober-bbq-web`/`bober-bbq-bot`
(напр. другий клієнт на тій самій VPS). Не задавайте їх, якщо юніти саме
так і називаються — значення за замовчуванням підійдуть.
## 4. Nginx + TLS
@ -59,35 +70,41 @@ Telegram Mini App і monobank webhook **вимагають дійсний HTTPS-
## 5. systemd-юніти
`/etc/systemd/system/bober-bbq-web.service`:
Оберіть імена юнітів для цього клієнта (типово `bober-bbq-web`/
`bober-bbq-bot`, якщо це єдиний інстанс на сервері; інакше — щось
унікальне, напр. `<client-slug>-web`/`<client-slug>-bot`, і не забудьте
задати ці ж імена в `.env` через `SERVICE_WEB`/`SERVICE_BOT`, інакше
`deploy.sh` перезапускатиме не ті юніти).
`/etc/systemd/system/<SERVICE_WEB>.service`:
```ini
[Unit]
Description=Bober BBQ backend (API + admin)
Description=<Cafe Name> backend (API + admin)
After=network.target
[Service]
WorkingDirectory=/opt/bober-bbq
ExecStart=/opt/bober-bbq/.venv/bin/python run_web.py
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/.venv/bin/python run_web.py
Restart=on-failure
EnvironmentFile=/opt/bober-bbq/.env
EnvironmentFile=$INSTALL_DIR/.env
[Install]
WantedBy=multi-user.target
```
`/etc/systemd/system/bober-bbq-bot.service`:
`/etc/systemd/system/<SERVICE_BOT>.service`:
```ini
[Unit]
Description=Bober BBQ Telegram bot
After=network.target bober-bbq-web.service
Description=<Cafe Name> Telegram bot
After=network.target <SERVICE_WEB>.service
[Service]
WorkingDirectory=/opt/bober-bbq
ExecStart=/opt/bober-bbq/.venv/bin/python run_bot.py
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/.venv/bin/python run_bot.py
Restart=on-failure
EnvironmentFile=/opt/bober-bbq/.env
EnvironmentFile=$INSTALL_DIR/.env
[Install]
WantedBy=multi-user.target
@ -95,9 +112,15 @@ WantedBy=multi-user.target
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now bober-bbq-web bober-bbq-bot
sudo systemctl enable --now <SERVICE_WEB> <SERVICE_BOT>
```
Якщо обрали нестандартні імена, також вкажіть логи цього інстансу в
`StandardOutput=append:/var/log/<SERVICE_WEB>.log` /
`StandardError=append:/var/log/<SERVICE_WEB>.log` (аналогічно для бота) і
задайте ті самі шляхи в `.env` через `LOG_FILE_WEB`/`LOG_FILE_BOT` — інакше
перегляд логів в Адмінці → Система буде показувати не той файл.
## 6. Telegram-налаштування
1. У [@BotFather](https://t.me/BotFather): `/setmenubutton` → вкажіть
@ -115,7 +138,7 @@ sudo systemctl enable --now bober-bbq-web bober-bbq-bot
SQLite: досить копіювати файл `instance/bober_bbq.db` за розкладом (cron):
```bash
0 3 * * * cp /opt/bober-bbq/instance/bober_bbq.db /opt/backups/bober_bbq-$(date +\%F).db
0 3 * * * cp $INSTALL_DIR/instance/bober_bbq.db /opt/backups/bober_bbq-$(date +\%F).db
```
Postgres: стандартний `pg_dump` за розкладом. Не забудьте також бекапити
@ -124,21 +147,44 @@ Postgres: стандартний `pg_dump` за розкладом. Не заб
## 8. Оновлення коду
Одна команда (є на живому сервері як `/opt/bober-bbq-bot/deploy.sh`):
`deploy.sh` — закомічений у репозиторії корінь (`$INSTALL_DIR/deploy.sh`):
```bash
/opt/bober-bbq-bot/deploy.sh
$INSTALL_DIR/deploy.sh
```
Робить послідовно: `git pull``pip install -r requirements.txt`
`npm install && npm run build` у `webapp/``systemctl restart bober-bbq-web bober-bbq-bot`.
`npm install && npm run build` у `webapp/``systemctl restart` тих
юнітів, що вказані в `.env` через `SERVICE_WEB`/`SERVICE_BOT` (або
`bober-bbq-web`/`bober-bbq-bot`, якщо не вказано). Той самий скрипт,
без змін, коректно працює для будь-якого клієнта на цьому коді — імена
юнітів він бере з `.env` цього конкретного інстансу.
Якщо розгортаєте вручну на новому сервері — ті самі кроки:
```bash
cd /opt/bober-bbq-bot
cd $INSTALL_DIR
git pull
.venv/bin/pip install -r requirements.txt
cd webapp && npm install && npm run build && cd ..
sudo systemctl restart bober-bbq-web bober-bbq-bot
sudo systemctl restart <SERVICE_WEB> <SERVICE_BOT>
```
## 9. Чекліст ребрендингу для нового клієнта
Після першого запуску (перед тим, як показувати клієнту):
1. Адмінка → Налаштування: `cafe_name`, телефон, адреса, графік роботи,
`logo_url`, кольори теми (адмінки й вебапки) — усе інше вже підхопить
ці значення (заголовки листування з Telegram-ботом, назва в API, титул
сторінки адмінки тощо).
2. `webapp/.env.local` (створити, НЕ редагувати закомічений `webapp/.env`):
`VITE_CAFE_NAME=<Назва Кафе>` — впливає на `<title>` вебапки та PWA
manifest (`name`/`short_name`); підхопиться при наступному
`npm run build`.
3. Замінити 4 файли іконок під `webapp/public/icons/icon-{32,180,192,512}.png`
на артворк клієнта (поточні — це бренд-арт Bober BBQ, бобер з шампуром;
автоматично не генеруються).
4. Адмінка → Меню: видалити демо-меню (Шашлик/BBQ категорії й товари, що
приходять із `seed.py` при першому запуску порожньої БД) і наповнити
реальним асортиментом клієнта.

View file

@ -13,6 +13,7 @@ from bober_bbq.app import create_app
from bober_bbq.config import config
from bober_bbq.extensions import db
from bober_bbq.migrate import migrate
from bober_bbq.models import Setting
from bober_bbq.seed import seed
from bober_bbq.utils.checkbox_prro import maybe_close_shift
from bober_bbq.utils.gdrive_backup import run_scheduled_backup
@ -60,5 +61,7 @@ if __name__ == "__main__":
scheduler.start()
port = int(os.getenv("PORT", "8000"))
print(f"Bober BBQ backend listening on http://0.0.0.0:{port}")
with app.app_context():
cafe_name = Setting.get("cafe_name", "Bober BBQ")
print(f"{cafe_name} backend listening on http://0.0.0.0:{port}")
serve(app, host="0.0.0.0", port=port)

8
webapp/.env Normal file
View file

@ -0,0 +1,8 @@
# Build-time branding for the webapp shell (index.html <title>/meta, and
# manifest.json name/short_name via scripts/gen-manifest.mjs). Committed
# with this instance's current default — safe, since it holds no secrets.
#
# To rebrand a per-client deployment, create webapp/.env.local instead of
# editing this file (Vite loads it with higher precedence, so it overrides
# this default without creating a diff against the shared codebase).
VITE_CAFE_NAME=Bober BBQ

View file

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<title>Bober BBQ</title>
<title>%VITE_CAFE_NAME%</title>
<link rel="manifest" href="/webapp/manifest.json" />
<link rel="icon" href="/webapp/icons/icon-32.png" />
<link rel="apple-touch-icon" href="/webapp/icons/icon-180.png" />
<meta name="theme-color" content="#0a0a0c" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Bober BBQ" />
<meta name="apple-mobile-web-app-title" content="%VITE_CAFE_NAME%" />
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"build": "node scripts/gen-manifest.mjs && tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {

View file

@ -0,0 +1,15 @@
{
"name": "{{CAFE_NAME}}",
"short_name": "{{CAFE_NAME}}",
"description": "Замовлення страв на доставку та самовивіз",
"start_url": "/webapp/",
"scope": "/webapp/",
"display": "standalone",
"background_color": "#0a0a0c",
"theme_color": "#0a0a0c",
"orientation": "portrait",
"icons": [
{ "src": "/webapp/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/webapp/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}

View file

@ -0,0 +1,32 @@
// Regenerates public/manifest.json from manifest.template.json, substituting
// the cafe name read the same way Vite resolves it (.env.local overrides
// .env). Needed because Vite's native %VAR% HTML replacement only applies
// to index.html — files under public/ are copied to the build as-is, never
// processed. Falls back to "Bober BBQ" if no env file overrides it, so a
// clone with no webapp/.env.local (like the current production instance)
// produces byte-identical output.
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
function readEnvFile(path) {
if (!existsSync(path)) return {};
const vars = {};
for (const line of readFileSync(path, "utf-8").split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
}
return vars;
}
const merged = { ...readEnvFile(join(root, ".env")), ...readEnvFile(join(root, ".env.local")) };
const cafeName = merged.VITE_CAFE_NAME || "Bober BBQ";
const template = readFileSync(join(root, "public", "manifest.template.json"), "utf-8");
writeFileSync(join(root, "public", "manifest.json"), template.replaceAll("{{CAFE_NAME}}", cafeName));
console.log(`manifest.json generated (VITE_CAFE_NAME="${cafeName}")`);