**Why.** Tapping a push notification bumps `NavigationState.notificationsRefreshTrigger` so the notifications tab reloads on arrival. The single-instance `NotificationsOverviewView` observes the trigger; the merged multi-instance overview never did, so multi-instance users landed on a stale list that still showed the tapped thread as unread until a manual pull-to-refresh.
**What changed.** `MergedNotificationsOverviewView` observes the same trigger with the same `onChange` the single-instance view uses, and reloads.
**Verification.** Full `ForjiTests` suite passes (218 passed / 0 failed, iPhone 17 Pro, iOS 26.5, rebased onto current main). SwiftLint and SwiftFormat clean. The push-open path needs a real APNs round trip, so I couldn't exercise it end-to-end locally; the change mirrors the single-instance wiring line for line.
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/71
The integration seeder gave no visible progress and could hang
indefinitely. A `docker compose exec` for the admin-user step wedged on
a transient Docker-on-macOS flake, and `DockerExec.run` waited on it
with no timeout, blocking the whole UI test suite forever with no output
(stdout was block-buffered, so the existing phase prints never flushed).
- Line-buffer the seeder's stdout so phase progress streams live instead
of being dumped all at once when stdout is a pipe.
- Add numbered "[n/7] <phase> (Ns elapsed)" headers for a clear progress
and timing signal.
- Add a 60s timeout to `docker compose exec` and retry the admin-user
step, so a hung exec fails fast and recovers instead of wedging the
suite.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/69
## What
Notification rows now put the repository name and timestamp on one line ("repo · time") instead of two stacked caption lines, so the list scans like Mail and fits more per screen. The unread dot drops its `glassEffect` for a plain filled circle (cleaner, and avoids GPU glass compositing on a 10pt element inside a scrolling list).
Before: title / repo / time on three lines.
After: title, then "repo · time", with the repo truncating in the middle and the timestamp fixed-width.
Verified: builds clean (Xcode 26, iPhone 17 Pro simulator), SwiftLint passes, confirmed in the simulator with preview data.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/55
## What
The Issues and Pull Requests overviews pinned a centered filter-summary caption ("Open") below the title even in the default state, which just duplicated the filter icon. Now the summary appears only when a non-default state or involvement scope is active, so the default view carries no extra chrome and the strip becomes a clear signal that the list is filtered.
Verified in the iPhone 17 Pro simulator: default state shows no strip and no gap, and the filter icon still reflects state. Builds clean on Xcode 26, SwiftLint passes.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/60
## What
The repository detail's segmented control has four segments (Code, Issues, Pull Requests, Actions). "Pull Requests" dominated the width, squeezed "Code", and truncated on narrower iPhones. Shortening it to "PRs" balances the segments and keeps them legible.
Verified in the iPhone 17 Pro simulator (segments now even); builds clean on Xcode 26, SwiftLint passes.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/59
The trailing Dismiss swipe called the same setNotificationRead path as the
leading Mark Read action, since ForgejoKit's NotificationService only exposes
markAsRead (PATCH to-status=read). Dismiss therefore did nothing distinct and
left the row visible under the All/Read filters, just relabeled.
Drop the redundant action in both NotificationsOverviewView and
MergedNotificationsOverviewView. The leading Mark Read swipe (shown for unread
threads, full-swipe by default) remains the single, honest action.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/64
logout deleted keychain credentials first, then removed the SwiftData instance
with a swallowed `try? modelContext.save()`. If the save failed, the instance
survived with its credentials gone, leaving an account that could not
authenticate. A `nil` default modelContext also let the instance removal be
skipped entirely while credentials were still wiped.
Reorder to delete + save the SwiftData instance first (matching
InstanceListView.deleteInstance) and only delete credentials once that succeeds;
bail out on save failure so the account stays usable. Make modelContext
required to remove the latent orphan path (all call sites already pass one).
Reviewed-on: https://codeberg.org/secana/Forji/pulls/65
IssueEditView and PullRequestEditView save by running several API calls in
sequence (replaceLabels, editIssue, editPullRequest, requestReviewers,
removeReviewers). Forgejo has no transaction, so if a later call fails the
earlier ones stay committed, yet the catch block only showed an error and never
fired onSaved, leaving the detail view with stale data.
On failure, re-fetch the issue/PR and push the authoritative server state via
onSaved (without dismissing, so the user can retry) before surfacing the error.
True rollback is not possible against the REST API; reflecting the real
committed state is the correct remedy.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/66
PaginationState computed hasMore as `fetched.count >= pageSize`, but merged
views return the combined, de-duplicated result of N instances, so the merged
count cleared the single-source threshold and kept hasMore true past the real
end (extra empty fetches). loadMore also appended without de-duping against
already-loaded items, so overlapping involvement queries and shifting pages
could produce duplicate TaggedItem.id entries in the ForEach.
Add an optional dedupeKey to PaginationState that de-duplicates across pages via
a running seen-set, and base hasMore on whether a page contributed new items
when dedupeKey is set. Single-source pagination keeps the page-size heuristic
unchanged. Merged Issues/PRs, Repositories, and Notifications views opt in with
dedupeKey = { $0.id }.
`deleteInstance` cleared only the keychain password, never the token (`logout` deletes both), so swipe-to-delete left a live API token orphaned; this routes both paths through a new `KeychainManager.deleteCredentials` and adds regression tests (186 ForjiTests green).
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/40
Adding the same server and username twice gives two connections one `sourceKey`, and `rehydrate`'s `Dictionary(uniqueKeysWithValues:)` traps on it, crashing every merged overview on construction; this keeps the first source and adds a regression test (185 ForjiTests green).
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/39
Fixes#32. Opening a notification now marks its thread read (via the detail's `.onAppear`) so the tab and icon badges clear; swipe actions unchanged. Adds a UI regression test covering open-to-clear.
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
## Summary
This changes the multi-instance fallback map to key bootstrapped connections by account instead of server URL only.
Previously, `connect(instances:)` built a dictionary keyed only by `instance.serverURL`. If two accounts used the same Forgejo instance, `Dictionary(uniqueKeysWithValues:)` could trap on duplicate keys, and fallback lookup could also reuse the wrong account for a failed restore.
## Changes
- Add an account key based on normalized server URL + username.
- Use that key for bootstrapped connection fallback in `MultiInstanceManager`.
- Add a regression test covering two token-auth accounts on the same server URL.
## Verification
- `git diff --check` passes.
- I could not run `xcodebuild` locally because the available Xcode install has not accepted the license on this machine (`xcodebuild` exits before build/test execution).
Co-authored-by: Piotr Durlej <pdurlej@users.noreply.github.com>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/29
Reviewed-by: secana <secana@noreply.codeberg.org>
Closes#3.
Adds an "Actions" tab to the repository view, surfacing Forgejo Actions runs and (experimentally) their jobs, steps, and logs.
## What's in this branch
1. ~~**`chore: point ForgejoKit at local checkout for Actions PR`** — temporary `XCLocalSwiftPackageReference` so the feature commit compiles against the unreleased ForgejoKit changes. Drop this commit before merge and replace with `chore: bump ForgejoKit to <new version>` once the ForgejoKit PR ships in a release.~~
**`chore: update ForgejoKit to released version 0.4.0`** + **`chore: add ForgejoKit 0.4.0 to Package.resolved`** — switches from local path override to a remote pin at `secana/ForgejoKit` `0.4.0`.
2. **`feat: add Actions tab to repository view`** — the actual feature.
## UI scope
- New `Actions` tab, shown only when `repository.hasActions == true`.
- Runs list with All / Running / Success / Failed filter, pagination, pull-to-refresh, empty state.
- Run detail with header, metadata (workflow file, event, trigger user, started/duration — zero-Date suppressed), and an experimental Jobs section.
- Job view with collapsible step rows; logs lazy-load on expand via `logCursors`.
- "Open in browser" toolbar item using each run's `html_url`.
## Experimental jobs/steps view
Forgejo's `/api/v1` doesn't expose jobs, steps, or step logs for a run. This PR opts into ForgejoKit's experimental `fetchRunView` (backed by Forgejo's web-UI route) so we can render a GitHub-Actions-style view today. The Jobs section and Steps screen are explicitly labelled "Experimental" in the UI, and a footer notes the output may change between Forgejo versions. Happy to drop this section if you'd rather ship public-API only first.
## Defensive handling
- Forgejo serialises an unset `time.Time` as `0001-01-01T00:00:00Z` (showed up as "Started 56 yrs, 4 mths" / "Duration 493 906h" on a cancelled run). Sanitised display helpers suppress any pre-2000 date.
- The experimental `fetchRunView` resolves Forgejo's `RedirectToLatestAttempt` server-side before POSTing, so attempts with non-zero attempt numbers work. (Without this, Forgejo returns 500 "task with job_id N and attempt 0: resource does not exist".)
## Tests
- Unit: `WorkflowRunFilterTests`, `WorkflowStatusIconTests` (filter mapping, status enum compatibility against Forgejo's documented values, status icon mapping, run helpers, zero-date guard).
- UI: `ActionsUITests` (read-only smoke test).
Co-authored-by: Voislav Vasiljevski <voislav@voioo.cz>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/28
Reviewed-on: https://codeberg.org/secana/Forji/pulls/24
Co-authored-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Co-committed-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Naive implementation for iOS notifications.
Problem: Forgejo does not support push notifications. We need to pull every X minutes for new notifications. The even bigger problem: iOS does not support background polling. So this is more a "as good as possible" but not good approach.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/23
Co-authored-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Co-committed-by: Stefan Hausotte <stefan.hausotte@gmx.de>