chore(autonomous-session): consolidate uncommitted work from prior session

Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -35,7 +35,7 @@ export function PageviewsChart({ data }: Props) {
// Merge the two series (Umami returns them separately when `compare` is
// requested) into one row per bucket so we can drive a single chart.
// `sessions` is optional on Umami v3 only present when the request
// `sessions` is optional on Umami v3 - only present when the request
// included a comparison directive. Guard the read so an undefined
// array doesn't crash the chart.
const byX = new Map<string, { x: string; pageviews: number; sessions: number }>();
@@ -68,7 +68,7 @@ export function PageviewsChart({ data }: Props) {
fontSize={11}
tick={{ fill: 'hsl(var(--muted-foreground))' }}
tickFormatter={formatXTick}
// Anchor first + last ticks then let Recharts thin out the middle
// Anchor first + last ticks then let Recharts thin out the middle -
// multi-week ranges previously crowded every day-bucket label onto
// the axis. minTickGap enforces ~52px between rendered ticks.
interval="preserveStartEnd"
@@ -110,7 +110,7 @@ export function PageviewsChart({ data }: Props) {
);
}
/** Compact tick labels: drop the timestamp entirely for multi-day ranges
/** Compact tick labels: drop the timestamp entirely - for multi-day ranges
* the hour component is meaningless (a "day" bucket aggregates the whole
* day) and just causes visual crowding. Keep MM-DD. */
function formatXTick(value: string): string {

View File

@@ -1,7 +1,7 @@
'use client';
/**
* Realtime panel Umami's "what's happening RIGHT NOW" view, surfaced
* Realtime panel - Umami's "what's happening RIGHT NOW" view, surfaced
* as a collapsible card at the top of the website-analytics page.
*
* Folds in five things from Umami's /api/realtime/<id> endpoint:
@@ -20,6 +20,7 @@ import { ChevronDown, ChevronUp, Globe, Activity, MapPin, ExternalLink } from 'l
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { CountryFlag } from '@/components/shared/country-flag';
import { getCountryName } from '@/lib/i18n/countries';
import { useUmamiRealtime } from './use-website-analytics';
@@ -28,7 +29,7 @@ export function RealtimePanel() {
const query = useUmamiRealtime(open);
const data = query.data?.data ?? null;
// Hide the entire bar when Umami reports a quiet 30-minute window
// Hide the entire bar when Umami reports a quiet 30-minute window -
// a "Live activity (0 visitors)" header is just noise. We still poll
// every 60 s while hidden so the bar reappears the moment traffic
// arrives.
@@ -53,7 +54,7 @@ export function RealtimePanel() {
<div className="text-xs text-muted-foreground">
{open
? 'Auto-refreshing every 5s · last 30 minutes'
: 'Click to expand top pages, countries, and a live event stream'}
: 'Click to expand - top pages, countries, and a live event stream'}
</div>
</div>
</div>
@@ -138,10 +139,19 @@ export function RealtimePanel() {
? 'Homepage'
: ev.urlPath}
</span>
<span className="ml-2 text-muted-foreground">
{[ev.country && getCountryName(ev.country, 'en'), ev.browser, ev.device]
.filter(Boolean)
.join(' · ')}
<span className="ml-2 inline-flex items-center gap-1.5 text-muted-foreground">
{ev.country ? (
<CountryFlag code={ev.country} className="h-2.5 w-3.5" decorative />
) : null}
<span>
{[
ev.country && getCountryName(ev.country, 'en'),
ev.browser,
ev.device,
]
.filter(Boolean)
.join(' · ')}
</span>
</span>
</div>
<span className="shrink-0 tabular-nums text-muted-foreground">

View File

@@ -9,6 +9,7 @@
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Skeleton } from '@/components/ui/skeleton';
import { useUmamiSessionActivity } from './use-website-analytics';
import { CountryFlag } from '@/components/shared/country-flag';
import { getCountryName } from '@/lib/i18n/countries';
import type { DateRange } from '@/lib/analytics/range';
import type { UmamiSession } from '@/lib/services/umami.service';
@@ -35,14 +36,21 @@ export function SessionDetailSheet({ session, range, onClose }: Props) {
{/* Top facts */}
<dl className="grid grid-cols-2 gap-y-2 text-sm">
<DtDd label="Location">
{getCountryName(session.country, 'en') || 'Unknown'}
{session.city ? ` · ${session.city}` : ''}
<span className="inline-flex items-center gap-1.5">
{session.country ? (
<CountryFlag code={session.country} className="h-3 w-4" decorative />
) : null}
<span>
{getCountryName(session.country, 'en') || 'Unknown'}
{session.city ? ` · ${session.city}` : ''}
</span>
</span>
</DtDd>
<DtDd label="Device">{session.device}</DtDd>
<DtDd label="Browser">{session.browser}</DtDd>
<DtDd label="OS">{session.os}</DtDd>
<DtDd label="Screen">{session.screen || ''}</DtDd>
<DtDd label="Language">{session.language || ''}</DtDd>
<DtDd label="Screen">{session.screen || '-'}</DtDd>
<DtDd label="Language">{session.language || '-'}</DtDd>
<DtDd label="First visit">{fmtTime(session.firstAt)}</DtDd>
<DtDd label="Last visit">{fmtTime(session.lastAt)}</DtDd>
<DtDd label="Visits">{session.visits.toLocaleString()}</DtDd>

View File

@@ -17,6 +17,7 @@ import { Globe, Smartphone, Monitor, Tablet, ChevronRight } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { CountryFlag } from '@/components/shared/country-flag';
import { getCountryName } from '@/lib/i18n/countries';
import { useUmamiSessions } from './use-website-analytics';
import { SessionDetailSheet } from './session-detail-sheet';
@@ -70,7 +71,10 @@ export function SessionsList({ range }: Props) {
<div className="flex min-w-0 flex-1 items-center gap-3">
<DeviceIcon device={s.device} />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{s.country ? (
<CountryFlag code={s.country} className="h-3 w-4" decorative />
) : null}
<span className="font-medium">
{getCountryName(s.country, 'en') || 'Unknown'}
</span>

View File

@@ -40,7 +40,7 @@ export function TopList({
<CardTitle className="text-base">{title}</CardTitle>
{viewAllHref ? (
<Link
// typedRoutes is enabled viewAllHref is constructed at the
// typedRoutes is enabled - viewAllHref is constructed at the
// call site from string interpolation, so opt out of the
// literal-string check here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -32,7 +32,7 @@ interface MetricResponse<T> {
range: DateRange;
data: T | null;
/** True when Umami isn't configured for the port. Surfaced as a 200
* response (not 4xx) so React Query doesn't infinitely retry that
* response (not 4xx) so React Query doesn't infinitely retry - that
* retry loop previously saturated the postgres pool and hung the dev
* server. UI consumers should check this flag and render the
* "set up Umami" empty state. */
@@ -111,7 +111,7 @@ export const useUmamiTopDevices = (range: DateRange, limit = 10) =>
export const useUmamiWebsiteInfo = (range: DateRange) =>
useUmamiQuery<UmamiWebsiteInfo>('website', range, '', 'website-info');
// Phase 2 sessions surface. Paginated list of recent sessions plus
// Phase 2 - sessions surface. Paginated list of recent sessions plus
// per-session detail + activity stream + weekly engagement heatmap.
export const useUmamiSessions = (
range: DateRange,
@@ -153,10 +153,10 @@ export const useUmamiSessionActivity = (range: DateRange, sessionId: string | nu
export const useUmamiSessionsWeekly = (range: DateRange) =>
useUmamiQuery<number[][]>('sessions-weekly', range);
// Realtime panel Umami's /api/realtime endpoint returns last-30-min
// Realtime panel - Umami's /api/realtime endpoint returns last-30-min
// activity. Two cadences: 5 s when the panel is expanded (so it feels
// live) and 60 s when collapsed (so we still know whether to show the
// "Live activity" bar at all the bar is hidden entirely when there
// "Live activity" bar at all - the bar is hidden entirely when there
// are zero visitors and zero events in the last 30 minutes).
export const useUmamiRealtime = (expanded: boolean) =>
useUmamiQuery<UmamiRealtime>(

View File

@@ -6,7 +6,7 @@
* click to filter the rest of the page to that country.
*
* Uses ECharts' own world.json (the GeoJSON shipped with their public
* examples) pre-cleaned, no antimeridian artifacts. Country features
* examples) - pre-cleaned, no antimeridian artifacts. Country features
* are matched on `properties.name` (English country name from the source).
*/

View File

@@ -86,7 +86,7 @@ export function WebsiteAnalyticsShell() {
const eyebrow = websiteInfo.data?.data?.domain || websiteInfo.data?.data?.name || 'Marketing';
// API surfaces `notConfigured: true` on a 200 response (not 4xx) so
// React Query doesn't infinite-retry that retry loop saturated the
// React Query doesn't infinite-retry - that retry loop saturated the
// postgres pool and stalled the dev server. Single empty state covers
// all widgets so the page doesn't show six loading spinners forever.
const notConfigured = stats.data?.notConfigured === true;
@@ -105,7 +105,7 @@ export function WebsiteAnalyticsShell() {
<NotConfiguredEmptyState portSlug={portSlug} />
) : (
<>
{/* Quiet-range nudge when the range has near-zero visitors,
{/* Quiet-range nudge - when the range has near-zero visitors,
surface a small banner suggesting a wider window. Avoids
the rep thinking the integration is broken on a fresh
port or during off-season. Threshold of 5 visitors keeps
@@ -287,14 +287,14 @@ export function WebsiteAnalyticsShell() {
nationality. A proper inline analytics filter (scoping the
session list + KPIs to the picked country) requires the
useUmami* hooks to accept a country param, which they don't
yet that's parked alongside the Phase 5 funnels work. */}
yet - that's parked alongside the Phase 5 funnels work. */}
<VisitorWorldMap
rows={allCountries.data?.data ?? null}
loading={allCountries.isLoading}
onCountryClick={(iso2) => {
const url = `/${portSlug}/clients?nationality=${encodeURIComponent(iso2)}`;
void navigator.clipboard?.writeText(window.location.origin + url);
toast.message(`${iso2} link copied`, {
toast.message(`${iso2} - link copied`, {
description: `Paste into the address bar to see all ${iso2} clients.`,
});
}}