docs+openspec: prior-art research (Organic Maps, Endurain, wanderer) and 15 proposals
Add docs/inspirations.md as the durable record of the 2026-07-05/06 prior-art research — per-project learnings with source paths, canonical credit lines, and the changes each spawned — and extend the acknowledgment lists in philosophy.md/architecture.md (Organic Maps, Endurain, wanderer). New OpenSpec changes (proposal/design/specs/tasks each): - Organic Maps: elevation-profile-hardening, gpx-parser-robustness, hiking-time-estimate, poi-index, hiking-foot-profile - Endurain: account-export, activity-duplicate-review, fit-parsing-hardening, activity-locations, self-hosting-guide, activity-privacy-controls - wanderer: federation-hardening, link-share-tokens - credits-page (user-visible acknowledgments) Updated in-flight changes with wanderer prior-art sections: route-federation, route-discovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
be4f7e4ae8
commit
5dd4968626
80 changed files with 2600 additions and 2 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-07-06
|
||||
58
openspec/changes/activity-privacy-controls/design.md
Normal file
58
openspec/changes/activity-privacy-controls/design.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
## Context
|
||||
|
||||
Activities carry `visibility` (private/unlisted/public) plus `audience` for federation; non-owner access control happens in the detail loaders and list scopes, and federation publishes activity objects via `federation-objects.server.ts`/outbox. Signals stored per activity: `startedAt`, geometry/GPX, elevation series (whose samples include lat/lng for chart↔map sync), distance, duration, moving time, derived pace/speed (`stats.ts`), photos, and (per the `activity-locations` change) a location label.
|
||||
|
||||
Endurain's reference: 13 boolean `hide_*` columns + user defaults stamped at import + `apply_visibility_mask()` nulling fields for non-owners + one frontend gate. trails needs fewer flags (no HR/power/cadence stored) but a wider enforcement surface (federation).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Share a ride without sharing when you left, where exactly, or how fast — four independent, comprehensible flags.
|
||||
- Masked data never reaches a non-owner through *any* surface, including ActivityPub objects, GPX downloads, and chart data.
|
||||
- Defaults make it a set-once decision; per-activity override stays available.
|
||||
|
||||
**Non-Goals:**
|
||||
- Privacy zones (geometry editing near saved places) — separate change layered on this mask.
|
||||
- Route-level flags, per-follower exceptions, HR/power flags (nothing stored to hide).
|
||||
- Retracting already-federated copies on flag changes (Update activities may be sent, but remote caches are best-effort — honest UI note instead of a false promise).
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Four flags, jsonb, typed
|
||||
|
||||
`activities.privacy: { hideStartTime, hideLocation, hideMap, hidePace }` (jsonb, default `{}` = nothing hidden) and `users.activity_privacy_defaults` with the same shape. Jsonb over boolean columns: the set will grow (privacy zones, photo location stripping), flags are never queried server-side as predicates (masking is row-level at serialization), and the schema convention already favors typed jsonb for extensible shapes (cf. `SurfaceBreakdown`).
|
||||
|
||||
Flag semantics (what a non-owner loses):
|
||||
- `hideStartTime` → `startedAt` truncated to date (feed ordering uses the real value server-side).
|
||||
- `hideLocation` → no location label; start/end markers not distinguished on the map (map itself governed by `hideMap`).
|
||||
- `hideMap` → no geometry, no map, no thumbnail, no GPX download, elevation profile served as distance/elevation only (positions stripped) or omitted.
|
||||
- `hidePace` → no pace/speed/moving time; distance + elapsed duration remain.
|
||||
|
||||
### 2. One mask at the serialization boundary
|
||||
|
||||
`applyActivityPrivacyMask(data, { isOwner })` lives beside `stats.ts` and is the only place flag semantics are implemented. Loaders/serializers call it before returning non-owner payloads; the GPX/thumbnail/elevation endpoints check `hideMap` explicitly (they return binary, not the masked object). Owners always receive everything plus the flag state (so the UI can show "hidden from others" badges).
|
||||
|
||||
*Alternative:* masking in the frontend gate (Endurain's `VisibilityGate`) — rejected as the primary mechanism: data must not reach the client at all; a UI gate is cosmetic. Frontend only renders what it receives.
|
||||
|
||||
### 3. Federation: mask at object-build time
|
||||
|
||||
`federation-objects.server.ts` builds AP objects through the same mask with `isOwner: false` — a hidden map means the published object carries no geometry attachment; hidden start time publishes date precision only. Flag changes after publish affect subsequent serves and trigger an AP `Update`; remote re-fetch honesty is best-effort (documented in UI copy: "already-shared copies may persist on other instances").
|
||||
|
||||
### 4. Stamping and editing
|
||||
|
||||
Defaults stamp at creation in the shared save path (upload, provider import, planner handoff). The activity edit surface exposes the four toggles; the settings page edits defaults (affecting future activities only, stated in copy). Demo-bot and e2e fixtures default to nothing hidden.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [A new endpoint forgets the mask] → Single helper + a table-driven regression test that walks every activity-serving route as a non-owner and asserts hidden fields absent for a fully-flagged fixture; code-review rule noted in the spec.
|
||||
- [Elevation chart without positions degrades chart↔map sync] → Intended: with `hideMap` there is no map to sync to; chart renders standalone.
|
||||
- [Federated copies predate a newly-set flag] → Honest UI note + AP Update on change; no false retraction promise.
|
||||
- [Flag sprawl later] → jsonb shape + single mask keep additions one-file changes.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Additive columns (default `{}` = current behavior — nothing hidden). Ship mask + UI together; existing activities unaffected until a user sets flags. Rollback = revert; stored flags become inert.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking. Privacy zones (trim/blur geometry within N meters of saved locations) are the designed-for follow-up on top of `hideMap`'s enforcement points.
|
||||
27
openspec/changes/activity-privacy-controls/proposal.md
Normal file
27
openspec/changes/activity-privacy-controls/proposal.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## Why
|
||||
|
||||
Activity visibility today is all-or-nothing (`private`/`unlisted`/`public`): sharing a ride means sharing exactly when you left home, where the track starts, and your pace. For a privacy-first platform this forces the wrong trade — users who'd happily share the route and photos keep activities private because one field is sensitive. Endurain (reviewed 2026-07-06) demonstrates the answer: independent per-signal hide flags with per-user defaults, per-activity overrides, and a single server-side mask for non-owners. trails should offer the equivalent for the signals it actually stores.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Four per-activity privacy flags**, each independent of `visibility`: `hideStartTime` (viewers see the date, not the time), `hideLocation` (no location label, per `activity-locations`), `hideMap` (no map, geometry, elevation-chart positions, or GPX download for non-owners — stats remain), `hidePace` (no pace/speed/moving time; distance and duration remain).
|
||||
- **Per-user defaults** in profile settings, stamped onto each new activity at creation/import; each activity's flags editable afterward on its edit surface.
|
||||
- **One server-side mask** applied everywhere a non-owner receives activity data: detail loaders, feeds, public API, GPX download, map thumbnails, and — critically — **federation**: masked fields are omitted from ActivityPub objects so they never leave the instance.
|
||||
- Scope: activities only (recorded presence is the sensitive data). Routes keep plain visibility; extending flags to routes is a possible follow-up.
|
||||
- Not in scope: privacy zones (geometry trimming/blurring near saved locations) — a natural later layer on top of this masking infrastructure; per-follower exceptions; retroactive re-federation of already-published objects (changing flags affects what is served/federated from then on; note in UI).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `activity-privacy`: The per-signal privacy flags — their semantics, user defaults, stamping, the non-owner mask, and its enforcement surface including federation.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- none — visibility requirements in existing specs are unchanged; masking is an additional filter applied on top. The mask's federation behavior is specified in the new capability rather than as a social-federation delta because no existing federation requirement is contradicted. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `packages/db`: `activities.privacy` jsonb (typed four-flag object, default all-false) + `users.activity_privacy_defaults` jsonb + migration.
|
||||
- Journal: `applyPrivacyMask(activity, viewerIsOwner)` helper used by detail loaders, feed/listing serializers, `api.v1` activity routes, GPX download route, thumbnail/elevation endpoints, and `federation-objects.server.ts` / outbox emission.
|
||||
- UI: defaults section in profile/privacy settings; per-activity toggles on the activity edit surface; owner-visible "hidden from others" indicators on masked fields (i18n en/de).
|
||||
- Interacts with in-flight changes: `activity-locations` (label obeys `hideLocation`), `activity-duplicate-review` (quarantine is orthogonal), privacy manifest update.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Per-signal privacy flags
|
||||
Each activity SHALL carry four independent privacy flags — `hideStartTime`, `hideLocation`, `hideMap`, `hidePace` — orthogonal to its visibility level. For non-owner viewers: `hideStartTime` reduces the start time to date precision; `hideLocation` removes the location label; `hideMap` removes geometry, map, thumbnail, GPX download, and position data in the elevation profile; `hidePace` removes pace, speed, and moving time while keeping distance and elapsed duration.
|
||||
|
||||
#### Scenario: Share the ride, hide the departure
|
||||
- **WHEN** a public activity has `hideStartTime` and `hidePace` set and a non-owner views it
|
||||
- **THEN** they see the date (not time), distance, duration, map, and photos, but no pace, speed, or moving time
|
||||
|
||||
#### Scenario: Hidden map removes all geometry surfaces
|
||||
- **WHEN** `hideMap` is set and a non-owner requests the activity page, its GPX download, its thumbnail, or its elevation data
|
||||
- **THEN** no coordinates reach them through any of those surfaces (elevation, if shown, is distance/altitude only)
|
||||
|
||||
#### Scenario: Owner sees everything
|
||||
- **WHEN** the owner views their flagged activity
|
||||
- **THEN** all data renders, with indicators showing which fields are hidden from others
|
||||
|
||||
### Requirement: User defaults stamped at creation
|
||||
Users SHALL have activity-privacy defaults in settings; every locally created or imported activity SHALL receive the defaults at creation, and the flags SHALL be editable per activity afterwards. Changing defaults SHALL NOT alter existing activities.
|
||||
|
||||
#### Scenario: Default applies to next import
|
||||
- **WHEN** a user enables `hideStartTime` in defaults and a Wahoo activity syncs
|
||||
- **THEN** the new activity has `hideStartTime` set
|
||||
|
||||
#### Scenario: Per-activity override
|
||||
- **WHEN** the user clears the flag on that one activity
|
||||
- **THEN** only that activity's start time becomes visible to others
|
||||
|
||||
### Requirement: Masking enforced server-side including federation
|
||||
Masked fields SHALL be removed before data leaves the server for any non-owner surface — detail loaders, feeds, the public API, and ActivityPub objects (masked fields are omitted from published objects; flag changes trigger an Update). Masking SHALL be implemented in a single shared helper.
|
||||
|
||||
#### Scenario: Federated object carries no hidden data
|
||||
- **WHEN** an activity with `hideMap` federates to a follower on another instance
|
||||
- **THEN** the published object contains no track geometry
|
||||
|
||||
#### Scenario: Every serving route masked
|
||||
- **WHEN** the regression suite requests a fully-flagged activity as a non-owner across all activity-serving endpoints
|
||||
- **THEN** no hidden field appears in any response
|
||||
26
openspec/changes/activity-privacy-controls/tasks.md
Normal file
26
openspec/changes/activity-privacy-controls/tasks.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
## 1. Schema
|
||||
|
||||
- [ ] 1.1 Add `activities.privacy` jsonb (typed `ActivityPrivacy` four-flag shape, default `{}`) and `users.activity_privacy_defaults` jsonb + migration
|
||||
|
||||
## 2. Mask helper
|
||||
|
||||
- [ ] 2.1 Implement `applyActivityPrivacyMask` beside `stats.ts`: flag semantics per design (date-truncated start, label removal, geometry/positions stripping, pace/speed/moving-time removal); unit tests per flag and combined
|
||||
- [ ] 2.2 Stamp defaults in the shared activity-creation path (upload, provider imports, planner handoff); demo bot and fixtures default to `{}`
|
||||
|
||||
## 3. Enforcement
|
||||
|
||||
- [ ] 3.1 Apply the mask in: activity detail loader, feed/list serializers, `api.v1` activity routes, GPX download route, thumbnail endpoint, elevation-series payload
|
||||
- [ ] 3.2 Federation: build AP objects through the mask (`federation-objects.server.ts` + outbox); flag edits fire an AP Update; UI copy notes remote copies are best-effort
|
||||
- [ ] 3.3 Table-driven regression test: fully-flagged fixture requested as non-owner across every activity-serving endpoint → no hidden field present; owner requests → all present
|
||||
|
||||
## 4. UI
|
||||
|
||||
- [ ] 4.1 Privacy defaults section in settings (four toggles, i18n en/de, copy explains each flag affects future activities)
|
||||
- [ ] 4.2 Per-activity toggles on the activity edit surface; "hidden from others" indicators on the owner's detail view
|
||||
- [ ] 4.3 Coordinate with `activity-locations` (label obeys `hideLocation`/`hideMap`) if that change has landed
|
||||
|
||||
## 5. Docs & verification
|
||||
|
||||
- [ ] 5.1 Update the privacy manifest: per-signal controls exist; federation masking semantics; remote-copy caveat
|
||||
- [ ] 5.2 E2E: set defaults → import → verify masked view as second user; toggle per-activity flag → verify change
|
||||
- [ ] 5.3 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e`
|
||||
Loading…
Add table
Add a link
Reference in a new issue