Commit Graph
19 Commits
Author SHA1 Message Date
vhaudiquet 552a76ba99 build: the claw stands alone
The claw product (the daemon, the feed integrations, the client
tools, and the web and android trees) extracted from the framework
repository, keeping its history. The framework crates the product
pulls are all on crates.io at 0.2.3, so the in-tree path
dependencies became versions and the workspace resolves without the
framework checkout.
2026-11-12 17:43:08 +01:00
vhaudiquet 3efafd9c63 chore: bump the workspace to 0.2.3 2026-11-12 11:03:29 +01:00
vhaudiquet 263184ff8e chore: bump the workspace to 0.2.2 2026-11-10 19:44:04 +01:00
kosmos d31dfeae27 fix(forge): a gitea 401 retries once as basic auth
A self-hosted Gitea answers 401 to `Authorization: token <password>` and
200 to the same value as `user:password`, and the account store's
`secret_kind` cannot tell the two apart — it is a label the human typed
at `identity_save`, so a login password saved as a token is
indistinguishable from a real PAT until the forge answers.

`GiteaForge` had one auth path: the token header, on every call. A
watch whose credential resolved to a password therefore 401'd on every
poll, forever, while the same secret worked for `git push` over https
(the credential helper sends it as a password). The watch looked quiet
instead of broken.

`GiteaForge::with_login` names the login that owns the secret, and
`send` tries the token header first, then — on a 401 only — the same
value as Basic auth. The retry is cloned before the token header lands,
so the fallback request carries one `Authorization` header and not two.
No login (a pasted watch token names none) or an unclonable body leaves
the 401 to surface exactly as it did before.

The login reaches the client in the next commit; this one is the adapter
half, tested against a mock that refuses the token header and accepts
the Basic one.
2026-11-08 17:54:15 +01:00
vhaudiquet 52ee642eee chore: bump the workspace to 0.2.1 2026-11-08 17:54:12 +01:00
vhaudiquet 3283d92227 chore: bump the workspace to 0.2.0 2026-11-06 15:32:41 +01:00
vhaudiquet 5a8be19b4c fix(calendar): fed slots stay fed across a restart
The reminder source skipped an already-fed slot only while the slot
sat in the dispatcher's pending inbox; a claw restart drained that
inbox, and every future lead of the same occurrence re-fed — a
reminder fired two or three times per calendar event. The source
keeps its own fed-slot ledger in the calendar store now (keyed by
calendar, occurrence, and lead), records each slot right after a
successful ingest, and prunes rows whose occurrence is an hour past
every lead, so a pruned row can never resurrect a reminder. The
store-backed source's ledger survives a restart; the static-list
constructor keeps an in-memory one for the process lifetime, said so
in the docs that previously claimed dedup was someone else's problem.
Store failures surface loudly (a silently lost row is a re-fired
reminder); the prune warns and continues. Offline tests cover the
once-only rule, the restart via a tempdir reopen, and the prune.
2026-11-05 19:06:41 +01:00
vhaudiquet dad99c7664 fix(calendar): the ICS address parser survives a multibyte prefix
bare_address sliced trimmed[..7] after a len() >= 7 check — byte
count against a byte slice is fine, but the guard and the slice
disagree once the text is lossy-decoded elsewhere; an ATTENDEE line
whose value opens with a multibyte run panicked the reminder source
on a feed the operator does not control. The slice is get(..7) now,
and a test feeds multibyte prefixes through both the ATTENDEE and
ORGANIZER paths.
2026-11-05 19:05:16 +01:00
vhaudiquet 00adf092ff fix(forge,claw): forge keys separate the issue from the pull
The watch key encoded source, repo, and number but not the thread
kind, and on GitLab an issue and a merge request share one number
space: iid 7 twice produced one key, one snapshot, and comments on
the merge request landing in the issue (or a 404 fallback that
misrouted the other way). The key carries the kind now —
{source}:{kind}:{owner}/{name}#{number}, with GitLab spelling the
pull variant mr — comment and snapshot route outright by kind, and a
kind-less payload parses to the unparsable key instead of guessing
issue. The claw's callers thread the kind from the payload and strip
the segment where the bare repo is wanted; pre-kind stored keys parse
as None rather than misroute. Four route-pinning wiremock tests cover
the GitLab split, and the GitHub/Gitea clients take the kind without
using it (their issues and PRs share the same REST space).
2026-11-05 19:03:52 +01:00
vhaudiquet e92739aa1b chore: collapse the ifs the 1.98 clippy flags
The pinned toolchain's clippy grew opinions the older lint let stand:
nested if/if-let pairs it wants as let-chains, and a modulo test it
wants as is_multiple_of. Machine-applied collapses, fmt-settled, no
behavior change — the gauntlet runs clean again.
2026-10-27 01:12:46 -05:00
vhaudiquet e4f4a3a10b chore: enforce missing_docs from the workspace manifest
One [workspace.lints.rust] table replaces the deny attribute
scattered across every lib.rs, and each crate opts in through
[lints]. The lint now also reaches the integration-test crates,
whose roots gain their one-line docs.
2026-10-25 07:02:48 +02:00
vhaudiquet 15d3478429 fix(calendar): the source constructor reports CalendarError 2026-10-25 07:02:48 +02:00
vhaudiquet 90212d1e0f refactor(claw): the forge kind is the integration's — config.rs carries no forge code
The last forge content in config.rs was vocabulary, not data:
`ForgeKind` (name/parse/token_env/default_base) and the pure
`resolve_token` helper. By the same rule that moved `Watch` to the
watch table and left `CalendarWatch` in the calendar integration,
the kind belongs to the crate that speaks the forges.

