From 1d44338037c5b44a0e019a5fcdbe4dedb2f1cd43 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 25 Sep 2026 17:34:39 +0700 Subject: [PATCH] prepare --- CHANGELOG.md | 1 + docs/repo-updated-timestamp.md | 146 +++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 docs/repo-updated-timestamp.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe5879..9409808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Added - Show an avatar in each panel's tab, using the repository owner's profile picture when set and a pixel avatar otherwise +- Add a Tab Bar setting to hide the previous/next tab buttons, hidden by default ### Changed diff --git a/docs/repo-updated-timestamp.md b/docs/repo-updated-timestamp.md new file mode 100644 index 0000000..84ad85e --- /dev/null +++ b/docs/repo-updated-timestamp.md @@ -0,0 +1,146 @@ +# Repository card "Updated" timestamps are stale until first open + +Feedback under investigation: + +> My repositories that I updated yesterday show up as being updated like 6 months ago, +> but then when I open them and they get cloned that timestamp is fixed, which means +> you're not reading the repository state event, only the date of the repository +> announcement event and the git history? + +## Summary + +The reviewer is essentially right. The backend *has* logic to include kind `30618` +repository state events in the "last activity" timestamp, but that logic only reads +events that are **already present in the local nostr database**. The app never +globally fetches state events for repositories the user has not opened, so for those +the timestamp falls back to the announcement's `created_at` — the repository's +creation date. Opening (and cloning) the repository fetches the state event, which is +why the timestamp corrects itself afterwards. + +The "git history" part of the hypothesis is not accurate for this label: no +git-derived timestamp feeds the repository card. The only git-derived times in the UI +are commit timestamps in commit lists and diffs, which are unrelated. + +This is a data-coverage bug, not a missing computation. The code that aggregates state +events is correct; it is simply starved of input on the explore list. + +## Where the timestamp comes from + +The label is rendered in `crates/workspace/src/views/repo_list.rs` (L209-212): + +```rust +let activity = last_activity + .map(relative_time) + .map(|label| SharedString::from(format!("Updated {label}"))) +``` + +`last_activity` is computed in `crates/signed_state/src/repos.rs`, +`RepoListStore::run_refresh` (L192-235), as a max over three sources: + +| Source | Code | Availability | +|---|---|---| +| `announcement.created_at` (kind 30617) | L195-198 | always; seeded for every listed repo | +| kind 30618 repository state events | L200-213 | only events already in the local database | +| NIP-34 activity (issues, PRs, patches, statuses, comments) | L215-235 | only local database, windowed to 90 days (`ACTIVITY_WINDOW`, L13-14) | + +The critical gap is the global sync scope. `RepoListStore::subscribe_remote` +(`crates/signed_state/src/repos.rs` L131-139) only syncs: + +```rust +backend.sync_bootstrap(filters::all_announcements(), cx); // kind 30617 only +backend.sync_bootstrap(filters::deletions(), cx); +``` + +State events are fetched **per repository, on open**, via `RepoStore`: + +- `repo_filters` (`crates/signed_state/src/repo.rs` L254-270) queries + `[GitRepoAnnouncement, RepoState]` plus activity and deletions. +- `subscribe_remote` (L296-306) runs those filters against the bootstrap relays. +- `connect_announced_relays` (L272-294) runs a negentropy sync of the same filters + against the relays in the announcement's NIP-34 `relays` tag. +- Both fire from `RepoStore::new`'s deferred initialization (L93-112). + +Publishes only ever create a state event, never re-publish the announcement: +`sign_state_event` in `crates/signed_state/src/backend.rs` (L1655-1682) and the +fan-out at L907-918. A repository created 6 months ago and pushed yesterday therefore +has an announcement dated 6 months ago and a state event dated yesterday. + +```mermaid +flowchart TD + A[App start / login] --> B[RepoListStore sync: 30617 announcements + deletions] + B --> C[Explore card: Updated = announcement created_at] + C --> D{Repo opened?} + D -- no --> C + D -- yes --> E[RepoStore fetches 30617 + 30618 from bootstrap + announced relays] + E --> F[State event stored in local DB] + F --> G[BackendEvent NostrUpdate RepoState] + G --> H[RepoListStore refreshes, last_activity recomputed] + H --> I[Card shows push date] +``` + +## Why the report reproduces exactly + +1. The announcement (kind 30617) is addressable and effectively static; pushes replace + only the state event (kind 30618). +2. On a database that has never fetched that repository (fresh install, another device, + or wasm where the database is in-memory per session), only the announcement is + known, so the card shows the creation date. +3. Opening the repository triggers the per-repo fetch; the state event lands in the + database, and `RepoListStore` does react to `RepoState` updates + (`crates/signed_state/src/repos.rs` L69-82), so the card corrects itself. + +## Secondary issues found in the same area + +1. **`RepoStore` ignores live state events.** + `subscribe_backend` (`crates/signed_state/src/repo.rs` L195-239) matches relay + updates via `Update.coordinate`, which is built from `a` tags only + (`crates/signed_nostr/src/update.rs` L13-21). A kind `30618` event carries `d`, not + `a`, and the kind check only accepts `GitRepoAnnouncement`. A state update arriving + from a relay therefore does not refresh an open repository panel's `head`. + `RepoListStore` is unaffected because its check is kind-based. + +2. **Locally published state events refresh nothing.** + `BackendEvent::Published` is ignored for `RepoState` by both stores + (`crates/signed_state/src/repos.rs` L83-92 and `crates/signed_state/src/repo.rs` + L221-231). The comment in `crates/signed_state/src/backend.rs` L907-909 + ("publishing notifies the repository views") is inaccurate: after an in-app push, + the explore card stays stale until the next unrelated refresh, sync or restart, + even though the event is in the local database. + +3. **Usage counts are similarly under-scoped.** + The Popular ranking (`crates/signed_state/src/repos.rs` L239-261) counts only + issues, PRs and patches present locally, i.e. mostly repositories the user has + opened. + +4. **The Recent tab is not "recently updated".** + `RepoFilter::Recent` (`crates/workspace/src/views/repo_list.rs` L73-84) truncates the + announcement-sorted list (`crates/signed_state/src/repos.rs` L189-190), so it orders + by creation, not by `last_activity`. + +5. **The 90-day activity window is deliberate.** + Activity-driven bumps decay after `ACTIVITY_WINDOW`; only state events would bump + indefinitely once fetched (L13-14). + +## Remediation options + +- **A. Global state sync (smallest change).** + Add `sync_bootstrap(Filter::new().kind(Kind::RepoState))` alongside the announcement + sync in `RepoListStore::subscribe_remote`. Kind `30618` is addressable, so relays + keep one event per repository — volume comparable to announcements. Caveat: coverage + depends on whether state events reach the bootstrap relays; if a grasp server is the + only holder, those repositories remain unseen. + +- **B. Targeted fetch for the user's own repositories.** + In `sync_inbox` (`crates/signed_state/src/backend.rs` L1126-1152), where the user's + own repositories' relays are already connected, also sync `filters::state(addr)` per + own repository. This directly addresses "my repositories updated elsewhere" at low + cost. Note the existing race: `sync_inbox` reads the announcement list right after + login, which may not be loaded yet. + +- **C. Reactivity fixes.** + Include `Kind::RepoState` in the `Published` refresh conditions, and in + `RepoStore::subscribe_backend` either match kind plus author, or extend `Update` to + carry the `d`-tag identifier. + +Suggested combination: A + C, plus B if own-repository freshness on fresh profiles +matters.