d99b9cb0bb7ae6801dbedcca7e06e3f09ca8bff4
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d99b9cb0bb |
Add deployment tooling for a single production VPS
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> |
||
|
|
b673367c6f |
Prepare the four landings for production deployment
Fixes that block or weaken a real deployment. Nothing here changes the rendered pages. Bind the lead API to loopback. Each server called app.listen() without a host, so it bound 0.0.0.0. Combined with a blanket `trust proxy: true` — which makes Express take the leftmost X-Forwarded-For entry as req.ip — the in-memory lead rate limiter was spoofable by anyone who could reach the port directly. HOST now defaults to 127.0.0.1 and trust is narrowed to 'loopback', so a request arriving from anywhere but the local proxy has its forged header ignored. Give each landing its own port. All four .env files claimed PORT=3000, and fitnes/.env.example collided with medcenter/.env.example, so three of the four could never have started on one host. Now 3000/3001/3002/3003 consistently across the code defaults, the env templates and the vite dev proxies, so all four also run side by side locally. Template the JSON-LD url. canonical and og:url already resolved from %VITE_SITE_URL%, but the JSON-LD block hardcoded an exodevices.ru sub-path that would not follow the environment. All four now read from the same variable. Declare the Node version. Nothing stated it, yet transitive deps impose a >=22.12 floor (@rolldown/binding, yargs, concurrently). Added engines and .nvmrc so a too-old runtime fails clearly. Typechecked and production-built on all four; verified the socket binds 127.0.0.1 only, health reports amo:true, the site still boots with the CRM unconfigured, and the limiter returns 429 with Retry-After on the ninth request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cd51cb5632 |
Fix horizontal truncation across the phone layer
Four defects, all of them real clipping rather than styling taste. The header's two action pills were `shrink-0` at 288px inside a 343px container, so flexbox took the difference out of the logo, which carried `min-w-0`. The brand mark rendered 37px wide at 375px and 0px at 320px, and the CTA ran past the right edge. The wordmark stays legible now and the call button goes square and icon-only below 760px; the CTA moves to a new MobileActionBar, which has room for it and puts it in the thumb zone. It waits for the hero's own buttons to scroll away and drops back down while the form is on screen, so it never covers the fields it points at. `.scroll-cards` bleeds to the viewport edge with a 16px inline padding and a matching negative margin, but had no `scroll-padding-inline`, so `scroll-snap-align: start` resolved to scroll position 16 and mandatory snapping forced that scroll on load: the first card sat on the bezel with no gutter, and one row had drifted a full card over. Matching the padding fixes the resting position on all seven rows. Card tracks move from a percentage of the row to `calc(100vw - 62px)`. A percentage peek grows with the screen, and at 82-90% it exposed ~68px of the next card -- enough legible half-words to read as broken text. The value stays in the markup as `auto-cols-*` so it keeps its sort position against `sm:grid-still`, which has to win at the breakpoint. The market stat card pinned its source line to the card bottom while the longest label wraps to four lines at phone width, running text through text by 75px. The source is in flow under `mt-auto` instead. The header CTA switches on `max-md` rather than the `narrow` variant: `narrow` is max-width 760 and the bar is min-width 760, so at exactly 760px both would have hidden and the page would have had no CTA at all. Verified at 320 / 375 / 759 / 760 / 800 and desktop: no horizontal page scroll, no in-flow overflow, desktop grids unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b4f93bb16 |
Redesign the phone layer of the landing
Three layout bugs and a mobile experience that had no wayfinding. The `#proposal` section was clipped, not merely cramped: the submit button carried `whitespace-nowrap` from the shared button base, giving it a min-content width of 358px, which sized the section's implicit `auto` grid track to 406px inside a 343px container. `overflow-hidden` then sliced the copy and every form field off the right edge of a 375px screen. Fixed at the root — `whitespace-nowrap` moves out of `base` and into `headerSize`, where buttons are sized by their label — plus an explicit `grid-cols-[minmax(0,1fr)]` on the section. The market stat cards put their source line in absolute position against a guessed min-height, so the longest label ran underneath it. It is in the flow now, pinned by `mt-auto`. Below 980px the header nav is not rendered, which left seventeen screens of scroll with no way to navigate them. The header gives that width back to the page — logo and call button only — and <ThumbBar /> takes over: scroll progress, a live section indicator that opens a section sheet, and one short CTA, hidden over the hero and over the form. Card rows now say what they are. Rows of unordered peers keep scrolling sideways and gain a measured segment indicator (<Rail />), replacing the two prose "swipe sideways" hints. The launch timeline goes the other way: sideways scrolling hid week 3-5 from someone reading week 1-2, so below 900px it becomes four disclosures on a spine — which also recovers the scroll length the taller hero spent. Below 560px the type scale steps down half a level and the display tracking opens back up, and the hero photo zooms past `cover` onto the wave wall, since fitting by height left only ceiling tiles above the copy. Also: the route card inside the market section split into two 165px columns at 760px, so that split now waits for 980px. Desktop above 980px is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |