RealityLens
Search Sreality.cz with an AI agent, then track, archive and analyse the listings you like.
RealityLens
A private tool for finding and tracking real estate from Sreality.cz. It is one application made of two halves:
- Search agent - classic parametric search, an AI preference profile (chat), listing scoring, a swipe deck and statistics. Everything you like or press "Sledovat" on is a tracked listing; every user tracks independently.
- Archive - every tracked listing is archived with its price history and events, and enriched with location context (crime, cadastre, flood, radon, geohazards, protected nature, noise, amenities, travel times, Rohlik delivery), duplicate detection, notes, documents, sharing and AI summaries. Price and status changes are sent to the owner of the listing by Web Push and e-mail (and to the admin's Slack for admin-owned listings).
Search like the agent, then press "Sledovat" on a result to track and analyse it like the archive. The product UI is Czech; code, logs and documentation are English.
It replaces the Python RealityLens (SrealityArchivator) and the Next.js Reality Agent (RealityAgent).
Stack
.NET 10, Blazor Web App (interactive server), EF Core with SQLite, Serilog, Bootstrap 5, Font Awesome, Leaflet. No external service is required to run; OpenAI, Google sign-in, Resend, Mapy.com and Slack are optional.
Run locally
dotnet run --project src/RealityLens.Web
With neither Google nor Basic auth configured the app runs in a clearly logged open dev mode (Development environment only): every visitor is one
synthetic admin. Data goes to <app dir>/data unless REALITYLENS_DATA_DIR is set.
dotnet build
dotnet test
Configuration
Everything is an environment variable and all of them are optional. Most short names below accept three spellings,
first non-empty wins: REALITYLENS_<NAME> (preferred), SREALITY_<NAME> (legacy RealityLens) and plain <NAME>
(legacy Reality Agent, e.g. ADMIN_EMAILS, OPENAI_API_KEY). Some settings only know the REALITYLENS_ and
SREALITY_ spellings (scheduler, refresh times, duplicate distance, Mapy key, context tunables).
| Variable | Default | Meaning |
|---|---|---|
REALITYLENS_DATA_DIR |
<app dir>/data |
SQLite file, documents, VAPID key, default log dir (see layout below). |
REALITYLENS_ROLE |
all |
What this process runs: all (UI, API and background work in one process), web (UI and API; heavy work is queued for the worker) or worker (no UI: schedulers and the queue consumer, /health only). See "Roles and the worker". |
REALITYLENS_WORKER_URLS |
http://+:8081 |
Worker role only: where its health listener binds (container-internal, never published). |
REALITYLENS_HEALTH_PORT |
8080 |
Only read by the Docker HEALTHCHECK: the port of /health inside the container (the worker container sets 8081). |
REALITYLENS_APP_PUBLIC_URL |
- | Public base URL (https://realitylens.cz); used in e-mails and test notifications and, when set, as the authority of the Google redirect URI ({url}/auth/google/callback) behind a reverse proxy. |
REALITYLENS_GOOGLE_CLIENT_ID / _SECRET |
- | Google OAuth. Register the redirect URI {public url}/auth/google/callback (same path the legacy apps used). |
REALITYLENS_ADMIN_EMAILS |
- | Comma separated admins (always admin, even without a DB row). |
REALITYLENS_REGISTRATION |
open |
open: any Google account with a verified e-mail may sign in and becomes a regular user (role viewer). invite: the previous invite-only behaviour (only env admins and users an admin added on /app/users). |
REALITYLENS_CONTACT_EMAIL |
first admin e-mail | Contact address on the privacy page /privacy and the terms /terms. |
REALITYLENS_AI_DAILY_LIMIT |
30 |
AI calls (profile wizard, AI parameters, AI summary) per non-admin user and UTC day; 0 = unlimited. Counted in memory, resets on restart; admins are unlimited. |
REALITYLENS_AUTH_RATE_LIMIT |
30 |
Requests per minute and client IP accepted on /auth/*. |
REALITYLENS_USERNAME / _PASSWORD |
- | Optional legacy Basic Auth (always admin, acts as the first admin e-mail, else admin@localhost). |
REALITYLENS_OPENAI_API_KEY |
- | Enables the AI features (profile chat, parameters from description, summaries). |
REALITYLENS_OPENAI_MODEL |
gpt-4o-mini |
OpenAI model. |
REALITYLENS_MAPY_API_KEY |
- | Mapy.com tiles; without it Esri World Imagery is used. |
REALITYLENS_RESEND_API_KEY |
- | Resend e-mail (invites, notifications). |
REALITYLENS_EMAIL_FROM |
- | Sender, e.g. RealityLens <noreply@realitylens.cz> (domain verified in Resend). |
REALITYLENS_NOTIFICATION_EMAIL |
- | Recipients of the admin test notification when none are saved in Nastavení. |
REALITYLENS_SLACK_WEBHOOK_URL |
- | Fallback Slack webhook when none is saved in Nastavení. |
REALITYLENS_VAPID_SUBJECT |
mailto:<first admin> |
Contact sent with every Web Push. |
REALITYLENS_REFRESH_TIMES |
08:00,20:00 |
Times of the archive refresh. |
REALITYLENS_DISABLE_SCHEDULER |
false |
1/true turns off the archive refresh and the daily search sync. |
REALITYLENS_DUPLICATE_DISTANCE_M |
40 |
Distance in metres for duplicate detection. |
REALITYLENS_FAMILY_MAX_MEMBERS |
8 |
Largest family (members plus pending invites; minimum 2). |
REALITYLENS_FAMILY_INVITES_PER_HOUR |
10 |
Family invitations a user may create (or re-send) per hour. |
SYNC_DAILY_TIME / REALITYLENS_SYNC_DAILY_TIME |
built-in | Time of day of the daily search sync. |
STATS_DAILY_TIME / REALITYLENS_STATS_DAILY_TIME |
05:00 |
Local time (HH:mm) of the daily statistics computation (after the 03:00 sync); REALITYLENS_DISABLE_SCHEDULER switches it off. |
STATS_TREND_MONTHS |
12 |
Months in the monthly market trend on the Statistiky page (1 to 24). |
SYNC_STALE_HOURS / REALITYLENS_SYNC_STALE_HOURS |
24 |
How long a finished search sync counts as fresh. |
SYNC_FAILURE_COOLDOWN_MINUTES / REALITYLENS_SYNC_FAILURE_COOLDOWN_MINUTES |
30 |
After a failed search sync of a scope, visiting a results page does not queue it again for this long (the daily run ignores it). |
SYNC_DETAIL_MAX_AGE_DAYS / REALITYLENS_SYNC_DETAIL_MAX_AGE_DAYS |
14 |
A cached listing detail older than this is refreshed by the incremental sync (oldest first). |
SYNC_STALE_REFRESH_PER_RUN / REALITYLENS_SYNC_STALE_REFRESH_PER_RUN |
500 |
Budget of stale-detail refreshes per sync run, so the load is spread over days (0 = never refresh by age). |
SYNC_NEW_DAYS / REALITYLENS_SYNC_NEW_DAYS |
7 |
How long a listing first seen by the sync shows the "Nové" badge (and is in "Jen nové"). |
ENRICH_CONCURRENCY / REALITYLENS_ENRICH_CONCURRENCY |
2 |
How many listings the worker enriches (crime, cadastre, flood, ... travel times) at the same time; they call third-party services, so keep it small (1-8). |
ALERT_MAX_PER_RUN / REALITYLENS_ALERT_MAX_PER_RUN |
20 |
New-listing alerts: the longest list one e-mail digest spells out; the rest is summed up as "a dalších N". |
ALERT_MAX_MESSAGES_PER_DAY / REALITYLENS_ALERT_MAX_MESSAGES_PER_DAY |
20 |
Alert digests per user per rolling 24 hours; matches beyond it wait and are folded into the next digest (given up after 3 days, recorded in profile_alert_matches.error). |
ALERT_MAX_PROFILES / REALITYLENS_ALERT_MAX_PROFILES |
10 |
How many saved searches per user may have alerts on. |
REALITYLENS_ALERT_TOKEN_KEY |
generated | Optional signing secret of the unsubscribe links; by default a random key is created once in <data dir>/.alerts.key (shared by web and worker). |
REALITYLENS_OSRM_BASE |
https://router.project-osrm.org |
OSRM server for travel times. |
REALITYLENS_CRIME_TYPES, _CRIME_TIME_FROM, _CRIME_TIME_TO |
101-903, 1-2020, 12-2020 |
Crime map query. |
REALITYLENS_{CRIME,CADASTRE,TRAVEL,ROHLIK,FLOOD,RADON,GEOHAZARD,PROTECTED_NATURE,NOISE,AMENITIES}_REFRESH_DAYS |
30, 120, 14, 14, 180, 180, 180, 180, 180, 60 | How long a cached context layer stays valid. |
REALITYLENS_AMENITIES_RADIUS_M, _AMENITIES_BRAND_RADIUS_M, _AMENITIES_BRANDS |
2000, 15000, Bauhaus,Makro,IKEA,Lidl,Kaufland |
Amenities search. |
REALITYLENS_LOG_DIR |
<data dir>/logs |
Daily rolling log files. |
REALITYLENS_LOG_RETENTION_DAYS |
14 |
Log files kept. |
REALITYLENS_LOG_LEVEL |
Information |
Default level. |
REALITYLENS_LOG_LEVELS |
- | Per-namespace overrides, Namespace=LEVEL,Other=LEVEL. The usual Serilog config section also works. |
GA_MEASUREMENT_ID |
- | GA4 measurement ID (also REALITYLENS_GA_MEASUREMENT_ID). Empty means no consent banner and no tag. |
Secrets (passwords, API keys, tokens, webhook URLs) are masked in every log sink.
Sign-in
The app is public: any Google account with a verified e-mail can sign in (REALITYLENS_REGISTRATION=open, the default) and is
created as a regular user (viewer). Admins are exactly REALITYLENS_ADMIN_EMAILS plus users an admin promoted on /app/users.
With REALITYLENS_REGISTRATION=invite a sign-in of an address that is not an env admin and not in the users table is refused
with an explanation on /auth/login. /auth/login is the branded sign-in page; it accepts returnUrl (only local /app...
paths are honoured, so deep links return after sign-in and open redirects are impossible) and shows a clear message when the
user cancels, the OAuth state does not match or Google is not configured.
The cookie (reality_session, HttpOnly, SameSite=Lax, Secure whenever the request is https, sliding 30 days) is
re-validated against the users table on every request (cached 30 s), so removing a user or changing a role takes effect
without a new sign-in. Sign-out (POST /auth/logout) and account deletion (POST /auth/delete-account) are POST endpoints
protected by the antiforgery token (forms in the sidebar and on /app/account); a plain GET does nothing. /auth/* is rate
limited per client IP. Behind nginx the Google redirect URI is built from Host and X-Forwarded-Proto
(ASPNETCORE_FORWARDEDHEADERS_ENABLED=true; deploy/nginx-realitylens.cz.conf sets both headers), or from
REALITYLENS_APP_PUBLIC_URL when that is set. The open dev mode works only when the environment is Development; elsewhere
without Google credentials nobody can sign in and /health is Degraded.
Public pages /privacy and /terms (Czech) describe what is stored, the cookies and the external services. Users delete their own
account and all their data on /app/account; admins remove users (with their data) on /app/users. The admin pages
/app/users, /app/settings and /app/logs stay admin-only. AI calls of regular users are limited per day
(REALITYLENS_AI_DAILY_LIMIT).
Everything under /app and /api requires a signed-in user; public are /, /privacy, /terms, /share/*, /api/share/*, /health,
/auth/* and static files. Admin pages use the Admin policy.
Family (Rodina)
Sharing a listing with a single picked user was replaced by a family: a small group of users (at most one family per user)
who invite each other by typed e-mail address - there is no user directory or picker. The page /app/family shows members, pending
invites, the invite form and the listings the family shares. A user creates a family ("Vytvořit rodinu", becomes its owner,
default name "Rodina") or accepts an invitation.
- Invites: single-use link
/app/family/join/{token}valid 14 days (32 random bytes; only the SHA-256 hash is stored, so a link is shown once - "Poslat znovu" issues a new one and invalidates the old). It is e-mailed through Resend when e-mail is configured and always shown as a copyable link. Accepting needs a sign-in with the invited Google e-mail (the loginreturnUrlbrings the user back to the link); someone who is already in a family must leave first. Members plus pending invites are capped byREALITYLENS_FAMILY_MAX_MEMBERS(default 8) and invite creation is rate limited per user (REALITYLENS_FAMILY_INVITES_PER_HOUR, default 10). - Sharing: the owner of a listing shares it with "Sdílet s rodinou" (detail, tab Sdílení, dashboard card). Members then have the
access level
shared: read-only detail, documents, price history, events and parameters; no refresh, archive, delete or sharing further. The owner's notes are never visible; every member keeps their own note on a shared listing (tablelisting_sharesis reused as that per-member overlay). Shared listings live on the Rodina page, not on the dashboard or map; "Sledovat také u mě" tracks the estate as the member's own copy. Leaving, removal, unsharing and dissolving the family remove access immediately. - Lifecycle: the owner can rename, remove members, cancel or re-send invites and dissolve the family (nothing is deleted, all sharing stops). A member can leave; the owner leaving hands the family to the longest-standing member (dissolves it when alone). Deleting an account does the same and removes the links of the deleted user's listings.
- Public read-only links (
/share/{token}) are unchanged. The legacy importer skips old per-user shares ("re-share through Rodina").
Notifications
The VAPID key pair is generated on first use and kept in <data dir>/.vapid.json; a legacy .vapid_private.pem is imported so existing
browser subscriptions stay valid. A failing channel never affects the others.
Price and availability changes of tracked listings
When a refresh finds that a tracked listing changed its price or stopped / started being available, only its owner is told - through the owner's own Web Push subscriptions and the owner's own e-mail address, never through anybody else's (listings shared with a family are not announced to its members). Before 1.12.0 every change went to Slack, to all push subscriptions of all users and to the admin addresses, which leaked what other people track.
- Detection (
ListingChanges.Detect): a price move counts when it passes the market-event noise rules (at least 0.5 % and 100 Kč); a price that stops / starts being stated (null, 0, 1 Kč = "info u RK") is reported in words; a listing that went away reports only that. The change is a structuredListingChange(kind, listing snapshot, old/new price and status), not a string. - One message per owner per run: a refresh run (scheduled 08:00 / 20:00, "Obnovit ceny", the worker job) collects the changes (
ListingChangeCollector) and hands them over once at its end - also when it fails or is cancelled, so what was already found is not lost. One change is a focused message, several are a digest (biggest drop first, up to 10 listed, then "a dalších N"). A single-listing refresh sends right away. The same listing reaching the same new price / status is sent once. - E-mail (
ChangeDigest, HTML + plain text, Czech, readable in light and dark mail clients, max 600 px): subject "Změna ceny: (-5 %)", "Inzerát už není dostupný: " or "3 změny u sledovaných inzerátů"; a card per listing with photo, title (link to/app/listing/{id}), locality, the old price crossed out -> the new one in bold with the difference in Kč and %, price per m² and area, the status in words, the buttons "Otevřít v RealityLens" / "Otevřít na Sreality", and a footer with the reason, a link to the account settings and a one-click unsubscribe (/changes/unsubscribe?t=..., signed like the alerts link: GET confirms, POST switches the e-mail off;List-Unsubscribeheaders included). WithoutREALITYLENS_RESEND_API_KEY+REALITYLENS_EMAIL_FROMno e-mail is sent (in the Development environment it is written to<data dir>/outbox/*.html|txt). - Push: title "Změna ceny: ", body "9 990 000 → 9 490 000 Kč (-5 %)" (digest: "N změn u sledovaných inzerátů"); a click opens the listing (the dashboard for a digest).
- User controls: section "Upozornění na změny sledovaných inzerátů" on Můj účet with two switches (
users.notify_changes_email,users.notify_changes_push, both on by default). - Slack and the Nastavení e-mail addresses are the admin's personal channel: Slack receives changes of listings owned by an admin (and of old ownerless rows) only; the notification addresses of the Nastavení page are no longer used for changes (the admin owner gets the mail at their own address) and remain the recipients of the test button, which also pushes only to the admin's own browsers.
New-listing alerts
The bell "Upozorňovat na nové inzeráty" on every saved search (classic and AI) sends the owner a message when a listing that the sync first saw after the bell was switched on falls into
the search. Channels per search: Web Push to the user's own browser subscriptions and/or an e-mail to the user's own address (default: push when the user already has a
subscription, else e-mail); the e-mail needs REALITYLENS_RESEND_API_KEY + REALITYLENS_EMAIL_FROM (in the Development environment without them it is written to <data dir>/outbox instead).
- Job: every finished sync that saw new listings of an already-synced scope (
first_seen_is_new; the first full crawl of a scope never alerts) queues one de-duplicated light-lane requestmatch-new-listings; the worker (or the all-in-one process) runs it. It takes the listings first seen aftermax(alerts_since, alerts_last_checked_at - 5 min)that are in the candidate pool of the search's own scope, decides withProfileMatching.Evaluate- the same code the results pages use (classic: the hard parameters; AI: price filter, thenScorer.ScoreListing, included unless a "Rozhodně vadí" preference excludes it; there is no minimum score) - and records every hit once per search inprofile_alert_matches(unique profile + listing; listings the user tracks or swiped away are skipped). - Delivery: one digest per search per run. One listing: "Nová nemovitost: " with the price and place and a link to
/app/estate/{id}; several: "N nových nemovitostí pro hledání " (push lists five, the e-mail up toALERT_MAX_PER_RUN, then "a dalších N") linking the results page. The e-mail is Czech with a plain-text part, links throughREALITYLENS_APP_PUBLIC_URL, carriesList-Unsubscribe/List-Unsubscribe-Postheaders and a signed, expiring (one year) unsubscribe link/alerts/unsubscribe?t=...that works without signing in (GET asks for a confirmation, POST - the button or a mail client's one-click - switches that one search off). - Limits:
ALERT_MAX_MESSAGES_PER_DAYandALERT_MAX_PROFILESper user; a failing channel or search never stops the others and its error is stored on the match rows (retried for 3 days). - User controls: the bell in the rows of Hledání / Moje profily and in the headers of the results and the profile detail, "Vypnout všechna upozornění" on Můj účet. Each pass is a run "Upozornění na nové inzeráty" on the Úlohy page ("Profilů: x, nových shod: y, odesláno: z", no personal data).
Analytics
GA4 is loaded through the shared consent script tomlabs-consent.js (Consent Mode v2, basic mode): nothing is sent to
Google before the visitor accepts. The "Cookies" link in the footer reopens the banner.
Data directory
<data dir>/
reality.db SQLite database (migrations are applied at startup)
documents/ uploaded PDF documents of listings
.vapid.json Web Push key pair
.aspnet/ ASP.NET data protection keys (Docker image: HOME=/app/data)
logs/ daily rolling logs (unless REALITYLENS_LOG_DIR is set)
Back up the whole directory.
Health and logs
GET /health (no authentication) answers { "status": "Healthy", "version": "1.0.0", "name": "reality-lens", "totalDuration": ..., "entries": {...} }
with HTTP 503 when Unhealthy. Degraded means the app runs with a reservation (for example open dev mode outside
Development, or nobody can sign in as admin). Other areas add an entry by registering an IHealthContributor.
The Logs page (/app/logs, admins only) shows the latest events live with filters by level, source and text, and lists
the log files with a tail view and download. The request log line of /health is dropped unless the default level is Verbose.
Roles and the worker
One codebase and one Docker image; what a process does is chosen by REALITYLENS_ROLE:
| Role | Serves | Runs |
|---|---|---|
all (default) |
UI, API, /health |
everything in one process: daily sync (03:00), price refresh (08:00 / 20:00), the queue consumer. Local development, tests and the old single-container deployment behave like this. |
web |
UI, API, /health |
no schedulers, no crawlers, no queue consumer - it only enqueues heavy work and shows what the worker does. |
worker |
only /health on http://+:8081 |
the schedulers and the queue consumer: Sreality syncs and bulk price refreshes, one heavy job at a time. |
Topology in production: nginx -> container reality-lens (role web, port published on 127.0.0.1) and a second container
reality-lens-worker (same image, role worker, no published port, own HEALTHCHECK on 8081). Both bind-mount the same host
directory as /app/data: one SQLite file reality.db in WAL mode (busy timeout 30 s on every connection) is the only thing the two share.
How they cooperate, all through that database:
- Work queue (
background_requests): the web enqueues a sync (results pages, the admin buttons on Statistiky and Úlohy) or a bulk price refresh ("Obnovit ceny"); the worker polls every few seconds and claims the oldest request with one atomicUPDATE. At most one queued or running request exists per scope (unique index), the worker runs one heavy job at a time and stops a job within seconds when it is cancelled from the Úlohy page. The schedulers also only enqueue (the daily sync) or share the one-job-at-a-time gate (the price refresh). - Incremental sync: a sync run walks the list pages of its scope (cheap, carries the price) and then downloads details only for listings that are new, whose list price differs
from the cached
price_czk, or whose detail is older thanSYNC_DETAIL_MAX_AGE_DAYS(stale ones oldest first, at mostSYNC_STALE_REFRESH_PER_RUNper run). The rest is only marked seen (last_seen_at). After a COMPLETE crawl (not truncated, no failed sub-scope) the ids of the previous finished run of the scope that are gone getremoved_atand drop out of results; a listing that reappears clears it. The run summary ("Nových: x, změněná cena: y, obnoveno starších: z, zmizelo: w, přeskočeno: n") is stored onsync_jobs. The "Nové" badge needs an earlier complete sync of the scope, so the first crawl of a scope flags nothing. - Statistics (
/app/stats): exact over all non-removed cached listings, not a sample. A light-lane jobcompute-stats(de-duplicated, task "Výpočet statistik" with "x z y" progress) streamsestate_detailsin keyset batches of 1,000 throughStatsAccumulatorand upserts one row ofstats_snapshots(JSON,computed_at,listing_count,duration_ms, formatversion). The page only reads that row. It is queued once a day by the scheduler atSTATS_DAILY_TIME(listed under "Naplánováno"), by the first visit of the page when no snapshot exists yet (the page shows "Počítám statistiky…" and polls every 3 s), and by the admin button "Přepočítat teď". A finished sync does not trigger it and opening the page never refreshes an existing snapshot - it shows "Statistiky aktualizovány ". - Market trends (
estate_events, section "Vývoj nabídky po měsících" on/app/stats): the cache only holds the current state, so the sync writes an event log from now on (no history before that exists). Kinds:new(a listing first seen - only in a scope that already had an earlier complete, non-truncated sync, the same baseline rule as the "Nové" badge, so the first crawl records nothing),removed/reappeared(where the sync sets / clearsremoved_at; removal only after a complete crawl),price_drop/price_rise(the list price differs from the stored baseline by at least 0.5 % and 100 Kč; missing prices, prices below 100 Kč and changes beyond 5x are ignored). Each row carries the region slug (from the listing, else the scope), category slug, scope key and the sync job id; (estate, kind, job) is unique, so a retried run cannot record twice. Events share the transaction of the state change. Rows older than 36 months are pruned by the daily statistics job. Thecompute-statsjob aggregates them into the snapshot (Trends: calendar months in Czech local time, zero-filled, whole country and per region; format version 2, an older snapshot is recomputed). The page shows a grouped bar chart (Přibylo / Ubylo, net per month), the price drops chart (count and average drop) and a region filter; months before the first recorded event are empty and the current month is marked in progress. Numbers cover only synced scopes, and a listing "disappears" when a complete sync no longer finds it (sold, withdrawn or relisted). Meaningful numbers start with the second complete sync of a scope (the daily sync, so within a day) and become a real monthly picture after the first full calendar month. - Two lanes: besides the heavy lane (sync, price refresh) the worker has a light lane for
enrich-listing, the surroundings of one tracked listing (crime, cadastre, flood, radon, geohazards, nature, noise, amenities, Rohlik, travel times). "Sledovat", the import and the manual GPS override save the listing at once and queue that job instead of waiting for about ten external sources; a crawl never delays it andENRICH_CONCURRENCYjobs run at a time. The state lives on the listing (enrichment_status:pending,running,done,failed; empty = an older row, treated as done); the dashboard card and the listing detail show a loader while it is pending or running, poll every 3 s and fill in the data by themselves, and offer "Zkusit znovu" after a failure. A pending or running state older than 10 minutes (no worker, or it died) is shown as failed so it can be retried. The price refresh stays synchronous. - Visibility: running jobs are mirrored to
background_runs(progress about once a second, a heartbeat every 15 s), scheduler state tobackground_schedulers; the web reads them every 2 s, so/app/tasksand the sidebar indicator show the worker's runs live, plus "Ve frontě" for waiting requests. A run whose heartbeat is older than two minutes belongs to a dead process and is marked "přerušeno". - Health: every process writes
process_heartbeatsevery 15 s. The web/healthhas aworkerentry - Healthy while a worker heartbeat is younger than two minutes, Degraded (not Unhealthy) without one; the worker's own/health(port 8081 inside its container) reports the same plus its database access. In roleallthere is noworkerentry. - Logs: the worker writes its own daily files
reality-lens-worker-YYYYMMDD.loginto the same log directory (listed on the Logs page; the live ring buffer of the Logs page shows only the web process).
Run locally in one process (all, the default): dotnet run --project src/RealityLens.Web.
Run locally as two processes against one data folder (the worker waits until the web has applied the migrations):
export REALITYLENS_DATA_DIR=/tmp/reality-data ASPNETCORE_ENVIRONMENT=Development
REALITYLENS_ROLE=web dotnet run --project src/RealityLens.Web --urls http://localhost:5396
REALITYLENS_ROLE=worker REALITYLENS_WORKER_URLS=http://localhost:5397 dotnet run --project src/RealityLens.Web
Background tasks
The Úlohy na pozadí page (/app/tasks) shows everything the app does on its own: the search synchronization (SyncService, daily at SYNC_DAILY_TIME),
the twice-daily price refresh and the manual "Obnovit ceny", travel-time recomputation, the crime-area catalogue download and startup maintenance.
Jobs register with BackgroundTaskRegistry (running state in memory and mirrored to the background_runs table, which also keeps the last 300 finished runs; runs of a process that died are
marked "přerušeno"; see "Roles and the worker" for how the web shows the worker's runs). Schedulers publish their next run; REALITYLENS_DISABLE_SCHEDULER switches both off. Admins see all runs and can start a sync or a refresh,
other users only their own runs. The health report has a background-tasks entry that is Degraded when a task runs for more than 6 hours or a scheduler fails 3 times in a row.
Project layout
src/RealityLens.Web/
Program.cs composition root
Modules/ AddXxxModule / MapXxxModule per feature area
Services/ domain code (Sreality, Agent, Archive, Context, Auth, Notifications, Logging, Ai, Health)
Components/ Blazor layout, pages, listing tabs, shared widgets
Data/ EF Core entities, DbContext, migrations
wwwroot/ css, js, PWA manifest and service worker
tests/RealityLens.Tests xunit tests, one folder per area
deploy/ VPS deploy script, nginx vhosts, CUTOVER.md
Docker
docker build -t reality-lens .
docker run -p 8080:8080 -v reality-data:/app/data reality-lens
That starts the all-in-one role. The production topology is two containers from the same image (-e REALITYLENS_ROLE=web and
-e REALITYLENS_ROLE=worker -e REALITYLENS_HEALTH_PORT=8081, same volume); deploy/smoke-roles.sh <image> starts exactly that and checks it (it runs in CI).
The image listens on 8080, keeps everything in the /app/data volume, runs as a non-root user and sets TZ=Europe/Prague.
CI/CD is azure-pipelines.yml (build, test, docker build, deploy to the VPS behind nginx, app store catalogue entry);
the one-time manual steps are in deploy/CUTOVER.md.
Migrating from the legacy apps
The application imports the data of both legacy apps (the SQLite database of the Python RealityLens and the database of
the Next.js Reality Agent) into the new database with --import-legacy:
RealityLens.Web --import-legacy --lens <sreality_archive.sqlite3> [--lens-data-dir <dir with documents/ and vapid key>]
[--agent <app.db>] [--data-dir <target REALITYLENS_DATA_DIR>] [--default-owner <e-mail>] [--dry-run]
The import is idempotent (re-running adds nothing twice), runs in one transaction and never writes to the legacy
files. Run it with --dry-run first and on copies of the legacy databases. Details: docs/IMPORT.md.
Release notes
See CHANGELOG.md.
Changelog
Changelog
1.15.1 - 2026-10-15
- Fixed: nightly statistics join historical sync metadata on disk instead of loading all matched listings into memory; cached details remain processed in bounded batches.
- Fixed: every worker creation, including deployment rollback, now applies a persistent 1536 MiB container memory limit.
1.15.0 - 2026-10-06
- Added: the Katastr tab has a clickable cadastral map. Click a parcel to outline it and see its number, cadastral area, kind, use and size; the bubble links to the parcel in Nahlížení do katastru, where the owner is shown after solving a CAPTCHA or signing in (the cadastre does not give owners out openly, so RealityLens cannot fetch them itself).
1.14.0 - 2026-10-06
Everything is separate per user now that the app is public.
- Fixed: an administrator no longer reaches other users' listings, notes, documents, viewing questions, AI texts or travel points - they work with their own data like everybody else (old listings without an owner stay reachable for the administrator).
- Fixed: the travel points page lists and edits only your own points, and a listing shared with your family no longer shows the owner's travel points and times.
- Fixed: the public link of a listing or of a list shows only what its creator owns; the public page no longer carries the owner's e-mail.
- Fixed: removing a push subscription, listing or revoking links and downloading documents work only for their owner; other requests answer as if the item did not exist.
- Fixed: the manual location of a listing and the AI actions work only on your own listing; the operator settings, the scheduler details and the raw sync error are visible to the administrator only.
- Changed: "Obnovit ceny" refreshes only your own listings; "Přepočítat teď" on the statistics page is for the administrator.
- Changed: API requests that are not allowed now answer 401/403 instead of an error page; links with secret tokens no longer appear in the log.
- Added: the isolation rules (
docs/ISOLATION.md) and a regression suite that checks them.
1.13.1 - 2026-10-06
- Fixed: the Valuo link now sends the condition "Novostavba" with the value the form actually accepts (new_building), so it is preselected.
1.13.0 - 2026-10-06
- Added: "Odhad ceny (Valuo)" on the listing detail and on the quick preview - a link that opens Valuo's free price-estimate form (profi.valuo.cz) with the property already filled in: type, location (GPS), usable and plot area, condition, construction and cellar area where Sreality states them. Nothing is sent anywhere until you click the link.
1.12.0 - 2026-10-06
- Fixed (privacy): a price or status change of a tracked listing used to be sent to every user's devices and to the admin's Slack and e-mail, so people saw what others track. A change is now sent only to the owner of the listing, to their own e-mail and their own browsers; listings shared with a family are not announced to its members. Slack gets the changes of the admin's own listings only.
- Changed: the change e-mail is readable now - a subject with the listing title and the percentage ("Změna ceny: (-5 %)"), a card with the photo, title, locality, old price crossed out and the new one in bold with the difference, price per m² and area, and buttons to the listing in RealityLens and on Sreality. A missing price is said in words ("cena už není uvedena"), a listing that went away says it may have been sold or withdrawn. It works in light and dark mail clients and on a phone.
- Changed: one message per run - after a refresh you get one e-mail (and one push) with all your changes, the biggest price drop first, instead of one per listing.
- Added: Můj účet has the section "Upozornění na změny sledovaných inzerátů" with switches for e-mail and push, and every e-mail has a one-click "Odhlásit se z e-mailů o změnách".
- Changed: the notification addresses on the Nastavení page are only used by the test notification, which now also pushes only to the admin's own browsers.
1.11.2 - 2026-10-06
- Fixed: the statistics "Počet podle kraje" showed only eight regions and put the rest (including Praha) under "Ostatní"; it now lists every region.
- Changed: the public link of a listing opens as a card at the top of the detail (button "Sdílet veřejně") instead of hiding in the Sdílení tab.
- Changed: the seller card sits in the Popis tab (listing detail, quick detail and shared detail) instead of the bottom of the page.
- Changed: the viewing questions are compact - question, answer and delete share one line (the answer wraps below on a phone and grows with its text).
1.11.1 - 2026-10-02
- Changed: the seller card (name, phone, e-mail) moved to the very bottom of the listing detail, the quick detail and the shared detail, below the photos, facts and tabs.
1.11.0 - 2026-10-02
Monthly development of the market on the Statistiky page.
- Added: section "Vývoj nabídky po měsících" - how many listings appeared and disappeared each month (with the net), and how many dropped their price (with the average drop), for the whole country or one region (kraj).
- Added: the sync now records an event log (new, disappeared, back again, price drop / rise). History before this release cannot be reconstructed, so the charts start empty and fill up as syncs run; the page says since when data are collected and marks the running month.
- Changed: the statistics snapshot has a new format and is recomputed once after the update.
- Added:
STATS_TREND_MONTHS(default 12, up to 24) sets the length of the monthly series.
1.10.0 - 2026-10-02
Seller and contact on every listing detail.
- Added: the seller of a listing with the contact on them - name, agency, photo, rating, phone numbers and e-mail from Sreality - on the tracked detail, on the quick detail (search results) and on public shares (single listing and shared list). Big "Zavolat" buttons open the dialer on a phone, "Napsat e-mail" opens a message with the listing title as subject, and small buttons copy the number or the address.
- Added: contact data are stored with tracked listings and kept up to date on every refresh; listings tracked before are filled in automatically from what was saved (no new request to Sreality). A refresh that returns nothing about the seller keeps the stored contact.
- Changed: the quick detail asks Sreality once more for listings cached earlier, to get the seller.
- Note: public share links now show the broker contact, the same data that is public on Sreality.
1.9.2 - 2026-10-01
- Fixed: the shared (public) listing detail had an empty "Parametry" tab - the parameters were read from data that is removed from the shared copy; they are now taken before it is removed.
- Fixed: the map marker in the shared listing detail was half a map tile (roughly 150-200 m) away from the real location; it now sits exactly on it.
1.9.1 - 2026-10-01
- Changed: opening a listing from a publicly shared list ("Sdílet seznam") now shows the same detail as a link to that single listing (photos, parameters, location, price history, documents), with a "Zpět na seznam" link, instead of jumping to Sreality.
- Fixed: the price history chart on shared pages is the new step chart with axes, tooltips and a summary (it was the old stretched line).
1.9.0 - 2026-10-01
Checklist for viewings.
- Added: "Moje otázky" (menu Otázky): your standard list of questions for viewings - add, edit, delete and reorder them. The optional button "Načíst doporučené otázky" adds a starter set for buying a house (it never overwrites or duplicates anything).
- Added: the tab "Prohlídka" on a tracked listing: your questions as a checklist for use on site - big tick boxes ("zjištěno"), an autosaving answer under every question, a progress bar, adding a question to this listing (optionally also to your standard list), "Doplnit chybějící z mých standardních otázek", a filter for unanswered ones and a print view. The first time the tab opens, your standard questions are copied into the listing; later changes of the standard list do not rewrite it.
- Added: "Byli jsme na prohlídce" - a button on the listing and a date on the Prohlídka tab. The card shows "Prohlédnuto " and the overview has the filter "Všechny / Prohlédnuté / Neprohlédnuté". The family sees only the date; questions, answers and notes stay private.
- Changed: deleting a listing or the account also deletes its questions and answers; the privacy page mentions them.
1.8.1 - 2026-10-01
- Changed: the cards of tracked listings are calmer - "Sdílet s rodinou" and "Přestat sledovat" are small icon buttons with tooltips (still with the confirmation), and the chips no longer show crime, the number of measurements and the house type.
- Fixed: a listing without a price showed "0 Kč"; it now says "Cena neuvedena".
1.8.0 - 2026-10-01
New-listing alerts for saved searches.
- Added: the bell "Upozorňovat na nové inzeráty" on every saved search and AI profile (rows of Hledání and Moje profily, headers of the results and the profile detail). Switch it on and choose where to send the alerts: oznámení v prohlížeči (Push) and/or e-mail. Only listings that appear after you switch it on can alert - the offer that is already there never floods you.
- Added: a listing that falls into the search (the same decision the results page makes: classic parameters, or the AI profile's hard filters and score) is announced once. New listings are sent as one message per search: "Nová nemovitost: ..." with a link to the quick detail, or "N nových nemovitostí pro hledání ..." with a link to the results.
- Added: every alert e-mail has an "Odhlásit se z upozornění" link (and the List-Unsubscribe header) that switches off the alerts of that search without signing in.
- Added: limits against noise (20 messages per day per user, alerts on 10 searches per user; configurable) - matches over the daily limit are carried into the next message.
- Added: Můj účet lists the searches with alerts and has "Vypnout všechna upozornění"; the privacy policy describes the alerts. Deleting a search or the account removes its alert history.
- Added: the Úlohy page shows the run "Upozornění na nové inzeráty".
1.7.1 - 2026-10-01
- Changed: the landing page headline reads "Najdi nemovitost, pak ji pohlídej - žádné otevřené záložky." and the three problem cards (price history, hidden risks, ten open tabs) are removed.
1.7.0 - 2026-10-01
- Added: a photo lightbox. Clicking the photo (or the new "Zvětšit" button) on the tracked listing detail and the quick detail opens the photos full screen: previous/next buttons, arrow keys, Home/End and Esc, swipe on touch screens, a thumbnail strip, a counter, click or double-tap to zoom, and a link to open the original image. The page behind is locked while it is open and focus returns to the photo when it closes.
1.6.2 - 2026-10-01
- Fixed: a tracked listing had no "Otevřít na Sreality" link when it was tracked by id or from a search card; the link is now always there (an address built from the listing id is used when none was stored, Sreality redirects it to the real page).
1.6.1 - 2026-10-01
- Fixed: a missing closing brace in the stylesheet (left by a merge) made every style after it stop applying - among them the quick detail page and the photo links, so the photos on the result cards (search, swipe) were shown zoomed in; they fill the card again. A test now guards the stylesheet against unclosed blocks.
1.6.0 - 2026-10-01
Quick detail of a listing straight from the search.
- Added: the page "Náhled inzerátu" (
/app/estate/{id}) shows any listing from the search cache - or loads it from Sreality when it is not cached - without tracking it: photos, price, locality, key parameters, description, nearby places with distances, a small map and the "Nové" badge. Actions: "Sledovat" (then "Sledováno" linking to the tracked detail; "Sledováno - otevřít" when you already track it), "Otevřít na Sreality" and "Zpět na výsledky", which returns to the same search. - Added: in the results (classic search, AI profile) and in the swipe deck the title and the photo open that page; the Sreality link and the "Sledovat" button stay.
- Added: the box "Otevřít inzerát podle odkazu nebo ID" on the Hledání page and in the header of the results - paste a Sreality link or a numeric ID.
- Changed: a listing that Sreality no longer serves shows "Inzerát už není dostupný"; a removed one still shows what is cached with a note.
- Changed: the search cache now also keeps the listing title, price, place and (shortened) description of newly downloaded details.
1.5.2 - 2026-10-01
Statistics over every listing, not the first 5,000.
- Fixed: the Statistiky page counted only an arbitrary 5,000 cached listings ("Appka doteď prošla 5 000 inzerátů"). Every figure (counts, shares, histograms, price per m² by region) is now exact over all listings that are still on offer.
- Changed: the statistics are precomputed by a background job (streamed in batches, so memory stays low) and stored as a snapshot, so the page opens instantly. They are recomputed
once a day at
STATS_DAILY_TIME(default 05:00; listed under "Naplánováno" on the Úlohy page), on the first visit when nothing has been computed yet, and on demand with the new admin button "Přepočítat teď". The page shows when the numbers were last updated.
1.5.1 - 2026-10-01
- Added: a notice that the service is a test run, for family houses only - on the landing page, on the sign-in card and as a slim bar at the top of the app.
- Fixed: the summary cards on the dashboard no longer break words in the middle (three per row unless the window is very wide).
1.5.0 - 2026-10-01
Searches only download what is new.
- Changed: the search synchronization still walks the list pages of a scope, but downloads a listing detail only when the listing is new, its price changed or its cached detail is
older than
SYNC_DETAIL_MAX_AGE_DAYS(default 14 days; at mostSYNC_STALE_REFRESH_PER_RUN, default 500, per run, oldest first). A repeated sync of an unchanged scope makes no detail requests at all, and the progress counts only the details that really have to be fetched. - Added: listings that disappear from a complete crawl are marked as removed and no longer appear in results or in the swipe deck; they come back if they reappear. Nothing is marked removed after an incomplete (truncated or partly failed) crawl.
- Added: a "Nové" badge on result cards for listings first seen within
SYNC_NEW_DAYS(default 7), plus a "Jen nové" filter in the classic search. The first crawl of a scope does not flag everything as new. - Changed: the run summary on Úlohy na pozadí reads "Nových: x, změněná cena: y, obnoveno starších: z, zmizelo: w, přeskočeno: n".
- Changed: result pages only fill missing details live (cached details count as fresh for 30 days); refreshing is the sync's job.
- Database: new columns on
estate_details(price_czk,first_seen_at,last_seen_at,removed_at,first_seen_is_new) and onsync_jobs(truncated, run counters,summary) (migrationIncrementalSync, additive; existing rows count as first seen when fetched and are not new; prices are backfilled from the next list crawl). - Fixed: the Statistiky page no longer counts listings that have disappeared from Sreality.
1.4.3 - 2026-10-01
- Changed: the "Úlohy" page (background tasks) and its sidebar indicator are for administrators only; other users no longer see the entry, and the sync banner shows no link to it.
1.4.2 - 2026-10-01
- Fixed: synchronising a region could fail with an error ("HTTP 404 ... strana=38"). Sreality answers 404 for the page after the last one; that is now treated as the end of the list instead of a failure.
- Removed: the "Rozšířit pokrytí" card on the Statistiky page (indexing is started from the Úlohy na pozadí page).
1.4.1 - 2026-10-01
"Sledovat" is instant.
- Changed: tracking a listing (search results, "Sledovat také u mě", the import on Přehled) saves it at once - the card flips to "Sledováno" in well under a second. The surroundings (crime, cadastre, flood, radon, geohazards, nature, noise, amenities, Rohlik, travel times), which used to make the button wait tens of seconds, are filled in afterwards by the worker.
- Added: while that runs, the card on Přehled shows "Dopočítávám okolí…" and the listing detail an info bar with placeholders in the surroundings rows (Parametry, Poloha, Katastr, Kriminalita). The data appears by itself when it is ready, no reload needed. If it fails, "Dopočet okolí se nepodařil" offers "Zkusit znovu".
- Changed: moving the GPS point of a listing by hand also saves at once and recalculates the surroundings in the background.
- Changed: the worker has a second, light lane for this, so a long Sreality sync never delays it;
ENRICH_CONCURRENCY(default 2) limits how many listings are enriched at the same time. The Úlohy page lists it as "Dopočet okolí". - Database: new columns
enrichment_status,enrichment_updated_at,enrichment_erroronlistings(migrationListingEnrichment, additive; existing rows keep an empty state and count as done).
1.4.0 - 2026-10-01
Sharing with a single user is replaced by a family.
- Added: "Rodina" (
/app/family) - a small group of people who invite each other by e-mail and see each other's shared listings. Create a family, invite by e-mail address (the invitee gets a link valid 14 days, you also see the link to copy), rename it, remove members, leave or dissolve it. A family has at most 8 members (REALITYLENS_FAMILY_MAX_MEMBERS) and you can be in one family at a time. - Added: "Sdílet s rodinou" / "Přestat sdílet s rodinou" on the listing detail, in the Sdílení tab and on dashboard cards. Family members see a shared listing read-only (including documents and price history) in the "Sdílené inzeráty" section of the Rodina page; your own notes stay private and everyone keeps their own note. "Sledovat také u mě" lets a member track the same listing as their own copy.
- Changed: leaving the family, being removed, unsharing or dissolving the family takes the access away immediately. Deleting your account while you own a family hands it to the longest-standing member.
- Removed: "Sdílet s uživatelem" and the user picker. Listings that were shared with a user before are no longer visible to that user - share them again through the family. Public share links are unchanged.
- Changed: the privacy page and the account page mention the family. The legacy importer no longer imports per-user shares.
- Database: new tables
families,family_members,family_invites,family_listings(migrationFamily, additive);listing_sharesis kept as the per-member notes. - Changed: deployments run one at a time (the pipeline batches pushes, uploads into a folder per run and the deploy script takes a lock on the VPS), so overlapping runs cannot disturb each other.
1.3.1 - 2026-10-01
- Changed: the price history chart ("Vývoj ceny") is redrawn as a step line with a soft area fill, price and date axes, a dot on every price change and a larger one on the current price; periods when the listing was unavailable are dashed.
- Added: tooltips with date, price, change against the previous price (with arrow and sign) and status - on hover, keyboard focus or tap (tap pins it).
- Added: a summary under the chart - current, first, lowest and highest price, total change and the observation period.
- Fixed: the chart no longer stretches on phones; a listing with a single measurement shows its price instead of an empty chart.
1.3.0 - 2026-10-01
The full-country search sync works again, and heavy background work moved out of the web process into a separate worker.
- Fixed: the sync of the whole country failed on every run because Sreality answers HTTP 500 from page 455 of a search (about 9,000 of the 22,000 houses). A scope above that limit is now split per region (and per category when a region is still too big), listings are de-duplicated across the parts, and when Sreality still stops answering deep in a list the sync keeps what it collected, goes on to download the details and says "zkráceno" in its result.
- Fixed: a failed page is retried a few times with growing pauses (and honours a Retry-After from Sreality) before the sync gives up with a clear error.
- Fixed: a scope whose sync failed was restarted on every visit of a results page. It now waits
SYNC_FAILURE_COOLDOWN_MINUTES(default 30); the daily run ignores the wait. - Added: a separate worker process. One image, the role is
REALITYLENS_ROLE=all(default, everything in one process as before),web(UI and API) orworker(schedulers and queued jobs, only/healthon a container-internal port). In production the deploy now runs two containers,reality-lensandreality-lens-worker. - Changed: syncs (results pages, Statistiky, Úlohy) and the "Obnovit ceny" button put a request into a work queue (new table
background_requests) that the worker processes one at a time; a sync or refresh that is already waiting or running for the same scope is not queued twice. The daily sync and the price refresh schedule run in the worker. - Added: on "Úlohy na pozadí" a "Ve frontě" section, a "Zrušit" button for running and waiting jobs, and the worker's runs and schedules shown live (refreshed every 2 s); the sidebar indicator counts running and waiting jobs. A run whose process died is marked "přerušeno" by its heartbeat.
- Added: the
/healthof the web has aworkerentry (Healthy with a heartbeat younger than two minutes, Degraded without one); the deploy waits for it and rolls back both containers when the worker does not come up. - Changed: SQLite runs in WAL mode with a 30 s busy timeout so the two processes can share the database file (migration
WorkerQueue; new tablesbackground_requests,background_schedulers,process_heartbeatsand extra columns onbackground_runs; nothing existing is changed). Back up the whole data directory (it now hasreality.db-walnext to the database). - Changed: the Docker HEALTHCHECK follows
REALITYLENS_HEALTH_PORTso it serves both roles;deploy/smoke-roles.shstarts both roles from the built image and runs in the pipeline.
1.2.2 - 2026-10-01
- Changed: the logo in the sidebar, on the sign-in page, on the landing page and on the legal pages is now the same lens-over-house icon as the browser tab icon (it replaces the plain house symbol).
1.2.1 - 2026-10-01
Cleanup of the settings, the sidebar, saved searches and the maps.
- Added: "Přestat sledovat" (with a confirmation) on the listing detail and on every dashboard card - it removes the listing with its history, events, documents and travel times in one step, no need to archive it first. A listing that was shared with you only disappears from your own list. The listing can be tracked again from the search results.
- Added: a "Smazat" button (with a confirmation) on saved searches - classic and AI - in the search list, on the profile list and on the search detail and result pages.
- Changed: all maps (overview, listing location, travel points) open on the basic street map; the aerial, tourist and winter layers stay in the layer control. Without a Mapy.com key the street map is OpenStreetMap. The chosen layer is remembered in the browser.
- Removed: the home point from Settings - use "Dojezdová místa" (an existing home point was already converted to a travel point).
- Removed: the admin page "Všechny inzeráty" and the ability to assign ownerless listings.
1.2.0 - 2026-09-30
Everything the app does in the background is visible in one place.
- Added: the "Úlohy na pozadí" page (
/app/tasks) - running tasks with live progress, scheduled tasks with their next run and last result, and a history of recent runs with status, duration, summary and error text. Admins see everything and can start a synchronization or a price refresh; other users see only the runs they started. - Added: a live indicator next to "Úlohy" in the sidebar while something is running, and a link from the sync progress banner.
- Added: a health entry that turns Degraded when a task seems stuck (running for more than 6 hours) or a scheduler keeps failing.
- Changed: the last 300 finished runs are kept in the database (new table
background_runs, migrationBackgroundRuns).
1.1.1 - 2026-09-30
- Fixed: interactive pages (search, maps, swipe, statistics, AI profile) did not work on the live site because the Blazor script was missing from the container image.
- Fixed: a leftover error bar ("Došlo k neočekávané chybě") was stuck at the bottom of every page; it now shows only when an error really happens.
- Fixed: the container no longer fails to start on servers with a restricted CPU set, and the deploy picks a free local port instead of a fixed one.
- Changed: the "AI profily" entry is removed from the sidebar (saved AI profiles stay reachable from "Hledání").
1.1.0 - 2026-09-30
RealityLens is ready for several users: everybody tracks listings independently, and favorites are gone.
- Changed: every user tracks any Sreality listing on their own - notes, archive flag, travel times, documents, price history, events and shares are per user. Tracking a listing another user already tracks no longer fails.
- Changed: favorites are merged into tracked listings. The "Oblíbené" page and the heart buttons are removed; "Sledovat" / "Sledováno" is the only action on a result card.
- Changed: swiping right now tracks the listing (and never shows it again); swiping left only remembers the decision. The "Co se mi líbilo" page is replaced by the tracked listings.
- Changed: the legacy import no longer imports favorites; it prints their Sreality ids so they can be tracked again.
- Fixed: duplicate warnings only compare a listing with the user's own listings.
1.0.0 - 2026-09-30
The two apps are now one: RealityLens (archive of tracked listings) and Reality Agent (search on Sreality.cz) are merged into a single application. Search like in the agent, then track and analyse like in RealityLens.
- Added: the search agent lives inside RealityLens - classic parametric search, AI preference profile (chat), scoring, swipe,
favorites and statistics under
/app/search,/app/profile,/app/swipe,/app/favorites,/app/stats. - Added: a "Sledovat" button on every search result puts the listing into the archive with price history, events, notes, location context (crime, cadastre, flood, radon, geohazards, nature, noise, amenities, travel times, Rohlik), duplicates, documents, sharing and AI summaries.
- Added: a public landing page on
/; the application itself moved to/app. - Added: Google Analytics 4 behind a consent banner (nothing is sent before the visitor accepts), with a "Cookies" link to change the choice.
- Changed: ported from Python / Next.js to a single Blazor Web App on .NET 10 with SQLite; behaviour, data sources and Czech wording of both apps are kept.
- Changed: result lists paginate, 24 listings at a time, instead of loading everything at once.
- Changed: a signed-in session is re-checked against the invite list on every request, so removing a user or changing a role takes effect immediately, without a new sign-in.
- Changed: configuration uses
REALITYLENS_*environment variables; the legacy names (SREALITY_*, plainADMIN_EMAILS,OPENAI_API_KEY, ...) are still read, so an existing environment keeps working. - Unchanged: the Google redirect URI is still
/auth/google/callback, so the existing Google OAuth registration keeps working. - Changed: the app lives at
https://realitylens.cz; the former addresses (reality.tomlabs.xyz,realitni-agent.tomlabs.xyz) are retired. - Added:
--import-legacycommand line option to move the data of the legacy apps into the new database (see--help).