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:
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
|
||||
|
||||
@@ -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.`,
|
||||
});
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user