Commit Graph
898 Commits
Author SHA1 Message Date
Arunavo Ray 4ac2c4323c fix(ui): hand over to the compact toolbar at lg, and let lists size themselves
All three list pages switched from the compact toolbar to the full one at
sm (640px), so a tablet in portrait got the desktop row with six controls
and the search box squeezed down to its icon. The compact layout already
carries every filter in its drawer, so the handover moves to lg (1024px)
and tablets get a usable search box.

Lists also sized themselves off the viewport with hard-coded pixel
offsets - max-h-[calc(100dvh-231px)] and -276px. Those went stale the
moment a toolbar changed height, which is what put a second scrollbar on
the page next to the table's own. Both now flex into the space the
toolbar leaves, so the visible row count follows the window instead of a
fixed guess: the repository table shows 13 rows at 800px and 23 at
1400px, where it used to stop at the same count either way.

Also drops the bottom status bar from both lists. Its count moves up into
the empty left half of the filter row, and its live indicator was saying
what the header's LIVE button already says.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 10:07:35 +05:30
Arunavo Ray cadeb64dce fix(ui): give the app a single scroll region
main was min-h-screen, so it could grow past the viewport, while the
content section sized itself with h-[calc(100dvh-4.55rem)] - a guess at
the header's height. Whenever the header came out taller than that
guess, the body scrolled behind the section's own scroll and every page
showed two scrollbars.

The shell is now pinned to the viewport and the section takes whatever
the header actually leaves, so the magic offset is gone and the height
is correct whatever the header does.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 10:07:25 +05:30
Arunavo Ray 7fcce46821 feat(ui): accept a GitHub URL in the add repository and organization dialogs
Both dialogs asked for the name and owner as separate fields, so adding
something you were looking at on GitHub meant reading the URL and
retyping it in pieces.

Each dialog now has a GitHub URL field above an OR divider; filling it
populates the fields below. Pasting a URL into the name field splits it
as well, rather than dropping the whole URL into one box.

Parsing handles what people actually have on the clipboard: browser
URLs, clone URLs, SSH remotes, the owner/repo shorthand, a bare host,
and deep links into a repo. It drops the .git suffix, tolerates trailing
slashes, query strings and angle-bracket wrapping, and rejects reserved
GitHub paths like /settings so they cannot be read as an account name.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 09:47:13 +05:30
Arunavo Ray 64e3b18e1e fix(ui): rework the repositories filter bar
The toolbar carried six controls plus Mirror All and overflowed on
narrower desktop windows, and the owner and organization pickers did not
match the dropdowns beside them.

- Moves the status, mirror options and sort dropdowns down to the row
  that carries "Showing N of M" and Clear filters, right-aligned. They
  live in RepositoryTable now because that is where the filtered count
  is computed, and the row renders in both the loading and loaded states
  so the controls do not appear only once data arrives.
- Makes the owner and organization triggers match the Select triggers.
  They kept a double-chevron icon and the outline Button styling; the
  Select trigger classes are now exported from ui/select so both use one
  definition instead of a copied class string. They stay comboboxes
  rather than Selects because those lists grow with the repository count
  and need the search box.
- Widens both to 200px (w-50), which fits longer owner names.
- Hides the moved dropdowns below sm. The mobile filter sheet already
  covers all five filters, so on mobile they were duplicates sitting
  loose on the page.

Also fixes hasAnyFilter, which used val?.toString(), yielding undefined
for an unset filter. undefined !== "" is true, so an unfiltered view
reported "Showing 697 of 697 repositories / Clear filters" and a
"Filters applied" footer with nothing filtered.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 09:47:05 +05:30
Arunavo Ray b3e8c69566 fix(ui): make the dashboard Repositories and Recent Activity cards match
The two cards sat at whatever height their own content needed, so they
ended at different points whenever the lists were different lengths.
That is the normal state: repositories and mirror jobs come from
independent tables, and activity accumulates on every sync while the
repository list only grows when a repo is added.

- Drops items-start from the row so the columns stretch to the row
  height, in both the skeleton and the loaded state so the layout does
  not shift on load.
- Adds h-full to both cards. Without it the column stretches but the
  card inside still hugs its content, so removing items-start alone
  changes nothing on screen.
- Aligns the activity row padding with the repository rows (py-3 to
  py-3.5) so the rows line up when the two lists do have equal counts.

