Skip to content

OmaBlock 1.5.0: full-stack fixes, tests and release notes - #1

Merged
ozdil merged 16 commits into
mainfrom
claude/fullstack-fixes-s2vzcs
Oct 16, 2026
Merged

ozdil merged 16 commits into
mainfrom
claude/fullstack-fixes-s2vzcs

Conversation

@ozdil

@ozdil ozdil commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Requested by ozan · project thread

Description

Fixes from a full-stack review. The bugs that were visible to users:

  • Every apply appended another copy of the nft rules.
  • Turning kernel enforcement off left the table in place.
  • Kernel enforcement and panic blackout never worked, because the unprivileged engine ran nft itself.
  • Removing the package left the hosts block, the nft table and the browser policies behind.
  • Packaged installs could not load the bar widget because BoundedTextInput.qml was missing.
  • Quick clicks in the panel silently dropped actions. Concurrent engine runs also lost config changes.
  • cargo test and the Python tests could touch the real /etc and config files.
  • The Ultimate level ignored the category and DoH toggles, and the DoH toggle had no effect.
  • A pause only ended when the panel happened to poll status.
  • The builtin list blocked reserved names (local, localhost.localdomain).

Privileged nft and locking

  • The helper owns every nft change, including the new --blackout-on and --blackout-off.
  • The helper publishes /run/omablock/kernel_state.json, which the engine reads for status.
  • A single run_privileged_helper() is used. sudo gets 30 s and pkexec 90 s, so a slow but authorised run is not repeated with a second password prompt.
  • Engine commands lock engine.lock with flock. --update reloads the config under that lock before its final save. With 12 parallel adds, 1 entry survived before and all 12 survive now.
  • Helper runs are serialised with flock, and /etc/hosts keeps its mode and owner.

Hosts helper (omablock-hosts-sync)

  • The nft table is replaced atomically and cleared when --with-kernel is absent.
  • An unterminated marker block keeps the user entries that follow it.
  • Non-UTF-8 bytes are preserved.
  • fsync runs around the atomic replace.
  • --clear always removes the browser DoH policies.
  • The rules file is checked with fstat on the opened fd.
  • Inner underscores are allowed; .localdomain, .test and .invalid are rejected.
  • New --restore-backup restores /etc/hosts.omablock.bak and keeps the current file as /etc/hosts.omablock.pre-restore.

Engine

  • The binary now uses the library crate instead of compiling a second copy of every module and of validate_domain.
  • active_rules reports the entries actually present in /etc/hosts.
  • A corrupt config.json is moved to config.json.corrupt-<ts> rather than overwritten. An unreadable one is left untouched.
  • The home directory comes from $HOME or the passwd entry; there is no /home/ozdil fallback.
  • Blocklist downloads are capped with curl --max-filesize.
  • DGA classifier:
    • Decodes punycode. Plain IDNs such as münchen.de and яндекс.рф are no longer flagged.
    • Mixed-script, whole-script and brand look-alike spoofs are still flagged.
    • Brand first-party domains (githubusercontent.com, googleapis.com) and compound suffixes such as co.uk no longer produce false positives.
  • Panic blackout:
    • Matches oifname, so the loopback DNS stub stays reachable.
    • Uses its own omablock_blackout table.
  • Exit codes: 1 when a change was saved but the privileged sync failed, 2 for an invalid domain, category or option, 4 when the system helper is not installed. Without the helper the engine no longer runs pkexec, so there is no password prompt that can only fail.

Level semantics and new commands

  • Ultimate filters categories like Aggressive and respects the DoH toggle. Leaving Ultimate resets DoH and kernel enforcement.
  • doh_prevention defaults to false, and a whitelisted DoH resolver stays reachable.
  • With AI protection on, whitelisting a look-alike domain prints a warning, sends a notification and sets the IDN flag. The flag stays on only while that domain is still whitelisted.
  • verify-integrity compares the binary with the hash in the install manifest: OK (0), MISMATCH (1), UNKNOWN (3).
  • --pause schedules a systemd-run --user timer that runs --expire-pause. The status-time expiry stays as a fallback.
  • --restore-hosts runs the helper's restore, then turns protection off so the next action does not re-apply the block.
  • Rule parsing moved to src/rules.rs and validates every domain, so a tampered cache cannot inject hosts syntax.

