100 Commits
Author SHA1 Message Date
Jens 2f7695c136 chore: log page layout hints to debug Astro redesign issues (#1330)
## ℹ️ Description
Adds debugging output that shows whether and how far a user is affected
by the redesigned (Astro) Kleinanzeigen pages.

- Related issue: #1329 (category selection fails with `No HTML element
found with ID 'cat_185'`); no behavior change, diagnostics only.
- Motivation: reporters currently only see the timeout message.
Diagnostic HTML dumps may contain PII, so users hesitate to share them.

## 📋 Changes Summary

- New `WebScrapingMixin.page_layout_summary()` returns URL,
`astro-island` count, logged-in marker presence, offered `cat_*` IDs,
category picker presence, viewport width and `readyState`. It never
raises.
- `set_category`: DEBUG log of the layout before selection; WARNING with
the layout when a `cat_<id>` element is missing.
- `has_logged_in_marker`: DEBUG log of the layout when no marker is
found.
- `_extract_island_props`: DEBUG log when no Astro island with ad data
exists.
- Publish diagnostics JSON (`capture_on.publish`) now contains
`page_layout`, so no HTML dump is needed to tell the layouts apart.
- German translation for the new warning, docs note in
`CONFIGURATION.md`, unit test for the helper.

Checked the probe against a live logged-in session: start page has 31
islands and the marker, the classic category page has 0 islands and
lists `cat_185`.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Diagnostics/logging only, none of the above applies.

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
utest`; `pdm run test` additionally launches real browsers and was not
run locally).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Improvements**
* Failed-publish diagnostics now include a page layout summary, helping
distinguish redesigned pages from classic ones without including the
full HTML.
* Additional layout details are recorded when login checks or category
selection encounter problems.
  * Added clearer German translations for category selection errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-08 20:48:25 +02:00
