Commit Graph
270 Commits
Author SHA1 Message Date
Bendik Aagaard Lynghaug 0097a58b22 release: bump to v0.13.1
Sync Flathub / flathub-sync (push) Failing after 41s
Release / Create GitHub Release (push) Failing after 3s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.13.1
2026-08-14 12:23:56 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 dbb606ab8c fix: v0.13.0 CI failures + refresh screenshots for the redesigned UI
Release CI: the aarch64 job's fedora:43 container ships libadwaita
1.8.7, but the app now needs >= 1.9 (AdwSidebar) — bump to fedora:44
(1.9.3). The other three targets already built 0.13.0 fine.

Flathub sync CI: the HTTPS-rewrite step wrote .git/config and then ran
`git submodule sync`, which copies URLs FROM .gitmodules INTO
.git/config — restoring the SSH URL it had just replaced and failing
identically to the run it was meant to fix. Rewrite .gitmodules itself
(config -f) so sync propagates HTTPS instead of undoing it.

Screenshots: retaken against a live two-peer session on the 0.13
build — new AdwSidebar navigation with the Circles section, aspect
search field, connected-peer sidebar row with synastry tone, populated
synastry view, and a circle page with an invited member.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:23:44 +02:00
Bendik Aagaard Lynghaug dd967b5be9 release: bump to v0.13.0
Sync Flathub / flathub-sync (push) Failing after 9s
Release / Create GitHub Release (push) Failing after 4s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.13.0
2026-08-14 11:45:08 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 6866aa7842 docs: write 0.13.0 release notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 11:44:21 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 8afb420213 feat: GNOME HIG redesign (AdwSidebar) + affirmation retraction
UI: migrate the sidebar off AdwOverlaySplitView/hand-rolled toggle
plumbing onto AdwNavigationSplitView, with Chart/Sky/Network and
Circles now living in adw::Sidebar sections (libadwaita 1.9). Bundle
the 9 previously system-theme-only icons so macOS/Windows builds don't
depend on an Adwaita-shaped icon theme. Replace the deprecated
AdwViewSwitcherTitle with a two-top-bar AdwWindowTitle+AdwViewSwitcher
design, drop .dim-label for .dimmed throughout, and add a search entry
above the natal aspect list (can run dozens of rows with no prior way
to narrow it). Peers intentionally stay on their existing GtkListBox +
FactoryVecDeque<PeerRow> — relm4 0.10 has no FactoryView impl for
AdwSidebarSection, and that list churns too often (presence, badges)
to hand-roll a full rebuild-on-every-change replacement safely.

Protocol/store/app: add DocOp::RetractAffirm so affirming a doc
revision is a real toggle instead of a one-way action — undoes a
misclick the same self-service way leaving a circle already does.
Threaded through ops/store/pipeline/sdk/app, with a new cucumber
feature (sdk/tests/features/affirmation_retraction.feature) proving
retraction propagates to peers and is attributed to the right voter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 11:44:18 +02:00
Bendik Aagaard Lynghaug 458d752c80 fix: Sync Flathub CI clones the submodule over SSH with no key configured
git submodule update --init used .gitmodules's SSH URL for the initial
clone (needed for local/manual syncs, which do have the maintainer's
own key) — CI has no SSH key at all, so this failed immediately with
"Permission denied (publickey)", before the workflow ever reached its
own HTTPS+PAT remote rewrite (which only ran *after* init, too late to
help the clone itself). Confirmed via the actual failed v0.12.0 run's
log. Fix: point .gitmodules at HTTPS before init — anonymous HTTPS
read access works fine for a public repo — then still layer the PAT
on top afterward for the push step, unchanged.

v0.12.0's own Flathub PR was opened manually in the meantime
(scripts/flatpak-sync.sh + gh pr create) rather than waiting on this
fix and re-triggering a tag push.
2026-08-14 09:53:28 +02:00
Bendik Aagaard Lynghaug 9b6dcfcba6 chore: update flathub submodule to v0.12.0 2026-08-14 09:52:29 +02:00
Bendik Aagaard Lynghaug 46a25e1eda release: bump to v0.12.0
Sync Flathub / flathub-sync (push) Failing after 10s
Release / Create GitHub Release (push) Failing after 6s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.12.0
2026-08-14 09:45:07 +02:00
Bendik Aagaard Lynghaug 9c57e358e0 docs: write 0.12.0 release notes 2026-08-14 09:44:20 +02:00
Bendik Aagaard Lynghaug 1fcab58c3f feat: optional AI-assisted interpretation drafts via local Ollama
Auto-detects a locally-running Ollama instance at startup (probing
http://localhost:11434, our own new zodia-llm crate — a minimal client,
never required for anything else to work) and, if found, offers a
"Generate with AI" button on the community-reading editor. Generation
uses our own house-style prompt (zodia_core::build_interp_prompt,
tuned per InterpKey kind — natal vs synastry vs transit read very
differently) against whichever model Ollama reports first.