QML

  • qml/EngineClient.qml holds status polling and the action queue for both Panel and MainWindow, and reports exit codes.
  • Toasts show the real update and test result, and a rejected or failed action, or a missing system helper, is reported.
  • MainWindow parses the test result and counts down a pause.
  • hoverEnabled is set on 7 buttons.
  • BoundedTextInput (a single copy in qml/, which Panel now imports):
    • emits textEdited() only where TextEdit provides it (Qt 6.9+), since declaring it there is an invalid override;
    • no longer inserts Tab characters;
    • counts the paste limit in UTF-8 bytes;
    • shows a toast in Panel when a paste is refused.

Packaging and scripts

  • PKGBUILD:
    • adds a pre_remove hook (--clear, --blackout-off);
    • adds the nftables optdepend;
    • adds a prepare() that runs cargo fetch, so build() uses --frozen.
  • The dashboard finds the plugin's qml/ directory.
  • build.sh checks ownership before compiling.
  • uninstall.sh disables the shield first and keeps the manifest when it preserved files.

Chrome extension 1.0.5

  • Whitelisting a site also adds a DNR allowAllRequests rule. The popup and content script share one matcher.
  • The badge shows ON/OFF per tab.
  • Cosmetic CSS is injected before the storage read, so ads no longer flash in.
  • The tabs permission is dropped.
  • The popup, name and description are localized (_locales/en, _locales/tr).
  • extensions/build-chrome-zip.sh builds the store package without the store images and without the key field. The stale 1.0.0 zip is removed.
  • The permission docs match the manifest.

Docs

  • README and README.tr: hosts filtering is described as resolver (NSS) based rather than kernel level, the nft tables are described as they are, Polkit asks once (auth_admin_keep), output caps are 16 to 32 KiB, and the zeroize claim is removed. New commands and exit codes are documented.
  • USB armor is documented as experimental; it only stores a flag.

CI

  • New ci.yml runs:
    • clippy with -D warnings;
    • cargo test;
    • the Python tests, including tests/test_qml_runtime.py, which loads EngineClient and MainWindow under PySide6 with Quickshell stand-ins (tests/qml/quickshell_stub.py);
    • shell syntax checks;
    • rules.json and version consistency checks.
  • CodeQL now also scans python and rust.

Type of Change

  • Bug fix
  • New feature
  • Code style / Refactor
  • Documentation update

Verification Checklist

  • Conforms to Zero-Trust and HANCORE guidelines
  • JetBrainsMono Nerd Font typography preserved
  • Tested locally on Omarchy Linux

Tests run

  • cargo test and cargo clippy --all-targets -D warnings: clean. The simulation tests now use the real rule parser, and a new test checks that every builtin rule is a valid domain.
  • Python tests pass 28/28. They cover the full build and uninstall lifecycle, the hosts restore and the QML runtime: action order, exit codes, refresh coalescing and status mapping.
  • The engine was run end to end with a temporary HOME. This covered level transitions, DoH whitelisting in the generated hosts list, the IDN flag, verify-integrity OK and MISMATCH, and exit codes 2 and 4.
  • The extension was loaded in headless Chromium in English and Turkish:
    • a tracker script is blocked with ERR_BLOCKED_BY_CLIENT;
    • whitelisting the site lifts the block and adds the allowAllRequests rule;
    • the badge switches from ON to OFF, and other sites stay blocked;
    • the popup is localized.
  • Both nft scripts were applied twice in an isolated netns, with no rule accumulation.
  • Blackout on/off was checked end to end through the state file.

Not tested: Panel.qml, which needs Omarchy's qs.* modules; the real Quickshell runtime; the sudo/polkit flow and the systemd pause timer on Omarchy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q

RetriggerView in GreptileConfidence Score: 2/5

Fix startup locking, unreadable configuration recovery, and blackout cleanup before merging.

Fix All in Claude CodeFindings

  1. P1 Startup reapplies old settings ▶
  2. P1 Unreadable lists get overwritten ▶
  3. P1 Blackout survives uninstall ▶
  4. P2 Active rules look absent ▶
  5. P2 Remaining look-alikes lose their warning ▶
  6. P2 Saved whitelist and blocking disagree ▶
  7. P2 Security CI runs movable action code ▶