Jens b9daab1121 refactor: keep list help in the literal help text blocks (#1327)
## ℹ️ Description

Follow-up to #1321. The CLI help text is the one place that keeps a
complete English and a complete German text block in the code, so `list`
belongs into both blocks instead of using a gettext lookup with a
temporary locale switch. Related issue: none.

## 📋 Changes Summary

- Write the `list` help line literally into the English and German
blocks of `help_text()` and drop the locale switch and the `help_text`
entry in `translations.de.yaml`.
- Replace the locale-preservation test with a simple check of the `list`
line per language.
- Document the exception for the CLI help text in `CONTRIBUTING.md`.

No dependencies or configuration changes.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Refactoring only, no user-visible change: the help output is identical.

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
utest` locally, the full suite runs in CI).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* CLI help now displays the complete English or German text for the
`list` command, regardless of the currently active language setting.
* **Documentation**
* Updated translation guidance to clarify that CLI help text should
include complete English and German wording.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-06 13:05:11 +02:00
Jens e4bca13df3 fix: ignore stray slashes in configured category paths (#1316)
## ℹ️ Description
Publishing hangs at category selection when the configured category path
contains a stray slash.

- Link to the related issue(s): Fixes #1314
- Since #1278 the bot no longer opens
`p-kategorie-aendern.html#?path={category}` but clicks one
`cat_<segment>` ID per path level. A trailing or doubled slash (e.g.
`161/176/`) splits into an empty segment, so the bot waits for `cat_`,
which never exists. That matches the `web_find(ID, cat_)` timeouts in
the issue's log. The old deeplink tolerated such slashes, which is why
it only broke after #1278.

## 📋 Changes Summary

- `set_category` drops empty path segments before the alias check and
the per-level clicks, so `161/176/`, `/80//87` etc. select the intended
category. The cleaned path is also what the category-suggestion fallback
receives.
- Extended the existing category selection test with trailing- and
double-slash cases; both fail on the previous implementation.
- No dependency, configuration or generated-artifact changes; no
translation changes.

Not verified live: I don't know which category the reporter configured.
The empty `cat_` ID in the log can only come from an empty path segment,
though.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Category paths with leading, trailing, or repeated slashes are now
handled correctly when selecting a category.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-04 21:42:12 +02:00
Jens 541c556530 ci: squash-merge dependency PRs automatically (#1317)
## ℹ️ Description
The automatic merge of dependency PRs always fails, so they have to be
merged by hand.

- Link to the related issue(s): none; seen on #1315 (and #1308 was
merged manually)
- The `dependabot-pr-auto-merge` and `pdm-pr-auto-merge` jobs call `gh
pr merge --auto --rebase`, but the repository only allows squash merges
(`allow_rebase_merge: false`, `allow_squash_merge: true`,
`allow_auto_merge: true`). The job fails with `GraphQL: Merge method
rebase merging is not allowed on this repository
(enablePullRequestAutoMerge)`.

## 📋 Changes Summary

- Both auto-merge jobs now use `gh pr merge --auto --squash`, the only
merge method the repository allows.
- No dependency or configuration changes. Auto-merge still waits for the
required checks and reviews of the `main` ruleset; this only fixes the
merge method.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Automatic dependency pull request merges now use squash merging
instead of rebase merging.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-04 20:04:49 +02:00
Jens 0aa0f738e6 fix: read the delete CSRF token from the redesigned manage-ads page (#1313)
## ℹ️ Description
Deleting ads fails on the redesigned (Astro) "Meine Anzeigen" page
because the bot only reads the CSRF token from `<meta name="_csrf">`,
which that page no longer renders. With `delete_old_ads: AFTER_PUBLISH`
the timeout even aborts the whole publish run after the new ad went
online, so the old ad stays and remaining ads are not processed.

- Link to the related issue(s): Fixes #1312, reported in #1279 (comment
by @DreckSoft)
- The redesigned page still carries the token inside the JSON
`data-initialprops` attribute of `#consentBanner` as
`webContext.csrfToken`. On a live classic page (which renders both) I
verified that this value is identical to the meta token, and that
`_csrf_header` is `X-CSRF-TOKEN`, i.e. the header the delete request
already sends.

## 📋 Changes Summary

- `delete_flow.delete_ad` reads the token with one `web_execute`:
`meta[name=_csrf]` first, then `webContext.csrfToken` from any
`[data-initialprops]` element (malformed props are skipped). It no
longer waits through 3 × `web_find` timeouts when the meta tag is
absent.
- Missing token still fails with the existing "Expected CSRF Token not
found in HTML content!" message, before any delete request is sent; no
translation changes.
- New integration test runs `delete_ad` in a real browser against
offline classic and redesigned markup (sanitized from the #1279 sample)
and asserts the `x-csrf-token` header that is sent. Against the previous
implementation 5 of the 6 cases fail; only the classic one passes.
- Unit tests now mock `web_execute` instead of `web_find`; added a
no-token case. Removed a CSRF mock in `test_publishing_form.py` that was
never reached.
- No dependency or configuration changes.

Verification beyond tests: ran the shipped JS on the live manage-ads
page with the configured browser: classic page → meta token; meta tag
removed in the DOM → identical token from `initialprops`; both removed →
`null`. My account currently gets the classic layout, so a real delete
on the redesigned layout is still unconfirmed; feedback from an affected
account (#1312, #1279) is welcome.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved ad deletion across classic and redesigned pages by checking
both supported CSRF token sources. Deletion stops without sending a
request if no valid token is available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-03 18:51:06 +02:00
Jens 5b9d94d1f6 ci: fail the build when code is not formatted (#1307)
## ℹ️ Description
*Fail CI when the code is not formatted with `pdm run format`.*

- Link to the related issue(s): none
- CI ran ruff, mypy and basedpyright but never checked formatting, so
`tests/unit/test_extract.py` was merged unformatted with #1282 and every
later `pdm run format` rewrote it (fixed in #1306). AGENTS.md lists `pdm
run format` as the first validation step; CI now enforces it.

## 📋 Changes Summary

- New build step "Check code formatting": runs `pdm run format` and
fails with an error annotation if `git diff --exit-code` reports
changes.
- Runs once, next to the other single-runner checks (`ubuntu-latest`,
Python 3.15); the formatter output does not depend on the OS.
- Verified locally: on `main` before #1306 the step fails (the 794-line
diff in `tests/unit/test_extract.py`); on current `main` it passes. The
generated `_version.py` that `pdm run format` also touches is
git-ignored, so it does not trip the check.
- No dependencies or configuration changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

CI-only change.

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`). — workflow-only change; the new step was simulated locally
against `main` before and after #1306.
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`). — including
`actionlint` for the workflow.
- [x] I have updated documentation where necessary. — no documentation
affected.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Added a formatting check to the build workflow; the check fails if
formatting changes are needed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-02 08:34:10 +02:00
Jens f94deb92d0 style: format tests/unit/test_extract.py (#1306)
## ℹ️ Description
*Apply `pdm run format` to `tests/unit/test_extract.py`.*

- Link to the related issue(s): none
- The file was merged unformatted with #1282 (e.g. `path: Path`,
`encoding="utf-8"` instead of the repo's `path:Path`, `encoding =
"utf-8"`). Since CI has no format check, every later `pdm run format`
rewrites it, so contributors either commit an unrelated 800-line diff or
have to revert it by hand (as in #1298, #1299, #1304, #1305).

## 📋 Changes Summary

- `pdm run format` output for `tests/unit/test_extract.py` only.
- Whitespace-only: `git diff -w` is empty (794 lines touched, 0 changed
when ignoring whitespace).
- No dependencies or configuration changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Formatting only (no behaviour change).

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`). — 1768 passed, 5 skipped.
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. — no documentation
affected.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-10-02 08:25:17 +02:00
Jens 86ae143cd9 fix: open the ad overview on its first, unfiltered page (#1304)
## ℹ️ Description
*Open the ad overview on its first, unfiltered page instead of the page
and search restored from the browser tab.*

- Link to the related issue(s): Fixes #1302
- The ad overview (`m-meine-anzeigen.html`) restores the UI-stored query
parameters `pageNumber` and `keyword` from `sessionStorage`
(`queryParams`) whenever its URL does not set them. In a tab that has
paged before, opening the plain URL therefore starts on a later page.
There the "Nächste" button is disabled, so the bot treats that page as
the only one:
- `download --ads=new|all` misses every ad on the earlier pages (#1302:
44 ads, 25 per page, the bot saw only the 19 ads of page 2),
- `extend`/`reserve`/`activate` report ads on the earlier pages as
missing,
- `update` (reaching the overview through the site navigation, #1278)
fails with "Could not reach edit form" for an ad on an earlier page once
a previous edit in the same run paged forward.

A stored search keyword filters the overview the same way. `sort` is not
affected: the page adds a default `sort` to every URL lacking one.

## 📋 Changes Summary

- New domain module `ad_overview.py`: the overview parameters the page
restores and `first_page_url()`; `navigate_paginated_ad_overview` opens
the overview with `pageNumber=1&keyword=`, keeping other query
parameters (download and bulk actions).
- New `WebScrapingMixin.reset_ad_overview_state()` drops only the stored
`pageNumber` and `keyword` from `sessionStorage`; `open_ad_for_edit`
calls it before the navigation click, keeping the click path from #1278
intact. A missing or unreadable entry is left untouched; a
`TimeoutError` is reported as a warning and the navigation continues as
before.
- Tests: the URL pinning is covered for the default URL, existing
parameters and a stale `pageNumber`/`keyword`. The bulk-action overview
fake now behaves like the real page (starts on the last viewed page
unless the URL sets `pageNumber`), which fails without the fix.
`open_ad_for_edit` asserts the reset happens before the navigation
clicks. A new integration test runs the reset against real
`sessionStorage` in the shared integration-test browser (#1305) on an
offline file page; it fails when the script stops deleting the keys.
- No new dependencies or configuration changes.

**Verified against the live site** (an account with 2 ads, `?pageSize=1`
to get two pages; nothing was saved, `sessionStorage` restored
afterwards):

| Situation | Overview requests | Result |
|---|---|---|
| click "Nächste" | stores `pageNumber: 2` | – |
| reopen plain URL (bot before fix) | `pageNum=2` | "Nächste" disabled,
treated as single page |
| reopen with `?pageNumber=1` (after fix) | `pageNum=1` | normal
pagination |
| stored keyword, `?pageNumber=1` only | `keyword=…` kept | 0 ads |
| stored keyword, `?pageNumber=1&keyword=` | no keyword | all ads |
| `open_ad_for_edit` with stored page 2, without reset | – | "Could not
reach edit form" |
| `open_ad_for_edit` with stored page 2, with reset | – | edit form of
the ad on page 1 |

**Open question for the reporter:** how the stale page got into a fresh
bot run is not verified. `sessionStorage` only survives a restart when
Chromium restores the last session; the reported config sets
`--restore-last-session=false`, and Chromium may treat that switch as
present (= enabled). The fix does not depend on where the stored state
comes from.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`). — 1768 passed, 5 skipped.
- [x] I have formatted the code (`pdm run format`). — it leaves the
changed files unchanged; its unrelated rewrite of
`tests/unit/test_extract.py` (already unformatted on `main`) is not part
of this PR.
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. — no user-facing
documentation affected.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-10-02 08:07:33 +02:00
Jens 52637ba421 test: share one browser across the browser integration tests (#1305)
## ℹ️ Description
*Share one browser across the browser integration tests instead of
starting one per test case.*

- Link to the related issue(s): none
- Every browser integration test started its own Chromium, and each
parametrized case started another one: **10 browser starts per run**, 9
of them visible windows (`test_init`, 2× `test_logged_in_marker_…`, 6×
`test_narrow_viewport_warning_…` run without `--headless`; only
`test_visible_field_errors_…` was headless). Starting Chromium is the
slowest and flakiest part of these tests. All of them only load offline
HTML and emulate viewport widths through CDP, so one session per run is
enough.

## 📋 Changes Summary

- New `tests/integration/conftest.py`:
- session-scoped `browser_session` fixture: starts the browser once with
the bot's default launch configuration (plus `--no-sandbox` on Linux, as
before) and retries `Failed to connect to browser` up to 5 times with a
10 s delay — the same budget as the former per-test `flaky` reruns;
cleanup between attempts is best effort.
- function-scoped `web` fixture: hands each test the shared session on a
freshly loaded `about:blank` with the device metrics override cleared.
- `test_narrow_viewport_warning_matches_mobile_layout_threshold` (6
cases) is removed: its threshold check compared `MIN_VIEWPORT_WIDTH`
with a breakpoint hand-written into the same fixture, so it could not
detect a change on the live site, and the warning logic is fully covered
by unit tests (`tests/unit/test_web_scraping_mixin.py`). Its only
browser-specific check — the measured width equals the emulated one —
moved into `test_logged_in_marker_…`, which already emulates 390 and
1024 px.
- `test_web_scraping_mixin_integration.py` and
`test_publishing_validation.py` use the `web` fixture and run on the
session event loop (`asyncio(loop_scope = "session")`); their assertions
are unchanged. `test_init` now checks browser detection and the started
session.
- `test_publishing_validation.py` no longer uses its own headless,
temporary-profile browser; it runs in the shared session like the others
and still only writes offline HTML into `about:blank`.
- No dependencies or configuration changes; CI still runs these tests
with `--workers 0` (under `xvfb-run` on Linux).

Result: **1 browser start instead of 10**, and 5 instead of 11 browser
test cases; `pdm run itest` 5 passed in ~6 s locally. The tests also
pass in reversed order, so they do not depend on state left by each
other. With `pdm run test` (xdist) each worker that picks up integration
tests starts at most one browser.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Test-only change (no user-facing behaviour).

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`). — 1762 passed, 5 skipped; `pdm run itest` 5 passed; with the
shared session the tests also passed in reversed order.
- [x] I have formatted the code (`pdm run format`). — it leaves the
changed files unchanged; its unrelated rewrite of
`tests/unit/test_extract.py` (already unformatted on `main`) is not part
of this PR.
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. — no documentation
affected.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-10-02 07:52:11 +02:00
Jens 7ecd44dc1f feat: warn when the browser viewport is too narrow for the desktop layout (#1293)
## ℹ️ Description

- Link to the related issue(s): #1279 (Astro redesign tracking);
follow-up to #1283 / d9bf177 — no dedicated issue.

#1283 fixed one symptom of small browser windows: the logged-in marker
sits inside a `hidden md:block` header and is not rendered below 768px.
It left open whether only the header is affected, and nothing told users
that their window was too narrow in the first place.

Windows stay too narrow because `_select_viewport_size_for_metrics()`
(`web_scraping_mixin.py`) skips the resize when no configured
`viewport_sizes` entry fits the available screen. The smallest default
is 1366px wide, so on small displays (Xvnc, small VMs, CI) the window
keeps its initial size.

### Measurements

Measured on 2026-09-25 against the live site, sweeping the viewport over
360/390/480/600/700/720/740/760/767/768/769/800/900/1024/1200/1366px.
For every element with an `id` or `data-testid` it was recorded whether
it is still rendered (non-empty `getClientRects()`, neither
`display:none` nor `visibility:hidden`) — existence alone is not enough,
since the elements stay in the DOM and CDP lookups keep resolving them.

**Start page — exactly one structural transition over the whole range,
between 767 and 768px:**

| Selector (source) | ≤767px | ≥768px |
|---|---|---|
| `header` | `display:none` | rendered |
| `.mr-medium`, `[data-testid=logged-in-user]` (`login_flow.py`) |
hidden¹ | rendered¹ |
| `a[href*="einloggen"]`, `a[href*="/m-einloggen"]` (`login_flow.py`) |
hidden | rendered |
| `#nav-menu-item-my-ads` (`login_flow.py`) | hidden | rendered |
| `button[aria-label="Willkommens-Popup Schließen"]` | hidden | rendered
|
| `#mobile-header`, `#hamburger-button`, `#mobile-menu`,
`#mobile-footer` | rendered | gone |

¹ not present while logged out; measured on
`tests/fixtures/astro_start_page_logged_in_header.html`, whose header
class matches the live page exactly (`hidden w-[970px] bg-surface
md:block custom:w-full`).

Above 768px only Google ad iframes appear (1024/1366px). The page's
stylesheets define the default Tailwind breakpoints only — 640, 768,
1024, 1280, 1536px — and of those just 768px moves an element the bot
uses (640px only swaps a decorative logo on the Auth0 page).

**Ad detail page (redesigned):** `#viewad-title`, `#viewad-price`,
`#viewad-description-text`, `#viewad-locality`, `#viewad-contact`,
`#vap-brdcrmb`, `a[href*='/s-bestandsliste.html']` are width-invariant
over 360–1366px. At 768px only the footer swaps (`mobile-footer` ↔
`footer`).

**Auth0 login page:** `#username`, the submit button, the
error/MFA/captcha selectors are all width-invariant over 360–1366px.

**Secondary observation:** the desktop header is a fixed `w-[970px]`, so
between 768 and 969px it renders but overflows the viewport —
`#nav-menu-item-my-ads` sits at x=906..970. Still reachable via CDP
clicks with scroll-into-view, so it is not used as the threshold, but
1024px is documented as the recommended minimum.

**Not measured:** the `publishing_form.py` selectors and the "Meine
Anzeigen" list (`#my-manageitems-adlist`, `.cardbox`) need a logged-in
session — `/p-anzeige-aufgeben-schritt2.html` redirects to Auth0 when
logged out. The legacy ad-page variant was not swept either; the variant
is an A/B assignment and, checked 3x each at 360 and 1366px, is not
width-dependent.

### Warning, not abort

The measurements show only header-dependent login detection breaks below
768px. Ad pages and the login page render identically at any width, so
`download`/`extract` runs work fine on narrow windows. A hard abort
would break those existing setups, so the bot warns and continues.

## 📋 Changes Summary

- `web_scraping_mixin.py`: new constants `MIN_VIEWPORT_WIDTH` (768) and
`RECOMMENDED_VIEWPORT_WIDTH` (1024); new `_effective_viewport_width()`
(reads `window.innerWidth`, the value CSS media queries evaluate against
— `clientWidth` would read low by the scrollbar width) and
`_warn_if_viewport_too_narrow()`, called from `web_open()` after
`_resize_viewport_after_open()` so it reports the width the run actually
ended up with. Warns once per session, only on kleinanzeigen.de pages.
- `translations.de.yaml`: German translations for the new warning and
the debug message.
- `config_model.py`: field descriptions for `browser.arguments` and
`humanization.viewport_sizes` mention the threshold;
`docs/config.default.yaml`, `schemas/config.schema.json` and `README.md`
regenerated via `pdm run generate-artifacts`.
- `docs/CONFIGURATION.md`: "Minimum window width" under `browser`.
- `docs/BROWSER_TROUBLESHOOTING.md`: new section "Issue: Viewport too
narrow (mobile layout)" with symptoms, cause, diagnosis and fixes.
- `tests/unit/test_web_scraping_mixin.py`: 22 tests — threshold
boundaries, message content, once-per-session, host check,
unusable/non-finite widths, probe failures, `window.innerWidth` usage.
- `tests/integration/test_web_scraping_mixin_integration.py`: itest
driving `cdp.emulation.set_device_metrics_override` over
360/520/767/768/1024/1366px against the header fixture, asserting that
the warning fires exactly where the marker header stops rendering.

Regression checks: with the threshold set to 0, 6 unit tests fail; with
`_warn_if_viewport_too_narrow()` turned into a no-op, 3 itests fail.

No new dependencies or configuration options.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1704 passed, 5 skipped; `pdm run itest`: 11 passed).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Added a warning when the browser viewport is too narrow for
Kleinanzeigen’ desktop layout, which can hide login and navigation
elements. The warning includes sizing suggestions and does not stop
browsing.
* **Documentation**
* Added guidance on viewport sizing, display limits, and troubleshooting
narrow-window issues. Clarified that ad downloads and extraction are
unaffected.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-25 11:51:49 +02:00
Jens 7f5c59b9e0 chore: Update Python dependencies (#1291)
## ℹ️ Description

Routine maintenance update of the locked Python dependencies.

- Link to the related issue(s): _none — recurring dependency
maintenance, same as #1275, #1269, #1266_
- Motivation and context: keeping `pdm.lock` in sync with current
upstream releases so local runs, CI and the PyInstaller builds resolve
the same, up-to-date packages instead of drifting behind.

## 📋 Changes Summary

- `pdm.lock`: update `platformdirs` 4.11.11 → 4.11.12 (version pin plus
the wheel and sdist SHA256 hashes).

Notes:

- `pyproject.toml` is untouched — `platformdirs` is a transitive
dependency in the `default` and `dev` groups, so no declared version
constraint changes.
- No configuration changes and no new requirements for users.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

None of the above applies: this is non-breaking dependency maintenance
(a patch-level bump of a transitive dependency) with no change to CLI
behavior, config, or generated artifacts.

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`) — 1668 passed, 5 skipped, total coverage 93.25%.
- [x] I have formatted the code (`pdm run format`) — no files changed.
- [x] I have verified that linting passes (`pdm run lint`) — ruff, mypy
(117 source files), pyright and actionlint all clean.
- [x] I have updated documentation where necessary — not applicable, no
user-facing or documented behavior changes.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-09-22 07:22:28 +02:00
Jens 94544e4883 fix: follow click navigation when editing ad categories (#1278)
## ℹ️ Description
Editing an ad can fail with HTTP 400/403 when the bot reloads the
category-selection page directly. Follow the site's navigation to retain
the edit context: homepage → Meins → Meine Anzeigen → edit → category
selection → Weiter.

Related issue: none; reported and reproduced during live browser
diagnosis. The complete click path returned HTTP 200 with the configured
category; the server's exact rejection mechanism is not established.

## 📋 Changes Summary
- Open existing ads through their edit links in the paginated management
overview. Restart this navigation on pre-submit retries.
- Prefer the category-change anchor over an ambiguous category text
match, then click each numeric or text category ID without loading a
category deeplink.
- Reject unresolved category aliases with a translated, non-retryable
error before attempting category clicks.
- Resolve offered category suggestions when a configured category link
is missing; retain the original timeout if no picker is rendered.
- Allow the existing overview pagination helper to operate on a page
already reached through navigation.
- Add regression coverage for numeric/text category paths, ads on later
pages, missing ads (including page 11 coverage), and preservation of the
current overview.

No dependencies, configuration, or generated artifacts change. The
edit-navigation timeout now has a German translation. New-ad entry
remains unchanged; its category picker uses the same click selection.

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1647 passed, 5 skipped).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (no user-facing setup
or command changes).

Live validation: ran the modified bot's edit-opening and
category-selection functions using the existing bot Chromium profile and
launch parameters, stopping before saving. Independently verified with
agent-browser that the returned form had `Haus & Garten > Weiteres Haus
& Garten` and HTTP 200. Automated tests do not contact kleinanzeigen.de.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Editing an existing ad now reliably navigates through the “My Ads”
overview to locate and open the correct listing.
* Category selection during ad publishing stays within the current form
session, helping preserve entered information.
* Overview navigation can now continue from the current page without
unnecessary reloads.

* **Bug Fixes**
* Invalid category aliases are detected before browser navigation, with
clearer error messages when category selection or edit-form navigation
fails.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-13 23:40:46 +02:00
Jens 250a857771 fix: make login entry an explicit choice (#1276)
## ℹ️ Description
Make the login entry an explicit user choice. The existing direct
`/m-einloggen-sso.html` route remains the default (`login.entry_mode:
SSO`). Users can opt in to homepage **Meins → Meine Anzeigen**
navigation with `login.entry_mode: NAVIGATION`.

The SSO route was introduced to avoid login-page JavaScript issues in
headless browsers. Preserve that behavior for existing configurations;
browser/headless mode never selects or changes the route automatically.

Related issue: #1274 (closed by its author; this PR adds the requested
alternative without claiming to resolve IP-range blocks).

## 📋 Changes Summary

- Add `login.entry_mode` with supported values `SSO` (default) and
`NAVIGATION`.
- For NAVIGATION, dismiss the optional welcome popup and click Meins →
Meine Anzeigen using selectors verified against the live DOM.
- Preserve session pre-checks, navigation-timeout diagnostics, and the
existing Auth0 credential flow for both routes.
- Test both explicit modes with visible/headless browser arguments,
popup handling, invalid configuration values, and timeouts on either
route.
- Document the option and regenerate the config schema and default YAML.
No new dependencies or changed runtime messages.

Validation: `pdm run format`, `pdm run lint`, `pdm run test`, and
generated-artifact verification passed. Tests ran outside the sandbox
because the pytest plugin requires a local socket.

Manual browser verification reached the Auth0 identifier and password
pages through the navigation route. Both submit buttons matched the
existing bot selector. After password submission, Kleinanzeigen returned
**IP-Bereich vorübergehend gesperrt**; successful post-login navigation
and session detection remain unverified. Neither route guarantees
avoidance of IP-range blocks. Manual inspection used agent-browser with
Chrome for Testing; the effect of browser choice has not been isolated.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added configurable login entry modes: direct SSO or homepage
navigation through the account menu.
- SSO remains the default and works independently of headless browser
settings.
- Added support for automatically handling the welcome popup during
homepage-based login.

- **Documentation**
- Documented the new login setting and added it to the default
configuration.

- **Tests**
- Added coverage for both login routes, popup behavior, navigation
failures, and configuration validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-13 23:20:59 +02:00
Jens b2806e226e fix: use PDM interpreter in generation and formatting scripts (#1277)
## ℹ️ Description
`pdm run generate-artifacts` could fail while creating the default
config when the shell's `python` differed from PDM's selected
interpreter. For example, Python 3.13 on PATH loaded dependencies from
`__pypackages__/3.12`, causing `ModuleNotFoundError:
pydantic_core._pydantic_core`.

Use PDM's interpreter consistently for config generation and Python
post-formatting. This fix is independent of login-entry PR #1276.

## 📋 Changes Summary

- Replace the shell-based config generator with a Python script that
invokes the CLI through `sys.executable`.
- Split `format:py` into a PDM composite so the Python post-formatter
also uses PDM's interpreter.
- Add an integration regression test that regenerates the exact config
snapshot with an unusable Python executable on PATH.
- Preserve supported Python versions, CLI generation behavior, and
generated artifacts. No dependency or user-configuration changes; no
documentation updates needed.

Validation: `pdm run format`, `pdm run lint`, `pdm run test`, `pdm run
generate-artifacts`, and generated-artifact verification passed. The
generator was also verified locally with PDM selecting Python 3.12 while
shell Python resolves to 3.13.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (no documentation
changes required).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Improved the configuration generation workflow by using the active
Python environment directly.
* Split Python formatting into separate automatic formatting and
follow-up processing steps for more consistent results.

* **Tests**
* Added integration coverage to verify configuration generation works
reliably even when conflicting Python executables are present on the
system path.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-13 22:26:25 +02:00
Jens 387d2eb153 refactor: modernize async timeout and deadline handling (#1273)
## ℹ️ Description

Browser polling previously checked elapsed time only between polls, so a
hanging browser call could exceed its timeout indefinitely. This
refactor uses absolute operation deadlines and native asyncio timeout
contexts while preserving configured retry and login phase budgets.

Closes #1236.

## 📋 Changes Summary

- Carry absolute deadlines through retry attempts and selector
alternatives; bound page readiness, predicates, reattachment, and
navigation.
- Replace login and browser-process `wait_for` calls with timeout
contexts, preserving phase durations, backoff, timing records, logs, and
existing errors.
- Propagate external cancellation without additional retries or login
fallbacks, and clean up owned browser resources without starting fresh
shutdown waits after cancellation.
- Document the timeout inventory, caller-owned total budgets, and
intentional navigation/polling changes in `docs/ASYNC_TIMEOUTS.md`;
narrow the global `ASYNC109` suppression to existing browser adapters
and corresponding test doubles.
- Await browser target refresh directly, avoiding nodriver's hidden
half-second pause and detached refresh tasks; document zero-budget
expiry at the first asynchronous suspension.
- Add behavior tests for hanging operations, parent deadlines,
cancellation, selector fallback, startup cleanup, and shutdown,
including real nodriver Tab regressions.

No dependencies, configuration defaults, or generated artifacts change.
Startup retains its existing lack of a default total timeout; callers
can supply an enclosing deadline. Viewport adjustment remains outside
the page-load budget.

Validation: `pdm run format`, `pdm run lint` (Ruff, mypy, basedpyright,
actionlint), and `pdm run test` all pass: **1,633 passed, 5 skipped**.
Tests use fakes or local fixtures and do not contact kleinanzeigen.de.
Independent review passed after fixing and retesting both identified
nodriver deadline issues.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved timeout handling so nested browser operations respect their
overall time limits.
* Improved cancellation behavior and cleanup during browser startup,
navigation, and shutdown.
* Preserved external cancellation and underlying navigation timeout
errors without unnecessary retries.
  * Improved handling of malformed or incomplete HTTP responses.

* **Documentation**
* Added documentation for browser timeouts, cancellation behavior, retry
policies, and operation deadlines.
  * Linked the new timeout guidance from contributor documentation.

* **Refactor**
* Updated login verification timeout handling while preserving existing
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-12 16:48:59 +02:00
Jens 2a311576ed refactor: use asyncio.to_thread for blocking operations (#1272)
## ℹ️ Description

Replace default-executor dispatch with `asyncio.to_thread()` for
ordinary blocking work, simplifying calls and propagating the caller's
context variables.

Closes #1238.

## 📋 Changes Summary

- Convert all 19 production `run_in_executor()` calls: 14 in extraction,
one timing flush, two browser-profile operations, and two filesystem
helpers. None require a custom executor or explicit Future handling.
- Pass keyword arguments directly where possible. Keep compound glob and
model-serialization operations inside worker threads.
- Preserve sequential downloads, ordering, exception propagation,
cancellation handling, rollback, cleanup, and log messages.
- Extend save-failure coverage to timeouts and verify original contents
survive; test worker context propagation and isolation.
- No dependencies, configuration, translations, or generated artifacts
need updating.

### ⚙️ Type of Change

Internal behavior-preserving refactor; none of the
feature/fix/breaking-change categories apply.

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (no documentation
changes needed).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Improvements**
* Improved asynchronous handling for file operations, downloads,
cleanup, browser profile preparation, and timing collection.
* Preserved existing behavior and error handling while maintaining task
context across asynchronous file checks.

* **Tests**
* Expanded coverage for YAML-write failures involving operating system
and timeout errors.
* Added validation that asynchronous file existence checks preserve task
context.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-12 14:47:47 +02:00
Jens 98ad45b62b fix: skip null auto price reduction when downloading ads (#1271)
## ℹ️ Description

Re-downloading an ad with `auto_price_reduction: null` currently emits a
preservation warning because the downloader attempts to convert `None`
into a dictionary. Treat this normal unset value like a missing key and
silently skip it while preserving other valid local settings.

Closes #1250.

## 📋 Changes Summary

- Skip APR preservation when the existing value is absent or null.
- Extend download tests for missing, null, empty-disabled, and enabled
APR settings through direct file lookup and the filename fallback.
- Verify that malformed non-null APR values still warn and other local
settings remain preserved.
- No dependency, configuration, schema, documentation, or translation
changes are required.

Validation: the two null regression cases failed before the fix. After
the fix, `pdm run format`, `pdm run lint`, and `pdm run test` pass (1612
passed, 5 skipped).

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (not required for
this fix).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Existing advertisements with an explicitly empty
`auto_price_reduction` setting are now handled correctly.
* Local settings preservation no longer attempts to reuse unavailable
price-reduction values.
* Clearer warnings are provided when an invalid price-reduction
configuration cannot be preserved.

* **Tests**
* Expanded coverage for missing, empty, disabled, valid, and incomplete
price-reduction settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-12 14:35:22 +02:00
Jens 3d1a355c77 perf: traverse the ad overview once for bulk actions (#1270)
## ℹ️ Description

Closes #1265.

Bulk `extend`, `reserve`, and `activate` currently restart the
manage-ads overview at page 1 for every eligible ad. They now share one
traversal, processing all selected rows on each page and stopping when
every requested entry has been handled or pagination ends. This reduces
repeated overview loads for accounts with many ads.

## 📋 Changes Summary

- Add a shared bulk-action coordinator that tracks pending IDs and
resolves each action button on the current page.
- Preserve per-entry progress/results, action delays, duplicate
configuration entries, timeout handling, extension metadata persistence,
and strict reservation-state verification.
- Move German translations to the new logging locations and adapt
existing flow tests.
- Add 21 browser-independent regression cases covering all three
commands, multiple rows/pages, early completion, missing IDs, click/open
timeouts, duplicate entries, empty selections, and persisted file
behavior.

No dependencies, configuration changes, or additional requirements are
introduced. No user documentation changes are needed for this internal
performance improvement. Tests use simulated pages and never contact
kleinanzeigen.de. Validation: `pdm run format`, `pdm run lint`, and `pdm
run test` all pass; the full suite reports **1,605 passed, 5 skipped**.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] ⚡ Performance improvement (reduces repeated overview navigation
without changing CLI usage)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (no documentation
changes needed).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added unified bulk processing for extension, reservation, and
activation actions across paginated ad listings.
- Improved handling and reporting of ads whose action buttons cannot be
found.
  - Added German translations for shared bulk-action messages.

- **Bug Fixes**
  - Reduced repeated page traversal during bulk ad operations.
- Improved reliability when action buttons time out or are unavailable.

- **Tests**
- Added and updated coverage for pagination, successful actions,
timeouts, missing buttons, and ad updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-12 13:58:16 +02:00
Jens a1f000664f enh: continue downloading after individual ad failures (#1262)
## ℹ️ Description

- Link to the related issue(s): Issue #1259
- Continue a download batch when navigation or extraction fails for one
ad, so remaining ads are still attempted.

## 📋 Changes Summary

- Isolate navigation and download failures per ad for all, new, and
numeric-ID selectors.
- Log failed ads with tracebacks and emit a unified success/failure
summary.
- Preserve task cancellation and add behavior-focused coverage for
continuation after failures.
- No dependencies, configuration changes, or additional requirements
were introduced.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Downloads now continue for remaining ads when an individual ad
encounters a navigation or download failure.
* Cancellation requests are preserved instead of being treated as
ordinary download failures.
* Download results now provide a consistent summary, including
successful and failed items.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 09:39:44 +02:00
Jens 7749b65f18 fix: remove unused shipping selectors (#1261)
## ℹ️ Description

Removes obsolete shipping-dialog XPath selector constants that CodeQL
reports as unused globals.

- Link to the related issue(s): N/A
- The CSS and text-based selector migration left these unused
declarations and implementation-specific test assertions behind.

## 📋 Changes Summary

- Remove three unused XPath selector constants from the publishing form.
- Remove stale test imports and assertions for the retired XPath path.
- No dependency or configuration changes.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [ ] I have updated documentation where necessary. (Not needed: no
user-facing behavior changed.)

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-08-31 09:05:04 +02:00
Jens 0705063fe9 fix: preserve decoded Astro island props (#1260)
## ℹ️ Description

- Link to the related issue(s): Fixes #1255
- `astro-island` props returned by browser `getAttribute("props")` are
already HTML-decoded. Applying `html.unescape()` first can corrupt
entity-like content inside otherwise valid JSON strings and prevent the
Astro fallbacks from receiving ad data.

## 📋 Changes Summary

- Parse the browser-returned props value with `json.loads()` before
attempting HTML unescaping.
- Retain `html.unescape()` as a compatibility fallback for responses
that still contain HTML entities.
- Add a regression test covering an entity-like quoted phrase in a
decoded description.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved extraction of listing details containing entity-like text,
such as `&quot;`, preserving the original content.
* Added safer handling for invalid or unexpected island properties,
returning an empty result when necessary.

* **Tests**
* Added coverage to verify descriptions with entity-like content are
parsed correctly.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 08:35:19 +02:00
Jens c98d6f3192 fix: show ad file path in validation errors (#1253)
## ℹ️ Description

- Link to the related issue(s): Fixes #1252
- Ad-validation errors now identify the source YAML/JSON file, so users
can locate invalid titles or descriptions immediately.

## 📋 Changes Summary

- Pass each discovered ad file path through both validation stages.
- Add regression tests for overlong titles, descriptions, and
final-model validation errors.
- No dependencies, configuration, or generated artifacts changed.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.
2026-08-30 20:41:50 +02:00
Jens a3ce63afe0 fix: handle idless update confirmations (#1244)
## ℹ️ Description

- Related issue: #1228
- Handles the redesigned ID-less success page when an existing listing
is updated.

## 📋 Changes Summary

- Recognize the existing ID-less success-page marker for updates as well
as publishes.
- Reuse the configured listing ID for a confirmed update; do not attempt
publish-style new-ID recovery.
- Preserve the strict published-ads recovery path for replacement
publishes.
- Add German translation coverage and a focused unit test.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] Documentation changes are not needed for this internal
recovery-path fix.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved publishing and updating reliability for redesigned
confirmation pages that do not display an ad ID.
* Updates now use the configured ad ID when available and report
uncertainty when it is missing.
* New listings recover their ad ID from published listings and report
uncertain outcomes when recovery fails or is ambiguous.
* Added German messaging for updates confirmed without a displayed ad
ID.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-15 22:38:36 +02:00
Jens 92da5f7c70 fix: support ptrace-restricted browser containers (#1242)
## ℹ️ Description

- Link to the related issue(s): Fixes #1240
- Allow ptrace-restricted Docker and LXC users to omit Chromium's
`--test-type` startup flag while preserving the existing default for
other users.

## 📋 Changes Summary

- Add `browser.suppress_unsupported_flag_warning`, defaulting to `true`.
- Warn in recognized Linux containers without `CAP_SYS_PTRACE` when the
flag remains enabled.
- Document the container configuration, regenerate config artifacts, and
cover default, opt-out, runtime propagation, and Linux-specific
behavior.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added a browser setting to control Chromium warning suppression,
enabled by default.
- Added support for disabling the setting in ptrace-restricted Docker or
LXC containers to help browser startup succeed.
- Added diagnostics and warnings for environments missing the required
system capability.

- **Documentation**
- Documented the new setting, container limitations, and configuration
guidance.

- **Tests**
- Added coverage for configuration validation, container detection,
capability handling, and browser launch warnings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-14 14:49:49 +02:00
Jens c966252da5 fix: use Python 3.12 for local act runs (#1241)
## ℹ️ Description

- Follow-up to #1235.
- Align local `act` runs with Python 3.12, the stable minimum version
used for local development, release artifacts, and Docker builds.

## 📋 Changes Summary

- Select the Ubuntu/Python 3.12 build-matrix entry in `.actrc`.
- Keep Python 3.15 prerelease compatibility testing in the full GitHub
Actions matrix.
- Introduce no dependency or runtime changes.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

Validation details:

- 1507 tests passed, 4 skipped
- 93.07% total coverage
- `.actrc` selects an existing `ubuntu-latest` / Python 3.12 matrix
entry
- The `act` executable was not available for a local dry run

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-08-14 12:12:22 +02:00
Jens f4b4e0fdf6 enh: support Python 3.12 through 3.15 (#1235)
## ℹ️ Description

- Closes #1226
- Raise the supported Python range from 3.10–3.14 to 3.12–3.15 before
Python 3.10 reaches end of life.
- Modernize the codebase with behavior-preserving Python 3.11 and 3.12
idioms while keeping CLI, configuration, persistence, and browser
behavior stable.

## 📋 Changes Summary

- Set `requires-python` and the PDM lock target to `>=3.12,<3.16`.
- Test Python 3.12 and Python 3.15 prereleases across all supported CI
platforms.
- Build and publish native release artifacts and Docker images with
stable Python 3.12; keep Python 3.15 as compatibility-only coverage
until its final release.
- Update CodeQL, dependency updates, local Act configuration, Ruff,
mypy, BasedPyright, README, CONTRIBUTING, and AGENTS guidance.
- Enable Ruff's `UP` rules and adopt applicable standard-library
imports, `datetime.UTC`, built-in `TimeoutError`, PEP 695 type aliases
and generic syntax.
- Replace deprecated `shutil.rmtree(onerror=...)` usage with Python
3.12's `onexc` callback API and update its tests.
- Regenerate `pdm.lock` for the full supported interpreter range and
integrate the latest dependency updates from `main`.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [x] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

Validation details:

- 1507 tests passed, 4 skipped
- 93.07% total coverage
- Generated schemas, default configuration, and README usage are up to
date
- Source CLI and PyInstaller binary smoke checks passed
- Local PyInstaller build and binary smoke checks passed on Python
3.12.14; Python 3.12/3.15 platform coverage is delegated to the updated
CI matrix

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added support for Python 3.12 through 3.15.
  * Improved compatibility with modern Python typing and datetime APIs.

* **Bug Fixes**
  * Improved timeout handling and directory cleanup behavior.
  * Enhanced update-checking and web-scraping reliability.

* **Documentation**
* Updated installation and contribution requirements to Python
3.12–3.15.

* **Tests**
* Expanded coverage for update checks, timeout messages, and edge cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-14 11:56:12 +02:00
Jens 7b31dddb49 fix: harden publishing against DOM variants (#1229)
## ℹ️ Description

- Closes #1227
- Closes #1228
- Hardens publishing against Kleinanzeigen A/B variants observed in the
shipping dialog and confirmation flow.
- Prevents duplicate listings when a successful publish page no longer
exposes an ad ID, while preserving the existing update flow.

The shipping dialog can either open directly on package sizes or require
"Andere Versandmethoden", and may retain a nested pane from earlier
state. Separately, the redesigned success page may show "Geschafft!" and
"Zu meinen Anzeigen" without an ID in the URL. Owned deleted ads can
also expose a decorated page title when canonical manage-ads metadata is
unavailable.

## 📋 Changes Summary

- Detect direct package-size panes before using "Andere
Versandmethoden".
- Bound shipping-dialog back navigation to two steps and fail clearly
for unsupported states.
- Capture a strict pre-submit published-ad baseline for replace
operations.
- Recover an ID-less successful publish only when exactly one new
exact-title ID appears, with bounded delayed retries.
- Fail closed on incomplete or ambiguous recovery and retain the
no-retry submit boundary.
- Strip the verified `Gelöscht • ` decoration only for owned-overview
page-title fallback; canonical API titles and manual downloads remain
unchanged.
- Add German translations and focused unit coverage for all new
branches.
- No configuration changes or new dependencies.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Validation

- `pdm run format`
- `pdm run lint`
- `pdm run test` — 1501 passed, 4 skipped
- Live managed-browser republish:
  - exercised the "Andere Versandmethoden" shipping route
  - synchronized both configured carriers
  - persisted the new ID
  - deleted the exact old ID
  - verified the refreshed overview contained only the new listing
- Local DOM verification helper was updated for diagnostics but remains
ignored and is not part of this PR.

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved downloads from owned-ad overviews by removing deleted-status
prefixes from saved titles.
* Improved shipping setup navigation across nested dialogs and
alternative shipping methods.
* Recovered newly published ads when confirmation pages do not display
an ad ID.
* Added safeguards to prevent duplicate or ambiguous publication
recovery.
* **Tests**
* Expanded coverage for owned-ad downloads, shipping navigation, and
publication recovery scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 10:19:33 +02:00
Jens 72538edaee fix: support redesigned shipping dialog actions (#1224)
## ℹ️ Description

- Link to the related issue(s): N/A
- Make the shipping-options flow resilient to Kleinanzeigen rendering
“Andere Versandmethoden” as a button, link, span, or another clickable
wrapper.
- The previous XPath required a `<button>`, although the redesigned UI
may use different element types.

## 📋 Changes Summary

- Scope the action lookup to an open or visible shipping dialog.
- Select the deepest text-bearing “Andere Versandmethoden” node so its
click bubbles to the current clickable ancestor.
- Reuse the selector during modify-mode back navigation.
- Add regression coverage for tag-agnostic dialog interaction.
- No dependencies or configuration changes.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1494 passed, 4 skipped).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (not applicable; no
user-facing configuration or workflow changes).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved “Andere Versandmethoden” selection to be tag-agnostic and
avoid duplicate/nested matches.
* Added a back-and-retry flow when the desired shipping option isn’t
available on the current dialog step.
* Updated the final selection to click the deepest matching text element
within the dialog.

* **Tests**
* Added regression tests covering tag-independent dialog actions and
retry navigation behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 14:35:06 +02:00
Jens 172aa61e6e fix: await browser session cleanup (#1223)
## ℹ️ Description

- Link to the related issue(s): N/A
- Fixes noisy asyncio teardown errors after the test suite by waiting
for nodriver websocket tasks and the Chromium subprocess to finish
before the event loop closes.
- The root cause was `Browser.stop()` scheduling asynchronous connection
cleanup without awaiting it; under fast pytest-xdist teardown,
keepalive/listener tasks survived until logging capture had already
closed.

## 📋 Changes Summary

- Make browser-session cleanup await nodriver connection shutdown and
Chromium process exit.
- Retain best-effort synchronous cleanup for object destruction, where
awaiting is impossible.
- Convert the Chromium integration test to pytest-managed async
execution and update affected mocks/unit tests.
- Let pytest own test logging handlers so late teardown records do not
leak into progress output.
- Isolate terminal-color tests from ambient `NO_COLOR`/`FORCE_COLOR`
settings.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1,489 passed, 4 skipped).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary (no documentation
changes required).

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved browser-session shutdown to use async teardown, including
reliable cleanup on normal exit and exceptions.
* Enhanced browser-process termination handling with configurable
timeouts, forced child-process cleanup, and safer exception handling
during viewport sizing.
* **Tests**
* Updated unit and integration tests to await browser-session cleanup
and align mocks with async behavior.
* Expanded async test coverage for process-exit waiting, timeout/kill
paths, and resilient teardown edge cases.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 12:19:09 +02:00
Jens a2d3fe3ed2 fix: decode cached download titles (#1218)
### Motivation
- The cached JSON title from `published_ads_by_id` could contain HTML
entities (e.g. `&#x2F;`, `&#x27;`, `&quot;`) and was returned stripped
but not decoded, causing encoded text to be stored in YAML, used for
filename rendering, and to inflate visible-length checks.
- The goal is to decode only cached JSON titles before validation and
filename rendering while preserving the existing page-title fallback and
filename sanitization behavior.
- closes #1215 and #1217

### Description
- Apply `html.unescape()` to the stripped cached title returned from
`_resolve_download_title()` so cached titles are decoded before further
processing. (`src/kleinanzeigen_bot/extract.py`).
- Add focused unit tests to cover entity decoding and the
length-validation regression:
`test_resolve_download_title_decodes_published_metadata_entities` and
`test_cached_title_entities_are_decoded_before_title_validation`
(`tests/unit/test_extract.py`).
- Keep the page-title fallback and all existing filename
sanitization/validation logic unchanged, and only decode the cached JSON
title path.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
  * Cached advertisement titles now correctly decode HTML entities.
* Decoded titles are validated and safely handled when generating file
and directory names, preventing encoded entities or raw slashes from
appearing in saved paths.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-19 12:11:12 +02:00
Jens a504622819 fix: narrow stale node protocol error handling (#1214)
## ℹ️ Description

- Link to the related issue(s): Follow-up to #1209
- Restrict login detection’s `ProtocolException` recovery to the
observed stale-DOM-node error so unrelated CDP failures propagate
instead of starting unnecessary SSO automation.

## 📋 Changes Summary

- Classify only the known stale-node CDP error by code and message.
- Preserve timeout and known stale-node fallback behavior at all login
detection boundaries.
- Re-raise unrelated protocol errors and add focused regression
coverage.
- No dependency, configuration, or API changes.

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project’s
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. (No documentation
change needed.)

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved login-state detection when temporary “stale node” browser
errors occur.
* Prevented these transient errors from incorrectly interrupting login
checks.
  * Preserved error reporting for unrelated protocol errors.

* **Tests**
* Added coverage for stale-node recovery and unrelated protocol error
handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 21:05:52 +02:00
Jens cf6662dd26 fix: require strict fetch for title cleanup (#1206)
## ℹ️ Description
- Link to the related issue(s): Issue #
- Fixes a remaining fail-closed gap in BEFORE_PUBLISH title cleanup for
ID-less ads when the published-ad list cannot be fetched completely.

## 📋 Changes Summary
- Require an additional strict published-ad fetch when BEFORE_PUBLISH
title cleanup may match by title for ID-less ads.
- Fail closed for ID-less publish candidates if strict published-ad
fetch is incomplete, before submit/delete actions.
- Keep ID-based candidates on the normal published-ad list so they can
continue safely.
- Add regression tests for strict-fetch usage and fail-closed behavior.
- Add German translations for the new log messages.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

Validation performed locally:
- `pdm run pytest tests/unit/test_publishing_workflow.py
tests/unit/test_delete_flow.py`
- `pdm run format`
- `pdm run lint`
- `pdm run test`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Enhanced publishing workflow to use more precise “published ad” data
when title-based cleanup is enabled, improving match-and-replace
behavior.
* **Bug Fixes**
* Publishing now fails safely when strict published-ad data is
incomplete, avoiding retries and deletion actions when precision can’t
be guaranteed.
* For ads lacking a known identifier, the workflow intelligently selects
the best available published-ad list or skips with a warning when strict
data is unavailable.
* **Tests**
* Added unit test coverage for strict-vs-non-strict published-ad
fetching scenarios and fail-closed behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-05 17:04:42 +02:00
Jens 819d039b71 fix: persist created_on on first publish (#1204)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Fixes first-publish persistence so newly published ads write
`created_on` when missing.

## 📋 Changes Summary

- Capture first-publish state before writing the new ad ID.
- Persist one publication timestamp to both `updated_on` and missing
first-publish `created_on`.
- Add regression tests for first publish with an already-populated ad ID
and update preservation of existing `created_on`.
- No dependency, configuration, or generated artifact changes.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved publish timestamp handling so first-time publishes and later
updates record dates consistently.
* Preserves the original creation date when it already exists, while
keeping the last-updated time current.

* **Tests**
* Added coverage for timestamp behavior on first publish and subsequent
updates.
* Verified date values are stored in ISO-8601 format with timezone
information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-05 00:51:25 +02:00
Jens 9cffbf6e8d fix: fail closed on persistence errors (#1203)
## ℹ️ Description

- Link to the related issue(s): No linked issue; follows the confirmed
P1 from `.slim/deepwork/p1-p2-validation.md`.
- Prevent publish/update workflows from reporting success or running
after-publish cleanup when local YAML persistence fails after a remote
submit succeeds.

## 📋 Changes Summary

- Add a non-retryable `PostPublishPersistenceError` carrying the remote
ad ID.
- Raise that error after logging post-submit local persistence failures.
- Count persistence failures as failed publish/update items and skip
post-submit confirmation / `AFTER_PUBLISH` cleanup.
- Add tests for direct persistence failure, publish cleanup-skip
behavior, and update non-retry/batch continuation.
- Add German translations for the new warnings.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of post-publish save failures so they’re surfaced as
a clear error instead of being silently logged.
* Publishing and updating now stop retrying when local persistence fails
after a successful remote action, while still logging details and
capturing diagnostics.
  * Other retry behavior for existing publish issues remains unchanged.

* **Tests**
* Added and updated coverage for persistence failures, including error
propagation, logging, and workflow behavior across multiple items.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-04 23:46:01 +02:00
Jens 077c5db01b fix: prevent ambiguous title deletes (#1202)
## ℹ️ Description
- Link to the related issue(s): Issue #
- Prevent title-based deletion from deleting multiple same-title remote
ads.
- Make explicit ID deletion target only the configured ID, while ID-less
title matching now fails closed on ambiguous matches.

## 📋 Changes Summary
- Split delete selection so configured IDs are exact-ID only and title
matching is only used for ID-less ads.
- Require strict published-ad fetching when ID-less title matching may
run, so ambiguity detection uses a complete list.
- Skip ambiguous title matches before browser delete requests, ID
mutation, or after-delete cleanup.
- Added German translation for the new skip message.
- Updated config/docs/generated schema wording for
`delete_old_ads_by_title`.
- Added unit coverage for ambiguous title fail-closed behavior, exact-ID
behavior, strict fetch selection, and exact delete URLs.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

Validation:
- `pdm run generate-schemas`
- `pdm run generate-config`
- `pdm run pytest tests/unit/test_delete_flow.py`
- `pdm run format`
- `pdm run lint`
- `pdm run test`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved old-ad deletion behavior for items without an ID, including
title-based matching only when needed.
* Made title-based deletion fail closed: ambiguous title matches are now
skipped.
* Ensured exact-ID deletions target only the specified ID and don’t
expand to same-title ads.
* Skips title-based matching for a batch when published-ad retrieval
can’t be completed, while continuing exact-ID deletions.
* **Documentation**
* Updated configuration documentation for title-based old-ad deletion to
reflect the new matching and skip behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-04 19:47:13 +02:00
Jens 25e2970ed0 feat: improve status APR report (#1201)
## ℹ️ Description

- Moves APR preview information out of verify output and into status
reporting.
- Improves status output into readable per-ad blocks with full relative
file paths and structured APR update/publish details.

## 📋 Changes Summary

- Keep verify focused on validation while preserving its full ad-loading
validation path.
- Show APR preview details in status without requiring verbose mode.
- Render status as path-first per-ad report lines with title, id,
status, APR update/publish details, and unchanged summary.
- Use structured APR detail data and translated human-readable reasons
instead of raw reason tokens.
- Remove the now-dead APR preview logger and stale translations.
- Update CLI help, README/docs, smoke tests, and focused unit tests.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

Validation run:
- pdm run format
- pdm run lint
- pdm run test — 1467 passed, 4 skipped

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* `status` now shows detailed APR preview information for ads in a
clearer, per-ad layout.
* Help text and documentation now point users to `status` for APR
preview details.

* **Bug Fixes**
* `verify` no longer displays APR preview output, making its results
focused on configuration and integrity checks.
* Improved status output readability and APR detail handling for publish
and update scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-04 01:43:20 +02:00
Jens 28da3865c6 refactor: simplify humanizer behavior (#1200)
## ℹ️ Description

- Related issue(s): none.
- Simplifies the humanizer after PR #1199 by removing premature feature
surface that was either speculative or default-off, while keeping the
useful behavior: baseline pacing, typing jitter, and post-open viewport
randomization.

## 📋 Changes Summary

- Remove mouse-movement, idle-action, and long-pause humanizer
configuration and implementation.
- Keep plain click behavior plus baseline web_sleep pacing.
- Keep typing jitter default-on with bounded per-character delays.
- Keep post-open viewport resize default-on, preserving user-supplied
--window-size behavior and no launch-time bot --window-size.
- Update config defaults, generated schema/default config, German
translations, and unit tests.
- Delete the weak viewport integration test that asserted private state
instead of user-observable behavior.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [x] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Removed config keys: mouse_movement, idle_action_probability,
long_pause_probability, long_pause_min_ms, long_pause_max_ms.

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

Validation: pdm run format && pdm run lint && pdm run test — 1468
passed, 4 skipped.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Simplified browser interactions to use direct clicks and per-character
typing more consistently.
* Updated viewport handling so browser size is applied after opening,
using screen metrics when available.
* Improved resizing behavior to skip invalid or unsupported cases
without interrupting browsing.
  * Adjusted humanization timing defaults for smoother, lighter pacing.

* **Refactor**
* Narrowed the humanization settings to focus on pacing, typing jitter,
and viewport behavior.
* Removed unused idle and movement-related behavior from the browsing
flow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 21:52:07 +02:00
Jens df6e308ab2 fix: resize viewport after opening page (#1199)
## ℹ️ Description

- Related issue(s): none.
- Fixes the screen-aware viewport regression by removing the temporary
pre-launch screen probe and resizing the already-open browser window
after the first kleinanzeigen.de page load.

## 📋 Changes Summary

- Remove the isolated probe browser path that could time out or appear
on a different screen.
- Open the main browser without bot-added `--window-size`, then resize
once after the first eligible kleinanzeigen.de page opens.
- Preserve user-supplied `--window-size` and skip
remote/headless/no-display or disabled randomization cases.
- Keep no-fallback behavior when screen metrics are invalid or no
configured viewport fits.
- Update viewport tests and German translations for moved log messages.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

Validation run locally: `pdm run format && pdm run lint && pdm run test`
— 1470 passed, 4 skipped.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Automatically resizes the browser window after a page loads, using
screen-aware metrics from the current page.
* Selects from configured viewport sizes that fit the available screen
area, with bounded variation.
* Adds runtime diagnostics for viewport resize (attempted/applied),
including skip reasons and failure details.

* **Bug Fixes**
* Improves resilience when URL/metrics are missing, malformed, or don’t
include valid dimensions.
* Prevents resize for non-eligible pages and when a user-supplied window
size is present; resize now occurs before human-like actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 19:42:29 +02:00
Jens 4c150f64a1 enh: Add screen-aware viewport sizing (#1198)
## ℹ️ Description

- Link to the related issue(s): PR #1193 follow-up
- Adds screen-aware humanization viewport selection so randomized
desktop sizes fit the available browser-reported screen area while
preserving permissive config validation.

## 📋 Changes Summary

- Add larger common desktop viewport defaults: `2560x1440`, `1920x1200`,
`1728x1117`, and `1512x982`.
- Probe browser-reported CSS-pixel screen metrics at launch time using
`window.screen.availWidth` / `window.screen.availHeight`.
- Filter configured viewport base sizes against available screen
dimensions, apply bounded jitter, and preserve explicit `--window-size`
overrides.
- Omit automatic `--window-size` when no configured size fits, screen
metrics are unavailable, the launch is headless, or no real
display/window manager is available; explicit user `--window-size`
remains preserved.
- Cache successful probe metrics and bound the probe with the
`chrome_remote_probe` timeout.
- Update generated default config and German translations.
- Add unit coverage and real-browser integration coverage for viewport
probing/filtering, headless/no-display skips, probe timeout/cache
behavior, and `create_browser_session()`.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Browser sessions now support screen-aware viewport selection
automatically when viewport randomization is enabled.
* Expanded the default set of candidate viewport sizes used for
randomized sizing.
* **Bug Fixes**
* Viewport sizing now filters out candidates that don’t fit within the
available screen area.
  * If a window size is already specified, it’s preserved.
* **Tests**
* Added/expanded slow integration tests for screen probing and viewport
fitting.
* Expanded unit tests for viewport filtering, jittering, fallback
behavior, and probe reuse/timeout.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 16:40:17 +02:00
Jens a051712dc8 fix: handle portal combobox attributes (#1195)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Fixes publishing for special-attribute button comboboxes whose option
list opens via a fuller click sequence or renders outside the control
parent element.

## 📋 Changes Summary

- Dispatch mouseup and click in addition to the existing pointer/mouse
down events when opening button comboboxes.
- Add document-level listbox/menu fallback lookup for portal-rendered
combobox popups.
- Normalize option text whitespace before text fallback matching.
- Add regression coverage for the combobox JS contract and no-options
failure diagnostics.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Bug Fixes**
* Improved dropdown/combobox selection reliability, including better
support for portal-rendered menus and document-level listbox/menu
fallbacks.
* Made option matching more tolerant by normalizing whitespace and
applying consistent case handling.
* Reduced selection failures by using a more complete native interaction
sequence when opening and choosing options.
* Enhanced unit coverage to validate the injected selection script and
clearer timeout error messaging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 14:48:00 +02:00
Jens 1bb8adc2cf feat: show filenames in status output (#1196)
## ℹ️ Description

Show the full ad filename in `pdm run app status` output so status rows
can be traced back to their source ad files.

- Link to the related issue(s): none
- Motivation: the status table currently shows ID, title, and status,
but not the source filename, which makes it harder to identify which ad
file a row came from.

## 📋 Changes Summary

- Add a filename field to status rows, populated from the relative ad
file path.
- Render a `Filename` column in the status table.
- Add the German translation for the new status table header.
- Update status rendering unit tests for the new column.
- No new dependencies or configuration changes.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Added a **Filename** column to the status table, displaying the
relative ad file path for each ad.

* **Tests**
* Updated automated tests to reflect the new table layout, verify
correct filename values, and adjust ANSI formatting checks to account
for the added column.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 14:13:52 +02:00
Jens 807633a6cc chore: remove CodeRabbit keyword ignores (#1194)
## ℹ️ Description

- Link to the related issue(s): N/A
- Remove CodeRabbit title keyword ignores so PRs with matching title
words are no longer skipped automatically.

## 📋 Changes Summary

- Removed `ignore_title_keywords` from `.coderabbit.yaml`.
- No runtime dependencies, configuration requirements, or application
behavior changes introduced.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] Chore / configuration maintenance

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [ ] I have tested my changes and ensured that all tests pass (`pdm run
test`). Not run — CodeRabbit config-only change.
- [ ] I have formatted the code (`pdm run format`). Not run — YAML line
deletion only.
- [ ] I have verified that linting passes (`pdm run lint`). Not run —
CodeRabbit config-only change.
- [x] I have updated documentation where necessary. No documentation
updates needed.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated automated review settings so keyword-based exclusions are no
longer used.
* Review behavior now relies on existing draft and label checks, which
may result in more consistent PR review coverage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-03 12:55:54 +02:00
Jens 4cc35cbd9f docs: clarify download shipping inference (#1192)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Clarifies what `download` can infer for shipping options after the old
cheapest-option limitation became inaccurate.

## 📋 Changes Summary

- Replaces the outdated README limitation with the current download
shipping inference behavior.
- Documents pickup handling, `Versand möglich` without price, visible
shipping prices, inferred `shipping_options`,
`download.include_all_matching_shipping_options`, and `sell_directly`
scope.
- No dependency or configuration changes.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated the README “download” limitations to clarify that shipping
details are inferred dynamically.
* Added a detailed explanation of how pickup vs. “Versand möglich”
listings are handled, how “shipping from” prices map to shipping costs
and options, and how direct-buy eligibility is determined.
* Documented the option to include all matching non-excluded shipping
options during download.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-01 18:25:26 +02:00
Jens eda06334d1 docs: generate README usage section (#1191)
## ℹ️ Description

- Link to the related issue(s): N/A
- Keeps the README Usage command block in sync with the CLI help by
generating it as part of the artifact pipeline.

## 📋 Changes Summary

- Added a README Usage generator that renders normalized English CLI
help between explicit README markers.
- Added `generate-readme-commands` and wired it into
`generate-artifacts`.
- Extended generated artifact checks to detect README Usage drift
without mutating files.
- Added focused unit tests for marker replacement, locale/executable
normalization, ANSI stripping, and command coverage.
- No dependency changes introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Kept the README “Usage” section automatically synchronized with the
app’s CLI help.
* Added a new `--preserve-local-settings` option to the CLI help output.

* **Bug Fixes**
* Improved and clarified CLI command/option descriptions (notably around
`verify`, `extend`, and `update` behavior).
* CI now fails early if the generated README “Usage” content is out of
date.

* **Documentation**
* Regenerated the README “Usage” console block to reflect the latest
help text and command details.
* Updated agent guidance to match the expanded README generation
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-01 16:36:03 +02:00
Jens 59185b8f6e docs: shorten pip-only install note (#1189)
## ℹ️ Description

- Link to the related issue(s): N/A
- Shortens the pip-only install note in the README so it is a side note
rather than prominent guidance.

## 📋 Changes Summary

- Condensed the pip-only note from a long block to two lines.
- No dependencies, configuration changes, or additional requirements
introduced.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [ ] I have tested my changes and ensured that all tests pass (`pdm run
test`). Not run; documentation-only change.
- [ ] I have formatted the code (`pdm run format`). Not run;
documentation-only change.
- [ ] I have verified that linting passes (`pdm run lint`). Not run;
documentation-only change.
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.
2026-07-01 09:18:20 +02:00
Jens f2da0355d5 fix: classify Auth0 IP range block (#1188)
## ℹ️ Description

- Link to the related issue(s): Issue #1120
- Classifies Kleinanzeigen IP-range block pages explicitly when Auth0
post-submit verification remains on the password URL.
- This makes the failure actionable without changing login/captcha
behavior or attempting to bypass the block.

## 📋 Changes Summary

- Add `IP_RANGE_BLOCKED` to post-submit Auth0 state classification when
the password-page DOM contains the Kleinanzeigen IP-range block heading.
- Keep existing classification facts such as `STILL_ON_PASSWORD_PAGE`,
so combined states remain visible.
- Keep detection scoped to the Auth0 password page and best-effort.
- Add focused unit tests for detection, gating, resilience, and
coexistence with Auth0 inline errors.
- No new dependencies, configuration changes, schema changes, or
translation updates.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test tests/unit/test_login_flow.py tests/unit/test_translations.py`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved sign-in diagnostics to detect when login is stalled on the
password step due to an IP-range restriction.
* Added support for preserving existing login-state facts when the
IP-block page probe fails, and for correctly combining IP-block signals
with inline error states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-01 07:59:25 +02:00
Jens ce0c5fa5b5 fix: handle DOM retry and button combobox selection (#1186)
## ℹ️ Description

- Link to the related issue(s): Issue #1184
- Fixes two publish-flow failures reported in issue #1184:
- persistent nodriver `ProtocolException(code=-32601)` could spin
forever in `web_await()` while repeatedly re-attaching the CDP session;
- special-attribute button comboboxes in the Handy & Telefon category
needed open/read/select to happen atomically in one browser execution.

## 📋 Changes Summary

- Bound `web_await()` CDP reattach retries by the configured timeout and
add a short nonzero retry delay to avoid event-loop starvation.
- Update special-attribute button-combobox selection to use one async
`web_execute()` script that opens via pointer/mouse events, discovers
options, matches API values, and clicks the selected option before the
dropdown can close.
- Add regression tests for bounded `-32601` retry behavior and atomic
button-combobox selection.
- Update the German translation for the enhanced combobox failure
message.
- Verified with the live `button-combobox` DOM probe.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

## Validation

- `pdm run format`
- `pdm run lint`
- `pdm run test` — 1370 passed, 4 skipped
- `pdm run python data/verify_dom_assumptions.py run --yes --probe
button-combobox --report
data/.temp/dom-assumptions-issue-1184-button-combobox.json` — passed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved dropdown/combobox option selection with stronger matching and
more informative timeout/error reporting.
* Tightened CDP re-attach retry logic to strictly follow the remaining
timeout budget.
* Added a startup runtime warning if the required nodriver CDP re-attach
patch isn’t detected.

* **Documentation / Chores**
* Updated source installation guidance (pip-only users) to ensure the
nodriver patch is applied.
* Made the nodriver fix script marker-based and idempotent, including
normalization of legacy patched installs.

* **Tests**
* Expanded unit tests for combobox selection, re-attach timeout/bounds
behavior, CLI patch warning behavior, and the nodriver fix script.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 10:39:41 +02:00
Jens 27b47f7561 fix: capture Auth0 post-submit diagnostics (#1183)
## ℹ️ Description

- Link to the related issue(s): Issue #1120
- Capture diagnostics when Auth0 post-submit verification remains
inconclusive, so future failures include a DOM/screenshot bundle plus
classified metadata.

## 📋 Changes Summary

- Reuse existing login-detection diagnostics for the final Auth0
post-password-submit timeout.
- Add sanitized JSON metadata with `event`, `classification`, and
`page_url`.
- Keep diagnostics best-effort so capture failures do not mask the
original classified timeout.
- Add focused tests for enabled/disabled capture, privacy sanitization,
failure preservation, and diagnostics argument forwarding.
- No new configuration, schema, translation, captcha, or identifier-flow
changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test tests/unit/test_login_flow.py tests/unit/test_translations.py`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. (Not needed.)

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved login timeout handling with clearer error messages and safer
URL reporting.
* Added fallback diagnostics during failed post-login verification,
while preserving the original timeout if diagnostics capture fails.
  * Prevented sensitive URL parts from appearing in error reports.

* **Tests**
* Expanded coverage for login diagnostics forwarding and timeout
behavior with diagnostics enabled and disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-29 10:40:04 +02:00
Jens 4acbc09fac fix: classify Auth0 post-submit uncertainty (#1181)
## ℹ️ Description

- Link to the related issue(s): Issue #1120
- Adds focused diagnostics/classification for the post-password-submit
Auth0 uncertainty path. This keeps the existing login/captcha/identifier
flow stable while making the new failure mode easier to classify from
logs.

## 📋 Changes Summary

- Classifies final post-submit uncertainty as still on the Auth0
password page, visible Auth0 error text, verification/MFA-like state, or
unknown redirect/session inconclusive state.
- Logs sanitized diagnostic context and raises clearer classified
messages while preserving the existing timeout prefix.
- Adds focused unit tests for the new classifications and snippet
sanitization.
- Updates German translations for new translatable messages.
- No new dependencies or configuration changes.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling when transitioning after Auth0 password submission
by using safer, sanitized diagnostic URLs.
* Added clearer, classification-aware timeout messages when the next
verification step can’t be identified.
* Strengthened detection of post-Auth0 states, including inline Auth0
errors and MFA/verification prompts, with more graceful degradation on
probing failures.

* **Tests**
* Expanded unit tests for post-submit state classification, URL
sanitization, and improved timeout/error-message expectations, including
resilience to probe/URL retrieval failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-27 00:50:04 +02:00
Jens 1c34da9298 feat: show APR status indicators (#1180)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Adds display-only APR indicators to the local status table so users
can see repost/update APR state without running publish/update flows.

## 📋 Changes Summary

- Adds optional `APR repost` and `APR update` columns to
`kleinanzeigen-bot status` when APR is effectively enabled for at least
one active ad.
- Reuses existing `evaluate_auto_price_reduction()` logic for REPLACE
and MODIFY modes; does not apply or mutate APR decisions.
- Keeps APR output plain while preserving existing status color
behavior.
- Adds German translations and unit coverage for rendering, evaluator
call modes, guardrails, and mutation safety.
- No new dependencies or configuration changes.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1344 passed, 4 skipped).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Status tables now show optional APR-related columns for repost and
update pricing decisions when available.
* APR results are displayed in a compact, easy-to-read format such as
due, not due, error, or off.

* **Bug Fixes**
* Improved status table layout and spacing so translated status text
fits more reliably.
* Status summaries and row rendering now handle coloured and uncoloured
cells more consistently.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 15:45:28 +02:00
Jens 25a379fe67 feat: colorize status output (#1178)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Adds optional color to the `kleinanzeigen-bot status` table using the
existing Colorama dependency.
- Keeps default status behavior local/read-only and gates ANSI output
via terminal/env policy.

## 📋 Changes Summary

- Adds `utils.color.should_use_color()` with `NO_COLOR` > `FORCE_COLOR`
> TTY detection.
- Colors only the status column while preserving plain ASCII layout and
summary output.
- Wires status rendering to the color gate in `_handle_status()`.
- Removes legacy `colorama.init()` from `update_checker.py`; package
init already enables Windows ANSI support.
- Adds unit tests for color gating and colored status rendering.
- No new dependencies or configuration changes.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`: 1332 passed, 4 skipped).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Status output can now be shown with optional terminal colors for
improved readability.
* Color display automatically follows your terminal and environment
settings.

* **Bug Fixes**
* Status tables keep proper alignment even when colored text is enabled.
  * Unknown statuses remain uncolored, avoiding inconsistent display.

* **Tests**
* Added coverage for color handling and status rendering to improve
reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 13:48:53 +02:00
Jens 50a4b570cf feat: add ad status command (#1177)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Adds a CLI status overview inspired by useful lifecycle features from
kleinanzeigen-bot-ui, while keeping this project CLI-only.
- The new command helps users see local ad state at a glance without
replacing the existing verify/APR preflight flow.

## 📋 Changes Summary

- Add `kleinanzeigen-bot status` with an ASCII table and summary for
local ad states: disabled, draft, changed, due, and published-local.
- Add shared pure helpers for content-change and republication-due
checks.
- Add a small neutral `load_ad_configs()` loader reused by `load_ads()`
and `status` without broad loader redesign.
- Add German translations and focused unit/guardrail tests.
- No schema/config model changes or generated artifacts.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

Validation:
- `pdm run format` passed
- `pdm run lint` passed
- `pdm run test`: 1315 passed, 4 skipped; coverage 93.18%


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a new `status` command to view the current state of configured
ads.
* Introduced a status summary that shows whether ads are disabled,
drafts, changed, due, or published locally.
* Added cleaner handling for ad loading and republication checks,
improving consistency in displayed results.
* **Bug Fixes**
* Improved change detection for ads, including better handling when
stored data is missing or empty.
  * Enhanced due-date checks so timing-based status is more reliable.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 12:23:26 +02:00
Jens d5d596d481 fix: target visible Auth0 submit button and broaden captcha detection (#1120) (#1176)
## ℹ️ Description

Fixes #1120 — Auth0 login fails after email submit because the bot
clicks a hidden submit button and cannot detect non-reCAPTCHA captcha
providers.

**Root causes:**
1. **Hidden submit button** — Auth0 renders two `button[type='submit']`
elements. The hidden one (`class=ulp-hidden-form-submit-button`,
`aria-hidden=true`) appears first in DOM. `querySelector()` was matching
it instead of the visible primary button.
2. **Captcha detection too narrow** — Only matched reCAPTCHA v2 iframes.
Kleinanzeigen.de uses Cloudflare Turnstile via Auth0 v2
(`data-captcha-provider='auth0_v2'`), which renders inline without an
iframe.

**Evidence:** @jochenberger provided HTML from the identifier page
confirming both issues. User CAN manually click 'Weiter' and login
succeeds.

## 📋 Changes Summary

- Add `_click_auth0_submit()` helper using
`[data-action-button-primary='true']` to target the visible button
- Broaden captcha detection: iframe selector covers 6 providers,
container selector uses `[data-captcha-provider]` + substring class
matching
- Simplify `handle_identifier_captcha_state()` with Turnstile token wait
via JS property check
- Use `_click_auth0_submit()` for both identifier and password submits
- Add Auth0 provider provenance comments to selectors

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved captcha/challenge detection to support both iframe-based
providers and Auth0 container-based challenges.
- Updated the Auth0 login flow to click the correct visible Auth0
primary submit button, avoiding hidden controls for identifier and
password steps.
- Centralized captcha detection timeout to make challenge handling more
consistent.

- **Tests**
- Expanded and tightened unit tests for the Auth0 identifier-step
captcha/submit behavior, including selector expectations and
control-flow scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 07:50:00 +02:00
Jens 8e70d0411d refactor: decompose WebScrapingMixin (static helpers, diagnostics, exception annotations) (#1174)
## ℹ️ Description

Decompose `WebScrapingMixin` — incremental internal refactors to reduce
class size and improve separation of concerns.

## 📋 Changes Summary

### Static method extraction
- Extract 5 `@staticmethod` members from `WebScrapingMixin` to
module-level private functions in `web_scraping_mixin.py`
- Update internal call sites and test call sites (no compatibility
aliases)

### Browser diagnostics extraction
- Create `src/kleinanzeigen_bot/utils/browser_diagnostics.py` with
module-level diagnostic functions
- `WebScrapingMixin.diagnose_browser_issues()` → thin delegator
(resolves `self` attrs, calls extracted logic)
- Replace `self`-coupled logic with explicit parameter dependencies
- Migrate translations to new module section in `translations.de.yaml`

### Exception annotations
- Add `# noqa: BLE001` + justification comments to 4 bare `except
Exception:` blocks (no behavior changes)

### ⚙️ Type of Change
- [x] 🔧 Refactor (internal code improvements, no behavior changes)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`) — 1287 passed, 4 skipped
- [x] I have formatted the code (`pdm run format`)
- [x] I have verified that linting passes (`pdm run lint`) — ruff, mypy,
pyright clean
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added enhanced browser troubleshooting with remote-debugging
connectivity checks, running-process inspection, and Chrome/Edge
compatibility validation including recommended configuration updates.
* **Refactor**
* Reorganized browser/session diagnostics and remote-debugging argument
handling into dedicated utilities; introduced a shared browser runtime
configuration container.
* **Chores**
* Suppressed linter warnings for intentionally broad exception handling.
* **Tests**
* Updated unit and integration tests to match the new diagnostics flow
and remote-debugging probing behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-24 10:36:42 +02:00
Jens b2dce90ef1 refactor: remove dead wrapper methods from KleinanzeigenBot (#1172)
## ℹ️ Description

Remove four private wrapper methods on `KleinanzeigenBot` in `app.py`
that wrap public module-level functions but have zero production call
sites. This is a cleanup-only change with no behavioral impact.

## 📋 Changes Summary

- **Remove** `_has_logged_in_marker` — delegates to
`login_flow.has_logged_in_marker`
- **Remove** `_check_publishing_result` — delegates to
`publishing_workflow.check_publishing_result`
- **Remove** `_delete_old_ad_if_needed` — delegates to
`publishing_workflow.delete_old_ad_if_needed`
- **Remove** `_capture_login_detection_diagnostics_if_enabled` —
delegates to `login_flow.capture_login_detection_diagnostics_if_enabled`
- **Remove** unused `Literal` import
- **Retarget** 4 helper-detail tests that called the deleted wrapper
directly to the real
`login_flow.capture_login_detection_diagnostics_if_enabled` function

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized login detection diagnostics functionality for improved
accessibility
* Simplified login status verification logic within the authentication
system

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 21:37:17 +02:00
Jens dedf4cb4f0 refactor: extract template name renderer from AdExtractor (#1171)
## ℹ️ Description

Extract the priority-based template rendering algorithm from
`_render_download_name_with_budget` into a module-level pure function
`_render_name_with_budget`. The method becomes a thin wrapper that
delegates rendering and handles truncation warnings.

## 📋 Changes Summary

- Added `_render_name_with_budget(template, ad_id, title, max_length) ->
tuple[str, bool, bool]` as a module-level pure function with no
AdExtractor dependency
- Rewrote `_render_download_name_with_budget` as a thin wrapper that
calls the pure function and handles logging
- Removed `_reserved_for_pending_placeholders` static method (logic
inlined in the pure function)

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved download name handling with enhanced truncation logic for
improved filename generation.
* Added warnings when download names are automatically shortened due to
length constraints.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 20:57:25 +02:00
Jens 1f142f8339 refactor: extract selector and filter helpers from load_ads (#1169)
## ℹ️ Description

Extract three private helpers from `load_ads()` to remove the last
remaining `# noqa: PLR0915` annotation in main source. `load_ads` goes
from ~100 statements to ~35 — a clean orchestration function.

## 📋 Changes Summary

- **`_parse_ad_selector`**: parse `ads_selector` string into numeric IDs
list or frozenset of tokens
- **`_should_include_ad`**: token-based ad filtering
("changed"/"new"/"due"/"all") — preserves exact ordering, side effects,
and "changed"/price-reduction-for-update logic
- **`_prepare_selected_ad_entry`**: description validation, category
resolution, image resolution — called only after filtering to avoid
validating skipped ads
- Translation keys moved to match new caller function names
(`_should_include_ad:`, `_prepare_selected_ad_entry:`)
- Added regression test: inactive ad with unresolvable images is skipped
without error

### ⚙️ Type of Change
- [x] ✨ New feature (adds new functionality without breaking existing
usage)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized ad loading process with improved selector parsing and
filtering logic for better code maintainability.
* Optimized validation workflow: inactive and non-selected ads now skip
description and image validation, improving overall efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 16:07:04 +02:00
Jens 388d0fb3c8 refactor: separate download ad persistence concerns (#1168)
## ℹ️ Description

Separate downloaded-ad persistence concerns from `download_ad()` in
`extract.py`. The original method mixed browser extraction,
local-settings preservation, and a filesystem transaction — three
independently meaningful and operationally sensitive concerns.

## 📋 Changes Summary

- **Extracted `_preserve_local_settings_from_existing_ad()`** —
preserves local-only settings (repost_count, republication_interval,
auto_price_reduction, price_reduction_count) from an existing ad YAML
into the new config. No-op unless `preserve_local_settings` is enabled.
- **Extracted `_commit_staged_download()`** — owns the full filesystem
transaction: YAML save, backup collision check, rename
final→backup→staging→final, backup cleanup, and rollback on failure.
- **Simplified `download_ad()`** — now a clean 3-step orchestrator:
extract → preserve → commit.
- **Restructured translation keys** in `translations.de.yaml` to follow
the new caller function names.

No behavior changes to preservation, rollback, YAML handling, or
exception propagation.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix
- [ ] ✨ New feature
- [ ] 💥 Breaking change

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Enhanced ad download process with improved backup and rollback
mechanisms for better reliability
* Refined handling of local settings preservation during re-downloads to
maintain data integrity

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 15:31:13 +02:00
Jens eeeae6ea5e refactor: extract shipping helpers from set_shipping (#1167)
## ℹ️ Description

Refactor `set_shipping` in `publishing_form.py` by extracting four
distinct concerns into focused private helpers. Simplifies a
~55-statement function into a ~20-statement orchestrator with a flat
if-return chain.

## 📋 Changes Summary

- Extract `_select_shipping_combobox_if_present` — PRO/commercial
combobox handling
- Extract `_set_pickup_shipping` — PICKUP radio + fieldset fallback
- Extract `_set_configured_shipping_options` — shipping options dialog +
MODIFY nav
- Extract `_enable_platform_default_shipping` — enable shipping toggle
- Keep legacy `shipping_costs` guard inline in `set_shipping` (per
review)
- Re-key translation entries under new caller-function names (duplicate
"Failed to set shipping..." under both combobox and pickup helpers)
- `set_shipping` slimmed from ~55 → ~20 ruff-counted statements

### ⚙️ Type of Change
- [ ] 🐞 Bug fix
- [ ] ✨ New feature
- [ ] 💥 Breaking change

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Refactor
* **Shipping configuration**: Refactored the shipping configuration
logic to improve internal code structure and maintainability, enabling
more reliable handling of shipping options across different scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 13:16:37 +02:00
Jens 780743aee2 refactor: separate download_ads workflow modes (#1166)
## ℹ️ Description

Extract four private async helpers from `download_ads()` to separate
per-mode workflow logic.

- Motivation: Reduce function complexity, enable tighter pylint
thresholds

## 📋 Changes Summary

- Extracted `_fetch_published_ads_by_id` — fetch and build published ads
lookup dict
- Extracted `_download_all_ads` — download all ads from overview page
- Extracted `_download_new_ads` — download only unsaved ads (preserving
URL dedup behavior)
- Extracted `_download_ads_by_ids` — download specific ads by numeric ID
(preserving distinct warning semantics)
- Slimmed `download_ads()` to ~48-line orchestrator (was ~124 lines)
- Restructured `translations.de.yaml` — messages moved to per-function
sections
- Ratcheted pylint thresholds: max-statements 90→76, max-branches 35→31

### ⚙️ Type of Change
- [ ] 🐞 Bug fix
- [ ] ✨ New feature
- [ ] 💥 Breaking change

Internal refactor, no user-visible behavior change.

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`): 1286 passed, 4 skipped
- [x] I have formatted the code (`pdm run format`)
- [x] I have verified that linting passes (`pdm run lint`)
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Strengthened code quality standards and refactored internal ad
downloading logic for improved maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 11:28:23 +02:00
Jens eaa646874d refactor: replace get_message_template match with dict lookup (#1165)
## ℹ️ Description

Replace the ~115-arm match/case in `__get_message_template` with a
module-level `_MESSAGE_TEMPLATES` dict of raw English strings plus a
3-line lazy `_(template)` lookup function.

- **Motivation**: Eliminate 5 complexity `noqa` annotations (C901,
PLR0911, PLR0912, PLR0915, E701) by removing the real architectural
complexity — a pure flat error-code → message mapping that has zero
branching or logic.
- **Approach**: A `dict[str, str]` of raw English strings (no `_()`
calls at import time) with lazy translation inside
`__get_message_template`. `"custom_error"` intentionally omitted —
handled by `.get()` default.
- **Translation**: Caller function name unchanged → translation keys
under `__get_message_template` preserved. AST-based translation test
extractor updated to recognize `_MESSAGE_TEMPLATES` dict values.

## 📋 Changes Summary

- `src/kleinanzeigen_bot/utils/pydantics.py`: Replaced ~115 match arms
with `_MESSAGE_TEMPLATES` dict + 3-line function; removed all 5 noqa
annotations
- `tests/unit/test_translations.py`: Added AST extraction for
`_MESSAGE_TEMPLATES` dict values to maintain translation coverage checks

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved error message handling system to support better
internationalization and translation workflows

* **Tests**
* Updated test suite to extract and verify translatable message strings

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 07:12:23 +02:00
Jens 32c6d3985f refactor: split published ads pagination (#1164)
## ℹ️ Description

Extract content-decoding and paging-navigation logic from
`fetch_published_ads` into focused module-level helpers, reducing
function complexity while preserving all behavior.

## 📋 Changes Summary

- **`published_ads.py`**: Extracted `_parse_published_ads_page()`
(content decode, JSON parse, payload validation, ad filtering) and
`_determine_next_page()` (paging metadata resolution,
last-page/empty-ads guards). Main `fetch_published_ads` is now a clean
orchestration loop: request → response check → parse → extend →
paginate.
- **`translations.de.yaml`**: Split the single `fetch_published_ads:`
translation section into three sections (`fetch_published_ads:`,
`_parse_published_ads_page:`, `_determine_next_page:`) matching the new
caller function names. Added missing translation for "No paging dict
found on page %s".
- **`pyproject.toml`**: Lowered `max-statements` from 115 to 90.
- **`pydantics.py`**: Added `PLR0915` noqa for `__get_message_template`
(already an approved exception for the generated pydantic error-code
mapping).

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved published-ads fetching by separating response decoding and
pagination handling into dedicated helpers, while preserving existing
behavior.
* Reworked strict-mode validation and stopping conditions for clearer
control flow and more consistent error handling.

* **Chores**
  * Updated linting limits to enforce stricter code quality.
  * Added targeted lint suppression for an overly complex section.

* **Tests**
* Expanded unit test coverage for published-ads content decoding and
strict-mode failure cases across multiple malformed/edge responses.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-21 22:39:36 +02:00
Jens b5e2769f2d refactor: extract special attribute helpers from publishing form (#1161)
## ℹ️ Description

Extract three module-level helpers from `set_special_attributes()` in
`publishing_form.py` to reduce local variable count and improve
readability. `set_special_attributes` had 27 locals — after extraction
it has 10.

- Behavior-preserving: no change to exception propagation, logging, or
control flow.
- `condition_s` probe stays in main to avoid translation-key churn.

## 📋 Changes Summary

- Extract `_build_special_attribute_xpath()` — Pure XPath construction
(fix comment: five → six patterns)
- Extract `_resolve_special_attribute_element()` — Wraps `web_find_all`
+ candidate pick, propagates exceptions uncaught
- Extract `_set_special_attribute_input()` — 6-branch input-type
dispatch; hidden-input returns without logging, caller logs once
- Rewire `set_special_attributes` to call the three helpers
- Ratchet `pyproject.toml`: PLR0914 max-locals 27 → 20
- Update `translations.de.yaml`: move 4 debug messages + add `Failed to
set attribute` under `_set_special_attribute_input`

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] ♻️ Refactor (non-breaking restructuring of existing code)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`). → 1268 passed, 4 skipped
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. → Translation file
updated


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Chores**
* Tightened linting rules by lowering the allowed maximum number of
local variables.

* **Refactor**
* Reworked special-attribute DOM handling to use clearer selection and
value-setting paths for different field types, including improved
fallback behavior.

* **Tests**
* Added async unit test coverage for `set_special_attributes`, including
input fallback behavior and error cases for unsupported checkbox values
and missing hidden input metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-21 19:36:56 +02:00
Jens d542f1ea92 feat: use configured --remote-debugging-host for browser diagnostics probes (#1160)
## ℹ️ Description

`diagnose_browser_issues()` previously hardcoded `127.0.0.1` for
port-open and `/json/version` API probes. When Chrome's remote debugging
is exposed on a different host (Docker, LAN, CI), the diagnostics miss
the running browser entirely.

This PR makes diagnostics host-aware by parsing
`--remote-debugging-host` from `browser_config.arguments` and using the
configured host instead of the hardcoded `127.0.0.1`. All diagnostics
probes (port, API, Chrome version) now respect the configured host.

## 📋 Changes Summary

- Added `_diagnostic_remote_debugging_endpoint()` — private diagnostics
parser with first-wins semantics for both host and port, defaults to
`127.0.0.1:0`
- Added `_format_url_host()` — IPv6-safe URL bracketing helper
- Threaded configured host into `net.is_port_open()`,
`_remote_debugging_api_browser()`, and
`_diagnose_chrome_version_issues()`
- Emit `LOG.warning` before probing a non-loopback configured host
(security/documentation guard)
- Updated German translations for changed log message strings
- Added tests: configured host `10.0.0.5` and `192.168.1.100`,
host-after-port ordering, IPv6 URL formatting, Chrome-version host
propagation
- Default behavior (no `--remote-debugging-host` argument) unchanged —
probes remain `127.0.0.1`

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved remote browser debugging diagnostics to work with non-local
hosts, including correct IPv6-safe URL formatting.
* Added host-aware reachability checks and clearer warnings when the
configured remote host may not be loopback.
* Enhanced diagnostic messaging to report the configured host and port
during remote debugging checks.

* **Tests**
* Expanded unit test coverage for configured remote-debugging hosts
(including IPv6) and updated expectations for host-specific diagnostic
outputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-21 09:26:52 +02:00
Jens d384f172b8 fix: remove individual shipping publishing (#1159)
## ℹ️ Description

- Link to the related issue(s): Issue #1157
- Kleinanzeigen no longer offers individual/custom shipping. This
updates publishing, validation, docs, schemas, translations, and tests
so existing `shipping_costs` configs are handled gracefully instead of
failing against removed DOM controls.

## 📋 Changes Summary

- Keep `shipping_costs` for legacy configs/downloaded ads, but mark it
deprecated in schema/docs and ignore it during publishing.
- Remove publishing interaction with removed individual-shipping DOM
controls.
- Require predefined non-empty `shipping_options` for `sell_directly`.
- Update docs, generated schemas/config artifacts, German translations,
and unit tests.
- Verified live DOM for `sell_directly: true` + `shipping_type:
SHIPPING` + predefined `shipping_options` via local ignored script; no
ad submission performed.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated shipping guides to deprecate custom individual shipping costs
and make publishing depend on per-ad predefined `shipping_options`
(DHL/Hermes; selectable options must come from a single size group).
* Added migration guidance replacing `shipping_costs` with the correct
`shipping_options`.
* Clarified “Sell directly” requires `shipping_type: SHIPPING`,
non-empty `shipping_options`, and `FIXED`/`NEGOTIABLE` pricing.

* **Bug Fixes**
* Publishing is blocked when `shipping_costs` is provided without
`shipping_options`.
* Removed unsupported individual-shipping handling and tightened “Sell
directly” eligibility rules.

* **Tests**
* Expanded unit coverage for deprecation behavior, content handling, and
direct-buy validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-21 08:11:02 +02:00
Jens 5ab4015251 refactor: simplify browser diagnostics (#1158)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Refactor browser diagnostics to reduce the complexity of the
diagnostics orchestration while preserving the generated diagnostics
report and browser workflow behavior.

## 📋 Changes Summary

- Extract factual browser diagnostic checks into focused helpers for
binary detection, remote debugging port parsing, remote API probing,
target browser name detection, and process discovery.
- Keep translated diagnostics logging as direct literal log calls in
`diagnose_browser_issues`.
- Add regression coverage for remote debugging host handling, invalid
ports, first-port precedence, and port `0` pass-through behavior.
- No dependencies, configuration changes, or additional requirements
introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] ♻️ Refactor (no user-facing behavior change)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved browser diagnostics by breaking down remote debugging checks
into clearer steps, enhancing resilience when probing the browser and
inspecting running processes, and refining success vs failure logging.

* **Tests**
* Expanded unit coverage to ensure remote debugging probing always
targets `http://127.0.0.1:<port>`, validates `--remote-debugging-port`
parsing (including invalid and multi-argument cases), and skips remote
debugging when the port is `0` while continuing Chrome version
diagnostics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-20 22:22:21 +02:00
Jens 0b4b9eb397 refactor: split browser session setup (#1156)
## ℹ️ Description

- Link to the related issue(s): N/A
- Split browser session setup into smaller, behavior-preserving helpers
so browser startup orchestration is easier to review and maintain.

## 📋 Changes Summary

- Extract remote debugging connection handling from browser session
creation.
- Extract launch argument, user-data-dir, Nodriver config, profile
preparation, and extension setup helpers.
- Add matching German translation entries for log messages moved into
helper functions.
- Leave lint thresholds unchanged because the diagnostics follow-up
remains separate.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Summary by CodeRabbit

* **Refactor**
* Improved browser session setup by separating remote-debugging
connections from fresh browser startup.
* Hardened launch argument construction and user data directory
resolution for more consistent configuration.
* Improved browser startup failure cleanup to ensure resources are
released reliably.
* Kept troubleshooting output behavior, including better guidance for
debugging scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-20 14:54:15 +02:00
Jens c8fb72ae9e refactor: split command orchestration handlers (#1155)
## ℹ️ Description
- Link to the related issue(s): Issue #
- Refactors the CLI command orchestration boundary so command dispatch
remains visible while command execution lives in focused handlers.

## 📋 Changes Summary
- Split command execution paths in `KleinanzeigenBot.run()` into named
handlers for configuration, verification, update checks, content
hashing, and ad workflows.
- Preserved literal command dispatch cases and command-specific behavior
for update checks, download validation, browser login, and cleanup.
- Moved German translation keys to the new handler caller names for
existing runtime messages.
- Added focused tests for update-check behavior, download validation
ordering, workspace state usage, and translation key placement.
- Lowered the configured branch and cyclomatic complexity limits now
that the command orchestration boundary is simpler.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Strengthened code quality standards by implementing stricter
complexity thresholds in the development configuration.

* **Refactor**
* Reorganized command handler architecture to improve internal code
structure and maintainability.

* **Tests**
* Significantly expanded test coverage to validate command execution,
argument handling, and translation data integrity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-20 12:33:06 +02:00
Jens 75f34775e2 refactor: reduce return-count complexity (#1154)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Reduces return-count complexity in the update checker while preserving
existing update-check behavior.

## 📋 Changes Summary

- Extracted GitHub release selection and commit-ish validation from
`check_for_updates` into a private helper.
- Moved German translation keys for logger calls that now originate from
the helper.
- Ratcheted Ruff/Pylint `max-returns` from 10 to 8.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* The update checker now correctly handles the latest update channel by
excluding pre-release versions and recommending only stable releases.

* **Chores**
  * Code quality configuration settings were adjusted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-20 10:49:22 +02:00
Jens 66cab91898 refactor: reduce return-count complexity (#1153)
## ℹ️ Description

This refactor reduces return-count complexity for the next PLR0911
ratchet while preserving existing behavior.

No related issue.

## 📋 Changes Summary

- Extract German and English pluralization helpers while keeping
translation and prefix handling in `pluralize()`.
- Remove one early return from `check_for_updates()` by sharing the
existing final state update/save path.
- Ratchet Ruff `PLR0911` `max-returns` from 12 to 10.
- Document the one-lower discovery result: `check_for_updates()` is the
next limiter at 10 returns for `max-returns = 9`.
- No dependencies, configuration requirements, documentation updates, or
generated artifacts are introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. No documentation
changes were needed.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized pluralization logic and simplified update-checking control
flow for improved code maintainability.

* **Tests**
* Expanded test coverage for pluralization with additional German and
English language variations.

* **Chores**
  * Updated code quality configuration standards.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 23:15:27 +02:00
Jens e8752ce10e refactor: reduce price reduction complexity (#1152)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Extracts the first complexity hotspot in the ratcheting sequence by
separating the final auto price-reduction application flow from
`apply_auto_price_reduction`.
- This keeps behavior unchanged while allowing the Ruff `max-returns`
transitional limit to move from 13 to 12.

## 📋 Changes Summary

- Extracted `_apply_price_reduction_decision` for the final
price-reduction mutation/logging phase.
- Moved German translations for the relocated `LOG.info` calls to the
new helper function key.
- Lowered `tool.ruff.lint.pylint.max-returns` from 13 to 12.
- No dependency or runtime configuration changes introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Refactor-only change; none of the listed user-facing change types apply.

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Updated linting configuration thresholds for code quality standards.

* **Refactor**
  * Improved internal code organization for better maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 16:19:33 +02:00
Jens 801ed0e223 refactor: remove KleinanzeigenBot re-export from package root (#1150)
## ℹ️ Description

- Link to the related issue(s): N/A
- Removes the unsupported package-root `KleinanzeigenBot` re-export
after tests were moved to their owning modules.
- Updates remaining tests and `conftest.py` to import `KleinanzeigenBot`
from `kleinanzeigen_bot.app` directly.
- Re-enables Ruff's mccabe complexity checks and tightens high Pylint
complexity thresholds left over from the old package-root monolith.

## 📋 Changes Summary

- Removed `KleinanzeigenBot` from `src/kleinanzeigen_bot/__init__.py`.
- Updated all remaining test imports to use `from kleinanzeigen_bot.app
import KleinanzeigenBot`.
- Enabled `C90`/mccabe complexity linting with an explicit
`max-complexity`.
- Lowered the broad Pylint limits for branches, locals, returns,
statements, and public methods.
- Kept the generated/static pydantic error-code mapping explicitly
exempted from complexity checks.
- No runtime behavior, CLI, config, translations, or ad YAML changes.
- No new dependencies.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [x] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized internal module import structure for consistency across
the codebase.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 15:00:45 +02:00
Jens 3c17f3c8dd refactor: move cross-drive fallback test (#1149)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Move the Windows cross-drive fallback coverage from the package-init
test module into the publishing workflow test suite.
- This is a behavior-neutral test organization cleanup.

## 📋 Changes Summary

- Moved the cross-drive auto-price-reduction path fallback test into
`TestAutoPriceReductionDispatch`.
- Deleted the now-empty `tests/unit/test_init.py` and its unused local
helpers.
- No runtime code, CLI behavior, config, translations, or ad YAML
changed.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Reorganized test structure for Windows cross-drive path handling
validation to improve maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 14:29:57 +02:00
Jens 48e88fccd3 refactor: create app test file and move shell tests (#1148)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Move app-shell unit tests into a dedicated test module matching
`src/kleinanzeigen_bot/app.py`.

## 📋 Changes Summary

- Added `tests/unit/test_app.py` for the app-shell test classes and
shared fixture.
- Kept the cross-drive path fallback coverage in
`tests/unit/test_init.py`.
- Updated the moved test module to import `KleinanzeigenBot` from
`kleinanzeigen_bot.app`.
- No runtime, CLI, config, translation, or ad YAML changes.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- Test organization refactor only.

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Added comprehensive unit tests covering command-line subcommands
(including defaulting/fallback behavior for ad selector options) and
unknown-command handling.
* Added assertions for expected initialization behavior and invalid
selector validation (including SystemExit code 2).
* Refined the unit test modules to simplify imports and improve
maintainability, while retaining key cross-drive publish fallback
verification.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 13:54:14 +02:00
Jens d0d8e5461b refactor: move shipping form tests (#1147)
## ℹ️ Description
- Link to the related issue(s): Issue #
- Move the shipping option mapping test to the publishing form test
module, where the browser-level shipping form behavior is owned.

## 📋 Changes Summary
- Moved `test_shipping_options_mapping` from `tests/unit/test_init.py`
to `tests/unit/test_publishing_form.py`.
- Kept the existing assertions and patch targets intact.
- Renamed the remaining `test_init.py` class to describe its cross-drive
path fallback coverage.
- No runtime behavior, CLI, config, translation, or ad YAML changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
  * Reorganized test coverage for publishing and shipping functionality
  * Added tests for Windows cross-drive path fallback behavior
  * Added tests for shipping options mapping verification

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 13:00:36 +02:00
Jens 824ff70e43 refactor: move publishing result tests (#1146)
## ℹ️ Description
- Link to the related issue(s): N/A
- Moves a focused set of publish-ad orchestration/result tests out of
the package initialization test module and into the publishing workflow
test module.

## 📋 Changes Summary
- Move pre-submit timeout retry coverage into
`tests/unit/test_publishing_workflow.py`.
- Move WANTED shipping delegation coverage into the publishing workflow
tests.
- Move auto price reduction dispatch coverage into the publishing
workflow tests.
- Remove now-unused local fixture/imports from
`tests/unit/test_init.py`.
- No runtime, CLI, config, translation, or ad YAML changes.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] Refactor / test organization only

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
  * Adjusted test coverage for internal functionality validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 12:25:01 +02:00
Jens 01e764a477 refactor: move publish uncertainty tests (#1145)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Moves publish-ad post-submit uncertainty tests into the publishing
workflow test module so `test_init.py` keeps shrinking toward
initializer-focused coverage.

## 📋 Changes Summary

- Moved the post-submit uncertainty `publish_ad()` test slice into
`tests/unit/test_publishing_workflow.py`.
- Kept the pre-submit retryability test and unrelated
initializer/CLI/login/shipping/ad-management tests in
`tests/unit/test_init.py`.
- Adjusted test imports only; no runtime behavior, configuration,
translation, or YAML changes.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Reorganized test coverage for publishing workflows, expanding
validation of post-submit error handling and confirmation fallback
mechanisms to ensure robust publication behavior under various
scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 11:40:26 +02:00
Jens f54521b8e6 refactor: move publishing diagnostics tests (#1144)
## ℹ️ Description

- Link to the related issue(s): N/A
- Move the publish failure diagnostics tests out of the
initializer-focused unit test module and into the publishing workflow
unit test module.
- This is a behavior-neutral test organization change.

## 📋 Changes Summary

- Moved `TestKleinanzeigenBotDiagnostics` from `tests/unit/test_init.py`
to `tests/unit/test_publishing_workflow.py`.
- Kept existing assertions and publishing workflow patch targets
unchanged.
- Removed stale imports from `tests/unit/test_init.py`.
- No runtime, CLI, config, translation, or ad YAML changes.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Enhanced test coverage for diagnostics capture during publishing
failures, including verification of log file handling and behavior when
diagnostics are disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 11:14:37 +02:00
Jens 2332682d07 refactor: move publishing orchestration tests (#1143)
## ℹ️ Description

- Link to the related issue(s): Issue #977
- Move another focused set of publishing/update orchestration tests out
of `tests/unit/test_init.py` and into
`tests/unit/test_publishing_workflow.py`.
- This is a behavior-neutral test organization change; no runtime, CLI,
config, translation, or ad YAML behavior changes are intended.

## 📋 Changes Summary

- Moved display counter progression coverage into the publishing
workflow test module.
- Moved publish-ads orchestration coverage into a dedicated publishing
workflow test class.
- Cleaned up imports left behind in `tests/unit/test_init.py`.
- Dependencies/configuration/additional requirements: none.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

Test-only refactor; none of the listed runtime change categories apply.

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. Not applicable;
test-only relocation.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Improved the ad publishing workflow test coverage by reorganizing
publish/cleanup, retry behavior, and “no-retry” scenarios.
* Added new async testing utilities (including a reusable mocked page)
to better simulate browser interactions.
* Expanded regression coverage for processing/counter progression across
publishing and updating, including paused ads and “not found” cases.
* Removed several redundant or replaced counter/retry/skip tests from
the prior initialization test module.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 10:40:02 +02:00
Jens 29aed68706 refactor: move publishing workflow tests (#1142)
## ℹ️ Description

- Link to the related issue(s): Follow-up to #1141
- Move a publishing/update orchestration test slice into the publishing
workflow test module.
- Share duplicated test fixtures/helpers that are now used by the
relocated publishing workflow tests and existing related tests.
- This remains behavior-neutral test organization only.

## 📋 Changes Summary

- Added `tests/unit/test_publishing_workflow.py` for publishing workflow
orchestration tests.
- Moved `TestKleinanzeigenBotUpdateAdsResilience` out of
`tests/unit/test_init.py`.
- Updated the relocated tests to import `KleinanzeigenBot` directly from
`kleinanzeigen_bot.app`.
- Moved the duplicated `base_ad_config` fixture into
`tests/conftest.py`.
- Extracted shared `build_update_ad` and `build_published_ads` test
helpers into `tests/conftest.py`.
- Removed duplicate fixture/helper definitions from affected unit test
files.
- No runtime behavior, CLI output, config parsing, translations, or ad
YAML handling changes.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] Maintenance/refactor (tests only, no runtime behavior change)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Added comprehensive tests for ad publishing resilience and
orchestration, covering retry mechanisms, error handling, timeout
scenarios, and failure propagation across different ad states
* Reorganized and consolidated test utilities, shared fixtures, and
builder helpers across test modules

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 09:35:36 +02:00
Jens ee581e383f refactor: clean up source hygiene (#1141)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Clean up minor post-extraction source hygiene without changing runtime
behavior.

## 📋 Changes Summary

- Import `KleinanzeigenBot` from its concrete `app` module in the CLI.
- Update stale source comments/docstrings that referenced the previous
package structure.
- No dependencies, configuration changes, CLI output changes,
translation changes, or ad YAML handling changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Refactored internal code organization by adjusting module imports and
updating code documentation references to enhance maintainability and
clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-19 08:09:14 +02:00
Jens f5f3399a66 refactor: move app shell out of package init (#1140)
## ℹ️ Description

Move the `KleinanzeigenBot` application class out of `__init__.py` into
a dedicated `app.py` module. This continues the maintainability-focused
refactoring of the package initializer that was started in prior PRs
(login flow extraction, publishing workflow extraction, etc.).

- Link to the related issue(s): N/A

## 📋 Changes Summary

- **New `src/kleinanzeigen_bot/app.py`** (525 lines): contains the full
`KleinanzeigenBot` class with all imports, constructor, `run()` command
dispatch, `load_ads()`, and delegator methods
- **`src/kleinanzeigen_bot/__init__.py`**: reduced from 545 lines to 27
lines — thin package initializer with `colorama` init, `LOG` setup,
`KleinanzeigenBot` re-export from `app.py`, `main()` delegator, and
`__name__` guard
- **Translations**: split `__init__.py` section header in
`translations.de.yaml`; class-owned translations moved to new
`kleinanzeigen_bot/app.py:` section
- **Tests**: updated patch targets from package-root paths to concrete
module paths (`kleinanzeigen_bot.UpdateChecker` →
`kleinanzeigen_bot.update_checker.UpdateChecker`)

### ⚙️ Type of Change
- [ ] 🐞 Bug fix
- [ ] ✨ New feature
- [ ] 💥 Breaking change
- [x] 🔧 Refactoring (no user-facing behavior changes)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Centralized runtime command handling in a new application entrypoint
class while keeping the same CLI-style command set.
* Slimmed the package initializer to re-export the main bot class and
delegate CLI execution through `main(args)`.
* **Bug Fixes**
  * Improved Windows console compatibility during startup.
* **Tests**
* Updated unit tests and mocks to reflect the new location of
command-handling logic and related wiring.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-18 21:44:37 +02:00
Jens 35bd393dc4 refactor: extract login/auth flow into login_flow.py (#1139)
## ℹ️ Description

Extract all login, Auth0 form-fill, captcha, SMS/email verification,
GDPR, login-state detection, and diagnostics-capture logic from
`__init__.py` into a new `login_flow.py` module.

## 📋 Changes Summary

- Create `login_flow.py` (~634 lines) with module-level async functions
accepting explicit `web: WebScrapingMixin` + keyword arguments
- Thin delegators in `__init__.py` bind `KleinanzeigenBot` instance
attributes to module calls
- Remove 12 dead delegator methods; tests call module functions directly
- Move `LoginDetectionResult`/`LoginDetectionReason` types to
`login_flow`
- Move `TestKleinanzeigenBotAuthentication` to
`tests/unit/test_login_flow.py`, update patch targets
- Move login translations to `login_flow.py` section in
`translations.de.yaml`
- Fix diagnostics guard reset per login attempt
- Remove `is_logged_in_web()`; pass username explicitly
- Make cross-module functions public: `login_flow` internal helpers and
`WebScrapingMixin` helpers (`dismiss_consent_banner`,
`effective_timeout`, `extract_visible_text`)

### ⚙️ Type of Change
- [x] ✨ New feature (adds new functionality without breaking existing
usage)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Reworked the authentication/login process to use a dedicated login
flow, improving handling of multi-step sign-in and related verification
prompts.
* Standardized login-status detection and diagnostics behavior when
login state can’t be determined.
* Updated text-extraction and timeout behavior for DOM-based fallbacks,
and aligned consent-banner dismissal to the public API.

* **Tests**
* Added extensive unit tests for the login flow and login-state
detection.
* Updated existing tests to reflect the refactored helpers and
consent-banner method naming.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-17 22:27:48 +02:00
Jens ecd3ea35cf refactor: extract publishing workflow (#1137)
## ℹ️ Description

- Link to the related issue(s): N/A
- Extracts publish/update orchestration from the monolithic bot class
into `publishing_workflow.py` as Step 21 of the monolith breakdown.

## 📋 Changes Summary

- Added `src/kleinanzeigen_bot/publishing_workflow.py` for
publish/update orchestration, retry/uncertainty handling, result checks,
and delete-before/after-publish wiring.
- Kept `KleinanzeigenBot` publish/update methods as narrow delegation
seams with explicit dependencies.
- Moved WANTED ad type selection into `publishing_form.fill_ad_form()`
so form details stay in the form module.
- Updated German translations and unit tests to patch concrete workflow
module paths.
- No dependency or configuration changes.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] Refactor (no user-facing behavior change)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved form rendering for WANTED ad type by ensuring the ad type is
pre-selected before populating other form fields.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-17 14:21:26 +02:00
Jens eae9dc9fc1 refactor: extract publishing form orchestrator (#1136)
## ℹ️ Description

Move the last form-filling remnant out of `__init__.py` into
`publishing_form.py`, where all individual form section functions
already live.

- `_fill_ad_form()` only delegated to the already-extracted section
functions (`set_category`, `set_special_attributes`,
`set_shipping_form`, `set_pricing_fields`, `set_contact_fields`,
`fill_image_section`).
- Replaced with a public module-level `publishing_form.fill_ad_form()`
that accepts explicit `web`, `root_url`, `ad_defaults`, `ad_file`,
`ad_cfg`, and `mode` parameters.
- `publish_ad()` now calls `_publishing_form.fill_ad_form(self, ...)`
instead of `self._fill_ad_form(...)`.

## 📋 Changes Summary

- Extracted `fill_ad_form()` into `publishing_form.py` as a public
module-level async function
- Removed `_fill_ad_form()` method from `KleinanzeigenBot` class
- Updated call site in `publish_ad()` to pass explicit parameters
- Updated test patch target in `test_publishing_persistence.py` and
docstring in `test_init.py`

### ⚙️ Type of Change
- [ ] 🐞 Bug fix
- [ ] ✨ New feature
- [ ] 💥 Breaking change

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized ad form-filling logic for improved maintainability and
code structure.

* **Tests**
  * Updated unit tests to reflect internal code reorganization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-17 13:22:39 +02:00
Jens 154c37b26e refactor: extract special-attributes/condition form section (#1135)
## ℹ️ Description

Extracts special-attribute and condition form logic from `__init__.py`
into `publishing_form.py` to continue module decomposition.

## 📋 Changes Summary

- **`publishing_form.py`**: Added `set_special_attributes`,
`_set_condition`, and helpers (`_pick_special_attribute_candidate`,
`_special_attribute_candidate_priority`,
`_describe_special_attribute_candidate`) adapted as module-level
functions taking `web: WebScrapingMixin`
- **`__init__.py`**: Removed the moved functions, cleaned up stale
imports (`re`, `_ad_form_helpers`, `_select_button_combobox`, `ensure`,
`Element`), updated `_fill_ad_form` to delegate via
`_publishing_form.set_special_attributes()`
- **`translations.de.yaml`**: Moved `_set_condition` and
`set_special_attributes` sections from `__init__.py:` →
`publishing_form.py:` block
- **`test_init.py`**: Moved special-attribute and condition tests to
`test_publishing_form.py`, updated orchestration patch targets
- **`test_publishing_form.py`**: Added `TestSpecialAttributes`,
`TestConditionSelector`, and `TestConditionFallbackToGenericHandler`
classes with adapted imports and patches

### ⚙️ Type of Change
- [x] Refactor — no behavior changes, internal module reorganization
only

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.
2026-06-17 12:21:07 +02:00
Jens 875b8b063a refactor: extract publishing shipping form section (#1134)
## ℹ️ Description
- Extracts the publishing shipping form workflow from `KleinanzeigenBot`
into `publishing_form.py`.
- Keeps runtime behavior, selectors, waits, messages, and browser
interactions unchanged while moving translation ownership.

## 📋 Changes Summary
- Move WANTED shipping handling, shipping radio/dialog handling,
shipping option toggling, and the private React-fiber button-combobox
helper into `publishing_form.py`.
- Update `__init__.py` to delegate shipping form work and keep only
orchestration/seam responsibilities.
- Move direct shipping behavior tests to
`tests/unit/test_publishing_form.py`; keep a shipping orchestration seam
test in `tests/unit/test_init.py`.
- Move German translation entries for shipping messages to the
`publishing_form.py` translation section.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary of changes

* **Refactor**
* Improved publishing ad shipping form handling by routing wanted vs
non-wanted ads to the correct UI flow.
* Enhanced Versand (shipping) combobox selection for wanted ads to
ensure the right option is chosen.
* Strengthened the shipping dialog flow for pickup vs
shipping-with-options, including more reliable shipping price
confirmation and retry behavior.

* **Tests**
* Expanded unit tests covering shipping dialog interactions,
carrier/option selection, wanted-shipping combobox behavior, and
timeout/error scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-17 08:13:35 +02:00
Jens e9ed698dce refactor: extract publishing pricing form section (#1133)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Extracts the publishing pricing/direct-buy/description form section
from the main bot class as the next monolith-breakdown step.

## 📋 Changes Summary

- Added publishing_form.set_pricing_fields() for price type/amount,
sell-directly/direct-buy radios, and description filling.
- Replaced the inline _fill_ad_form() pricing block with the new
publishing form section call.
- Moved direct unit coverage for pricing/direct-buy/description behavior
into test_publishing_form.py and kept orchestration seam tests in
test_init.py.
- Moved existing German translation ownership entries to the new
function path without wording changes.
- No dependencies, configuration changes, CLI changes, schema changes,
or generated artifacts.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)


## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Centralized pricing-related ad form filling (price type, price amount,
ad description with affixes, and direct-buy/buy-now controls) into a
shared publishing helper for more consistent behavior.

* **Bug Fixes**
* Improved handling of pricing/buy-now selection failures by surfacing
clearer, localized timeout errors.

* **Tests**
* Added dedicated coverage for pricing field interactions, including
dropdown skipping when not applicable, correct amount input behavior,
and description generation usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-16 22:06:28 +02:00
Jens 137a7691a7 refactor: extract publishing image form section (#1132)
## ℹ️ Description

- Link to the related issue(s): N/A
- Extracts the publishing image cleanup and upload section from the bot
monolith into `publishing_form.py` as Step 16 of the monolith breakdown
plan.

## 📋 Changes Summary

- Move image cleanup/upload behavior into `fill_image_section()` and
`upload_images()`.
- Replace the monolith image block with a publishing form seam call.
- Move direct image behavior tests from `test_init.py` to
`test_publishing_form.py`.
- Update German translation ownership paths without changing translated
text.
- No dependency, configuration, CLI, schema, or generated artifact
changes.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)
- [x] ♻️ Refactor (no intended behavior change)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Image upload handling has been reorganized and consolidated in the
publishing module for improved code organization. The image processing
workflow now includes enhanced timeout management and marker-based
completion detection to ensure all images are reliably processed during
ad publication.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-16 06:57:26 +02:00
Jens 9d146c2b96 refactor: extract publishing contact form section (#1131)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Refactors the publishing form code by moving the contact/location form
section out of the package initializer into the publishing form module.

## 📋 Changes Summary

- Move contact field handling, contact location handling, and city
combobox helpers into publishing_form.py.
- Keep the publishing form orchestration delegating through an explicit
web/contact boundary.
- Update unit tests to patch and call the new publishing form functions.
- Move German translation ownership paths for the relocated messages
without changing translated text.
- No dependencies, configuration changes, or additional requirements
introduced.

### ⚙️ Type of Change

Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Updated ad publishing to populate contact and location fields via
dedicated public publishing-form helpers, with revised import wiring for
ad model types.
* Removed older internal city/contact handling logic and replaced it
with clearer helper-driven flow.
* **Bug Fixes**
* Improved city/location hardening: safer city extraction,
combobox-style selection with ambiguity checks, timeout-based fallbacks,
and correct handling for read-only city inputs.
* More resilient contact field population (zipcode/location, street,
name, phone), with graceful handling when optional elements are missing.
* **Tests**
* Added/updated unit tests to cover city/contact hardening and category
probe/picker selection behaviors, asserting helper calls rather than
private methods.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-16 06:16:22 +02:00
Jens f02816cb06 refactor: extract publishing category form section (#1130)
## ℹ️ Description
- Link to the related issue(s): N/A
- Extracts the publishing category form section from the monolithic bot
class into a focused publishing form module while preserving category
selection and suggestion fallback behavior.

## 📋 Changes Summary
- Move category selection and category suggestion fallback into
`src/kleinanzeigen_bot/publishing_form.py`.
- Update publish form orchestration to call the extracted category
function through a narrow web/root_url boundary.
- Update unit tests and German translation ownership for the new module
path.
- No dependencies, configuration changes, or additional requirements
introduced.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Restructured category selection logic for improved maintainability and
code organization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-15 20:51:02 +02:00
Jens dd6a1955ef refactor: extract publishing form helpers (#1129)
## ℹ️ Description
- Extract browser-independent publishing form helpers from the monolith
into `ad_form_helpers.py`.
- Prepares later publishing form extraction without changing browser
behavior.

## 📋 Changes Summary
- Move the Versand combobox selector constant to `ad_form_helpers.py`.
- Move location matching logic to `ad_form_helpers.py`.
- Update production call sites and helper ownership tests.
- Keep form sections and browser-dependent logic in `__init__.py`.

### ⚙️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved internal code organization by consolidating shared helpers
for form-related utilities and location matching logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-15 17:34:26 +02:00
Jens fda56b109c refactor: prepare web helpers for publishing form extraction (#1128)
## ℹ️ Description

- Link to the related issue(s): Issue #930
- Moves a shared consent-banner browser helper onto the generic web
scraping mixin so later publishing form refactors can use it without
depending on the main bot class.

## 📋 Changes Summary

- Move `_dismiss_consent_banner()` from `KleinanzeigenBot` to
`WebScrapingMixin` without changing selector, timeout, click, or sleep
behavior.
- Move the helper's focused unit tests from `test_init.py` to
`test_web_scraping_mixin.py`.
- No dependencies, configuration changes, or additional requirements
introduced.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

None of the listed categories apply; this is an internal
behavior-equivalent refactor.

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Moved consent-banner dismissal logic into a shared web-scraping
helper, and updated the bot flow to proceed directly after SMS
verification.

* **Tests**
* Removed unit tests that covered the old bot-level consent-banner
handling.
* Added new async unit tests to verify the shared helper clicks the
consent accept button when present and does nothing when absent.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-15 14:10:47 +02:00
Jens 816cf3ed9e refactor: rename publishing persistence module (#1127)
## ℹ️ Description

- Link to the related issue(s): Issue #
- Rename the persistence-only publishing module so the module name
matches its current responsibility.

## 📋 Changes Summary

- Rename `publishing_flow.py` to `publishing_persistence.py`.
- Update production imports/calls and test patch paths.
- Move persistence tests to `test_publishing_persistence.py` and keep
submission tests under `test_publishing_submission.py`.
- Update the German translation section header for the renamed module.

### ⚙️ Type of Change

- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved the way published ad changes are saved after submitting or
updating, ensuring related fields (such as image updates and pricing
adjustments) are correctly retained.

* **Bug Fixes**
* Publishing now continues even if the follow-up save step fails,
preventing unnecessary interruptions.

* **Tests**
  * Updated existing tests to match the revised persistence behavior.
* Added new unit tests covering persistence outcomes, logging, rollback
behavior, and image/price update handling.
  * Simplified the publishing submission test suite setup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-15 13:34:39 +02:00
Jens 5c22795728 fix: handle shipping select for pro accounts (#1126)
## ℹ️ Description

- Link to the related issue(s): Issue #1125
- Fixes PRO/commercial account publishing where the Versand field is
rendered as a custom `button[role="combobox"]` dropdown by the
PostListingForm Astro island, not as a native `versand_s` `<select>` and
not as the private-account radio-button UI.
- Uses the confirmed PRO DOM model from the issue comments/screenshots:
category-prefixed IDs such as `kunst.versand` / `uhren.versand`,
placeholder `Bitte wählen`, and options `Versand möglich` (`ja`) / `Nur
Abholung` (`nein`).

## 📋 Changes Summary

- Detect the custom Versand combobox before falling back to the private
radio/dialog shipping flow.
- Locate the combobox by the confirmed category-prefixed id shape and by
the selected-option label relationship
(`aria-labelledby$="versand-selected-option"`) so the lookup is not tied
to one category prefix.
- Select `Versand möglich` for `shipping_type: SHIPPING` and `Nur
Abholung` for `shipping_type: PICKUP` by visible option text via
`web_select_button_combobox()`.
- Apply the same safer Versand combobox selector to the existing
WANTED-ad shipping combobox path.
- Keep the private/non-commercial radio-button and
shipping-options-dialog path as the fallback when the custom combobox is
absent.
- Add regression coverage for the PRO combobox path and relax existing
shipping-flow assertions so they check behavior instead of exact probe
ordering/timeout plumbing.
- Add the German translation for the combobox error message.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved the WANTED shipping flow to detect and use native “Versand”
dropdown controls when available, selecting the correct shipping option
directly.
* **Bug Fixes**
* Added clearer errors for unsupported shipping types and standardized
timeout behavior when native selection fails; continues to fall back to
the existing radio/dialog approach when the control is missing.
* **Tests**
* Expanded unit tests to cover the native Versand combobox route,
including selector coverage and updated probe/call-order expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-14 21:42:53 +02:00
Jens f94567a983 fix: use canonical title for owned ad downloads (#1123)
## ℹ️ Description

- Link to the related issue(s): Issue #1119
- Fixes owned-ad downloads where the public/detail page title may
include UI status decoration that makes the title exceed the semantic
65-character title limit.

## 📋 Changes Summary

- Resolve download titles once via manage-ads metadata when available.
- Reuse the resolved title for folder naming, file stems, and persisted
ad config data.
- Keep manual page title extraction as the fallback without stripping or
truncation.
- Add regression coverage for canonical metadata titles and fallback
extraction.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (pdm run
test).
- [x] I have formatted the code (pdm run format).
- [x] I have verified that linting passes (pdm run lint).
- [x] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Enhanced ad title resolution to prioritize cached published metadata
when available, with fallback to page extraction for improved
reliability.
* Improved directory handling to consistently use the resolved ad title
for naming and saved info templates.
* **Bug Fixes**
* Corrected title/metadata extraction flow to avoid redundant title
reads and ensure creation dates are reliably sourced from the expected
page elements.
* **Tests**
* Updated and added unit test coverage for title resolution, directory
behaviors, and extraction call ordering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-14 11:22:16 +02:00
Jens 439c36e851 fix: click Auth0 Weiter after captcha (#1120) (#1122)
## ℹ️ Description

Fixes issue #1120 by handling the Auth0 captcha branch after email
submit. When captcha appears, the bot now probes for a visible/clickable
"Weiter" button and clicks it before waiting for the password page. The
normal no-captcha login flow remains unchanged.

- Link to the related issue(s): Issue #1120
- Describe the motivation and context for this change.

## 📋 Changes Summary

- Added a captcha-only Auth0 identifier handler that waits for captcha
completion, probes for "Weiter" with `web_probe`, and clicks it if
present.
- Kept the existing no-captcha login path unchanged.
- Updated German translations for the new log messages.
- Adjusted unit tests for the new login helper.

### ⚙️ Type of Change

- [x] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist

- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary. No additional docs
changes were needed beyond translations.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved login robustness to detect optional reCAPTCHA on the
email/identifier step; users are notified when a captcha appears, can
provide input, and the login resumes once advanced.
* **Tests**
* Updated unit tests to cover and control the captcha-handling behavior
during the login flow, including the missing-password-step scenario.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-13 23:28:04 +02:00
Jens 5d5c25d753 refactor: extract publishing submission flow (#1121)
## ℹ️ Description
*Refactor the publishing submission/confirmation flow into a dedicated
module while keeping behavior identical.*

- Link to the related issue(s): N/A
- Describe the motivation and context for this change: Reduce coupling
in the publishing workflow and keep the persistence boundary separate.

## 📋 Changes Summary

- Added `src/kleinanzeigen_bot/publishing_submission.py` for captcha,
submit, confirmation, and ad-id recovery.
- Removed the submission/confirmation helpers from `publishing_flow.py`,
leaving persistence helpers there.
- Updated the `publish_ad()` call site and tests to use the new concrete
module path.
- Moved the translated log-message entries for the extracted submission
flow.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

## ✅ Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [ ] I have updated documentation where necessary.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Ad submission and confirmation logic moved into a dedicated submission
module for clearer separation.

* **Bug Fixes**
* Improved publish confirmation: better captcha handling, post-submit
dialogs (upsell/payment, imprint guidance, “no image”) and avoidance of
stale-referrer fallbacks.
* Now fails loudly and avoids persisting when ad confirmation is
uncertain.

* **Tests**
* Unit tests updated to target the new submission flow and
recovery/persistence behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-13 21:35:41 +02:00
Jens 9172af0c69 enh: skip runtime bootstrap for update-check command (#1117)
## ℹ️ Description

The `update-check` command was calling `_bootstrap_runtime()` which
triggered config file creation (`create_default_config`), file logging
setup, and browser config application. None of these are needed for a
read-only GitHub release version check.

This PR skips the full bootstrap and instead uses `UpdateChecker` with
sensible defaults from `Config()` and a state path resolved from the
already-available workspace.

Closes #1090.

## 📋 Changes Summary

### 🚀 Enhancement

- **`src/kleinanzeigen_bot/__init__.py`** — `update-check` case: removed
`_bootstrap_runtime()` call, uses `self.config = Config()` with sensible
defaults, state path from already-resolved `self.workspace`.

### ⚙️ Type of Change
- [x] 🚀 New feature / Enhancement (non-breaking change which adds
functionality)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.
2026-06-13 10:37:43 +02:00
Jens 97256c1d70 fix: apply after_delete policy when title-mode delete returns all-404 with id=None (#1116)
## ℹ️ Description

When `delete_old_ads_by_title=True` and `ad_cfg.id=None` and all remote
DELETE requests return 404, the `after_delete` policy (DISABLE/RESET)
was silently skipped because the orchestrator's `id_before`/`id_after`
heuristic could not distinguish "not attempted" from "attempted but all
404."

This PR replaces the heuristic with an explicit `DeleteResult`
NamedTuple returned from `delete_ad()`, communicating whether HTTP
DELETE requests were actually sent (`attempted`) separately from whether
any returned 200 (`deleted`).

Closes #1103.

## 📋 Changes Summary

### 🐞 Bug fix

- **`delete_flow.py`** — New `DeleteResult(deleted, attempted)`
NamedTuple replaces the `bool` return from `delete_ad()`. The
orchestrator now uses `result.attempted` directly to decide whether to
apply `after_delete` policy.
- Removed the fragile `id_before`/`ad_cfg.id`-based heuristic entirely.

### 🧪 Tests

- Updated all 9 existing `delete_ad()` tests for the new tuple return
type
- Updated 3 orchestrator tests (`delete_ads`) for the new `DeleteResult`
API
- Added regression test
`test_cleanup_on_title_match_all_404_with_id_none` — `ad_cfg.id=None`,
title-match, all responses 404, `after_delete=RESET` is applied

### ⚙️ Type of Change
- [x] 🐞 Bug fix (non-breaking change which fixes an issue)

## ✅ Checklist
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved deletion tracking to distinguish attempted vs successful
deletions, ensuring cleanup policies run correctly across edge cases.

* **Tests**
* Updated unit tests to reflect the new deletion outcomes and added a
regression test to verify cleanup behavior when title-mode deletions all
fail.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-13 09:41:24 +02:00