SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%

brand: WebSensor pulse mark — SVG logo, multi-size favicon.ico, apple-touch and PWA icons (incl. maskable), redesigned OpenGraph/Twitter share image with live counters, mark in nav and event share images

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 days ago (Sep 11, 2026) parent 4d60438

13 changed files +114 −24

added apps/web/public/icons/icon-192.png +0 −0

Binary file not shown.

added apps/web/public/icons/icon-512-maskable.png +0 −0

Binary file not shown.

added apps/web/public/icons/icon-512.png +0 −0

Binary file not shown.

added apps/web/public/logo.svg +13 −0
@@ -0,0 +1,13 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2 + <title>WebSensor</title>
3 + <rect width="64" height="64" rx="14" fill="#0a0e15"/>
4 + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#22d3a5" stroke-opacity="0.18"/>
5 + <!-- baseline of the signal -->
6 + <path d="M8 46H56" stroke="#2d394e" stroke-width="2" stroke-linecap="round"/>
7 + <!-- the pulse: a W-shaped waveform, grey until the detected change, signal green after -->
8 + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke="#c9d1e0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
9 + <path d="M43 46L54 22" fill="none" stroke="#22d3a5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
10 + <!-- sensor reading -->
11 + <circle cx="54" cy="22" r="7" fill="#22d3a5" fill-opacity="0.22"/>
12 + <circle cx="54" cy="22" r="4" fill="#22d3a5"/>
13 +</svg>
added apps/web/src/app/apple-icon.png +0 −0

Binary file not shown.

modified apps/web/src/app/event/[slug]/opengraph-image.tsx +2 −1
@@ -1,4 +1,5 @@
1 1 import { ImageResponse } from "next/og";
2 +import { Mark } from "@/components/brand";
2 3 import { api } from "@/lib/api";
3 4 import { fmtScore, utcDateTime } from "@/lib/format";
4 5
@@ -47,7 +48,7 @@ export default async function OpenGraphImage({ params }: { params: Promise<{ slu
47 48 <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 56, background: BG, color: FG, fontFamily: "Inter, system-ui, sans-serif" }}>
48 49 <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
49 50 <div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 30, fontWeight: 700 }}>
50 − <div style={{ width: 16, height: 16, borderRadius: 16, background: SIGNAL, boxShadow: "0 0 0 8px rgba(34,211,165,0.22)" }} />
51 + <Mark size={44} />
51 52 <span>
52 53 Web<span style={{ color: SIGNAL }}>Sensor</span>
53 54 </span>
added apps/web/src/app/favicon.ico +0 −0

Binary file not shown.