The key design decision: disclosure that a contribution was
AI-drafted is a real protocol field, not a local-only UI note.
DocOp::Edit gained ai_generated: bool (#[serde(default)] so older
encoded ops still decode, honestly defaulting to false), threaded all
the way through StateEvent::DocEdited, the block-author ring
(doc_block_authors gained a matching column), and ZodiaClient::edit.
A real two-peer cucumber scenario proves the disclosure travels with
the edit over the network, not just locally. The reading shows a
"🤖 Includes AI-drafted content" banner (doc_has_ai_generated_content)
and each ring entry that used it is marked in the Recent Editors list
— both readable by anyone, not just the original author.

Known scope limit, not a bug: apply_doc_edit skips the local ring push
for self-authored edits (pre-existing behavior, unrelated to this
feature), so the badge shows correctly to everyone who *receives* your
AI-assisted contribution but not on your own device's view of your own
just-published edit. Noted in memory for whoever picks this up next.
2026-08-14 09:44:05 +02:00
Bendik Aagaard Lynghaug d1919c9e1f fix: Flathub CI sync needs a PAT, not a deploy key
Every release since 0.9.3 has failed the Flathub sync step, always
with "the ssh-private-key argument is empty" — the FLATHUB_DEPLOY_KEY
secret this workflow expected was never actually configured. Tried to
fix that today by generating a deploy keypair, but adding it to
flathub/io.github.bendik.Zodia requires admin on that repo, and a
maintainer is normally only a push-access collaborator there (Flathub
keeps admin for itself) — confirmed via `gh api .../permissions`
returning admin:false, push:true. A fine-grained PAT hits an
equivalent wall: repos outside your own account need the resource-
owning org to pre-approve fine-grained token access, which Flathub
hasn't opted into.

A classic PAT (repo scope) on the maintainer's own account has neither
restriction — it just inherits whatever access the account already
has. Swapped the workflow to rewrite the flathub submodule's remote to
HTTPS + that PAT instead of an SSH deploy key.

Also closes a second, separate gap: the workflow only ever pushed the
update branch and left opening the actual PR to the maintainer
clicking a printed link. Flathub requires updates via PR (master is
protected, opening one triggers their automatic test build), so the
workflow now opens it directly with `gh pr create`.