Verified in the browser at 8 vs 8 and 8 vs 5 items: both cards measure
657px in each case.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 09:30:34 +05:30
Arunavo Ray f06f6695bb chore: bump version to 3.28.2
Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
v3.28.2
2026-09-01 09:08:09 +05:30
Arunavo Ray a000073a68 fix(ui): rebuild the Identity Providers card to match Sign-in Methods
The card used shadcn Card/CardHeader while its neighbour used the plain
settings markup, so the two headers were different heights and their
dividers did not line up. Rebuilt it on the same markup: icon, title,
divider, body, footer.

- Drops the header description, which wrapped under the Add provider
  button and collided with it.
- Moves Add provider into the header row. The header uses py-3 rather
  than py-4 because the h-8 button makes the row taller than a text-only
  one; both headers now measure 56px.
- Removes mr-2 from the Plus icons. The button already applies its own
  gap, so mr-2 stacked an extra 8px and pushed the label off-centre.
- Gives the provider detail labels weight and a fixed 96px column.
  Everything was one muted weight with no hierarchy, and "Organization:"
  overflowed the old 80px column so its value broke the alignment.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 09:08:05 +05:30
Arunavo Ray 2355284d27 chore: bump version to 3.28.1
Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
v3.28.1
2026-09-01 08:57:39 +05:30
Arunavo Ray 6f3727c47e fix(ui): tighten settings card labels so they fit on one line
Database Maintenance footer said "Last cleanup"/"Next cleanup", which
wrapped onto two lines next to a full timestamp. The card is already
titled Database Maintenance, so "Last run"/"Next run" says the same
thing and fits.