- `rhema-forge-integration` gains `kind::ForgeKind` and
  `resolve_token`, with the parse and token-fallback tests moved
  alongside — the claw's config tests were testing the integration's
  type through a re-export.
- The claw's surfaces import the type from the integration directly;
  the crate root re-exports it, so `rhema_claw::ForgeKind` keeps
  resolving for everything downstream.

config.rs is now config: credentials, the surface, paths, cadence,
and nothing that knows what a forge or a calendar is.
2026-10-24 16:48:18 +02:00
vhaudiquet 7f6c13f608 chore: apply cargo fmt 2026-10-24 02:02:39 +02:00
vhaudiquet 818079d1f4 refactor(claw): the calendar table is the one truth — live, not config
Same move as the watches, and the config's last piece of integration
state goes with it:

- `rhema-calendar-integration` owns the calendar table now
  (`store.rs`): SQLite in the shared dispatch database, the pattern
  the claw's watch table established. `CalendarWatch` gains the
  `validate` the config's boot check used to run — per-kind URL and
  credential rules, plus the lead-under-horizon check — so an invalid
  calendar is loud at the one door every writer shares.
- `CalendarSource::from_store` reads the table fresh on every detect
  pass (the forge source's pattern): a calendar added over the route
  is live next tick, a removed one stops polling, no restart in the
  story. The static constructor stays for tests.
- `POST/DELETE/GET /v1/calendars` on the daemon: the POST body is the
  integration's `CalendarWatch` shape, validated at the table; DELETE
  takes `?name=`; the api listing names the door.
- `ClawConfig.calendars` is gone. A config still carrying it is
  refused at parse time, like the retired `watches:` seed. The
  config-side validation and its tests moved with the type.

The config file now carries only what boot needs: credentials, the
surface, paths, the poll cadence. Everything the daemon reconfigures
at runtime lives in its tables, behind its HTTP doors.
2026-10-24 01:52:09 +02:00
vhaudiquet b70a56c325 refactor(calendar): move the reminder source beside the feeds
CalendarWatch, the client resolution, and the slot policy were all
feed domain living in the claw's config and source tree; only the
extension adapter was the claw's. The reminder source now ships in
the integration as the default answer to "what does a start time
mean": one slot per lead time, calendar:<name> keys, fed to the
consumer's dispatcher. The crate doc drops its no-policy claim to
match, and the claw's resident prefixes take the calendar prefix
from the integration so the spelling cannot drift.
2026-10-20 05:53:43 +02:00
vhaudiquet fc0cbbcb6b refactor(forge): classify account notifications beside the vocabulary
The triage mapped the forges' notification reasons onto the claw's
verdicts, but it co-evolves with the vocabulary, not with the claw:
a reason that enters it (Gitea's collapsed shape) needs a verdict in
the same change, and its tests pin the mapping against the
vocabulary's own types. It now lives in the integration crate as the
default classification; a consumer may reclassify on top. The claw
imports it like the rest of the forge seam.
2026-10-20 05:49:14 +02:00
vhaudiquet 04549a7df5 fix(claw): dispatch reason-less forge notifications to the thread's agent
Gitea's notifications feed names no per-row reason, so the adapter
collapses every row under one fixed NotificationReason::Other spelling
— and the claw's triage mapped every Other to Surface: the notification
reached the device queue and never a dispatch. A comment on a thread
the account authored could therefore never wake the agent on a
reason-less forge, whatever the watches said.

The adapter now publishes that spelling as gitea::COLLAPSED_REASON —
it is the seam's contract, not a detail — and the triage treats it as
the known shape it is: a notification on a thread of the account's own
dispatches the implementer. A spelling no adapter owns still surfaces;
an unknown shape remains the user's to judge, never the agent's to
guess at.
2026-10-19 19:40:44 +02:00
vhaudiquet 81da26b955 feat(rhema-code): add the colors setting with four palettes
The stored `colors` key names a palette — rhema (the default,
Jadeite), aquamarine, black-and-white, or dracula — and the
Appearance page of /settings cycles it live, like the theme and
icon toggles beside it. Each palette carries a dark and a light
appearance under the existing `theme` key; dracula's light side is
derived, the official palette ships only dark.

The appearance decision moves off token comparison: Theme now
answers is_dark() from the ground/text luminance, so palettes
beyond the original two get the right per-profile fallback tones.
2026-10-17 04:28:31 +08:00