v0.11.0 itself was already synced manually while diagnosing this
(https://github.com/flathub/io.github.bendik.Zodia/pull/10) — this is
what makes the next tagged release go through CI unattended.
2026-08-06 17:56:27 +02:00
Bendik Aagaard Lynghaug 610dd570b4 chore: update flathub submodule to v0.11.0 2026-08-06 17:48:37 +02:00
Bendik Aagaard Lynghaug e4bcc0ff10 release: bump to v0.11.0
Sync Flathub / flathub-sync (push) Failing after 7s
Release / Create GitHub Release (push) Failing after 5s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.11.0
2026-08-06 14:16:55 +02:00
Bendik Aagaard Lynghaug d3642cdae4 docs: write 0.11.0 release notes 2026-08-06 14:04:29 +02:00
Bendik Aagaard Lynghaug 33d17a4139 Tests pass my friend 2026-08-06 14:02:59 +02:00
Bendik Aagaard Lynghaug 40ea459eab feat: show "Last seen Xm ago" for offline peers
An offline peer's page previously gave no information beyond a gray
dot — no sense of whether they stepped away a minute ago or haven't
been online in a week. Persists each peer's direct-channel close time
(new `peer_last_seen` store table, covered by an upsert-overwrite
test) and shows it in the peer page's title once they go offline,
e.g. " Bobby · Last seen 5m ago".

`format_last_seen` is a pure function (takes `now` explicitly rather
than reading the clock, so it's deterministically testable) with six
tests covering each unit boundary (just-under-a-minute, exact minute/
hour/day boundaries, the week cutoff to "a while ago") plus a
clock-skew case verifying `last_seen > now` can't underflow or print
a negative time.
2026-08-05 17:41:44 +02:00
Bendik Aagaard Lynghaug 5d6e1507a2 feat: show an at-a-glance synastry tone next to connected peers
The Network tab already previews rough compatibility for peers you
haven't connected with yet (approximate_aspects, sun-sign only, since
that's all you know before consent exchange). Once actually connected
you have their exact birth data, but the only way to see how you two
read together was opening their page and reading the full Synastry
tab aspect-by-aspect — no glanceable summary existed for people you're
already talking to.

core: `SynastryTone` (Harmonious/Balanced/Challenging) classifies a
synastry aspect list by a simple weighted tally (trine/sextile +1,
square/opposition -1, minor aspects neutral), with a generous ±2
middle band so a handful of aspects doesn't tip to an extreme on a
thin margin. Explicitly documented as a rough glance-value signal, not
a real reading — real synastry weighs which specific bodies and houses
are involved far more than a raw count ever could. Six tests written
against the tally logic before wiring anything up, including a
boundary test (score 2 vs 3) to pin the exact cutoff.

app: connected peers' sidebar rows now show a small glyph (/•/)
after their name, tooltip spelling out the label, computed from their
already-known exact birth data + the local user's own chart.
2026-08-05 17:10:19 +02:00
Bendik Aagaard Lynghaug a13f3c7010 feat: read receipts ("Seen") in direct peer chat
Natural companion to the typing indicator: chat had no way to know
whether the other person had actually read what you sent. Same
architecture as typing — a new `ChannelMsg::ChatRead` (payload-less:
this chat is a simple ordered 1:1 log, so "seen" means "caught up,"
not a per-message cursor) forwarded as `ZodiaNetEvent::ChatSeen`,
covered by a real two-endpoint QUIC round-trip test before the variant
existed.

Sent when the local user opens a peer's message page (`OpenStargazer`
is the clearest available signal of "I'm now looking at this
conversation" without needing to thread visible-tab tracking across
the update_cmd/update_view boundary). A small "Seen" caption appears
under the input row once it arrives, but only while the most recent
message in the conversation is one of ours — sending a new message
resets it, matching ordinary 1:1 chat UX.
2026-08-05 16:35:12 +02:00
Bendik Aagaard Lynghaug c775c15d63 feat: show a live "typing…" indicator in direct peer chat
Real-time chat had no signal that the other person was actually
composing a reply — the classic messaging-UX gap. Added end-to-end:

- net: new `ChannelMsg::TypingIndicator { active }` over the same
  reliable QUIC stream chat/presence already use, plus the
  `ZodiaNetEvent::TypingIndicatorChanged` it forwards as. Red-first: wrote
  `typing_indicator_round_trips_over_a_real_channel` (two real loopback
  QUIC endpoints, matching this crate's existing `exchange_consent`
  integration-test style) before the variant existed, watched it fail
  to compile, then added it.
- app/stargazer_page: the message entry's `changed` signal debounces
  into a single "started typing" send plus a 3s-idle "stopped" send
  (clearing the entry — by hand or via a just-sent message — also
  clears it immediately, no need to wait out the idle timer). A small
  dim-label "Typing…" line appears above the input row on the
  receiving end.
- Deliberately did not add a receiver-side fallback timeout: a fixed
  timer would falsely clear the indicator for a message that takes
  longer to type than the timeout, since the sender only pings once
  per burst rather than heartbeating. The one case a "stopped" send can
  legitimately never arrive — the connection drops mid-compose — is
  already a distinct event (`PeerChannelClosed`), so the indicator is
  cleared there instead.
2026-08-05 16:03:56 +02:00
Bendik Aagaard Lynghaug ca6b50ae07 feat: let a user mute a peer's activity in their own feed
Real gap: there was no way to stop a specific peer's noise (new
readings, replies, hearts) from showing up, short of disconnecting
them entirely. Mute is deliberately narrower than a network-level
block: their content is still fully synced and stored (protocol
correctness for the rest of the network is untouched), only the local
social feed and store insertion for their InterpAuthored/AffirmAdded/
ResponseAdded activity is suppressed. Collaborative-doc mechanics,
display names, and circle invites always apply regardless — muting
someone's chatter shouldn't corrupt a shared document or hide a
functional message addressed to you.