Same for the Smart backup tile: "Snapshot only on history rewrites"
wrapped, and "history" is redundant since a force-push is the only
rewrite the sync sees.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
2026-09-01 08:57:36 +05:30
Arunavo Ray 1bb096aa93 chore: bump version to 3.28.0 v3.28.0 2026-08-24 09:26:07 +05:30
ARUNAVO RAY feb13ff58a feat(mirror): per-repo release limit, description sync on every sync, tidier mobile cards (#370)
Follow-up to #362 and #365 for the remaining points raised in #361.

- Add `releaseLimit` to the per-repository and per-organization mirror
  overrides (repo > org > global > 10), surface it in the Mirror Options
  dialog with gating and validation, and pass the resolved value into the
  release mirror. Tags need no equivalent: Gitea's git mirror fetches every
  ref and they cost nothing beyond the commits already in the clone.
- Reconcile the Gitea description and topics with GitHub on every sync, not
  only at migration time. Read both live in one call, write the refreshed
  description back to the local row, and skip writes that would not change
  anything. Mirrors created before #224 or whose upstream description
  changed since now pick it up on the next sync.
- Rework the mobile repository card: name, Custom badge and menu on one
  vertically centred row, secondary badges on their own row below, status
  and last sync stacked on the right.
2026-08-24 09:10:45 +05:30
ARUNAVO RAY 41592cb8fd fix(ci): skip Docker Scout on fork PRs and add a Trivy scan for every PR (#369)
Fork PRs (and Dependabot PRs) never receive repository secrets, so the
Docker Hub login ran with empty credentials and Docker Scout then died
with "user githubactions not entitled to use Docker Scout". Every
external contributor got a red docker check they could not do anything
about (#368, #356, #357, #359).

- Gate the Docker Hub login and both Scout PR steps on the PR coming
  from this repo, the same guard the login/push/comment steps already use.
- Add a Trivy scan of the locally loaded image on every PR. It needs no
  credentials, so forks get real vulnerability feedback. Output goes to
  the job summary because GITHUB_TOKEN is read-only on fork PRs.
- Only upload the Scout SARIF when a file exists, so fork PRs stop
  logging "Path does not exist: scout-results.sarif".
- Update the workflows README, which still described a docker-scan.yml
  that no longer exists.

Not using pull_request_target on purpose: it would run untrusted PR code
with the Docker Hub token available.
2026-08-24 07:52:41 +05:30
Arunavo Ray 8446811e8d chore: bump version to 3.27.2 v3.27.2 2026-08-21 17:37:40 +05:30
ARUNAVO RAY 0c41fac9c0 fix(auth): trust registered SSO provider origins and honor deleteFromGitea (#366) (#367)
Three fixes for the two problems reported in #366:

1. Auto-trust registered SSO identity provider origins. better-auth
   1.6.23 (shipped in v3.21.0) added SSRF hardening to the SSO plugin:
   sign-in rejects IdP endpoints whose hostnames resolve to private
   addresses unless the origin is in trustedOrigins. Homelab split-DNS
   setups (IdP domain resolving to a LAN IP from inside the container)
   broke on every sign-in with a 400. Registering a provider is an
   explicit operator action, so its issuer and endpoint origins are now
   added to trusted origins automatically.

2. Surface SSO sign-in errors in the login form. The auth client
   resolves with { data, error } instead of throwing, so server-side
   rejections were silently swallowed - the button flipped back from
   "Redirecting..." with no feedback and nothing in the logs.

3. Honor deleteFromGitea (CLEANUP_DELETE_FROM_GITEA). It was documented
   as "Delete repositories from Gitea" defaulting to false, but cleanup
   never read it and always archived/deleted orphans on the Gitea side.
   It now gates the Gitea-side operation: when disabled (default),
   orphans are only marked archived or removed in gitea-mirror's own
   database and the Gitea/Forgejo copies stay untouched.

Verified end to end against a live server: sign-in with a private-IP
IdP returns 400 discovery_private_host on v3.27.1 and a 200 with the
authorization URL on this branch; the login form now shows the server
error as a toast; delete cleanup with the flag off removes only the DB
row while the flag on contacts Gitea.
2026-08-21 17:37:32 +05:30
Arunavo Ray f18fc4bfe6 chore: bump version to 3.27.1 v3.27.1 2026-08-21 07:09:35 +05:30
ARUNAVO RAY d63d55f52d fix(ui): surface Mirror Options on mobile repo cards and desktop org menu (#365)
The per-repo overrides from #361 were reachable only from the desktop
repositories table. The mobile card layout had no dropdown at all, and
the organizations page had the inverse gap: the item existed in the
mobile card menu but not the desktop one.

Mobile repo cards get a three-dot menu in the header opening the same
MirrorOverridesDialog, plus the Custom badge shown when a repository
overrides something. The organizations desktop dropdown gains the same
Mirror Options item the mobile variant already had.
2026-08-21 07:09:32 +05:30
ARUNAVO RAY 2867076d71 docs(www): align use-case articles with the actual configuration UI (#364)
Four older articles directed readers to UI elements by names that no
longer exist. Verified against src/components/config/:

  "Content & Data" section        -> "Mirror Content"
  "Mirror metadata" toggle        -> "Repository metadata"
  "Git LFS (Large File Storage)"  -> "Git LFS objects"
  "Mirror releases"               -> "Releases & tags"
  "Preserve structure" strategy   -> "Preserve Structure"

The new articles from #363 already used current names or generic
wording, so only the older four needed changes.
2026-08-20 06:41:41 +05:30
ARUNAVO RAY 7a935ff1b1 feat(www): SEO overhaul with sitemap, metadata, interlinking, and five new articles (#363)
* feat(www): generated sitemap, article metadata, and de-orphaned comparison page

The hand-written sitemap listed one URL of 24 and was dated January 2025.
astro.config now sets `site` and uses @astrojs/sitemap, so every built
page lands in sitemap-index.xml on each build. robots.txt points at the
new index and drops the duplicate user-agent block.

Use-case layouts gain the OG/Twitter tags the docs layout already had,
and all three article layouts now emit TechArticle JSON-LD.

Nothing on the site linked to /comparison/github-backup-tools/. It now
has a header nav item, a footer link, and a homepage CTA next to the
use-cases button. The footer Documentation link points at the site docs
instead of the GitHub tree, and footer internal links no longer open in
new tabs.

* feat(www): five new articles and an interlinking pass

New guides, each grounded in shipped behavior:

- Migrate from GitHub to Gitea (parallel-run migration playbook)
- Mirror Git LFS repositories (why one bad LFS repo fails its whole
  migration, and the per-repo override from v3.27.0 that fixes it)
- GitHub API rate limits (ETag conditional requests making 304s free)
- Mirror issues and pull requests (what transfers, the PR-to-issue
  conversion, why processing is sequential)
- GitHub backup on Unraid (based on the RayLabsHQ CA template)

All five are registered in the use-cases index; appended after the
existing entries so the homepage's featured three stay unchanged.

Interlinking pass across the existing articles: contextual inline
links where the text already referenced a topic, plus a short related
guides section per article. The comparison page's next steps now cover
the new guides, and the docs quickstart bridges to the use-case section.
Every article now carries 4 to 9 internal links; a build-time check
found 0 broken links across 440.
2026-08-20 06:33:04 +05:30
Arunavo Ray 85b72e9366 chore: bump version to 3.27.0 v3.27.0 2026-08-20 06:03:34 +05:30
ARUNAVO RAY 0f700c7197 feat: per-repository and per-organization mirror option overrides (#362)
* feat: per-repository and per-organization mirror option overrides

Closes the gap reported in #361: a repository whose LFS fetch fails could
not be mirrored at all, because LFS was a single global switch. Gitea runs
the LFS fetch inside its own migration, so a failure aborts the whole
migration with nothing to salvage. The only fix available to us is to stop
asking for LFS on that repository.

Mirror options now resolve across three tiers, per flag, most specific
first: repository override, then organization override, then the global
config. NULL at a tier means inherit, so overriding LFS on one repo leaves
its other options alone.

Adds resolveMirrorOptions(), which replaces 24 scattered reads of
config.giteaConfig.* across three near-identical blocks: the two mirror
paths in gitea.ts and the sync path in gitea-enhanced.ts. The third was
easy to miss and mattered: without it, overrides would have been honored
on first mirror and silently ignored on every scheduled sync afterwards.

The starredCodeOnly clamp is folded into the resolver and deliberately
outranks explicit overrides, preserving the existing behavior that starred
repos mirror code only.

Editing is on the objects themselves, via the existing three-dot menus on
the Repositories and Organizations pages, not in Configuration, which keeps
holding the global defaults. A repositories filter and a row badge make it
possible to find which repos deviate.

Malformed override JSON degrades to "inherit" rather than throwing, so a
bad value can never break a mirror run.

* fix(ui): disable mirror toggles that cannot take effect, and explain why

A toggle the runtime will ignore should not look editable. Three cases
were doing exactly that, so they now share one mechanism:
getMirrorOverrideGating() returns a reason string per flag, and the dialog
disables the control and prints that reason underneath.

Starred clamp. When a repo is starred and starredCodeOnly is set, the
resolver forces every metadata flag off regardless of the override. The
dialog previously let the user set them anyway. The clamped set now lives
in STARRED_CLAMPED_KEYS, shared by the resolver and the gating helper so
the flags the UI disables cannot drift from the ones the runtime clamps.
LFS is deliberately excluded from that set, since turning LFS off per
repository is the point of #361, and a test pins that.

Labels. shouldMirrorLabels is `mirrorLabels && !mirrorIssues` in both
mirror paths, so labels cannot take effect while issues are mirrored. That
gate reacts live to the in-progress edit rather than only the saved state.

Inherit hint. For a repository the hint now reflects global -> org instead
of global alone, via a new name-keyed GET
/api/organizations/mirror-overrides that reuses
loadOrganizationMirrorOverrides. Personal repos skip the fetch, a failed
fetch degrades to the global values rather than blocking the dialog, and
the hint is suppressed while in flight so it is never briefly wrong.

Widens the UI to mirrorLabels and mirrorMilestones. mirrorMetadata stays
out: no mirror path reads it, so a per-object override would resolve
correctly and then do nothing. See the report for that finding.

useGiteaConfig additionally returns advancedOptions so callers can read
starredCodeOnly without a second request.

* fix(ui): read inherited mirror flags from mirrorOptions, not giteaConfig

/api/config does not return the mirror flags on giteaConfig. On the way
out, mapDbToUiConfig reshapes them into a separate mirrorOptions object
using different names (mirrorLFS, not lfs) and nesting the metadata flags
under metadataComponents with short names (issues, not mirrorIssues).

The dialog was written against the DB shape, so every lookup returned
undefined. Coerced to false, that is indistinguishable from a real "off",
which produced two symptoms: every inherit hint read "currently off"
regardless of the actual global config, and the labels gate never fired
because the effective issues value it keys on came from the same dead
source.

Adds mirrorOptionsToFlags() as the single conversion point, matching the
derivation in mapUiToDbConfig exactly, including that mirrorMetadata is a
master switch over the metadata components while lfs and mirrorReleases
sit outside it. Both dialogs now go through it, and useGiteaConfig returns
mirrorOptions alongside advancedOptions.

Server-side mirroring was never affected: the resolver reads config
straight from the DB, where the flags really do live on giteaConfig.

The existing tests could not catch this because they build a synthetic
config already in DB shape, so a client/server mismatch is invisible to
them. The new tests push flags through the real config-mapper in both
directions and assert the derived flags equal what mapUiToDbConfig would
persist, so they fail if that mapping changes again. One test pins the
root cause directly: that giteaConfig in the API payload carries no flags.

* fix(ui): surface the mirror-options filter on desktop, and on organizations

The overrides filter only existed inside the mobile filter drawer, so on
desktop the Repositories page showed a "Custom" badge on overridden rows
with no way to filter to them. Being able to answer "which repos deviate
from my defaults" on this page is the reason the Configuration-page
listing was dropped, so desktop could not do the one job it was given.

Adds the control to the desktop filter row next to status and sort, using
the bare Select-with-placeholder style those use rather than the drawer's
labelled markup. The mobile control is unchanged.

The Organizations page had a wider version of the same gap: it renders the
"Custom options" badge but never had this filter on either layout, and
OrganizationsList did not filter on it at all. Added the predicate plus
the control in both its layouts, so the two pages behave the same.

Also folds hasOverrides into activeFilterCount on both pages and into the
Organizations clear-all reset. Without that the mobile filter badge
undercounted an active overrides filter, and clearing filters left it set.

* fix(ui): stop double-applying the mirrorMetadata switch when reading config

mirrorOptionsToFlags ANDed each metadata component with mirrorMetadata on
the way in. That is a write-path rule: mapUiToDbConfig already applies it
when persisting, so a config saved through the settings UI has it baked
into the stored flag. Applying it again on read double-applies it.

The read path does not need it. mapDbToUiConfig puts the raw stored values
into metadataComponents, so metadataComponents.issues is exactly
giteaConfig.mirrorIssues, which is the field gitea.ts and
gitea-enhanced.ts read. Mapping the components straight through is 1:1
with runtime behavior.

Double-applying was invisible while the stored state was self-consistent,
since false && false is still false. It diverged when the state did not
come from the UI write path, which env vars allow: MIRROR_METADATA=false
with MIRROR_ISSUES=true stores mirrorMetadata:false, mirrorIssues:true.
The runtime mirrors issues; the dialog reported off. Measured against a
config in that state, five flags misreported.

I previously described this as an unrecoverable loss in the API shape.
That was wrong. Both values reach the client separately and unmerged, so
the payload carries full information and the loss was one we introduced.

Rewrites the test that asserted the derived flags match what
mapUiToDbConfig would persist. Comparing against the write derivation is
what pinned the bug in place. It now asserts the property that matters,
that flags derived from the API payload equal the stored flags the runtime
reads, plus a case checking agreement with resolveMirrorOptions on an
inconsistent config. Both fail if the AND returns.
2026-08-20 06:03:19 +05:30
Arunavo Ray a295a73066 fix(deps): patch nanoid, js-yaml and postcss in the www lockfile
Clears three Dependabot alerts on the marketing site:

  nanoid   3.3.16 -> 3.3.18  (infinite loop when size is zero)
  js-yaml  4.3.0  -> 4.3.1   (quadratic CPU on !!omap resolution)
  postcss  8.5.19 -> 8.5.26  (attacker-controlled source parsing)

All three are transitive and moved within their existing ranges, so the
change is version and integrity hashes only with no graph churn.
Verified with a frozen-lockfile install and a full site build.
2026-08-17 14:16:02 +05:30
Arunavo Ray 67cd0e2c60 docs: restore star history chart with scoped access token
GitHub restricted the stargazers API to repo admins and collaborators,
which broke the unauthenticated chart. Star History now needs a token
sealed into the image URL.

Uses a fine-grained token limited to this repository instead of a
classic public_repo token. The stargazers endpoint requires Contents
write, and classic public_repo grants that across every public repo the
account can push to, so scoping it to one repo keeps the blast radius
small. Legend moved to top-left to sit clear of the curve.
2026-08-17 14:11:31 +05:30
ARUNAVO RAY c9edc2132b Update star history links and image sources 2026-08-16 21:19:36 +05:30
Arunavo Ray d4db47c76e chore: bump version to 3.26.2 v3.26.2 2026-08-07 17:03:41 +05:30
Arunavo Ray 91eb195494 fix(ui): smooth marquee scrolling and widen its hover target
Animating text-indent forced a relayout every frame, which made the
marquee visibly choppy. The scroll is now a compositor-driven transform.
The content stays plain inline text while at rest so the native ellipsis
still renders, and swaps to an inline-block only for the duration of the
animation.

The hover target is now the whole repository cell instead of just the
text, via a shared MarqueeTrigger wrapper. Name and path scroll together
at the same speed, so the motion reads as one block.
2026-08-07 13:38:47 +05:30
Arunavo Ray e0c39b697f chore: bump version to 3.26.1 v3.26.1 2026-08-07 13:23:51 +05:30
Arunavo Ray 03fce1e299 fix(ui): keep repo name and path on one line with hover marquee
Long repository names and owner/repo paths in the repositories table
wrapped to multiple lines, breaking row alignment. Both now truncate
with an ellipsis and scroll horizontally on hover to reveal the hidden
part.

The new MarqueeText component animates text-indent instead of a
transform because ellipsis rendering only works on inline content, and
a transform would require an inline-block wrapper.
2026-08-07 13:23:48 +05:30
Arunavo Ray 7fc53cfad8 Revert "fix(security): upgrade better-auth family to 1.7.0-rc.4"
This reverts commit 2f6af22e25.
2026-08-06 07:19:13 +05:30
Arunavo Ray b33e7d5596 fix(security): patch image CVEs and unbreak Scout SARIF uploads
- Build git-lfs with Go 1.25.12 (stdlib CVEs fixed in 1.25.10) and pull
  golang.org/x/net past the fix for CVE-2026-39821; x/net 0.54.0 was
  flagged critical in the image scan.
- Cap SARIF relatedLocations at 100 per result before upload. GitHub
  rejects SARIF with >1000 related locations per result, and Docker
  Scout exceeds that for common OS packages, so every upload since May
  had failed silently (continue-on-error) and the Security tab was
  frozen on a stale scan. With uploads flowing again, already-fixed
  alerts (samlify, libgnutls via the existing apt-get upgrade) close on
  the next scan.
2026-08-06 07:00:03 +05:30
Arunavo Ray 2f6af22e25 fix(security): upgrade better-auth family to 1.7.0-rc.4
Fixes the @better-auth/oauth-provider advisory (unbound resource
indicators could yield access tokens for unauthorized audiences,
Dependabot #55). No patched 1.6.x exists; 1.7.0-rc.4 is the first
patched line.

The 1.7 oauth-provider expects a wider schema: new nullable columns on
oauth_clients / oauth_access_tokens / oauth_refresh_tokens /
oauth_consents (back-channel logout, DPoP, resource indicators, refresh
token rotation) and three new tables (oauth_resources,
oauth_client_resources, oauth_client_assertions). Migration 0014 is
purely additive; validate-migrations gains the matching upgrade fixture.
2026-08-06 07:00:03 +05:30
Arunavo Ray 185946984b chore: bump version to 3.26.0 v3.26.0 2026-08-06 06:43:47 +05:30
Arunavo Ray bcc9cf0120 feat(github): cap ETag cache by size and scope tokenless clients
Follow-up to #356. The conditional-request store was bounded only by
entry count; a single 100-PR page can run to a few hundred KB, so 5000
entries could grow to gigabytes. The store now also tracks approximate
body bytes (64MB budget by default) and evicts oldest-first until both
caps hold. A body larger than the whole budget is not cached at all.

Clients created with only a token (the metadata mirroring path) all
shared the "default" cache scope across users. They now fall back to a
SHA-256 hash of the token, so distinct tokens never share entries and
the raw token never appears in cache keys.
2026-08-06 06:43:40 +05:30
Josh FreeandCopilot 2b455354f9 feat(github): reuse ETags across syncs via conditional requests (#356)
* feat(github): reuse ETags across syncs via conditional requests

The mirror re-lists every repository's pull requests on each scheduled
sync with `state: "all"`, and the Octokit client was created with the
throttling plugin but no conditional-request/ETag layer. Every sync
therefore re-downloaded unchanged data as full 200s and spent full
rate-limit budget.

Add an ETag cache wired into `createGitHubClient`: for each GET it
replays the previously stored `If-None-Match`, so GitHub returns
`304 Not Modified` (which does not count against the token's primary
rate limit) when nothing changed, and the cached body is reused. The
store is process-lifetime and scoped per user so ETags survive the
per-sync client re-creation without leaking data across tokens. Non-GET
requests are untouched.

Refs #355

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79

* test(github): cover conditional-request ETag cache

Add unit tests driving a real Octokit instance with a stubbed fetch:
the second GET replays `If-None-Match`, a `304` is transparently served
from cache as a 200 with the same body, responses without an ETag are
re-fetched, non-GET requests are never made conditional, and cache
entries are isolated by scope.

Refs #355

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79

* fix: key conditional-request cache by expanded URL

The request hook built the cache key from requestOptions.url, which is
still the route template (/repos/{owner}/{repo}/pulls) at hook time.
Every repo therefore shared one entry per user + endpoint, so with more
than one repo per user the stored ETag never matched and the 304 path
stopped firing. Expand the route via octokit.request.endpoint.parse
before building the key, and add a two-repo regression test that fails
on the shared-key behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4959e1f9-f8e6-4e97-a487-f395a0123c79
2026-08-06 06:36:31 +05:30
Arunavo Ray 428c97441c chore: bump version to 3.25.0 v3.25.0 2026-08-03 11:59:44 +05:30
Arunavo Ray edf9c14be0 fix: honor DATABASE_URL for the SQLite database location
The variable was defined and documented but never read; the database
path was hardcoded to <cwd>/data/gitea-mirror.db. It now accepts
sqlite://, the legacy file: scheme, or a plain path, with relative
paths resolving against the working directory and the parent directory
created on demand. Defaults are unchanged, including the compose files
that pass the historical default value through. Docs updated to match.
2026-08-03 11:59:44 +05:30
ARUNAVO RAY 5c49191d61 Move canonical docs to the website (#354)
* docs: move canonical documentation to the website

The website now hosts the full documentation at /docs with a proper docs
layout: sidebar navigation, on-page table of contents, mobile nav, theme
support, canonical and OG meta, and overflow-safe code blocks and tables.
Ten pages, all rewritten from the current code rather than copied from
the old in-app docs: quickstart, deployment (Docker, Helm, Nix, LXC,
bare metal), configuration, environment variable reference,
notifications (all four providers including webhook payload signing),
authentication (including header auth), force-push protection,
architecture, advanced, and custom CA certificates.

This fixes every inaccuracy found in the docs audit: the wrong
raylabs/gitea-mirror image name, JWT_SECRET presented as the live auth
secret instead of BETTER_AUTH_SECRET, the missing auth env vars, both
wrong DATABASE_URL defaults, the contradicting starred-org default, and
health endpoint fields the API deliberately does not return.

The in-app /docs pages are retired: a stub redirects old bookmarks to
the website, and the sidebar and 404 links point there directly. The
markdown files under docs/ stay as the versioned offline reference;
NOTIFICATIONS.md now covers Gotify and Webhook. README links the docs
site, mentions notifications, and drops stale version markers. The app
viewport meta gains initial-scale=1.

* docs: mark new-repo notification as unimplemented, bump helm appVersion to 3.24.0
2026-08-03 11:56:51 +05:30
Arunavo Ray 63a4c9359a chore: bump version to 3.24.0 v3.24.0 2026-08-03 11:29:01 +05:30
ARUNAVO RAY 882e147504 Redesign dashboard and configuration screens (#353)
* feat(ui): redesign dashboard and configuration screens

New settings design language built from the design/giteamirror.pen file:
cards with icon headers and status footers, header-level enable switches,
toggle switches instead of checkboxes, uppercase section titles, selection
tiles with icon chips and a check on the active option, segmented controls,
and an indigo accent. Implemented via shared primitives in
src/components/config/settings-ui.tsx and applied across:

- Automation: header switches, schedule card, one-line auto-mirror copy
  with info tooltip, full-width Repository Cleanup card with Skip/Archive/
  Delete tiles and dry run row
- Notifications: segmented provider picker (ntfy/Apprise/Gotify/Webhook),
  events card with per-event switches
- Connections: GitHub/Gitea connection cards with token creation guide and
  field helpers, Repository Selection and Mirror Content cards covering
  every mirror option, Organization Structure card with strategy tiles,
  destructive update protection tiles (BETA label removed)
- Authentication: sign-in methods status card, identity providers restyle
- Dashboard: flatter stat cards, icon panel headers, indigo view-all links

All existing state handling, autosave and API behavior is unchanged.
Light mode keeps working via theme tokens. README and website screenshots
regenerated, docs references to renamed cards updated.

* fix(ui): design polish pass from local review

- Recent Activity rows get status icon circles (check, sync, sparkles, alert)
- Connections tab restructured: connection cards share a stretched grid row
  so GitHub and Gitea stay equal height; Mirror Content moved to the right
  column; forms split into placeable cards via a part prop
- Token guide panel: link moved to header as icon, larger text; redundant
  card footers removed (scopes line, test-connection hint)
- Repository Selection gains a footer note; retention explanation moved
  below the selector
- Authentication tab matches the design: side-by-side cards, row dividers,
  disabled state-reflecting switches with info hints, footers; SSO dialog
  restyled (segmented protocol tabs, field labels, indigo primary)
- Import GitHub Data button is the indigo primary; disabled state is muted
- Time format menu redesigned (locale pill, live examples, live clock in
  the trigger); theme switcher moved to sidebar as icon segmented control,
  system preference now persists correctly; legacy ModeToggle removed
- Automation timezone pill no longer shows stored legacy UTC as a choice
- Config tab bar wraps 2x2 on narrow screens instead of overflowing
- README, website and PR screenshots regenerated
2026-08-03 11:27:45 +05:30
Arunavo Ray da75f10f26 chore: bump version to 3.23.0 v3.23.0 2026-08-03 08:06:31 +05:30
Arunavo Ray e7758badbf feat: add generic webhook notification provider (#352)
Adds Webhook alongside ntfy, Apprise and Gotify: posts a JSON payload
(title, message, type, timestamp) to any URL, with an optional signing
secret that adds an X-Webhook-Signature header (HMAC-SHA256 of the body,
sha256=<hex>) so receivers can verify authenticity. Secret is encrypted
at rest like the other provider tokens. Settings UI section, provider
and service tests included. No database migration needed.
2026-08-03 08:06:26 +05:30
Arunavo Ray 69198c047a chore: bump version to 3.22.0 v3.22.0 2026-08-01 10:08:47 +05:30
Arunavo Ray 4f55cb406c ci(nix): make the bun.nix drift check blocking
The check added in #350 only echoed a message (with backticks that bash
executed as command substitution, mangling it) and never failed the job,
so a stale bun.nix would still pass CI. Fail fast right after the
regenerate step instead, with a GitHub error annotation and the diffstat.
2026-08-01 10:08:31 +05:30
Jordan Dominion c7932faecb fix(nix): regenerate bun.nix from bun.lock (#350)
bun.nix (last regenerated in #298) drifted from bun.lock after the dependency updates in #348, leaving the Nix flake build broken: 254 packages missing and 195 stale entries. Regenerated with bun2nix; all 929 entries now match bun.lock exactly.

Also adds a CI step that detects drift after the regenerate step (made blocking in a follow-up commit).

Thanks @Cyberboss.
2026-08-01 10:07:51 +05:30
Arunavo Ray e40eaabf4c docs(www): fix inaccurate claims in use-case and comparison pages
- Remove references to nonexistent /api/export and /api/repos/:id/logs endpoints
- Correct storage model: mirrored repos and LFS live in Gitea, not the data/ volume
- Fix default sync interval (daily, not 1 hour) and startup log line
- Add write:organization to required Gitea token scopes
- Remove nonexistent metrics endpoint and per-repo interval claims from Helm page
- Fix rate-limit advice (limits are per account, not per IP/token)
- Refresh stale comparison content (outage dates, BackHub/Rewind acquisition)
- Add missing git clone step to comparison quick start
2026-07-29 08:26:08 +05:30
云与原 204922570d feat: add Gotify as a notification provider (#337)
Adds Gotify alongside ntfy and Apprise: new provider module posting to {url}/message with X-Gotify-Key auth, configurable default priority (errors always send at priority 8), token encrypted at rest like the other providers, settings UI section, and provider + service tests. No database migration needed.
2026-07-16 22:20:15 +05:30
Arunavo Ray 97d98b82c6 chore: bump version to 3.21.0 v3.21.0 2026-07-16 21:52:01 +05:30
ARUNAVO RAY 8b81ffa975 chore(deps): security fixes and dependency updates across app and www (#348)
better-auth 1.6.23 (fixes GHSA high stored XSS via javascript: redirect_uri), esbuild >=0.28.1 override for www (GHSA low), Astro 7 / @astrojs/node 11 / @astrojs/react 6 / @astrojs/mdx 7 / lucide-react 1.x (inline GitHub icon replaces removed brand icon) and all in-range updates on both the app and the website. The remaining @better-auth/oauth-provider medium advisory is patched upstream only in 1.7-rc and will be picked up when 1.7 stable lands.
2026-07-16 21:51:41 +05:30
Arunavo Ray bb57b52de3 test: isolate bulk-mirror destination tests in a child process
bun's mock.module and the globalThis.fetch swap are process-wide; on CI
(bun 1.3.13) this file's mocks of @/lib/db and @/lib/gitea-enhanced leaked
into gitea-enhanced.test.ts and stuck-status-recovery tests, failing main.
The file now registers nothing in the shared test process and instead
re-runs itself via bun test in a child process where the mocks are contained.
2026-07-16 21:46:08 +05:30
Arunavo Ray b3aad9d80f test: make org ids order-independent in bulk-mirror destination tests
The previous call-order counter diverged between the mocked flow and the
assertions when bun re-instantiates mock factories (green on bun 1.3.6
locally, red on 1.3.13 in CI). Ids are now a pure function of the org name.
2026-07-16 21:35:56 +05:30
Arunavo Ray 3b8625634c docs: add time format toggle screenshot 2026-07-16 21:02:21 +05:30