diff --git a/CHANGELOG.md b/CHANGELOG.md index e991ffd..7d4bd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to SearchMob Desktop are documented here. The version scheme ## [Unreleased] +### Fixed +- **Applying a scope or rule from the browser no longer throws away your results.** On a SearchMob + search page in your browser, changing the scope (or blocking/raising/lowering/pinning a result's + site) used to send you to the home page with an empty search box, losing the results you were + looking at. It now returns you to the same search with the new ranking applied, so you keep your + results instead of starting over. + ## 26.06.05 — 2026-06-05 ### Added diff --git a/src/searchmob_desktop/server/app.py b/src/searchmob_desktop/server/app.py index 3d6bf50..184f488 100644 --- a/src/searchmob_desktop/server/app.py +++ b/src/searchmob_desktop/server/app.py @@ -34,7 +34,7 @@ from collections.abc import Awaitable, Callable, Sequence from dataclasses import asdict, dataclass, replace from ipaddress import ip_address -from urllib.parse import parse_qsl, urlsplit +from urllib.parse import parse_qsl, urlencode, urlsplit from starlette.applications import Starlette from starlette.middleware import Middleware @@ -770,6 +770,20 @@ async def _form(request: Request) -> dict[str, str]: raw = await request.body() return dict(parse_qsl(raw.decode("utf-8", "replace"))) + def _redirect_to_results(form: dict[str, str]) -> Response: + # Return to the results page a scope/rule POST came from, rebuilt from the hidden q/sort/ + # vertical fields the served forms carry, so the new ranking is applied to the same search + # instead of dumping the owner on the home page. (Our no-referrer policy strips the Referer, + # so the Referer-based _redirect_back always fell through to "/".) No query - the home-page + # or settings scope selector - falls back to home. 303 -> re-fetch with GET. + query = form.get("q", "").strip() + if not query: + return RedirectResponse("/", status_code=303) + sort_mode = form.get("sort", "").strip() or "fresh" + vertical = form.get("vertical", "").strip() or "web" + target = "/search?" + urlencode({"q": query, "vertical": vertical, "sort": sort_mode}) + return RedirectResponse(target, status_code=303) + async def set_domain_rule(request: Request) -> Response: if ranking_rules_saver is None: return PlainTextResponse("Personalization is read-only here.", status_code=503) @@ -778,7 +792,7 @@ async def set_domain_rule(request: Request) -> Response: action = form.get("action", "").strip().upper() if domain and action in RankRule.__members__: ranking_rules_saver(rules_provider().with_domain_rule(domain, RankRule[action])) - return _redirect_back(request) + return _redirect_to_results(form) async def set_scope(request: Request) -> Response: if ranking_rules_saver is None: @@ -786,7 +800,7 @@ async def set_scope(request: Request) -> Response: form = await _form(request) lens = form.get("lens", "").strip() ranking_rules_saver(rules_provider().with_active_lens(lens or None)) - return _redirect_back(request) + return _redirect_to_results(form) def _csv_tuple(raw: str) -> tuple[str, ...]: # Split a comma- or newline-separated field into clean, de-duplicated, lowercased entries. diff --git a/src/searchmob_desktop/server/templates.py b/src/searchmob_desktop/server/templates.py index b5fe087..28f8dfe 100644 --- a/src/searchmob_desktop/server/templates.py +++ b/src/searchmob_desktop/server/templates.py @@ -620,7 +620,20 @@ def _home_scope(rules: RankingRules) -> tuple[str, str]: return select, form -def _scope_bar(rules: RankingRules) -> str: +def _search_context_fields(query: str, sort_mode: str, vertical: str) -> str: + """Hidden q/sort/vertical fields so a mutation POST can return to the same results page. + + The served forms carry the current search this way because our ``Referrer-Policy: no-referrer`` + strips the Referer, so the handler cannot recover the originating page from it. + """ + return ( + f'' + f'' + f'' + ) + + +def _scope_bar(rules: RankingRules, query: str, sort_mode: str, vertical: str) -> str: """A scope (lens) selector. Renders only when the profile has at least one lens defined.""" if not rules.lenses: return "" @@ -634,7 +647,8 @@ def _scope_bar(rules: RankingRules) -> str: # onchange auto-submits when JS is on; the noscript button covers the JS-off case. return ( '