Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Research/
# NEVER committed. The tiny committed test fixture (*.fixture.csv) is kept.
showcase/server/data/dbip-city-lite.*
!showcase/server/data/dbip-city-lite.fixture.csv
!showcase/server/data/dbip-city-lite.ipv6.fixture.csv
.claude/worktrees/
BUG-REPORT-*.md
dist/skill/
Expand Down
8 changes: 5 additions & 3 deletions fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ primary_region = 'sjc'
NODE_ENV = 'production'
# Worldwide IP->region dataset (DB-IP IP-to-City Lite, generated by
# showcase/server/scripts/refresh-dbip-dataset.mjs). Lives on the mounted
# /data volume (NOT baked into the image). Safe to set before the file exists:
# ip-geo.js degrades to 'unknown' until it is uploaded, then restart the
# machine so the lazy loader re-reads. See showcase/server/data/README.md.
# /data volume (NOT baked into the image). Safe to set before the files exist:
# ip-geo.js degrades to 'unknown' until they are uploaded, then restart the
# machine so the lazy loader re-reads. IPv4 + sibling IPv6. See
# showcase/server/data/README.md.
DBIP_DATASET_PATH = '/data/dbip-city-lite.csv'
DBIP_IPV6_DATASET_PATH = '/data/dbip-city-lite.ipv6.csv'

