Commit Graph
7 Commits
Author SHA1 Message Date
yuriy.p 56ff5839b8 Merge pull request 'Add the Top.Mail.Ru counter to all four landings with a lead goal' (#1) from topmail-counter into main
Reviewed-on: #1
2026-09-20 17:47:40 +06:00
yuriy.pandClaude Opus 5 b32e7da506 Add the Top.Mail.Ru counter to all four landings with a lead goal
The counter (id 3795059) was only in fitnes and only counted pageViews.
Carry the snippet over to hotel, medcenterstart and medcenterphysio — the
same counter serves all four, as the Metrika one already does — and fire
goal 'form' from submitLead(), the single place both the request form and
the callback dialog pass through.

The goal sits inside the existing `leadId !== 0` guard: that response is
the honeypot decoy, so a bot submission must not register a conversion,
in Top.Mail.Ru no more than in Metrika.

The noscript pixel goes in the body next to the Metrika one, per 9af36cc;
the fitnes one, added in the head, moves down to join it.

Note that fitnes/index.html also carries a Prettier pass over the whole
file — the reflowed Metrika block is formatting only, no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:08:29 +06:00
yuriy.p be7741e269 domains 2026-08-29 16:07:27 +06:00
yuriy.pandClaude Opus 5 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>
2026-08-29 15:57:49 +06:00
yuriy.pandClaude Opus 5 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>
2026-08-29 15:57:24 +06:00
yuriy.pandClaude Opus 5 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>
2026-08-29 11:51:56 +06:00
yuriy.pandClaude Opus 5 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>
2026-08-29 11:32:28 +06:00