Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dab119e
rtbit-fluent: Files tab as a folder Tree with include/skip priority (…
thedancingdeveloper Nov 2, 2026
5050349
rtbit-ui-core: drop last egui reference from error doc comment (WI-2770)
thedancingdeveloper Nov 2, 2026
c3bf6dc
Revert "rtbit-fluent: Files tab as a folder Tree with include/skip pr…
thedancingdeveloper Nov 2, 2026
ed955e2
rtbit-fluent: Files tab as a folder Tree with include/skip priority (…
thedancingdeveloper Nov 2, 2026
0e4432e
test(swarmforge): router-level tests for POST /torrents/{id}/files/{i…
thedancingdeveloper Nov 2, 2026
dcd04c0
qbit: real app/preferences share limits and dht/pex/lsd; apply on set…
thedancingdeveloper Nov 2, 2026
95629be
ci: report nightly release dry-run failures (WI-2898)
thedancingdeveloper Nov 2, 2026
b76ed4f
rtbit-fluent: main-screen keyboard shortcuts (WI-2869)
thedancingdeveloper Nov 2, 2026
885e749
ci: enforce cargo-deny dependency policy
thedancingdeveloper Nov 2, 2026
a5b312b
Add torrent table keyboard navigation
thedancingdeveloper Nov 2, 2026
ec86914
rtbit-fluent: add detail pane empty states
thedancingdeveloper Nov 2, 2026
ff25cec
Improve toolbar accessibility and focus visibility
thedancingdeveloper Nov 2, 2026
76b1b5f
rtbit-fluent: dialogs close on Escape and confirm on Enter (WI-2871)
thedancingdeveloper Nov 2, 2026
6dd9174
rtbit-fluent: daemon-lost banner with 1s-30s backoff reconnect (WI-2874)
thedancingdeveloper Nov 2, 2026
5b67159
rtbit-fluent: tests for detail pane tabs (WI-2875)
thedancingdeveloper Nov 2, 2026
c1b96ba
rtbit-fluent: tests for status bar, shell navigation and bridge error…
thedancingdeveloper Nov 2, 2026
58ba03f
qbit API: app/getFreeSpaceAtPath and real free_space_on_disk in sync/…
thedancingdeveloper Nov 2, 2026
88404ad
rtbit-fluent: right-click context menu on torrent rows (WI-2878)
thedancingdeveloper Nov 2, 2026
d2a075f
qbit API: add torrents/pieceAvailability (WI-2886)
thedancingdeveloper Nov 2, 2026
1ce86c8
rtbit-fluent: persist table sort and column widths; add Up column (WI…
thedancingdeveloper Nov 2, 2026
6dddab9
docs: audit Fluent client parity (WI-2877)
thedancingdeveloper Nov 2, 2026
fcebfe6
feat(rtbit-fluent): open add dialog from torrent drops
thedancingdeveloper Nov 2, 2026
f397f9e
rtbit-fluent: add RSS rule priority selector (#287)
thedancingdeveloper Nov 2, 2026
eed2f28
rtbit-fluent: add trackers from torrent detail (#286)
thedancingdeveloper Nov 2, 2026
fbdf09f
Add per-category share limits (WI-2889)
thedancingdeveloper Nov 2, 2026
fc6c4d0
Report real qbit API build information
thedancingdeveloper Nov 2, 2026
882d4e4
Resolve metadata test merge
thedancingdeveloper Nov 2, 2026
0dfca3b
CI: check generated parity summary drift
thedancingdeveloper Nov 2, 2026
786e26d
ci: test fluent app for client dependency changes
thedancingdeveloper Nov 2, 2026
ec0ee45
Add Radarr and Lidarr replay fixtures (WI-2896)
thedancingdeveloper Nov 2, 2026
718cba2
Add missing payload diagnosis
thedancingdeveloper Nov 2, 2026
7a92547
Add bulk category and tag editing
thedancingdeveloper Nov 2, 2026
818463f
Add real per-file priority levels
thedancingdeveloper Nov 2, 2026
9edbde4
Release: smoke-test fluent artifacts before publishing
thedancingdeveloper Nov 2, 2026
9509ec3
CI: smoke test daemon container in merge queue
thedancingdeveloper Nov 2, 2026
4743e49
Add fluent per-file priority controls
thedancingdeveloper Nov 2, 2026
6b5d459
librtbit: crash-safe atomic state writes; quarantine corrupt state files
thedancingdeveloper Nov 2, 2026
e4f2962
Low-disk guard: default 1 GiB floor and 'low disk space' status error
thedancingdeveloper Nov 2, 2026
d97570a
Fixes #290: bound graceful shutdown save
thedancingdeveloper Nov 2, 2026
0feda53
fix: add tracker and DHT retry backoff (Fixes #299)
thedancingdeveloper Nov 2, 2026
fea0e7b
rtbit-fluent: add filter sidebar (Fixes #297)
thedancingdeveloper Nov 2, 2026
c616c39
rtbit-fluent: add status bar speed controls (Fixes #296)
thedancingdeveloper Nov 2, 2026
bd60b95
fix(fluent-1): restore build after concurrent lands
thedancingdeveloper Nov 2, 2026
b80349b
Fixes #291: add torrent metadata and management controls
thedancingdeveloper Nov 2, 2026
8cac4f0
Add torrent location and rename dialogs; Fixes #298
thedancingdeveloper Nov 2, 2026
e1b0ad1
Implement qbit app network and shutdown endpoints (Fixes #293)
thedancingdeveloper Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
rust: ${{ steps.classify.outputs.rust }}
rust_full: ${{ steps.classify.outputs.rust_full }}
rust_packages: ${{ steps.classify.outputs.rust_packages }}
rust_fluent: ${{ steps.classify.outputs.rust_fluent }}
desktop: ${{ steps.classify.outputs.desktop }}
webui: ${{ steps.classify.outputs.webui }}
website: ${{ steps.classify.outputs.website }}
Expand All @@ -46,11 +47,11 @@ jobs:
run: |
set -euo pipefail
if [[ "$EVENT" != pull_request ]]; then
printf 'rust=true\nrust_full=true\nrust_packages=\ndesktop=true\nwebui=true\nwebsite=true\n' >>"$GITHUB_OUTPUT"
printf 'rust=true\nrust_full=true\nrust_packages=\nrust_fluent=true\ndesktop=true\nwebui=true\nwebsite=true\n' >>"$GITHUB_OUTPUT"
exit 0
fi
files="$(git diff --name-only "$(git merge-base "$BASE_SHA" HEAD)" HEAD)"
rust=false full=false desktop=false webui=false website=false
rust=false full=false fluent=false desktop=false webui=false website=false
packages=()
add_package() {
local pkg
Expand Down Expand Up @@ -80,8 +81,15 @@ jobs:
# The menu description has no window toolkit, so CI can test it.
# The Tauri binary stays excluded: the runner has no webkit.
desktop/menu/*) rust=true desktop=true; add_package rtbit-desktop-menu ;;
# The native app consumes the daemon's API types directly.
crates/librtbit/src/http_api_types.rs)
rust=true; fluent=true ;;
crates/librtbit/*) rust=true desktop=true; add_package swarmforge ;;
crates/rtbit/*) rust=true; add_package rtbit ;;
# The native client and UI crates are reverse dependencies of the
# fluent app, so changes to either must test the app as well.
crates/rtbit-client/*|crates/rtbit-ui-core/*)
rust=true; fluent=true ;;
website/*) website=true ;;
# An unrecognised crate path (a new member, or a path we have not
# mapped yet) is not safe to narrow, so it takes everything.
Expand All @@ -90,11 +98,11 @@ jobs:
done <<<"$files"
# A Rust change we could not attribute to a package still needs the
# full workspace rather than no check at all.
if [[ "$rust" == true && "$full" == false && ${#packages[@]} -eq 0 ]]; then
if [[ "$rust" == true && "$full" == false && "$fluent" == false && ${#packages[@]} -eq 0 ]]; then
full=true
fi
printf 'rust=%s\nrust_full=%s\nrust_packages=%s\ndesktop=%s\nwebui=%s\nwebsite=%s\n' \
"$rust" "$full" "${packages[*]-}" "$desktop" "$webui" "$website" | tee -a "$GITHUB_OUTPUT"
printf 'rust=%s\nrust_full=%s\nrust_packages=%s\nrust_fluent=%s\ndesktop=%s\nwebui=%s\nwebsite=%s\n' \
"$rust" "$full" "${packages[*]-}" "$fluent" "$desktop" "$webui" "$website" | tee -a "$GITHUB_OUTPUT"

# Shell checks for the release scripts and repository layout. They take
# seconds and touch no network, so they run on every event.
Expand Down Expand Up @@ -181,10 +189,14 @@ jobs:
- name: Test
env:
SCOPE_ARGS: ${{ steps.scope.outputs.args }}
RUST_FLUENT: ${{ needs.changes.outputs.rust_fluent }}
CARGO_PROFILE_TEST_DEBUG: 0
run: |
read -r -a args <<<"$SCOPE_ARGS"
cargo nextest run "${args[@]}" --all-targets --locked --no-default-features --features default-tls
if [[ "$RUST_FLUENT" == true ]]; then
cargo nextest run -p rtbit-fluent --all-targets --locked
fi
# The shipped binary is worth a release-profile compile on a PR that
# touches it. The queue builds it for every entry anyway.
- name: Build the release binary
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches: [main, dev]
tags: ['v*']
workflow_dispatch:
merge_group:
types: [checks_requested]

permissions:
contents: read
Expand All @@ -16,6 +18,7 @@ concurrency:

jobs:
publish:
if: github.event_name != 'merge_group'
# Privileged publication capacity is selected only for this repository and
# this workflow deliberately has no pull_request trigger.
runs-on: [self-hosted, linux, docker, publish]
Expand Down Expand Up @@ -80,3 +83,61 @@ jobs:
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"https://ghcr.io/v2/thedancingdeveloper-org/rusttorrent/manifests/$tag" > manifest.json
jq -e '.schemaVersion == 2' manifest.json >/dev/null

smoke:
if: github.event_name == 'merge_group'
runs-on: [self-hosted, linux, docker, publish]
permissions:
contents: read
env:
IMAGE: rusttorrent-container-smoke:${{ github.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Build the daemon image
run: docker build --tag "$IMAGE" --file Dockerfile .
- name: Start and probe the daemon
shell: bash
run: |
set -euo pipefail
volume="rusttorrent-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
container="rusttorrent-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
cleanup() {
docker rm --force "$container" >/dev/null 2>&1 || true
docker volume rm "$volume" >/dev/null 2>&1 || true
docker image rm "$IMAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT

docker volume create "$volume" >/dev/null
docker run --detach --name "$container" --volume "$volume:/home/rtbit/db" \
--publish 3030:3030 "$IMAGE" >/dev/null

deadline=$((SECONDS + 30))
setup_token=''
while (( SECONDS < deadline )); do
if ! setup_token=$(docker exec "$container" cat /home/rtbit/db/session/setup-token 2>/dev/null); then
setup_token=''
fi
if [[ -n "$setup_token" ]] && curl --fail --silent --show-error \
--output /dev/null http://127.0.0.1:3030/healthz; then
break
fi
sleep 1
done
[[ -n "$setup_token" ]]
(( SECONDS < deadline ))

curl --fail --silent --show-error \
--header 'Content-Type: application/json' \
--data "{\"username\":\"smoke\",\"password\":\"smoke-password\",\"setup_token\":\"$setup_token\"}" \
http://127.0.0.1:3030/api/v2/auth/setup >/dev/null
version=$(curl --fail --silent --show-error \
--user smoke:smoke-password http://127.0.0.1:3030/api/v2/app/webapiVersion)
[[ "$version" == '2.11.3' ]]
curl --fail --silent --show-error \
http://127.0.0.1:3030/healthz >/dev/null

docker stop --time 30 "$container" >/dev/null
[[ "$(docker inspect --format '{{.State.ExitCode}}' "$container")" == 0 ]]
docker run --rm --volume "$volume:/data" --entrypoint sh "$IMAGE" \
-c 'test -s /data/session/session.json'
19 changes: 19 additions & 0 deletions .github/workflows/policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,22 @@ jobs:
- name: Verify migration policies
shell: bash
run: bash ci/migration-policy.sh
- name: Detect dependency policy changes
id: dependency-policy
if: github.event_name == 'pull_request'
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if git diff --quiet "$BASE_SHA" HEAD -- Cargo.lock ':(glob)**/Cargo.toml' deny.toml; then
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29
if: github.event_name != 'pull_request' || steps.dependency-policy.outputs.run == 'true'
with:
tool: cargo-deny@0.20.2
- name: Verify Rust dependency policy
if: github.event_name != 'pull_request' || steps.dependency-policy.outputs.run == 'true'
run: cargo deny check advisories licenses bans sources
62 changes: 62 additions & 0 deletions .github/workflows/release-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ jobs:
cargo build -p rtbit-fluent --release --target x86_64-pc-windows-msvc
rm -rf target/desktop-stage
bash packaging/scripts/build-desktop.sh windows "$tag" target/desktop-stage
- name: Smoke-test Windows fluent artifact
shell: bash
run: |
set -euo pipefail
version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/rtbit/Cargo.toml | head -1)"
tag="v$version"
expected="rtbit-fluent $version"
actual="$(target/desktop-stage/rtbit-fluent-"$tag"-windows-x86_64.exe --version)"
test "$actual" = "$expected"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-dry-run-windows-fluent-${{ github.sha }}
Expand Down Expand Up @@ -157,6 +166,14 @@ jobs:
rm -rf target/desktop-stage
bash packaging/scripts/build-desktop.sh linux "$tag" target/desktop-stage
bash packaging/scripts/build-desktop.sh sbom "$tag" target/desktop-stage
- name: Smoke-test Linux fluent artifact
shell: bash
run: |
set -euo pipefail
tag="${{ steps.tag.outputs.tag }}"
expected="rtbit-fluent ${tag#v}"
actual="$(target/desktop-stage/rtbit-fluent-"$tag"-linux-x86_64 --version)"
test "$actual" = "$expected"
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
continue-on-error: true
with:
Expand Down Expand Up @@ -278,3 +295,48 @@ jobs:
done
test "$count" -eq 2 || { echo "::error::expected 2 signed executables (daemon and fluent), verified $count"; exit 1; }
(cd release-artifacts && sha256sum -c "SHA256SUMS-$tag.txt")

report-failure:
name: Report release dry-run failure
if: >-
${{ always() && github.event_name != 'pull_request' &&
(needs.build-windows-fluent.result == 'failure' || needs.dry-run.result == 'failure') }}
needs: [build-windows-fluent, dry-run]
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Open or update the tracking issue
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const title = '[CI] Release dry run is failing';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const report = [
`The ${context.eventName} release dry run failed.`,
'',
`Run: ${runUrl}`,
`Commit: ${context.sha}`,
'',
'This workflow does not publish release artifacts.',
].join('\n');
const {data: issues} = await github.rest.issues.listForRepo({
...context.repo,
state: 'open',
per_page: 100,
});
const existing = issues.find(issue => !issue.pull_request && issue.title === title);
if (existing) {
await github.rest.issues.createComment({
...context.repo,
issue_number: existing.number,
body: report,
});
} else {
await github.rest.issues.create({
...context.repo,
title,
body: report,
});
}
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ jobs:
rm -rf target/desktop-stage
bash packaging/scripts/build-desktop.sh linux "$tag" target/desktop-stage
bash packaging/scripts/build-desktop.sh sbom "$tag" target/desktop-stage
- name: Smoke-test Linux fluent artifact
shell: bash
run: |
set -euo pipefail
tag="${{ inputs.tag || github.ref_name }}"
expected="rtbit-fluent ${tag#v}"
actual="$(target/desktop-stage/rtbit-fluent-"$tag"-linux-x86_64 --version)"
test "$actual" = "$expected"
- name: Stage and checksum artifacts
shell: bash
run: |
Expand Down Expand Up @@ -178,6 +186,14 @@ jobs:
cargo build -p rtbit-fluent --release --target x86_64-pc-windows-msvc
rm -rf target/desktop-stage
bash packaging/scripts/build-desktop.sh windows "$tag" target/desktop-stage
- name: Smoke-test Windows fluent artifact
shell: bash
run: |
set -euo pipefail
tag="${{ inputs.tag || github.ref_name }}"
expected="rtbit-fluent ${tag#v}"
actual="$(target/desktop-stage/rtbit-fluent-"$tag"-windows-x86_64.exe --version)"
test "$actual" = "$expected"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-artifacts-windows-fluent
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 21 additions & 27 deletions crates/librtbit-dht/src/dht.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1114,38 +1114,32 @@ impl DhtWorker {
v4.or(v6)
}

async fn bootstrap_hostname_with_backoff(&self, addr: &str) -> crate::Result<()> {
async fn bootstrap(&self, bootstrap_addrs: &[String]) -> crate::Result<()> {
let backoff = ExponentialBuilder::new()
.with_max_delay(Duration::from_secs(60))
.with_max_delay(Duration::from_secs(60 * 60))
.with_jitter()
.with_total_delay(Some(Duration::from_secs(86400)))
.without_max_times();

(|| self.bootstrap_hostname(addr))
.retry(backoff)
.notify(|error, retry_in| {
warn!(?retry_in, ?addr, "error in bootstrap: {error:#}");
})
.instrument(debug_span!("bootstrap", hostname = addr))
.await
}

async fn bootstrap(&self, bootstrap_addrs: &[String]) -> crate::Result<()> {
let mut futs = FuturesUnordered::new();

for addr in bootstrap_addrs.iter() {
futs.push(self.bootstrap_hostname_with_backoff(addr));
}
let mut successes = 0;
while let Some(resp) = futs.next().await {
if resp.is_ok() {
successes += 1
(|| async {
let mut futs = FuturesUnordered::new();
for addr in bootstrap_addrs {
futs.push(
self.bootstrap_hostname(addr)
.instrument(debug_span!("bootstrap", hostname = addr.as_str())),
);
}
}
if successes == 0 {
return Err(Error::BootstrapFailed);
}
Ok(())
while let Some(resp) = futs.next().await {
if resp.is_ok() {
return Ok(());
}
}
Err(Error::BootstrapFailed)
})
.retry(backoff)
.notify(|error, retry_in| {
warn!(?retry_in, "all DHT bootstrap nodes failed: {error:#}");
})
.await
}

async fn bucket_refresher(&self, is_v4: bool) -> crate::Result<()> {
Expand Down
Loading
Loading