modified apps/web/src/app/icon.svg +13 −1
@@ -1 +1,13 @@
1 −<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0b0f17"/><circle cx="32" cy="32" r="8" fill="#22d3a5"/><circle cx="32" cy="32" r="16" fill="none" stroke="#22d3a5" stroke-opacity="0.55" stroke-width="3"/><circle cx="32" cy="32" r="25" fill="none" stroke="#22d3a5" stroke-opacity="0.25" stroke-width="3"/></svg>
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2 + <title>WebSensor</title>
3 + <rect width="64" height="64" rx="14" fill="#0a0e15"/>
4 + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#22d3a5" stroke-opacity="0.18"/>
5 + <!-- baseline of the signal -->
6 + <path d="M8 46H56" stroke="#2d394e" stroke-width="2" stroke-linecap="round"/>
7 + <!-- the pulse: a W-shaped waveform, grey until the detected change, signal green after -->
8 + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke="#c9d1e0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
9 + <path d="M43 46L54 22" fill="none" stroke="#22d3a5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
10 + <!-- sensor reading -->
11 + <circle cx="54" cy="22" r="7" fill="#22d3a5" fill-opacity="0.22"/>
12 + <circle cx="54" cy="22" r="4" fill="#22d3a5"/>
13 +</svg>
modified apps/web/src/app/manifest.ts +10 −5
@@ -2,13 +2,18 @@ import type { MetadataRoute } from "next";
2 2
3 3 export default function manifest(): MetadataRoute.Manifest {
4 4 return {
5 − name: "WebSensor",
5 + name: "WebSensor — Real-time Internet change intelligence",
6 6 short_name: "WebSensor",
7 − description: "Detect What Changed. Know Why It Matters.",
7 + description: "The Web is changing. We are watching. Detect what changed, know why it matters.",
8 8 start_url: "/",
9 9 display: "standalone",
10 − background_color: "#0b0f17",
11 − theme_color: "#0b0f17",
12 − icons: [{ src: "/icon.svg", sizes: "any", type: "image/svg+xml" }],
10 + background_color: "#0a0e15",
11 + theme_color: "#0a0e15",
12 + icons: [
13 + { src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
14 + { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
15 + { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
16 + { src: "/icons/icon-512-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
17 + ],
13 18 };
14 19 }
modified apps/web/src/app/opengraph-image.tsx +31 −14
@@ -1,30 +1,47 @@
1 1 import { ImageResponse } from "next/og";
2 +import { BRAND, Mark, PulseBackdrop } from "@/components/brand";
3 +import { api, type Stats } from "@/lib/api";
4 +import { fmtInt } from "@/lib/format";
2 5
3 6 export const runtime = "nodejs";
4 −export const alt = "WebSensor — The Web, Live.";
7 +export const alt = "WebSensor — The Web is changing. We are watching.";
5 8 export const size = { width: 1200, height: 630 };
6 9 export const contentType = "image/png";
7 10
8 −export default function OpenGraphImage() {
11 +/** Site-wide share image (spec §82–83). Live counters come from the API; nothing is fabricated. */
12 +export default async function OpenGraphImage() {
13 + const s: Stats = await api.stats().catch(() => ({}) as Stats);
14 + const facts = [s.sources ? `${fmtInt(s.sources)} sources` : null, s.sensors ? `${fmtInt(s.sensors)} sensors` : null, s.events_24h ? `${fmtInt(s.events_24h)} events / 24 h` : null].filter(Boolean) as string[];
9 15 return new ImageResponse(
10 16 (
11 − <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 64, background: "#0b0f17", color: "#e5e7eb", fontFamily: "Inter, system-ui, sans-serif" }}>
12 − <div style={{ display: "flex", alignItems: "center", gap: 16, fontSize: 36, fontWeight: 700 }}>
13 − <div style={{ width: 20, height: 20, borderRadius: 20, background: "#22d3a5", boxShadow: "0 0 0 10px rgba(34,211,165,0.25)" }} />
14 − <span>
15 − Web<span style={{ color: "#22d3a5" }}>Sensor</span>
16 − </span>
17 + <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 64, background: `linear-gradient(135deg, ${BRAND.bg} 0%, #0f1622 100%)`, color: "#e6e8ee", fontFamily: "Inter, system-ui, sans-serif", position: "relative" }}>
18 + <PulseBackdrop width={1200} height={630} />
19 + <div style={{ display: "flex", alignItems: "center", gap: 20 }}>
20 + <Mark size={72} />
21 + <div style={{ display: "flex", flexDirection: "column" }}>
22 + <div style={{ display: "flex", fontSize: 44, fontWeight: 700, letterSpacing: -1 }}>
23 + Web<span style={{ color: BRAND.signal }}>Sensor</span>
24 + </div>
25 + <div style={{ display: "flex", fontSize: 20, color: BRAND.subtle, fontFamily: "monospace", letterSpacing: 3 }}>REAL-TIME INTERNET CHANGE INTELLIGENCE</div>
26 + </div>
17 27 </div>
18 − <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
19 − <div style={{ display: "flex", flexDirection: "column", fontSize: 76, fontWeight: 700, letterSpacing: -2, lineHeight: 1.05 }}>
28 + <div style={{ display: "flex", flexDirection: "column", gap: 20, maxWidth: 900 }}>
29 + <div style={{ display: "flex", flexDirection: "column", fontSize: 80, fontWeight: 700, letterSpacing: -3, lineHeight: 1.02 }}>
20 30 <span>The Web is changing.</span>
21 − <span>We are watching.</span>
31 + <span style={{ color: BRAND.muted }}>We are watching.</span>
22 32 </div>
23 − <div style={{ fontSize: 30, color: "#a3adc2" }}>Detect What Changed. Know Why It Matters.</div>
33 + <div style={{ display: "flex", fontSize: 28, color: BRAND.muted, lineHeight: 1.35 }}>What changed, where, when, whether it was silent, how important it is — with immutable evidence.</div>
24 34 </div>
25 − <div style={{ display: "flex", justifyContent: "space-between", fontSize: 24, color: "#6b7591", fontFamily: "monospace" }}>
35 + <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 24, color: BRAND.subtle, fontFamily: "monospace" }}>
36 + <div style={{ display: "flex", gap: 28 }}>
37 + {facts.map((f) => (
38 + <span key={f} style={{ display: "flex", alignItems: "center", gap: 10 }}>
39 + <span style={{ width: 8, height: 8, borderRadius: 8, background: BRAND.signal }} />
40 + {f}
41 + </span>
42 + ))}
43 + </div>
26 44 <span>www.websensor.io</span>
27 − <span>LIVE · UTC</span>
28 45 </div>
29 46 </div>
30 47 ),
added apps/web/src/app/twitter-image.tsx +1 −0
@@ -0,0 +1 @@
1 +export { default, alt, size, contentType, runtime } from "./opengraph-image";
added apps/web/src/components/brand.tsx +42 −0
@@ -0,0 +1,42 @@
1 +import type { CSSProperties } from "react";
2 +
3 +/**
4 + * WebSensor brand mark: a dark rounded tile with a W-shaped pulse — grey until the detected change,
5 + * signal green after it, ending in the sensor reading. The same geometry is used for the favicon
6 + * (app/icon.svg), the nav wordmark, the OpenGraph images and the PWA icons (public/icons/*).
7 + */
8 +export const BRAND = {
9 + bg: "#0a0e15",
10 + fg: "#c9d1e0",
11 + signal: "#22d3a5",
12 + line: "#2d394e",
13 + muted: "#a3adc2",
14 + subtle: "#6b7591",
15 +} as const;
16 +
17 +export function Mark({ size = 20, className = "", style, title = "WebSensor" }: { size?: number; className?: string; style?: CSSProperties; title?: string }) {
18 + return (
19 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width={size} height={size} className={className} style={style} role="img" aria-label={title}>
20 + <rect width="64" height="64" rx="14" fill={BRAND.bg} />
21 + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke={BRAND.signal} strokeOpacity="0.18" />
22 + <path d="M8 46H56" stroke={BRAND.line} strokeWidth="2" strokeLinecap="round" />
23 + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke={BRAND.fg} strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" />
24 + <path d="M43 46L54 22" fill="none" stroke={BRAND.signal} strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" />
25 + <circle cx="54" cy="22" r="7" fill={BRAND.signal} fillOpacity="0.22" />
26 + <circle cx="54" cy="22" r="4" fill={BRAND.signal} />
27 + </svg>
28 + );
29 +}
30 +
31 +/** Large decorative pulse used as the OpenGraph backdrop (no tile, stroke only). */
32 +export function PulseBackdrop({ width, height, opacity = 0.16 }: { width: number; height: number; opacity?: number }) {
33 + const y0 = height * 0.72;
34 + const d = `M-20 ${y0} H${width * 0.28} L${width * 0.36} ${y0 - height * 0.42} L${width * 0.47} ${y0 + height * 0.06} L${width * 0.56} ${y0 - height * 0.18} L${width * 0.63} ${y0} H${width * 0.74} L${width * 0.8} ${y0 - height * 0.62} L${width * 0.86} ${y0} H${width + 20}`;
35 + return (
36 + <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ position: "absolute", top: 0, left: 0 }}>
37 + <path d={d} fill="none" stroke={BRAND.signal} strokeWidth={6} strokeLinecap="round" strokeLinejoin="round" strokeOpacity={opacity} />
38 + <circle cx={width * 0.8} cy={y0 - height * 0.62} r={22} fill={BRAND.signal} fillOpacity={opacity * 0.6} />
39 + <circle cx={width * 0.8} cy={y0 - height * 0.62} r={11} fill={BRAND.signal} fillOpacity={opacity * 1.6} />
40 + </svg>
41 + );
42 +}
modified apps/web/src/components/nav.tsx +2 −3
@@ -4,6 +4,7 @@ import Link from "next/link";
4 4 import { usePathname } from "next/navigation";
5 5 import { Activity, Bell, Bookmark, Compass, Eye, Globe2, LayoutGrid, Radar, Search, Siren, X, Zap } from "lucide-react";
6 6 import { useState } from "react";
7 +import { Mark } from "./brand";
7 8 import { DensityToggle } from "./prefs";
8 9 import { ThemeToggle } from "./theme";
9 10 import { Kbd } from "./ui";
@@ -24,9 +25,7 @@ const NAV = [
24 25 export function Wordmark() {
25 26 return (
26 27 <Link href="/" className="flex items-center gap-2 font-semibold tracking-tight" aria-label="WebSensor home">
27 − <span className="relative inline-flex size-2.5 items-center justify-center">
28 − <span className="absolute inset-0 rounded-full bg-signal animate-pulse-dot" />
29 − </span>
28 + <Mark size={22} className="shrink-0 rounded-[5px]" />
30 29 <span className="text-[15px]">
31 30 Web<span className="text-signal">Sensor</span>
32 31 </span>
33 32