Fix with agent prompt
### Issue 1
src/main.rs:1117-1120
`--startup` skips `engine.lock` but still applies the configuration loaded at process start. If a user disables protection while startup is loading rules, startup can apply its older enabled configuration afterward. Hosts rules then remain active while the saved setting says protection is off.

`--startup` and `--update` also run pause-expiry saves without the lock. Lock and reload before these writes and privileged changes; release the lock during downloads.

### Issue 2
src/main.rs:467-474
The unreadable-file branch returns defaults, but callers still save those defaults over `config.json`. If a user-owned file exceeds 1 MiB or contains invalid UTF-8, it fails the read but passes the write checks. Running `--enable` then replaces the original whitelist and blacklist without a backup.

Return a load error that stops mutations, rather than a writable default configuration.

### Issue 3
uninstall.sh:14
`uninstall.sh` only runs `--disable`, which calls the helper's `--clear`. That removes `omablock`, not the newly separate `omablock_blackout` table. Uninstalling while blackout is active leaves external DNS blocked after the user executables are removed.

Run `--resume-network` before deleting them, as the package removal hook already does through `--blackout-off`.

### Issue 4
src/rules.rs:44
`count_managed_hosts_rules` stops at the first line containing invalid UTF-8. The helper now deliberately preserves those bytes. If an existing comment contains bytes such as `caf\xe9` before the managed block, status reports zero active rules even after a successful apply.

Read hosts lines as bytes, or decode them without stopping, before matching the ASCII markers.

### Issue 5
src/main.rs:565-566
Each flagged whitelist addition replaces `idn_detected.state` with one domain. If two look-alike domains are added and the last one is removed, removal deletes the flag even though the first remains whitelisted.

Track all flagged entries, or compute the alert from the remaining whitelist, so removing one domain does not hide the others.

### Issue 6
extensions/chrome/popup.js:81-82
The popup saves `omablock_whitelist` before updating the browser's allow rules. If the popup closes during those awaits, or the rule update fails, the saved list and network rules stay different. The background listener only refreshes badges, including after restart.

Move rule updates into the background listener, with retry or startup repair, so a closed popup cannot leave a site showing OFF while requests remain blocked.

### Issue 7
.github/workflows/ci.yml:21
The new workflow executes `actions/checkout@v4` and `dtolnay/rust-toolchain@stable` through movable references. If either reference is repointed, CI runs changed code without a repository change. Read-only permissions limit the damage but do not prevent this.

Pin both actions to full commit hashes so updates are reviewed explicitly.

**How this was verified:** Both action references execute in the test job and neither identifies an immutable commit.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This release moves privileged changes into the helper and adds shared QML action queues, browser whitelist rules, safer hosts writes, and broader tests.

  • The privileged helper applies system rules and reports their state.
  • The engine applies validated rules and gives callers clearer control results.
  • The domain classifier recognizes ordinary international domains without losing spoof detection.
  • Panel actions run in order and show their actual results.
  • Chrome whitelists now update network rules and show status per tab.
  • OmaBlock installs, updates, and removes with the right cleanup steps.
  • Release notes and automated checks now describe OmaBlock 1.5.0.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  UI["Panel / MainWindow"] --> Queue["EngineClient action queue"]
  Queue --> Engine["Rust engine"]
  Timer["Pause timer"] --> Engine
  Engine --> Config["User configuration"]
  Engine --> Auth["sudo / pkexec"]
  Auth --> Helper["Privileged helper"]
  Helper --> Hosts["Hosts file and browser policies"]
  Helper --> NFT["Enforcement and blackout tables"]
  Helper --> State["Published kernel state"]
  State --> Engine
Loading

Reviews (1) · Last reviewed commit: "Release 1.5.0: changelog and what's new ..." · Reviewed by Greptile

…kaging

- hosts-sync: replace the nft table atomically instead of appending rules on
  every apply, clear it when kernel enforcement is off, keep user entries after
  an unterminated marker block, preserve non-UTF-8 bytes, fsync before/after
  the atomic replace, always remove browser DoH policies on --clear, fstat the
  opened rules file, accept inner underscores like the engine does