[http_service]
internal_port = 3847
Expand Down
2 changes: 1 addition & 1 deletion package.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions showcase/angular/src/app/core/stats/fsb-telemetry.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export interface FSBTelemetryHeadline {
popular_agents: Array<{ label: string; uniq: number }>;
/** Latest day's coarse region aggregate with a k>=5 floor. */
popular_regions: Array<{ label: string; uniq: number }>;
/**
* Last-known coarse region per install across the retained 365-day rollups,
* k>=5 floored. Anonymous (country / US-state labels only). Powers the globe
* so location survives the 7-day raw-event wipe.
*/
users_by_region_365d?: Array<{ label: string; uniq: number }>;
/** Compatibility alias for avg_agents_per_reporting_user. */
avg_agents_per_user: number;
/** active_agents_now / active_agents_reporting_users_now. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
/* Blink ignores a fully transparent select background and keeps UA Field
* (white unless color-scheme is dark). --bg-card is opaque and matches the footer. */
background-color: var(--bg-card);
border: 0;
color: inherit;
color-scheme: inherit;
font: inherit;
padding: 0.25rem 1.25rem 0.25rem 0.5rem;
cursor: pointer;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="privacy-header">
<div class="section-label" i18n="@@privacy.header.kicker">Legal</div>
<h1 i18n="@@privacy.header.title">Privacy Policy</h1>
<p class="privacy-last-updated" i18n="@@privacy.header.updated">Last updated: August 2026</p>
<p class="privacy-last-updated" i18n="@@privacy.header.updated">Last updated: September 2026</p>
</div>

<div class="privacy-content">
Expand Down Expand Up @@ -194,7 +194,7 @@ <h2 i18n="@@privacy.section.sessionReplay.title">Session Replay and Screenshots<

<!-- Anonymous Usage Telemetry (v0.9.69 / Phase 275) -->
<h2 id="telemetry-disclosure" i18n="@@PRIVACY_TELEMETRY_HEADING">Anonymous Usage Telemetry</h2>
<p i18n="@@PRIVACY_TELEMETRY_INTRO"><span [attr.translate]="'no'">FSB</span> v0.9.69 introduced an opt-out anonymous usage telemetry pipeline so the project can publish aggregate adoption numbers (see <a routerLink="/stats">/stats</a>) without ever touching the pages you browse. The only location signal is a coarse country/state label the server derives from your request <span [attr.translate]="'no'">IP</span> at ingest (never from page content), kept only in aggregate &mdash; see <a href="#telemetry-region">Region (state-level) metric</a> below. Telemetry is on by default but can be disabled with a single toggle, and the per-install data can be erased on request.</p>
<p i18n="@@PRIVACY_TELEMETRY_INTRO"><span [attr.translate]="'no'">FSB</span> v0.9.69 introduced an opt-out anonymous usage telemetry pipeline so the project can publish aggregate adoption numbers (see <a routerLink="/stats">/stats</a>) without ever touching the pages you browse. The only location signal is a coarse country/state label the server derives from your request <span [attr.translate]="'no'">IP</span> at ingest (never from page content), published only in aggregate &mdash; see <a href="#telemetry-region">Region (state-level) metric</a> below. Telemetry is on by default but can be disabled with a single toggle, and the per-install data can be erased on request.</p>

<h3 i18n="@@PRIVACY_TELEMETRY_COLLECT_HEADING">What we collect</h3>
<ul>
Expand All @@ -210,20 +210,20 @@ <h3 i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_HEADING">What we do NOT collect</h3>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_URLS">Page <span [attr.translate]="'no'">URLs</span>, hostnames, or browsing history.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_PROMPTS">Prompts, instructions, task descriptions, or any natural-language text you send to your model provider.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_DOM">Page <span [attr.translate]="'no'">DOM</span>, screenshots, page content, site <span [attr.translate]="'no'">API</span> payloads, or <span [attr.translate]="'no'">AI</span> responses.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_IP">Plaintext <span [attr.translate]="'no'">IP</span> addresses. The server uses the request <span [attr.translate]="'no'">IP</span> transiently and inline for exactly two purposes &mdash; a daily-rotating-salt hash for rate limiting, and deriving a coarse country/state label (see below) &mdash; then discards it immediately. The plaintext <span [attr.translate]="'no'">IP</span> is never stored or logged.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_IP">Plaintext <span [attr.translate]="'no'">IP</span> addresses. The server uses the request <span [attr.translate]="'no'">IP</span> transiently and inline for exactly three purposes &mdash; a daily-rotating-salt hash for rate limiting, deriving a coarse country/state label (see below), and noting whether the address is IPv4 or IPv6 &mdash; then discards it immediately. The plaintext <span [attr.translate]="'no'">IP</span> is never stored or logged.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_NAMES">Names, usernames, account handles, or any free-form identity fields.</li>
<li i18n="@@PRIVACY_TELEMETRY_NOT_COLLECT_EMAILS">Email addresses, phone numbers, or contact information.</li>
</ul>

<h3 id="telemetry-region" i18n="@@PRIVACY_TELEMETRY_REGION_HEADING">Region (state-level) metric</h3>
<ul>
<li i18n="@@PRIVACY_TELEMETRY_REGION_DERIVED">The server derives a coarse country and US-state (subdivision) label from your request <span [attr.translate]="'no'">IP</span> at ingest, using a self-hosted <span [attr.translate]="'no'">DB-IP</span> <span [attr.translate]="'no'">IP</span>-to-City Lite dataset and our own lookup &mdash; <strong>not</strong> a live third-party geolocation service, and never <span [attr.translate]="'no'">MaxMind</span>. The plaintext <span [attr.translate]="'no'">IP</span> is consumed inline and discarded; only the derived label is kept.</li>
<li i18n="@@PRIVACY_TELEMETRY_REGION_KFLOOR">Region is stored <strong>only</strong> in the daily aggregate, behind a k&ge;5 anonymity floor: any state with fewer than 5 distinct installs on a given day collapses into a single &ldquo;Other&rdquo; bucket (suppressed entirely when even that combined count is below 5). No individual state label ever represents fewer than 5 installs.</li>
<li i18n="@@PRIVACY_TELEMETRY_REGION_NOPROFILE">There is no per-install location profile. The coarse label lives only on the short-lived raw event (dropped by the 7-day retention) and in the k&ge;5-floored daily rollup; we never build or retain a durable mapping from your install to a place. Geolocation data is by <span [attr.translate]="'no'">DB-IP</span> (<a href="https://db-ip.com" rel="noopener" [attr.translate]="'no'">https://db-ip.com</a>).</li>
<li i18n="@@PRIVACY_TELEMETRY_REGION_DERIVED">The server derives a coarse country and US-state (subdivision) label from your request <span [attr.translate]="'no'">IP</span> at ingest, using a self-hosted <span [attr.translate]="'no'">DB-IP</span> <span [attr.translate]="'no'">IP</span>-to-City Lite dataset and our own lookup &mdash; <strong>not</strong> a live third-party geolocation service, and never <span [attr.translate]="'no'">MaxMind</span>. The plaintext <span [attr.translate]="'no'">IP</span> is consumed inline and discarded; only the derived label and the address family (IPv4 or IPv6) are kept.</li>
<li i18n="@@PRIVACY_TELEMETRY_REGION_KFLOOR">Region is published <strong>only</strong> in aggregate, behind a k&ge;5 anonymity floor: any state with fewer than 5 distinct installs collapses into a single &ldquo;Other&rdquo; bucket (suppressed entirely when even that combined count is below 5). No published state label ever represents fewer than 5 installs.</li>
<li i18n="@@PRIVACY_TELEMETRY_REGION_NOPROFILE">The coarse label is stored on the raw event (dropped by the 7-day retention) and on your install's daily rollup, which is kept for 365 days so <a routerLink="/stats">/stats</a> can count each install once by its most recent region. It is never an <span [attr.translate]="'no'">IP</span> address, city, or coordinates; it is deleted with the rest of your per-install data when you request erasure; and it is only published behind the k&ge;5 floor above. Geolocation data is by <span [attr.translate]="'no'">DB-IP</span> (<a href="https://db-ip.com" rel="noopener" [attr.translate]="'no'">https://db-ip.com</a>).</li>
</ul>

<h3 i18n="@@PRIVACY_TELEMETRY_RETENTION_HEADING">Retention</h3>
<p i18n="@@PRIVACY_TELEMETRY_RETENTION_TEXT">Raw events are retained for 7 days. Daily rollups (one row per install per day) are retained for 365 days. Global aggregates (one row per day, no per-install dimension) are retained indefinitely so historical charts on <a routerLink="/stats">/stats</a> remain stable.</p>
<p i18n="@@PRIVACY_TELEMETRY_RETENTION_TEXT">Raw events are retained for 7 days. Daily rollups (one row per install per day, including the coarse region label) are retained for 365 days. Global aggregates (one row per day, no per-install dimension) are retained indefinitely so historical charts on <a routerLink="/stats">/stats</a> remain stable.</p>

<h3 i18n="@@PRIVACY_TELEMETRY_OPTOUT_HEADING">How to opt out</h3>
<p i18n="@@PRIVACY_TELEMETRY_OPTOUT_TEXT">Open the <span [attr.translate]="'no'">FSB</span> Control Panel, scroll to Advanced Settings, and toggle <strong>Send anonymous usage data</strong> off. The change takes effect immediately; no further events will be sent from your install.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ <h1><span i18n="@@stats.header.title">Stats</span> <span class="muted" i18n="@@s
<p class="globe-annotation">
<i class="ph ph-info" aria-hidden="true"></i>
@if (hasPlottableRegions) {
<span i18n="@@SHOWCASE_STATS_FSB_GLOBE_ANNOTATION">Regional distribution from today's hourly aggregate.</span>
<span i18n="@@SHOWCASE_STATS_FSB_GLOBE_ANNOTATION">Where installs were last seen, by coarse region, over the past 365 days.</span>
} @else {
<span i18n="@@SHOWCASE_STATS_FSB_GLOBE_EMPTY">Not enough anonymous activity yet to break down by region — check back soon.</span>
}
Expand Down
17 changes: 11 additions & 6 deletions showcase/angular/src/app/pages/stats/stats-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,14 @@ export class StatsPageComponent implements OnInit, OnDestroy {
}

get accessibleGlobeData(): readonly AccessibleDatum[] {
return (this.latestFsbHeadline?.popular_regions ?? []).map((item) => ({
return this.globeRegionList.map((item) => ({
label: this.displayLabel(item.label),
value: this.fmtNum(item.uniq),
}));
}

get globeAriaLabel(): string {
return $localize`:@@stats.globe.aria:Globe showing today's hourly FSB regional distribution`;
return $localize`:@@stats.globe.aria:Globe showing where FSB installs were last seen over the past 365 days, by coarse region`;
}

get tabMetrics(): readonly TabMetric[] {
Expand Down Expand Up @@ -415,8 +415,13 @@ export class StatsPageComponent implements OnInit, OnDestroy {
// an explicit "still gathering data" message when this is false rather
// than silently showing a globe with no nodes.
get hasPlottableRegions(): boolean {
const regions = this.latestFsbHeadline?.popular_regions ?? [];
return regions.some((r) => regionCentroid(r.label) !== null);
return this.globeRegionList.some((r) => regionCentroid(r.label) !== null);
}

private get globeRegionList(): readonly { label: string; uniq: number }[] {
const persistent = this.latestFsbHeadline?.users_by_region_365d;
if (persistent && persistent.length > 0) return persistent;
return this.latestFsbHeadline?.popular_regions ?? [];
}

get fanItemsLeft(): readonly FanItem[] {
Expand Down Expand Up @@ -793,7 +798,7 @@ export class StatsPageComponent implements OnInit, OnDestroy {

if (this.selectedView === 'fsb-active-now') {
const key = JSON.stringify({
regions: this.latestFsbHeadline?.popular_regions ?? [],
regions: this.globeRegionList,
reducedMotion: this.prefersReducedMotion,
theme: typeof document === 'undefined'
? ''
Expand Down Expand Up @@ -888,7 +893,7 @@ export class StatsPageComponent implements OnInit, OnDestroy {
// moderate jitter radius since we only have a single centroid per label,
// not a real distribution.
private buildGlobeRegions(): GlobeRegion[] {
const list = this.latestFsbHeadline?.popular_regions ?? [];
const list = this.globeRegionList;
const regions: GlobeRegion[] = [];
for (const { label, uniq } of list) {
const centroid = regionCentroid(label);
Expand Down
56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.de.xlf

Large diffs are not rendered by default.

56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.es.xlf

Large diffs are not rendered by default.

56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.ja.xlf

Large diffs are not rendered by default.

56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.ko.xlf

Large diffs are not rendered by default.

62 changes: 39 additions & 23 deletions showcase/angular/src/locale/messages.xlf

Large diffs are not rendered by default.

56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.zh-CN.xlf

Large diffs are not rendered by default.

56 changes: 38 additions & 18 deletions showcase/angular/src/locale/messages.zh-TW.xlf

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion showcase/angular/src/styles.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css');
@import url('https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css');

/* Shared showcase foundation: tokens, reset, typography, and utilities only. */
/* Shared showcase foundation: tokens, reset, typography, and utilities only.
* color-scheme is required so native form controls (the footer <select> especially)
* use a dark Field palette instead of light-on-light in Blink/Gecko. */
:root {
color-scheme: dark;
--primary: #ff6b35;
--primary-hover: #e55a2b;
--primary-gradient: linear-gradient(135deg, #ff6b35, #ff8c42);
Expand Down Expand Up @@ -45,6 +48,7 @@
}

[data-theme="light"] {
color-scheme: light;
--bg-body: #ffffff;
--bg-card: #f8f9fa;
--bg-elevated: #f1f5f9;
Expand Down
Loading
Loading