The repo had no infrastructure at all: no Dockerfile, no nginx config, no
systemd unit, no deploy script, no CI. Deployment guidance existed only
as two prose lines in each README.
Target shape is one reg.ru KVM box serving all four landings, each on its
own domain root. nginx serves the built SPA off disk and proxies only
/api/* to a small Express process per app, so a crashed or restarting API
takes down the form rather than the page. Releases are built on the
server into a timestamped directory and swapped in by renaming the
`current` symlink.
deploy/apps.conf domains and ports, the one place to edit
deploy/README.md hardware/software spec, provisioning, runbook
deploy/bin/ exo-deploy, exo-render-nginx
deploy/nginx/ vhost template, shared snippets, http extras
deploy/systemd/ one template unit for all four instances
deploy/env/ annotated templates for the two env files
exo-deploy asserts the VITE_SITE_URL substitution actually happened,
prunes devDependencies after building, precompresses for gzip_static,
health-checks after the swap and rolls back on its own if the new release
fails to answer. exo-render-nginx refuses to emit a vhost while a
placeholder domain is still in apps.conf.
Secrets are split across two env files: the amoCRM token stays in
/etc/exo at mode 640 and is injected by systemd, while only public
build-time vars are copied into a release. dotenv does not override
variables already in the environment, so the two coexist safely.
Chose systemd over Docker deliberately — no database, no conflicting
runtimes, no CI or registry to build images in, and containerising fights
the serve-static-from-disk design. Reasoning and the triggers to revisit
it are recorded in the README.
Domains are still placeholders; nothing here has been run against a
server yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port the single-file landing (2.6 MB of inlined CSS, JS and base64 images,
kept as fitnes/legacy/index.html) to Vite + React 19 + TypeScript, with an
Express API that files every form submission into amoCRM pipeline 10980758.
- Extract the 14 embedded images to src/assets/images and public/
- Rebuild the design system as Tailwind v4 @theme tokens; the stock palette and
breakpoints are cleared so only the EXO scale is reachable from utilities
- Split the page into 15 components; all copy moves to src/data
- Lead endpoint: find-or-create the contact (Russian phone spellings compared on
the last 10 digits), create the lead in the pipeline's first stage, map the
fields the account already has and put the rest in a note. If amoCRM is
unreachable the payload is logged and kept in localStorage rather than lost.
- Add a callback modal as a second entry point, tagged separately in the pipeline
- Self-host Inter Variable so the layout's 760/850/900 weights render as real
weights instead of snapping to bold
Fidelity was checked by comparing section offsets and heights against the
original at 375/480/640/900/1120/1440 px; every section and the total page
height matched exactly. Loading Inter deliberately changes text metrics, so the
byte-exact comparison holds against the pre-font build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>