Prod: new admin on admin.*, old admin moves to admin.*/old/

Dockerfile builds apps/backoffice into /srv/admin and the old admin with
base /old/; nginx serves /old/ with its own uncached index.html.
Worker notification links point to the new admin pages (moves, requests,
chats); BACKOFFICE_URL defaults to ADMIN_URL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 04:22:36 +04:00
1 parent dcba7c200c
commit dda917fc71
5 files changed
+24 -8

No files matched your search

+2
View File
@@ -2,7 +2,9 @@ import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
// В разработке API проксируется — Mini App и API на одном origin (удобно и для туннеля https) // В разработке API проксируется — Mini App и API на одном origin (удобно и для туннеля https)
// ADMIN_BASE=/old/ — в продакшене старая админка живёт по адресу admin.barevclub.com/old/
export default defineConfig({ export default defineConfig({
base: process.env.ADMIN_BASE ?? '/',
plugins: [react()], plugins: [react()],
server: { server: {
host: true, host: true,
+5 -4
View File
@@ -102,11 +102,12 @@ async function runBroadcast(id: string) {
} }
/** Новая админка, если уже на сервере; иначе — раздел переписки в старой */ /** Новая админка, если уже на сервере; иначе — раздел переписки в старой */
const BACKOFFICE_URL = process.env.BACKOFFICE_URL ?? ''; // новая админка живёт на admin.* (старая — admin.*/old/), отдельный адрес не обязателен
const BACKOFFICE_URL = process.env.BACKOFFICE_URL || ADMIN_URL;
const chatButton = (threadId: string | null, peer: string): InlineButton | null => const chatButton = (threadId: string | null, peer: string): InlineButton | null =>
BACKOFFICE_URL.startsWith('https://') && threadId BACKOFFICE_URL.startsWith('https://') && threadId
? { text: adminT('bot.admin.open'), url: `${BACKOFFICE_URL}/#/chats/${threadId}` } ? { text: adminT('bot.admin.open'), url: `${BACKOFFICE_URL}/#/chats/${threadId}` }
: adminButton(`/concierge?${peer.startsWith('c:') ? 'client' : 'provider'}=${peer.slice(2)}`); : adminButton('/chats');
const who = (u: { firstName: string | null; lastName?: string | null; username: string | null }) => const who = (u: { firstName: string | null; lastName?: string | null; username: string | null }) =>
`${escapeHtml(`${u.firstName ?? ''} ${u.lastName ?? ''}`.trim())}${u.username ? ` (@${escapeHtml(u.username)})` : ''}`; `${escapeHtml(`${u.firstName ?? ''} ${u.lastName ?? ''}`.trim())}${u.username ? ` (@${escapeHtml(u.username)})` : ''}`;
@@ -395,7 +396,7 @@ async function handle(event: DomainEvent) {
const pkg = lead.package ? adminT(`landing.quiz.package_${lead.package}`) : '—'; const pkg = lead.package ? adminT(`landing.quiz.package_${lead.package}`) : '—';
await toAdmins( await toAdmins(
`🧳 <b>${adminT('bot.admin.newLead', { number: lead.number })}</b>\n${escapeHtml(lead.name)} · ${escapeHtml(lead.contact)}\n${when} · ${pkg}${lead.adminNote ? `\n${escapeHtml(lead.adminNote)}` : ''}`, `🧳 <b>${adminT('bot.admin.newLead', { number: lead.number })}</b>\n${escapeHtml(lead.name)} · ${escapeHtml(lead.contact)}\n${when} · ${pkg}${lead.adminNote ? `\n${escapeHtml(lead.adminNote)}` : ''}`,
compactRows([[adminButton('/leads')]]), compactRows([[adminButton('/moves')]]),
); );
return; return;
} }
@@ -405,7 +406,7 @@ async function handle(event: DomainEvent) {
const what = r.kind === 'SUBSCRIPTION' ? adminT('admin.requests.subscription', { plan: r.plan === 'ELITE' ? 'Barev Elite' : 'Barev+' }) : adminT(`services.${r.serviceCode}.name`); const what = r.kind === 'SUBSCRIPTION' ? adminT('admin.requests.subscription', { plan: r.plan === 'ELITE' ? 'Barev Elite' : 'Barev+' }) : adminT(`services.${r.serviceCode}.name`);
await toAdmins( await toAdmins(
`⭐ <b>${adminT('bot.admin.newRequest', { number: r.number })}</b>\n${who(r.client.user)}\n${escapeHtml(what)}${r.comment ? `\n${escapeHtml(r.comment)}` : ''}`, `⭐ <b>${adminT('bot.admin.newRequest', { number: r.number })}</b>\n${who(r.client.user)}\n${escapeHtml(what)}${r.comment ? `\n${escapeHtml(r.comment)}` : ''}`,
compactRows([[{ text: adminT('bot.admin.reply'), callback_data: `ar:c:${r.clientId}` }, adminButton('/app-requests')]]), compactRows([[{ text: adminT('bot.admin.reply'), callback_data: `ar:c:${r.clientId}` }, adminButton('/requests')]]),
); );
return; return;
} }
+5 -2
View File
@@ -1,6 +1,7 @@
# Barev Club — образы для продакшена. # Barev Club — образы для продакшена.
# app — Node: API + воркер + боты (один процесс) и лендинг Next.js (тот же образ) # app — Node: API + воркер + боты (один процесс) и лендинг Next.js (тот же образ)
# web — nginx: статика Mini App и админки + прокси /trpc и /files на API # web — nginx: статика Mini App и админки + прокси /trpc и /files на API
# admin.* — новая админка (apps/backoffice), admin.*/old/ — старая (apps/admin)
FROM node:22-alpine AS deps FROM node:22-alpine AS deps
RUN corepack enable RUN corepack enable
WORKDIR /repo WORKDIR /repo
@@ -15,13 +16,15 @@ ARG CLIENT_BOT_USERNAME=barevclub_bot
ENV VITE_LANDING_URL=$LANDING_URL NEXT_PUBLIC_API_URL=$API_PUBLIC_URL CLIENT_BOT_USERNAME=$CLIENT_BOT_USERNAME NEXT_TELEMETRY_DISABLED=1 ENV VITE_LANDING_URL=$LANDING_URL NEXT_PUBLIC_API_URL=$API_PUBLIC_URL CLIENT_BOT_USERNAME=$CLIENT_BOT_USERNAME NEXT_TELEMETRY_DISABLED=1
RUN pnpm --filter @barev/miniapp-client build \ RUN pnpm --filter @barev/miniapp-client build \
&& pnpm --filter @barev/miniapp-pro build \ && pnpm --filter @barev/miniapp-pro build \
&& pnpm --filter @barev/admin build \ && ADMIN_BASE=/old/ pnpm --filter @barev/admin build \
&& VITE_OLD_ADMIN_URL=/old/ pnpm --filter @barev/backoffice build \
&& pnpm --filter @barev/landing build && pnpm --filter @barev/landing build
FROM nginx:alpine AS web FROM nginx:alpine AS web
COPY --from=build /repo/apps/miniapp-client/dist /srv/client COPY --from=build /repo/apps/miniapp-client/dist /srv/client
COPY --from=build /repo/apps/miniapp-pro/dist /srv/pro COPY --from=build /repo/apps/miniapp-pro/dist /srv/pro
COPY --from=build /repo/apps/admin/dist /srv/admin COPY --from=build /repo/apps/backoffice/dist /srv/admin
COPY --from=build /repo/apps/admin/dist /srv/admin/old
COPY infra/prod/nginx.conf /etc/nginx/conf.d/default.conf COPY infra/prod/nginx.conf /etc/nginx/conf.d/default.conf
FROM node:22-alpine AS app FROM node:22-alpine AS app
+2 -2
View File
@@ -16,8 +16,8 @@ LANDING_URL=https://barevclub.com
CLIENT_MINIAPP_URL=https://app.barevclub.com CLIENT_MINIAPP_URL=https://app.barevclub.com
PRO_MINIAPP_URL=https://pro.barevclub.com PRO_MINIAPP_URL=https://pro.barevclub.com
ADMIN_URL=https://admin.barevclub.com ADMIN_URL=https://admin.barevclub.com
# новая админка — когда выложим: тогда «Открыть» в уведомлениях о сообщениях ведёт в её чаты # новая админка — на ADMIN_URL (старая — ADMIN_URL/old/); BACKOFFICE_URL нужен, только если она на другом адресе
# BACKOFFICE_URL=https://team.barevclub.com # BACKOFFICE_URL=
# серверный адрес API для лендинга (внутри docker-сети) # серверный адрес API для лендинга (внутри docker-сети)
API_URL=http://barev-app:4000 API_URL=http://barev-app:4000
API_PORT=4000 API_PORT=4000
+10
View File
@@ -75,6 +75,16 @@ server {
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
} }
# старая админка: admin.barevclub.com/old/ — своя index.html, тоже без кеша
location ^~ /old/ {
root /srv/$barev_site;
try_files $uri $uri/ /old/index.html;
location ~ \.html$ {
root /srv/$barev_site;
add_header Cache-Control "no-cache";
}
}
# статика с хешами в имени — кешируем надолго # статика с хешами в имени — кешируем надолго
location /assets/ { location /assets/ {
root /srv/$barev_site; root /srv/$barev_site;