- engine: blackout uses oifname (loopback DNS stub stays reachable) in its own
  table, resolve the nft binary path, pure pause-expiry helper so tests no
  longer write real config or run the privileged sync, normalise trailing dots
  on removal, non-zero exit on invalid domain/category and failed update,
  declare rust-version 1.88
- QML: queue engine actions (Quickshell ignored overlapping commands), re-run a
  status refresh requested while one was in flight, real update/test toasts,
  parsed test result and pause countdown in MainWindow, hoverEnabled on hover
  buttons, declare textEdited() and stop Tab insertion in BoundedTextInput
- packaging: install BoundedTextInput.qml, pre_remove hook clears hosts/nft/
  policies, nftables optdepend, dashboard finds the plugin's qml dir,
  command -v instead of which
- build.sh runs ownership checks before compiling; uninstall.sh disables the
  shield first and keeps the manifest when files were preserved
- extension 1.0.4: whitelisting a site adds a DNR allowAllRequests rule,
  popup uses the same suffix match as content.js, version badge from the
  manifest, observer only on the two first-party sites and rAF-throttled,
  permission docs match the manifest
- tests: sandbox the helper tests so they never touch /etc or run nft/resolvectl,
  skip the mode-000 case as root, add regression tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
@ozdil ozdil self-assigned this Oct 16, 2026
- Kernel enforcement and panic blackout status was always false because the
  unprivileged engine ran `nft` without CAP_NET_ADMIN. The helper now owns
  every nft change (new --blackout-on/--blackout-off), feeds scripts on stdin
  and publishes /run/omablock/kernel_state.json, which the engine reads for
  --json. Blackout/resume go through sudo -n then pkexec like hosts sync.
- One run_privileged_helper() replaces the duplicated sudo/pkexec code, with
  deadlines long enough for a large apply and a password prompt, so a slow but
  authorised sudo run is no longer re-run through pkexec.
- Mutating commands take an flock on engine.lock; --update reloads the config
  under the lock before its final save, so concurrent panel actions are not
  lost (12 parallel --whitelist-add: 1 survived before, 12 now). Status polls
  stay lock-free and only lock when they have to expire a pause.
- pre_remove also clears the blackout table.
- CI workflow: clippy -D warnings, cargo test, Python tests, shell syntax,
  rules.json and version consistency. CodeQL now also scans python and rust.
- Installer lifecycle test finds a rustup cargo under its temp HOME.
- Fix the two existing clippy warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
@ozdil ozdil changed the title fix: kernel rules, hosts sync safety, UI action queue and packaging fix: kernel rules, hosts sync safety, privileged nft, config lock, UI queue, CI Oct 16, 2026
…nfig and DGA

- main.rs uses the library crate instead of compiling a second copy of every
  module and of validate_domain
- active_rules reports the sinkhole entries actually in /etc/hosts
- an invalid config.json is moved to config.json.corrupt-<ts> instead of being
  overwritten; an unreadable one is left untouched
- home directory comes from $HOME or the passwd entry, no hard-coded fallback
- blocklist downloads are capped with curl --max-filesize
- DGA classifier: punycode decoding, plain IDNs (muenchen.de, yandex.rf) are no
  longer phishing, whole-script and mixed-script look-alikes still are; brand
  first-party domains (githubusercontent.com, googleapis.com, ...) and
  compound suffixes (co.uk) no longer produce false positives

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
… zip script

- badge shows ON/OFF per tab (OFF on whitelisted sites) and refreshes when the
  whitelist changes
- cosmetic CSS is injected before the async storage read and removed for
  whitelisted sites, so ads no longer flash in at document_start
- content script shares the whitelist matcher; dead omablock_disabled removed
- 'tabs' permission dropped: <all_urls> already exposes tab URLs
- extensions/build-chrome-zip.sh builds the versioned store package without
  store images and the 'key' field; the stale 1.0.0 zip is removed
- extension 1.0.5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
…, CI copy check

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
Comment thread src/main.rs Fixed
Addresses the CodeQL cleartext-logging alert on the new load_config branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
…hared QML client, i18n