- store: new `muted_peers` table + mute_peer/unmute_peer/muted_peers,
  covered by a roundtrip unit test.
- app/feed_item: `is_muted_social_event` is a small pure predicate,
  unit-tested directly (red-first: wrote the five cases, then the
  three-line match) rather than needing network-level BDD coverage
  for what's an entirely local, app-layer concern.
- app: AppModel caches the muted set in memory (loaded once at start)
  so the hot SyncStateEvent path never awaits a DB round-trip; a new
  "Mute"/"Unmute" button on each connected peer's sidebar row (dimming
  their name when muted, mirroring the existing pending-row dim-label
  convention) toggles it.
2026-08-04 20:57:13 +02:00
Bendik Aagaard Lynghaug 826fc1947d feat: let a non-owner circle member leave from the circle page UI
Wires the backend self-leave capability (see the prior BDD commit) into
circle_page.rs: your own row shows "(You)" and, unless you're the
circle's manager, a "Leave circle" button in place of the revoke
control (leaving as the sole manager would orphan the circle, so that
path isn't offered). Leaving asks for confirmation via a destructive
adw::AlertDialog, then drops the now-stale page from the content stack
and navigates back to Chart if it was the visible page.

Also hides the "Revoke access" button on other members' rows unless
the local viewer is themselves a manager — the backend already
rejects a non-manager's revoke attempt, so showing it to everyone was
a dead-end click.
2026-08-04 20:19:48 +02:00
Bendik Aagaard Lynghaug 6f8ee1c714 feat: let a circle member leave on their own, without owner action
p2panda-auth's group CRDT already allows this: remove() bypasses the
Manage-access requirement specifically when remover == removed (verified
directly against the vendored source). Nobody had exercised that path in
this app before - every prior revoke scenario was the owner acting on
someone else. Adds a "leaves the circle" step (revoke_from_circle
targeting the caller's own key, no new SDK/backend code needed) and a
BDD scenario proving both sides converge: the leaver's own view and,
after a settle window for the op to actually reach them, the owner's.
2026-08-04 20:14:25 +02:00
Bendik Aagaard Lynghaug adbb93cb97 release: bump to v0.10.0
Sync Flathub / flathub-sync (push) Failing after 2m24s
Release / Create GitHub Release (push) Failing after 9s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.10.0
2026-08-04 13:21:46 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 9d8b0cfd31 docs: write 0.10.0 release notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:17:56 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 c2fe51938f fix: DocAffirmed always reported targets_me=false — real bug in the live model
The legacy InterpOp::Affirm path always correctly computed whether an
affirmation targeted the local user's own content, but StateEvent::
DocAffirmed — the path every current edit actually goes through, since
0.9+ writes exclusively use the collaborative doc model — hardcoded
targets_me = false unconditionally. The bell badge and feed targeting
have never actually detected a heart on your own writing for the
model real users hit today.

The collaborative model has no single "author" the old check relied
on, so added ZodiaStore::doc_has_contributor (does this peer have any
block still in interp_key's author ring) as the closest equivalent —
test-driven, including a case proving the ring holds multiple
contributors rather than getting treated as single-author.

Also surfaces a real-time "X affirmed your reading" toast reusing the
ToastOverlay infrastructure from the circle-invite feature. Per
explicit feedback: purely-informational events like this are a better
fit for a desktop notification (notify::send) than an in-app Toast —
shipping as a Toast for this release since the app is early-stage, and
flagged as something to reconsider if it feels tiresome moving
forward, not changed now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:16:12 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ada587a6ef fix: flathub-sync SSH key setup failed with "error in libcrypto"
Reported failure on the v0.9.3 run: ssh-agent/git couldn't parse the
FLATHUB_DEPLOY_KEY secret at all. The workflow was writing it with
`echo "${{ secrets.FLATHUB_DEPLOY_KEY }}" > ~/.ssh/id_ed25519` — a
fragile pattern for multi-line secrets, which commonly lose their
trailing newline (or otherwise get mangled) when pasted into GitHub's
secret textarea, and `echo` had nothing to catch that before writing
whatever came through straight to disk.

Switched to webfactory/ssh-agent, a purpose-built action for loading
an SSH key from a secret, which sidesteps the shell-redirection
mangling entirely and fails with a clearer error if the secret itself
is genuinely missing or malformed. If this run also fails, the next
thing to check is the secret's actual content — regenerate the deploy
keypair and re-paste carefully (select-all including the trailing
newline) to rule out a corrupted paste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 22:17:14 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 b7f587dd4a feat: circle-invite toasts with a Join action — the invitee-side gap, closed
Wires the previous commit's backend into the actual app: inviting
someone now also calls notify_circle_invite right after
invite_to_circle succeeds, and the recipient's own client shows an
adw::Toast ("X invited you to a circle") with a Join button — no
timeout, since an invite shouldn't silently expire the way a status
message would. Accepting it calls open_circle, then asks the new
member to name the circle for their own sidebar (the inviter's name
for it is intentionally never synced — see the backend commit's note
on why circle names stay local-only).

Required wrapping the whole window's content in an adw::ToastOverlay
(previously nothing in the app used AdwToast at all) so a toast can
surface from any page, not just one specific tab — general-purpose
infrastructure the next notification-worthy event can reuse without
repeating this setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 22:12:02 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 02d013fe89 feat: real circle-invite notifications (backend) — closes a gap the app never had a fix for
Every circle scenario in this suite has always had to call open_circle
directly from the test, standing in for the invitee somehow already
knowing the circle exists — the feature file's own header called this
"out of scope" from the day circles shipped. It wasn't actually out of
scope, just unbuilt: the invitee genuinely had no in-app way to learn
about an invite at all.

Adds InterpOp::CircleInviteNotify (log 0, always-on global topic, same
propagation as Author/SetDisplayName) → StateEvent::CircleInviteReceived
→ ZodiaClient::notify_circle_invite. Deliberately carries no circle
name: it rides the same plaintext global topic every InterpOp does,
and a circle's name is private, chosen locally by whoever's naming it
for themselves — putting it on that topic would leak private metadata
to every connected peer regardless of whether they could ever decrypt
the circle's actual content. A `recipient` field lets every receiver
but the intended one recognize it isn't theirs.

BDD scenario proves both halves: the invitee genuinely receives it,
and — the part worth getting right rather than assuming — an
uninvited third peer also receives the broadcast (it's global) but can
tell from the recipient field it isn't hers. Filtering that out of her
own UI is the app layer's job; the SDK doesn't pretend to hide it from
her, since that would be a false privacy guarantee this transport
layer can't actually make.

Next: wire this into the GTK app as an adw::Toast with a Join action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 22:02:12 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 a083eb915d feat: critical/anaretic degree marker (last degree of a sign)
A ninth gap in the same vein — a placement at 29° of any sign is a
well-known, unambiguous astrological concept (urgency/culmination
themes) that needed nothing beyond the longitude already computed for
every placement. Considered and deliberately skipped void-of-course
Moon in this same pass: it needs real-world validation data (known VOC
windows) I don't have access to, and getting timing advice wrong is a
different risk class than a decorative label — astrologers actually
use VOC for real decisions. Critical degree has no such risk: it's
pure, unambiguous arithmetic on data already computed.

Test-driven against the real edge case (30.0° must read as 0° Taurus,
not critical, not as "still near a 29 boundary" from a sloppy
implementation checking the wrong modulus).

Shown as a "· critical degree" note on the affected placement's degree
label (planets and angles both), plus added to the glyph legend from
the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 20:59:48 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ba26202948 feat: house cusps table — computed for every chart, never actually shown
chart.houses.cusps has held all 12 house cusp longitudes since houses
were first implemented, but the only place that ever read it was a
stub-detection check (all-zero cusps == geohash too coarse). A full
per-house sign+degree table is a standard chart display every other
astrology tool provides, and this app had the data for it the whole
time with nowhere to put it.

Along the way, added an explicit test for something only ever implied
by other passing tests: House 1's cusp is exactly the Ascendant's
degree in every degree-based system (Placidus/Koch/Equal). The first
version of this test included Whole Sign too and failed — correctly:
Whole Sign houses start at the *sign boundary*, not the Ascendant's
literal degree, a deliberate astrological convention, not a bug. Fixed
by scoping the test to the three systems where the invariant actually
holds, rather than "fixing" correct behavior to match a wrong
assumption.

Surfaced as a new "Houses" section on the Chart tab, hidden for stub
charts (no real houses to show).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 20:53:22 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 24eb487c7b feat: glyph legend — an ease-of-use gap, not just a missing feature
Every tab shows dense astrological notation (planet symbols, aspect
glyphs, sign glyphs, now also retrograde marks and Moon phases) with
zero explanation anywhere in the app — a real barrier for anyone not
already fluent in the notation, and the opposite of the "genuinely
social, welcoming to newcomers" experience the app is going for.

Adds a "?" button to every tab's header (shared via make_tab_toolbar,
so it's consistent everywhere rather than duplicated per page) opening
one dialog covering all four categories. Built from AspectKind::all()
(newly exposed — the enumeration already existed privately for
detection) and Planet::all(), so it can't silently drift out of sync
with what the app actually renders the way a hand-maintained list
would; a new aspect kind shows up here automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 18:08:39 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 45563a0c8f feat: detect Grand Trine and T-Square aspect patterns
compute_aspects already finds every individual aspect within orb, but
nothing looked for the specific *combinations* of three that form the
named geometric configurations astrologers specifically watch for.
Adds zodia_core::detect_patterns: Grand Trine (three planets mutually
trine) and T-Square (an opposition, both ends squared by a third —
the "apex"). Test-driven against both the positive shape and the
negative space around it — two legs of a triangle without the third,
or an opposition with only one of the two required squares, correctly
detect nothing rather than a false positive.

Added Ord/PartialOrd to Planet (pure enum, no fields, safe to derive)
so pattern-matching aspect pairs could use a plain canonical
(min, max) ordering instead of a string-comparison workaround.

Surfaced as extra rows in the Chart tab's Balance section alongside
stelliums — silent when a chart has neither pattern, the common case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 18:03:19 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3c16176fd4 feat: "Big Three" headline (Sun/Moon/Rising) at the top of the Chart tab
Sun sign, Moon sign, and Ascendant sign were each already computed
separately (placements, houses) but never combined into the single
most commonly asked-for astrology summary in casual/pop use ("what's
your Big Three?"). Adds Chart::big_three(), test-driven against the
real bug class this kind of code invites: three near-identical
longitude lookups where a copy-paste swaps which field gets which
body — the test uses three signs far enough apart that a swap would
be caught immediately, not two similar ones that might coincidentally
still pass.

Shown as a single prominent centered line at the very top of the Chart
tab, above Balance and Placements — the first thing a user sees.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:57:05 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 cbbc6532b3 feat: stellium detection (3+ planets clustered in one sign or house)
A fourth classic natal-chart pattern with no prior support — "you have
a stellium in Capricorn" is a well-known astrological observation this
app had no way to surface despite already deriving each planet's sign
and house. Adds zodia_core::stelliums_by_sign/stelliums_by_house
(conventional 3-body minimum), test-driven with synthetic
PlanetPositions so the grouping logic is verified independent of real
ephemeris output. House-based grouping correctly returns empty for
stub charts (all-zero cusps) rather than producing meaningless groups
from house 0 for every planet.

Surfaced as extra rows in the Chart tab's Balance section, one per
detected stellium — silent (no rows) for charts with no stellium,
which is the common case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:52:00 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 2924a799ea feat: elemental/modal chart balance ("Fire-dominant", "a lot of Cardinal energy")
A third fundamental astrology concept Zodia never computed, despite
already deriving each planet's sign for placements. Adds
zodia_core::natal_balance — counts of the 10 tracked planets across
the four classical elements (Fire/Earth/Air/Water) and three
modalities (Cardinal/Fixed/Mutable), derived directly from each sign's
fixed element/modality (no ephemeris dependency, pure classification).
Test-driven against the actual classical mapping table, plus a
dedicated check on Cancer specifically since index 3 is where the
4-sign element cycle restarts — the kind of boundary an off-by-one
would hide in a purely cyclic-pattern test.

Surfaced as a new "Balance" section on the Chart tab, above Placements
— two plain summary rows, no InterpKey plumbing needed since this is a
derived summary rather than contributable content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:46:30 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 9de665daa8 feat: Moon phase — natal and live, another concept the app never tracked
Same gap as retrograde: Zodia already computes Sun and Moon longitude
for every chart, but never derived phase from their angular separation
despite it being one of the most universally recognized astrology
concepts. Adds zodia_core::moon_phase (8-phase enum from the Sun-Moon
separation angle), test-driven: the first test revealed a real bug in
the *test's* own scan logic, not the implementation — New Moon's 45°
segment straddles the 0/360 wraparound, so a naive linear scan across
one cycle sees it twice non-contiguously. Fixed by checking phase
reachability as a set instead of an artificial "N contiguous runs"
count, which was fighting the circularity rather than testing it.

Surfaced in two places: the natal Placements section now shows "Born
under a [phase] Moon" (a real, commonly-discussed birth-chart trait),
and the Sky tab shows today's live phase at the top — useful with zero
natal data at all, and changes slowly enough (~3.5 days between named
phases) that no ticker is needed, just computed once per page build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:39:42 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 0392921877 feat: retrograde indicators on natal chart placements
Zodia tracked zero retrograde-motion awareness anywhere in the
codebase — a real gap for an astrology app, since retrograde Mercury
is one of the most commonly discussed astrological concepts. Adds
zodia_core::is_retrograde(planet, jdn), detected numerically (sign of
the one-day-back longitude delta, handling 0/360 wraparound) since the
simplified ephemeris model has no per-body analytic velocity term.
Verified against real-world retrograde patterns before writing
assertions: Mercury shows ~69 retrograde days/year in this model
(matches its real ~3-4 annual retrograde periods), outer planets
substantially more (Earth laps them yearly), Sun/Moon never (a real
geocentric-astrology invariant, not a model artifact).

Surfaced as a classical "℞" glyph next to any retrograde planet on the
Chart tab's placement rows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:27:31 +02:00
Bendik Aagaard Lynghaug fbea7a0708 release: bump to v0.9.3
Sync Flathub / flathub-sync (push) Failing after 11s
Release / Create GitHub Release (push) Failing after 3s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.9.3
2026-08-03 17:16:24 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 216a5a4627 docs: write 0.9.3 release notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:16:02 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 87e46fa199 fix: surface circles from every aspect page, not just a hidden feed icon
Circles had no real front door — the only way to share into one was to
publish/edit a reading, then go find it in the Sky feed and notice a
small avatar icon on your own card. Adds a "Share to a circle" button
directly on every aspect/placement detail page, reusing the existing
OpenShareToCirclePicker flow (loads the doc's current body when the
page hasn't sent one, same as the feed-card path) — no new backend
plumbing needed, just a second entry point into what already worked.

Also: "Talk about this" (the voice-room button, previously "Start
discussion") silently no-op'd when nobody else was present on that
reading — confirmed by a real user clicking it and seeing nothing
happen. It now sends a notification explaining why, instead of failing
silently. Real fix (disabling the button until presence exists) is
follow-up UI work, tracked as a stopgap for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:12:18 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 e494803534 fix: rename voice feature to stop colliding with Circles, add onboarding name
"Start discussion" / "voice circle" and the Circles privacy feature
both used "circle" in their copy despite being completely unrelated —
confirmed confusing in practice: a real user clicked "Start discussion"
expecting to create a private circle and got a voice room instead.
Renamed to "Talk about this" / "voice room", no circle language left.

Also adds an optional display-name field to first-run setup, right
after the welcome header and before birth details — introducing
yourself before your chart, rather than leaving your own display name
undiscoverable behind a pencil icon on the Network tab (the only place
it lived before). Saved via the same LocalConfig::save_display_name
path the Network tab uses, and broadcast on the first network connect
like any other change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:02:15 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 3f35abe9b0 test: four more circle scenarios, plus two real findings along the way
New scenarios (all user-suggested): revoke-then-reinvite recovers
access at a new level, a member of one circle can't read another
circle's shares, a revoked member's own view converges too, and
circle-shared content survives pruning today (a documented scope
limit, not a bug).

Two things turned out different from what the tests initially assumed,
found by running against the real network rather than trusting the
assumption:

- The pruning scenario expected 0 operations removed; got 1. Root
  cause: every test peer now self-tags with a display name on connect
  (zodia_sdk::TEST_PEER_DISPLAY_NAME), which is itself a real, foreign,
  timestamped op the other peer receives — unrelated to circles at all,
  confirmed by an isolated check with no circle content whatsoever.
  Fixed the expected count instead of the code.

- A revoked peer's own circle_members() call doesn't converge on one
  outcome: sometimes it errors ("no such circle" — local space state
  already orphaned), sometimes it succeeds with a list that excludes
  them. Observed both outcomes for the identical scenario across
  repeated runs. Rewrote the assertion to poll and accept either
  terminal state, since which one happens is a timing race, not a bug.

Also fixes two recurring Gherkin authoring bugs in this suite: steps
registered only as #[when] silently no-op when used as "And" after a
"Given" or "Then" (cucumber resolves "And" to the nearest real
keyword) — hit again for revoke/share/prune steps, fixed by using
"When" explicitly. And documents circle_sharing.feature's new
isolation scenario as an accepted flaky characteristic (same class of
p2panda-net multi-topic session jitter as multi_key_isolation.feature)
after confirming more settle time didn't help — the invite's own 25s
retry loop is what matters, not front-loaded delay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:02:07 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 574b5ff4ec ci: automate Flathub sync on release, was never actually wired up
Flathub had been stuck at v0.7.0 through five releases (0.7.1-0.9.2).
Root cause: scripts/flatpak-sync.sh's own header called itself a
"post-release hook for cargo-release", but cargo-release only supports
pre-release-hook — confirmed against its docs. There was never a
mechanism that could have run this script automatically, and nobody
had been running it by hand either.

Adds flathub-sync.yml, triggered by the same tag push release.yml
already reacts to, so this can't silently go stale again. Corrects the
README and script comment to stop claiming cargo-release handles this
itself, and fixes a wrong package name in the setup docs (pip install
target is tomlkit, not toml — the generator script never imported the
latter).

Also manually ran the sync for v0.9.2 to catch Flathub up: pushed
flatpak/flathub's update/v0.9.2 branch (master is protected), PR ready
to open at https://github.com/flathub/io.github.bendik.Zodia/pull/new/update/v0.9.2

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 20:51:56 +02:00
Bendik Aagaard Lynghaug 441c839e0b chore: update flathub submodule to v0.9.2 2026-07-30 20:50:17 +02:00
Bendik Aagaard Lynghaug ea9405c68d release: bump to v0.9.2
Release / Create GitHub Release (push) Failing after 7s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.9.2
2026-07-30 18:41:22 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 ca087129d8 docs: write 0.9.2 release notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:32:32 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 a3eed17dab test: cover explicit unsubscribe() and multi-peer sync_status aggregation
Two more zodia-sdk gaps with zero prior BDD coverage:

- ZodiaClient::unsubscribe() itself was untested — every existing
  scenario either never subscribed to a key or let a subscription
  expire via the grace period. A no-op unsubscribe() (topic left open)
  would have gone unnoticed.
- sync_status()'s peers_caught_up was only ever proven to move off
  zero with a single peer. Adds a three-peer scenario proving the
  count actually aggregates instead of latching at 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:32:15 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 e1fbc743db feat: peers can broadcast a self-chosen display name
Adds InterpOp::SetDisplayName (legacy log 0, always-on global topic —
same propagation as Author/Revoke), materialised as
StateEvent::DisplayNameSet and exposed as ZodiaClient::set_display_name.
Receivers persist the newest name per peer (last-writer-wins by the
op's own timestamp, not local receipt time, since local time would let
a replayed stale op incorrectly "win"). The GTK app resolves a shown
name as: local nickname > peer's broadcast name > truncated hex,
threaded through every place a peer's name was previously
hex-only. A pencil button next to the Network tab's status line lets
the user set their own name, persisted via a new display_name.txt in
LocalConfig and re-broadcast on every reconnect.

Also tags every zodia-sdk cucumber test peer with a reserved sentinel
name (zodia_sdk::TEST_PEER_DISPLAY_NAME) and has the app filter any
peer broadcasting it out of the Network tab's discoverable-peers list —
mDNS reaches the whole LAN, so a local test run was showing up as
"new people" in a real running instance on the same network. A local
nickname still overrides the filter, for a developer who wants to keep
a specific test peer visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:25:07 +02:00
Bendik Aagaard Lynghaug 9ae0b96eb7 release: bump to v0.9.1
Release / Create GitHub Release (push) Failing after 7s
Release / Build x86_64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-unknown-linux-gnu (push) Has been skipped
Release / Build aarch64-apple-darwin (push) Has been skipped
Release / Build x86_64-pc-windows-gnu (push) Has been skipped
v0.9.1
2026-07-30 15:24:06 +02:00
Bendik Aagaard LynghaugandClaude Sonnet 5 4454a97762 docs: write 0.9.1 release notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 15:23:43 +02:00