- Ultimate respects category and DoH toggles; leaving Ultimate resets DoH/kernel
- doh_prevention defaults to false and skips whitelisted DoH resolvers
- AI protection warns before whitelisting look-alike domains
- verify-integrity compares against the install manifest (OK/MISMATCH/UNKNOWN)
- pause expiry runs from a systemd user timer (--expire-pause)
- --restore-hosts restores the pre-OmaBlock /etc/hosts backup
- failed privileged sync exits 1, rejected input exits 2; UIs report it
- qml/EngineClient.qml shares status polling and the action queue
- drop the duplicate root BoundedTextInput.qml
- rule parsing moved to the lib (rules.rs) and validates every domain;
  removes reserved names (local, *.omablock.local, localhost.localdomain)
  from the builtin list
- Chrome popup localized (en, tr)
- USB armor documented as experimental; README claims corrected

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
@ozdil ozdil changed the title fix: kernel rules, hosts sync safety, privileged nft, config lock, UI queue, CI fix: kernel rules, hosts sync safety, privileged nft, config lock, UI queue, level semantics, CI Oct 16, 2026
… restore

- The IDN radar only stays on while the flagged look-alike domain is still
  whitelisted, and --whitelist-remove clears the flag for that domain.
  Before, one warning lit the alert permanently.
- --restore-hosts turns protection off afterwards, so the next action does
  not silently re-apply the block and the status matches /etc/hosts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
…e tests

- BoundedTextInput no longer declares textEdited(): TextEdit has it from
  Qt 6.9, where the declaration is an invalid override. It is emitted only
  where it exists.
- When omablock-hosts-sync is not installed, the engine skips sudo/pkexec
  (no pointless password prompt), saves the change and exits 4. Panel and
  MainWindow say the system helper is needed instead of "Action failed".
- tests/test_qml_runtime.py loads EngineClient and MainWindow under PySide6
  with Quickshell stand-ins (tests/qml/quickshell_stub.py): action order,
  exit codes, refresh coalescing and status mapping. CI installs the Qt
  runtime for it; the test skips where PySide6 is missing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
The stub runs with HOME set to a temp dir, which hid the `pip --user`
install of PySide6 from the child interpreter on the CI runner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
tests/e2e_privileged.sh re-execs itself in private mount and network
namespaces, bind-mounts a copy of /etc and drives the real engine through
sudo and omablock-hosts-sync: enable, whitelist, Ultimate (kernel table,
DoH), panic blackout on/off, back to Standard, pause/resume and hosts
restore, checking /etc/hosts, nft tables and the status JSON each time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
A user-level CARGO_TARGET_DIR or build.target/target-dir in
~/.cargo/config.toml moved the binary away from target/release, so
package() failed with 'cannot stat target/release/omablock-engine'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
@ozdil ozdil changed the title fix: kernel rules, hosts sync safety, privileged nft, config lock, UI queue, level semantics, CI OmaBlock 1.5.0: full-stack fixes, tests and release notes Oct 16, 2026
@ozdil
ozdil marked this pull request as ready for review October 16, 2026 13:20
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-16T13:20:10.131384Z 505b5a3 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/main.rs Resolved
Comment thread src/main.rs Resolved
Comment thread uninstall.sh Resolved
Comment thread src/rules.rs Resolved
Comment thread src/main.rs Resolved
Comment thread extensions/chrome/popup.js Resolved
Comment thread .github/workflows/ci.yml Resolved
- Refuse to save or apply defaults when config.json exists but is unreadable,
  so the whitelist and blacklist are never overwritten.
- --startup reloads and applies the config under engine.lock; --startup and
  --update take the lock for the pause-expiry save.
- uninstall.sh lifts an active panic blackout before removing the engine.
- Count managed hosts entries on raw bytes so non-UTF-8 lines do not hide them.
- Derive the IDN alert from the whitelist so it covers every flagged domain.
- Chrome 1.0.6: the service worker owns the allow rules (serialised, resynced
  on change and on startup); the popup waits for it before reloading.
- Pin CI actions to commit hashes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkEeVbVXsojuz4Jb4j693q

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ozdil has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ozdil has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@ozdil
ozdil merged commit b8b8442 into main Oct 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants