6 Commits

Author SHA1 Message Date
7d3021efbf Remove tracked generated/data files and fix map link on status page
- Remove byty_*.json, mapa_bytu.html, .DS_Store and settings.local.json from git tracking
  (already in .gitignore, files kept locally)
- Fix "Otevřít mapu" link on scraper status page: / → /mapa_bytu.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:50:16 +01:00
23d208a5b7 Merge pull request 'Add scraper status collection and presentation' (#3) from add-scraper-statuses into main
Reviewed-on: #3
2026-02-26 09:04:23 +00:00
Jan Novak
00c9144010 Fix DATA_DIR usage in stats/history paths, set env in Dockerfile, add validation docs
All checks were successful
Build and Push / build (push) Successful in 5s
- scraper_stats.py: respect DATA_DIR env var when writing stats_*.json files
- generate_status.py: read stats files and write history from DATA_DIR instead of HERE
- build/Dockerfile: set DATA_DIR=/app/data as default env var
- docs/validation.md: end-to-end Docker validation recipe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 09:46:16 +01:00
Jan Novak
44c02b45b4 Increase history retention to 20, run scrapers every 4 hours
All checks were successful
Build and Push / build (push) Successful in 7s
- generate_status.py: raise --keep default from 5 to 20 entries
- build/crontab: change schedule from 06:00/18:00 to every 4 hours (*/4)
  covers 6 runs/day ≈ 3.3 days of history at default retention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 08:53:27 +01:00
Jan Novak
5fb3b984b6 Add status dashboard, server, scraper stats, and DATA_DIR support
All checks were successful
Build and Push / build (push) Successful in 7s
Key changes:
- Replace ratings_server.py + status.html with a unified server.py that
  serves the map, scraper status dashboard, and ratings API in one process
- Add scraper_stats.py utility: each scraper writes per-run stats (fetched,
  accepted, excluded, duration) to stats_<source>.json for the status page
- generate_status.py: respect DATA_DIR env var so status.json lands in the
  configured data directory instead of always the project root
- run_all.sh: replace the {"status":"running"} overwrite of status.json with
  a dedicated scraper_running.json lock file; trap on EXIT ensures cleanup
  even on kill/error, preventing the previous run's results from being wiped
- server.py: detect running state via scraper_running.json existence instead
  of status["status"] field, eliminating the dual-use race condition
- Makefile: add serve (local dev), debug (Docker debug container) targets;
  add SERVER_PORT variable
- build/Dockerfile + entrypoint.sh: switch to server.py, set DATA_DIR,
  adjust volume mounts
- .gitignore: add *.json and *.log to keep runtime data files out of VCS
- mapa_bytu.html: price-per-m² colouring, status link, UX tweaks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:30:25 +01:00
6f49533c94 Merge pull request 'Rewrite PSN + CityHome scrapers, add price/m² map coloring, ratings system, and status dashboard' (#2) from ui-tweaks/2026-02-17 into main
Reviewed-on: #2
2026-02-25 21:26:51 +00:00
28 changed files with 912 additions and 3159 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,31 +0,0 @@
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"WebFetch(domain:www.sreality.cz)",
"WebFetch(domain:webscraping.pro)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(python3:*)",
"Bash(open:*)",
"WebFetch(domain:www.realingo.cz)",
"WebFetch(domain:api.realingo.cz)",
"Bash(curl:*)",
"Bash(grep:*)",
"WebFetch(domain:www.realitni-pes.cz)",
"WebFetch(domain:www.bezrealitky.cz)",
"WebFetch(domain:apify.com)",
"WebFetch(domain:www.bezrealitky.com)",
"WebFetch(domain:reality.idnes.cz)",
"Bash(# Final checks: robots.txt and response time for rate limiting clues curl -s -L -H \"\"User-Agent: Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36\"\" \"\"https://reality.idnes.cz/robots.txt\"\")",
"WebFetch(domain:www.cityhome.cz)",
"WebFetch(domain:www.psn.cz)",
"WebFetch(domain:www.city-home.cz)",
"WebFetch(domain:psn.cz)",
"WebFetch(domain:api.psn.cz)",
"Bash(done)",
"Bash(# Final summary: count total units across all projects\n# Get the total count from the unitsCountData we already extracted\necho \"\"From unitsCountData on /prodej page:\"\"\necho \"\" type_id 0 \\(Prodej bytů a ateliérů\\): 146\"\"\necho \"\" type_id 1 \\(Prodej komerčních nemovitostí\\): 14\"\"\necho \"\" type_id 2 \\(Pronájem bytů\\): 3\"\"\necho \"\" type_id 3 \\(Pronájem komerčních nemovitostí\\): 48\"\"\necho \"\"\"\"\necho \"\"Total for-sale projects: 19\"\"\necho \"\"\"\"\necho \"\"Disposition counts from the data:\"\"\npython3 << 'PYEOF'\n# Extract disposition counts from prodej page\nimport re\n\nwith open\\('/tmp/psn_prodej_p1.html', 'r', encoding='utf-8'\\) as f:\n html = f.read\\(\\)\n\n# Find disposition data\nidx = html.find\\('\\\\\\\\\"disposition\\\\\\\\\":['\\)\nif idx >= 0:\n chunk = html[idx:idx+2000].replace\\('\\\\\\\\\"', '\"'\\)\n # Extract name and count pairs\n import re\n pairs = re.findall\\(r'\"name\":\"\\([^\"]+\\)\",\"count\":\\(\\\\d+\\)', chunk\\)\n for name, count in pairs:\n print\\(f\" {name}: {count}\"\\)\nPYEOF)",
"Bash(ls:*)",
"Bash(chmod:*)"
]
}
}

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
.vscode/ .vscode/
__pycache__/ __pycache__/
.DS_Store
byty_*.json byty_*.json
*.json
*.log
mapa_bytu.html

View File

@@ -3,9 +3,13 @@ CONTAINER_NAME := maru-hleda-byt
VOLUME_NAME := maru-hleda-byt-data VOLUME_NAME := maru-hleda-byt-data
VALIDATION_CONTAINER := maru-hleda-byt-validation VALIDATION_CONTAINER := maru-hleda-byt-validation
VALIDATION_VOLUME := maru-hleda-byt-validation-data VALIDATION_VOLUME := maru-hleda-byt-validation-data
DEBUG_CONTAINER := maru-hleda-byt-debug
DEBUG_VOLUME := maru-hleda-byt-debug-data
DEBUG_PORT ?= 8082
PORT := 8080 PORT := 8080
SERVER_PORT ?= 8080
.PHONY: build run stop logs scrape restart clean help validation validation-local validation-stop validation-local-debug serve serve-debug .PHONY: build run stop logs scrape restart clean help serve validation validation-local validation-stop validation-local-debug debug debug-stop
help: help:
@echo "Available targets:" @echo "Available targets:"
@@ -20,8 +24,9 @@ help:
@echo " validation-local-debug - Run validation locally with DEBUG logging" @echo " validation-local-debug - Run validation locally with DEBUG logging"
@echo " restart - Restart the container (stop and run again)" @echo " restart - Restart the container (stop and run again)"
@echo " clean - Stop container and remove the Docker image" @echo " clean - Stop container and remove the Docker image"
@echo " serve - Run server.py locally (DATA_DIR=., port $(PORT))" @echo " serve - Start server.py locally on port 8080"
@echo " serve-debug - Run server.py locally with DEBUG logging" @echo " debug - Build and run debug Docker container with limited scrape (port $(DEBUG_PORT))"
@echo " debug-stop - Stop and remove the debug Docker container"
@echo " help - Show this help message" @echo " help - Show this help message"
build: build:
@@ -61,18 +66,33 @@ validation-stop:
@docker rm $(VALIDATION_CONTAINER) 2>/dev/null || true @docker rm $(VALIDATION_CONTAINER) 2>/dev/null || true
@echo "Validation container stopped and removed" @echo "Validation container stopped and removed"
debug: build
@docker stop $(DEBUG_CONTAINER) 2>/dev/null || true
@docker rm $(DEBUG_CONTAINER) 2>/dev/null || true
docker run -d --name $(DEBUG_CONTAINER) \
-p $(DEBUG_PORT):8080 \
-v $(DEBUG_VOLUME):/app/data \
-e LOG_LEVEL=DEBUG \
$(IMAGE_NAME)
@sleep 2
docker exec $(DEBUG_CONTAINER) bash /app/run_all.sh --max-pages 1 --max-properties 10
@echo "Debug app at http://localhost:$(DEBUG_PORT)/mapa_bytu.html"
@echo "Debug status at http://localhost:$(DEBUG_PORT)/scrapers-status"
debug-stop:
@docker stop $(DEBUG_CONTAINER) 2>/dev/null || true
@docker rm $(DEBUG_CONTAINER) 2>/dev/null || true
@echo "Debug container stopped and removed"
serve:
DATA_DIR=. SERVER_PORT=$(SERVER_PORT) python3 server.py
validation-local: validation-local:
./run_all.sh --max-pages 1 --max-properties 10 ./run_all.sh --max-pages 1 --max-properties 10
validation-local-debug: validation-local-debug:
./run_all.sh --max-pages 1 --max-properties 10 --log-level DEBUG ./run_all.sh --max-pages 1 --max-properties 10 --log-level DEBUG
serve:
DATA_DIR=. PORT=$(PORT) python server.py
serve-debug:
DATA_DIR=. PORT=$(PORT) python server.py --verbose
restart: stop run restart: stop run
clean: stop clean: stop

View File

@@ -5,14 +5,14 @@ RUN apk add --no-cache curl bash tzdata \
&& echo "Europe/Prague" > /etc/timezone && echo "Europe/Prague" > /etc/timezone
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV DATA_DIR=/app/data
RUN pip install --no-cache-dir flask
WORKDIR /app WORKDIR /app
COPY scrape_and_map.py scrape_realingo.py scrape_bezrealitky.py \ COPY scrape_and_map.py scrape_realingo.py scrape_bezrealitky.py \
scrape_idnes.py scrape_psn.py scrape_cityhome.py \ scrape_idnes.py scrape_psn.py scrape_cityhome.py \
merge_and_map.py regen_map.py run_all.sh server.py ./ merge_and_map.py regen_map.py generate_status.py scraper_stats.py \
run_all.sh server.py ./
COPY build/crontab /etc/crontabs/root COPY build/crontab /etc/crontabs/root
COPY build/entrypoint.sh /entrypoint.sh COPY build/entrypoint.sh /entrypoint.sh

View File

@@ -1 +1 @@
0 6,18 * * * cd /app && bash /app/run_all.sh >> /proc/1/fd/1 2>> /proc/1/fd/2 0 */4 * * * cd /app && bash /app/run_all.sh >> /proc/1/fd/1 2>> /proc/1/fd/2

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
DATA_DIR="/app/data" export DATA_DIR="/app/data"
# Create symlinks so scripts (which write to /app/) persist data to the volume # Create symlinks so scripts (which write to /app/) persist data to the volume
for f in byty_sreality.json byty_realingo.json byty_bezrealitky.json \ for f in byty_sreality.json byty_realingo.json byty_bezrealitky.json \
@@ -18,5 +18,5 @@ crond -b -l 2
echo "[entrypoint] Starting initial scrape in background..." echo "[entrypoint] Starting initial scrape in background..."
bash /app/run_all.sh & bash /app/run_all.sh &
echo "[entrypoint] Starting combined server on port 8080..." echo "[entrypoint] Starting server on port 8080..."
exec DATA_DIR="$DATA_DIR" python3 /app/server.py exec python3 /app/server.py

View File

@@ -1,40 +0,0 @@
[
{
"hash_id": 981278,
"name": "Prodej bytu 3+kk 70 m²",
"price": 11890000,
"price_formatted": "11 890 000 Kč",
"locality": "Argentinská, Praha - Holešovice",
"lat": 50.1026043,
"lon": 14.4435365,
"disposition": "3+kk",
"floor": 3,
"area": 70,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/981278-nabidka-prodej-bytu-argentinska-praha",
"source": "bezrealitky",
"image": "",
"first_seen": "2026-02-15",
"last_updated": "2026-02-15"
},
{
"hash_id": 991217,
"name": "Prodej bytu 3+kk 71 m²",
"price": 11490000,
"price_formatted": "11 490 000 Kč",
"locality": "Kolbenova, Praha - Vysočany",
"lat": 50.1113213,
"lon": 14.5106858,
"disposition": "3+kk",
"floor": 3,
"area": 71,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/991217-nabidka-prodej-bytu-kolbenova-praha",
"source": "bezrealitky",
"image": "",
"last_updated": "2026-02-15",
"first_seen": "2026-02-15"
}
]

View File

@@ -1,38 +0,0 @@
[
{
"hash_id": "cityhome_na-vaclavce-34_Byt A2.3",
"name": "Prodej bytu 3+1, 99 m² — Na Václavce 34",
"price": 13490000,
"price_formatted": "13 490 000 Kč",
"locality": "Na Václavce 34, Praha 5",
"lat": 50.0652858,
"lon": 14.3931318,
"disposition": "3+1",
"floor": 2,
"area": 99.1,
"building_type": "Cihlová",
"ownership": "neuvedeno",
"url": "https://www.city-home.cz/projekty/na-vaclavce-34/nabidka-nemovitosti/byt-a23",
"source": "cityhome",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "cityhome_na-vaclavce-34_Byt A3.2",
"name": "Prodej bytu 3+1, 95 m² — Na Václavce 34",
"price": 13490000,
"price_formatted": "13 490 000 Kč",
"locality": "Na Václavce 34, Praha 5",
"lat": 50.0652858,
"lon": 14.3931318,
"disposition": "3+1",
"floor": 3,
"area": 95.6,
"building_type": "Cihlová",
"ownership": "neuvedeno",
"url": "https://www.city-home.cz/projekty/na-vaclavce-34/nabidka-nemovitosti/byt-a32",
"source": "cityhome",
"image": "",
"scraped_at": "2026-02-25"
}
]

View File

@@ -1,290 +0,0 @@
[
{
"hash_id": "699ed0af74468ff4c2079aa1",
"name": "Prodej bytu 3+1 86 m²",
"price": 4600000,
"price_formatted": "4 600 000 Kč",
"locality": "Hynka Puce, Praha 5 - Stodůlky",
"lat": 50.049168412058556,
"lon": 14.302095927878957,
"disposition": "3+1",
"floor": 8,
"area": 86,
"building_type": "Cihlová",
"ownership": "družstevní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-13-hynka-puce/699ed0af74468ff4c2079aa1/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "699ecf47513757ba150e0c74",
"name": "Prodej bytu 3+kk 83 m²",
"price": 11390000,
"price_formatted": "11 390 000 Kč",
"locality": "Kytlická, Praha 9 - Prosek",
"lat": 50.1251431182,
"lon": 14.5077027612,
"disposition": "3+kk",
"floor": 8,
"area": 83,
"building_type": "2011",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-9-kytlicka/699ecf47513757ba150e0c74/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "699c09d25d571b3c7b026d3e",
"name": "Prodej bytu 3+kk 93 m²",
"price": 11890000,
"price_formatted": "11 890 000 Kč",
"locality": "Kříženeckého náměstí, Praha 5 - Hlubočepy",
"lat": 50.03137852,
"lon": 14.39175816,
"disposition": "3+kk",
"floor": 3,
"area": 93,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-5-krizeneckeho-namesti/699c09d25d571b3c7b026d3e/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "693690e98418631b48025208",
"name": "Prodej bytu 3+1 87 m²",
"price": 11323000,
"price_formatted": "11 323 000 Kč",
"locality": "Libušská, Praha 4 - Libuš",
"lat": 50.009743674736,
"lon": 14.460835345662,
"disposition": "3+1",
"floor": 2,
"area": 87,
"building_type": "Cihlová",
"ownership": "družstevní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-12-libusska/693690e98418631b48025208/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "699487a84abe8029bd065570",
"name": "Prodej bytu 3+1 96 m²",
"price": 13490000,
"price_formatted": "13 490 000 Kč",
"locality": "Na Václavce, Praha 5 - Smíchov",
"lat": 50.0652882346,
"lon": 14.3931192571,
"disposition": "3+1",
"floor": 4,
"area": 96,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-5-na-vaclavce/699487a84abe8029bd065570/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "697c7e54d08e16f19902d777",
"name": "Prodej bytu 3+kk 76 m²",
"price": 11590040,
"price_formatted": "11 590 040 Kč",
"locality": "Žilinská, Praha 4 - Záběhlice",
"lat": 50.04710645755815,
"lon": 14.473057214055794,
"disposition": "3+kk",
"floor": 5,
"area": 76,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-4-zilinska/697c7e54d08e16f19902d777/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "68f5f1e873fec2e50c0cc20e",
"name": "Prodej bytu 3+kk 85 m²",
"price": 13499900,
"price_formatted": "13 499 900 Kč",
"locality": "Hořejší nábřeží, Praha 5 - Smíchov",
"lat": 50.0724036111,
"lon": 14.4103030556,
"disposition": "3+kk",
"floor": 3,
"area": 85,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-5-horejsi-nabrezi/68f5f1e873fec2e50c0cc20e/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "6941cf632ff10124be08ce19",
"name": "Prodej bytu 4+kk 94 m²",
"price": 13249900,
"price_formatted": "13 249 900 Kč",
"locality": "V dolině, Praha 10 - Michle",
"lat": 50.0579944444,
"lon": 14.4682905556,
"disposition": "4+kk",
"floor": 14,
"area": 94,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-10-v-doline/6941cf632ff10124be08ce19/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "690c2cba1c264f9f43027912",
"name": "Prodej bytu 3+kk 74 m²",
"price": 10631123,
"price_formatted": "10 631 123 Kč",
"locality": "Voskovcova, Praha 5 - Hlubočepy",
"lat": 50.0290438889,
"lon": 14.3641566667,
"disposition": "3+kk",
"floor": 6,
"area": 74,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-5-voskovcova/690c2cba1c264f9f43027912/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "68404b3d8178bbed020f1742",
"name": "Prodej bytu 3+kk 71 m²",
"price": 10990000,
"price_formatted": "10 990 000 Kč",
"locality": "Praha 10 - Uhříněves",
"lat": 50.026899,
"lon": 14.613713,
"disposition": "3+kk",
"floor": 5,
"area": 71,
"building_type": "Skeletová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-22/68404b3d8178bbed020f1742/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "6932bf9dc9442dc194054416",
"name": "Prodej bytu 3+kk 71 m²",
"price": 8100000,
"price_formatted": "8 100 000 Kč",
"locality": "Štětínská, Praha 8 - Bohnice, okres Praha",
"lat": 50.1297302,
"lon": 14.4286652,
"disposition": "3+kk",
"floor": 5,
"area": 71,
"building_type": "1974",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-8-stetinska/6932bf9dc9442dc194054416/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "699eacc0a56ec9b4a80069b6",
"name": "Prodej bytu 3+kk 81 m²",
"price": 13000000,
"price_formatted": "13 000 000 Kč",
"locality": "Hlučkova, Praha 9 - Letňany",
"lat": 50.141739,
"lon": 14.522086,
"disposition": "3+kk",
"floor": 17,
"area": 81,
"building_type": "Smíšená",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-18-hluckova/699eacc0a56ec9b4a80069b6/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "682b20ae5fcffc3dc8072856",
"name": "Prodej bytu 3+kk 78 m²",
"price": 12463000,
"price_formatted": "12 463 000 Kč",
"locality": "Kubelíkova, Praha 3 - Žižkov",
"lat": 50.0823325029164,
"lon": 14.451052236466976,
"disposition": "3+kk",
"floor": 5,
"area": 78,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-3-kubelikova/682b20ae5fcffc3dc8072856/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "68f0b7b4263df471cb050df9",
"name": "Prodej bytu 4+kk 75 m²",
"price": 10363000,
"price_formatted": "10 363 000 Kč",
"locality": "Karla Guta, Praha 10 - Uhříněves",
"lat": 50.030382258,
"lon": 14.5931238354,
"disposition": "4+kk",
"floor": 4,
"area": 75,
"building_type": "Cihlová",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-22-karla-guta/68f0b7b4263df471cb050df9/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "695cdf5113e97880200d9e62",
"name": "Prodej bytu 3+kk 82 m²",
"price": 11133000,
"price_formatted": "11 133 000 Kč",
"locality": "K Vinoři, Praha 9 - Kbely",
"lat": 50.132835725,
"lon": 14.5613326001,
"disposition": "3+kk",
"floor": 3,
"area": 82,
"building_type": "2026",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-19-k-vinori/695cdf5113e97880200d9e62/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": "69930de7098209b20e066a6c",
"name": "Prodej bytu 3+kk 91 m²",
"price": 11000000,
"price_formatted": "11 000 000 Kč",
"locality": "Formanská, Praha 4 - Újezd u Průhonic, okres Praha",
"lat": 50.0114383,
"lon": 14.5469,
"disposition": "3+kk",
"floor": 3,
"area": 91,
"building_type": "2017",
"ownership": "osobní",
"url": "https://reality.idnes.cz/detail/prodej/byt/praha-11-formanska/69930de7098209b20e066a6c/",
"source": "idnes",
"image": "",
"scraped_at": "2026-02-25"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
[
{
"hash_id": "8941",
"name": "Prodej bytu 3+kk, 102 m² — JITRO",
"price": 13994000,
"price_formatted": "13 994 000 Kč",
"locality": "Litevská 1174/8, Praha 10",
"lat": 50.0729,
"lon": 14.4767,
"disposition": "3+kk",
"floor": 2,
"area": 102.7,
"building_type": "neuvedeno",
"ownership": "osobní",
"url": "https://psn.cz/prodej/ubytovaci-jednotka-3-kk-litevska-praha-10-vrsovice-lit4219",
"source": "psn",
"image": "",
"scraped_at": "2026-02-25"
}
]

View File

@@ -1,164 +0,0 @@
[
{
"hash_id": 24515963,
"name": "Prodej bytu 3+kk 83 m²",
"price": 11390000,
"price_formatted": "11 390 000 Kč",
"locality": "Kytlická, Praha",
"lat": 50.1251431182,
"lon": 14.5077027612,
"disposition": "3+kk",
"floor": 4,
"area": 83,
"building_type": "WIREFRAME",
"ownership": "Osobní",
"url": "https://www.realingo.cz/prodej/byt-3+kk-kytlicka-praha/24515963",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24515884,
"name": "Prodej bytu 3+kk 81 m²",
"price": 13000000,
"price_formatted": "13 000 000 Kč",
"locality": "Hlučkova 869, Praha",
"lat": 50.142303781599,
"lon": 14.522362316941,
"disposition": "3+kk",
"floor": 5,
"area": 81,
"building_type": "OTHER",
"ownership": "Osobní",
"url": "https://www.realingo.cz/prodej/byt-3+kk-hluckova-869-praha/24515884",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24515669,
"name": "Prodej bytu Atypický None m²",
"price": 8487297,
"price_formatted": "8 487 297 Kč",
"locality": "Praha, 190 00",
"lat": 50.106598,
"lon": 14.506245,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-190-00/24515669",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24515653,
"name": "Prodej bytu Atypický None m²",
"price": 8890000,
"price_formatted": "8 890 000 Kč",
"locality": "Praha, 130 00",
"lat": 50.087602,
"lon": 14.470882,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-130-00/24515653",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24515514,
"name": "Prodej bytu Atypický None m²",
"price": 7490000,
"price_formatted": "7 490 000 Kč",
"locality": "Praha, 141 00",
"lat": 50.045786,
"lon": 14.470711,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-141-00/24515514",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24514922,
"name": "Prodej bytu Atypický None m²",
"price": 12132000,
"price_formatted": "12 132 000 Kč",
"locality": "Praha, 120 00",
"lat": 50.076449,
"lon": 14.435263,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-2+kk-slezska-praha/24514922",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24514813,
"name": "Prodej bytu Atypický None m²",
"price": 8490000,
"price_formatted": "8 490 000 Kč",
"locality": "Praha, 100 00",
"lat": 50.074273,
"lon": 14.493284,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-100-00/24514813",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24514769,
"name": "Prodej bytu Atypický None m²",
"price": 6980000,
"price_formatted": "6 980 000 Kč",
"locality": "Praha, 154 00",
"lat": 50.010056,
"lon": 14.353809,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-154-00/24514769",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
},
{
"hash_id": 24514708,
"name": "Prodej bytu Atypický None m²",
"price": 5362000,
"price_formatted": "5 362 000 Kč",
"locality": "Praha, 155 00",
"lat": 50.030571,
"lon": 14.308491,
"disposition": "Atypický",
"floor": null,
"area": null,
"building_type": "neuvedeno",
"ownership": "neuvedeno",
"url": "https://www.realingo.cz/prodej/byt-ostatni-byty-praha-155-00/24514708",
"source": "realingo",
"image": "",
"scraped_at": "2026-02-25"
}
]

View File

@@ -1,548 +0,0 @@
[
{
"hash_id": 938877772,
"name": "Prodej bytu 3+kk 99 m²",
"price": 12990000,
"price_formatted": "12 990 000 Kč",
"locality": "Čeljabinská, Praha 10 - Vršovice",
"lat": 50.069641,
"lon": 14.470198,
"disposition": "3+kk",
"floor": 3,
"area": 99,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-celjabinska/938877772",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_D/kBfrbpoeNBLdvLCneFodIxL/21cc.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2036855628,
"name": "Prodej bytu 3+kk 83 m²",
"price": 10490000,
"price_formatted": "10 490 000 Kč",
"locality": "Na Výrovně, Praha 5 - Stodůlky",
"lat": 50.039608,
"lon": 14.316702,
"disposition": "3+kk",
"floor": 2,
"area": 83,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-na-vyrovne/2036855628",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_C/nPXMbbUsvqW7e6cQFkEl5P/7399.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2148991820,
"name": "Prodej bytu 3+kk 72 m²",
"price": 10990000,
"price_formatted": "10 990 000 Kč",
"locality": "Pod Marjánkou, Praha 6 - Břevnov",
"lat": 50.084381,
"lon": 14.372257,
"disposition": "3+kk",
"floor": 4,
"area": 72,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-brevnov-pod-marjankou/2148991820",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_C/kOzkBkwYBTCNNSBPI1FiGB0F/c3a0.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3226313292,
"name": "Prodej bytu 3+kk 83 m²",
"price": 13500000,
"price_formatted": "13 500 000 Kč",
"locality": "Na Neklance, Praha 5 - Smíchov",
"lat": 50.060715,
"lon": 14.401836,
"disposition": "3+kk",
"floor": 4,
"area": 83,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-na-neklance/3226313292",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/kBfrbpoeND2I1YDy2Fq7ErU/6389.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"last_updated": "2026-02-15",
"first_seen": "2026-02-15"
},
{
"hash_id": 475530060,
"name": "Prodej bytu 3+kk 83 m²",
"price": 12250000,
"price_formatted": "12 250 000 Kč",
"locality": "Radouňova, Praha 5 - Stodůlky",
"lat": 50.039043,
"lon": 14.314881,
"disposition": "3+kk",
"floor": 3,
"area": 83,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-radounova/475530060",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_A/nDJ4VEZEqxQDMR0LFbAhGV/d4cf.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2303799884,
"name": "Prodej bytu 3+kk 88 m² (Jednopodlažní)",
"price": 12860000,
"price_formatted": "12 860 000 Kč",
"locality": "Spojovací, Praha 9 - Vysočany",
"lat": 50.100174,
"lon": 14.492079,
"disposition": "3+kk",
"floor": 3,
"area": 88,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vysocany-spojovaci/2303799884",
"image": "https://d18-a.sdn.cz/d_18/c_img_oV_A/kQOIvbF2D1DN63hulCAKv40/3667.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3493290828,
"name": "Prodej bytu 3+kk 83 m²",
"price": 11390000,
"price_formatted": "11 390 000 Kč",
"locality": "Kytlická, Praha 9 - Prosek",
"lat": 50.125145,
"lon": 14.507703,
"disposition": "3+kk",
"floor": 4,
"area": 83,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-prosek-kytlicka/3493290828",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/nPVpfd5QLLDqk1BGdrF3rQMW/0fe5.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 151528268,
"name": "Prodej bytu 3+kk 86 m²",
"price": 11390000,
"price_formatted": "11 390 000 Kč",
"locality": "Spojovací, Praha",
"lat": 50.101852,
"lon": 14.486118,
"disposition": "3+kk",
"floor": 2,
"area": 86,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha--spojovaci/151528268",
"image": "https://d18-a.sdn.cz/d_18/c_img_of_C/kPxr1WDRoIBXSQV6LE550j7/1607.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 1837527884,
"name": "Prodej bytu 3+kk 73 m² (Jednopodlažní)",
"price": 12790000,
"price_formatted": "12 790 000 Kč",
"locality": "Vrázova, Praha - Smíchov",
"lat": 50.071224,
"lon": 14.407872,
"disposition": "3+kk",
"floor": 3,
"area": 73,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-vrazova/1837527884",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/kY1K2LlXQDnuVLD65F2mjiY/96d4.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3330433868,
"name": "Prodej bytu 3+kk 93 m²",
"price": 11890000,
"price_formatted": "11 890 000 Kč",
"locality": "Kříženeckého náměstí, Praha 5 - Hlubočepy",
"lat": 50.03138,
"lon": 14.391757,
"disposition": "3+kk",
"floor": 2,
"area": 93,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hlubocepy-krizeneckeho-namesti/3330433868",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/nPVpfd5QLLChvUCFgIF2b8p9/bffb.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2053579340,
"name": "Prodej bytu 3+kk 76 m²",
"price": 11858981,
"price_formatted": "11 858 981 Kč",
"locality": "Za Novákovou zahradou, Praha - Satalice",
"lat": 50.122192,
"lon": 14.57646,
"disposition": "3+kk",
"floor": 3,
"area": 76,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-satalice-za-novakovou-zahradou/2053579340",
"image": "https://d18-a.sdn.cz/d_18/c_img_oe_B/nO1Ur3YPjB17k9qAElHGe3/e889.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"last_updated": "2026-02-15",
"first_seen": "2026-02-15"
},
{
"hash_id": 3651539788,
"name": "Prodej bytu 3+kk 69 m² (Jednopodlažní)",
"price": 13500000,
"price_formatted": "13 500 000 Kč",
"locality": "Zvěřinova, Praha 3 - Strašnice",
"lat": 50.084606,
"lon": 14.482681,
"disposition": "3+kk",
"floor": 12,
"area": 69,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-strasnice-zverinova/3651539788",
"image": "https://d18-a.sdn.cz/d_18/c_img_og_D/nDJ4VEZEqxmIDK5SFVQ67V/47d3.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 4005061452,
"name": "Prodej bytu 3+kk 101 m² (Jednopodlažní)",
"price": 12875000,
"price_formatted": "12 875 000 Kč",
"locality": "U Hostavického potoka, Praha 9 - Hostavice",
"lat": 50.086601,
"lon": 14.5636,
"disposition": "3+kk",
"floor": 5,
"area": 101,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hostavice-u-hostavickeho-potoka/4005061452",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_B/kY1K2LlXQBdLY8B5hFyVzb6/c266.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 589460300,
"name": "Prodej bytu 3+kk 75 m²",
"price": 13126000,
"price_formatted": "13 126 000 Kč",
"locality": "Ke Slivenci, Praha - Lochkov",
"lat": 50.004192,
"lon": 14.355805,
"disposition": "3+kk",
"floor": 2,
"area": 75,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-lochkov-ke-slivenci/589460300",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/kBfrbpoeNCgOJyFn9Fq0T4y/bed6.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2926625612,
"name": "Prodej bytu 3+kk 85 m²",
"price": 13499900,
"price_formatted": "13 499 900 Kč",
"locality": "Hořejší nábřeží, Praha 5 - Smíchov",
"lat": 50.072403,
"lon": 14.410302,
"disposition": "3+kk",
"floor": 3,
"area": 85,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-horejsi-nabrezi/2926625612",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/nPVpfd5QLLBPLz5GC0F3Hshx/c1f4.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3672994636,
"name": "Prodej bytu 3+kk 81 m²",
"price": 12390000,
"price_formatted": "12 390 000 Kč",
"locality": "Stochovská, Praha 6 - Ruzyně",
"lat": 50.082985,
"lon": 14.311815,
"disposition": "3+kk",
"floor": 3,
"area": 81,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-ruzyne-stochovska/3672994636",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/nPVpfd5QLLD53UiFNrF2fBro/c20c.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 4070581580,
"name": "Prodej bytu 3+kk 77 m²",
"price": 12207113,
"price_formatted": "12 207 113 Kč",
"locality": "Marie Podvalové, Praha - Čakovice",
"lat": 50.157696,
"lon": 14.519159,
"disposition": "3+kk",
"floor": 2,
"area": 77,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/4070581580",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/kBfrbpoeNBFLmuFzPFuWw0w/0867.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2772919116,
"name": "Prodej bytu 3+kk 81 m²",
"price": 13000000,
"price_formatted": "13 000 000 Kč",
"locality": "Hlučkova, Praha 9 - Letňany",
"lat": 50.141739,
"lon": 14.522086,
"disposition": "3+kk",
"floor": 5,
"area": 81,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-letnany-hluckova/2772919116",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/nPVpfd5QLLDqzReGOMF3mju3/0593.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2242032460,
"name": "Prodej bytu 3+kk 98 m²",
"price": 12762764,
"price_formatted": "12 762 764 Kč",
"locality": "Lodžská, Praha 8 - Bohnice",
"lat": 50.13076,
"lon": 14.423249,
"disposition": "3+kk",
"floor": 5,
"area": 98,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-bohnice-lodzska/2242032460",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_B/kY1K2LlXQBsPCGB2DFyc98H/0138.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3617202764,
"name": "Prodej bytu 3+kk 79 m²",
"price": 12959520,
"price_formatted": "12 959 520 Kč",
"locality": "Komárkova, Praha 4 - Chodov",
"lat": 50.036095,
"lon": 14.48035,
"disposition": "3+kk",
"floor": 3,
"area": 79,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-chodov-komarkova/3617202764",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_B/kY1K2LlXQCEs3VCXcFzFm2Z/52f3.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2860663372,
"name": "Prodej bytu 3+kk 78 m² (Jednopodlažní)",
"price": 12463000,
"price_formatted": "12 463 000 Kč",
"locality": "Kubelíkova, Praha",
"lat": 50.082317,
"lon": 14.450463,
"disposition": "3+kk",
"floor": 4,
"area": 78,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha--kubelikova/2860663372",
"image": "https://d18-a.sdn.cz/d_18/c_img_oZ_C/nsLxLojIBfZuBIjJDOugLv/2953.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 158065228,
"name": "Prodej bytu 3+kk 73 m²",
"price": 10947779,
"price_formatted": "10 947 779 Kč",
"locality": "Marie Podvalové, Praha - Čakovice",
"lat": 50.157696,
"lon": 14.519159,
"disposition": "3+kk",
"floor": 4,
"area": 73,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/158065228",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/nPXMbbUsvqawIkHTbFrh4zH/4f1d.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 4227625804,
"name": "Prodej bytu 3+kk 81 m²",
"price": 10990000,
"price_formatted": "10 990 000 Kč",
"locality": "Ukrajinská, Praha 10 - Vršovice",
"lat": 50.065208,
"lon": 14.450711,
"disposition": "3+kk",
"floor": 5,
"area": 81,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-ukrajinska/4227625804",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/nPXMbbUsvqDzGaHEfWFp5WXH/fd47.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 1313456972,
"name": "Prodej bytu 3+kk 87 m²",
"price": 3641309,
"price_formatted": "3 641 309 Kč",
"locality": "Praha 9",
"lat": 50.106956,
"lon": 14.510207,
"disposition": "3+kk",
"floor": 5,
"area": 87,
"building_type": "Smíšená",
"ownership": "Družstevní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-praha-9-/1313456972",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_B/kY1K2LlXQBsPCGXMFvI8II/9010.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 1671439692,
"name": "Prodej bytu 3+kk 77 m²",
"price": 12556524,
"price_formatted": "12 556 524 Kč",
"locality": "Marie Podvalové, Praha - Čakovice",
"lat": 50.157696,
"lon": 14.519159,
"disposition": "3+kk",
"floor": 3,
"area": 77,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/1671439692",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_A/nPXMbbUsvqW7e6HmhFuWvTy/67cb.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 576226124,
"name": "Prodej bytu 3+kk 71 m²",
"price": 12026912,
"price_formatted": "12 026 912 Kč",
"locality": "Hábova, Praha 5 - Stodůlky",
"lat": 50.04636,
"lon": 14.310556,
"disposition": "3+kk",
"floor": 3,
"area": 71,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-habova/576226124",
"image": "https://d18-a.sdn.cz/d_18/c_img_of_A/nO5OZtPbfGXuqXa5EzIqYl/4c78.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 473465676,
"name": "Prodej bytu 3+kk 73 m²",
"price": 12349349,
"price_formatted": "12 349 349 Kč",
"locality": "Hábova, Praha 5 - Stodůlky",
"lat": 50.04636,
"lon": 14.310556,
"disposition": "3+kk",
"floor": 3,
"area": 73,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-habova/473465676",
"image": "https://d18-a.sdn.cz/d_18/c_img_of_A/nO5OZtPbfGCc8bBbKEzIlyN/5708.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 2185458508,
"name": "Prodej bytu 3+kk 76 m²",
"price": 11978000,
"price_formatted": "11 978 000 Kč",
"locality": "Matoušova, Praha - Smíchov",
"lat": 50.074284,
"lon": 14.405826,
"disposition": "3+kk",
"floor": 2,
"area": 76,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-matousova/2185458508",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_C/kY1K2LlXQJGqrEMbF29iKr/c977.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3988325196,
"name": "Prodej bytu 3+kk 83 m² (Jednopodlažní)",
"price": 13190000,
"price_formatted": "13 190 000 Kč",
"locality": "Práčská, Praha",
"lat": 50.053101,
"lon": 14.507191,
"disposition": "3+kk",
"floor": 2,
"area": 83,
"building_type": "Kamenná",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha--pracska/3988325196",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_A/kOzkBkwYBTDPt7SL0bFanjav/5b75.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3019572044,
"name": "Prodej bytu 3+kk 76 m²",
"price": 10790000,
"price_formatted": "10 790 000 Kč",
"locality": "Plzákova, Praha - Kbely",
"lat": 50.13237,
"lon": 14.53639,
"disposition": "3+kk",
"floor": 3,
"area": 76,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-kbely-plzakova/3019572044",
"image": "https://d18-a.sdn.cz/d_18/c_img_p7_A/nDJ4VEZEqCaOxZQ8PFZch2R/b90f.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 3704697676,
"name": "Prodej bytu 3+kk 72 m²",
"price": 8000000,
"price_formatted": "8 000 000 Kč",
"locality": "Litevská, Praha 10 - Vršovice",
"lat": 50.072536,
"lon": 14.476557,
"disposition": "3+kk",
"floor": 5,
"area": 72,
"building_type": "Cihlová",
"ownership": "Družstevní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-litevska/3704697676",
"image": "https://d18-a.sdn.cz/d_18/c_img_of_A/nOztZkD4ZlC2Y2EU6E0MiZv/15fc.png?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
},
{
"hash_id": 1137623884,
"name": "Prodej bytu 3+kk 71 m²",
"price": 12318349,
"price_formatted": "12 318 349 Kč",
"locality": "Praha 9",
"lat": 50.106956,
"lon": 14.510207,
"disposition": "3+kk",
"floor": 4,
"area": 71,
"building_type": "Skeletová",
"ownership": "Osobní",
"url": "https://www.sreality.cz/detail/prodej/byt/3+kk/praha-praha-9-/1137623884",
"image": "https://d18-a.sdn.cz/d_18/c_img_p8_B/nPVpfd5QLLksKHbwFvjCd6/56ac.jpeg?fl=res,400,300,3|shr,,20|jpg,90",
"scraped_at": "2026-02-25"
}
]

123
docs/validation.md Normal file
View File

@@ -0,0 +1,123 @@
# Validation Recipe
End-to-end check that scraping, data persistence, history, and the status page all work correctly in Docker.
## What it verifies
- All scrapers run and write output to `DATA_DIR` (`/app/data`)
- `stats_*.json` land in `/app/data/` (not in `/app/`)
- `status.json` and `scraper_history.json` land in `/app/data/`
- `/api/status`, `/api/status/history`, and `/scrapers-status` serve correct data
- History accumulates across runs
## Steps
### 1. Build the image
```bash
make build
```
### 2. Start a clean validation container
```bash
# Stop/remove any leftover container and volume from a previous run
docker stop maru-hleda-byt-validation 2>/dev/null; docker rm maru-hleda-byt-validation 2>/dev/null
docker volume rm maru-hleda-byt-validation-data 2>/dev/null
docker run -d --name maru-hleda-byt-validation \
-p 8081:8080 \
-v maru-hleda-byt-validation-data:/app/data \
maru-hleda-byt
```
Give the container ~3 seconds to start. The entrypoint launches a background full scrape automatically — suppress it so only controlled runs execute:
```bash
sleep 3
docker exec maru-hleda-byt-validation pkill -f run_all.sh 2>/dev/null || true
docker exec maru-hleda-byt-validation rm -f /app/data/scraper_running.json 2>/dev/null || true
```
### 3. Run a limited scrape (run 1)
```bash
docker exec maru-hleda-byt-validation bash /app/run_all.sh --max-pages 1 --max-properties 10
```
Expected output (last few lines):
```
Status uložen: /app/data/status.json
Historie uložena: /app/data/scraper_history.json (1 záznamů)
```
### 4. Verify data files are in `/app/data/`
```bash
docker exec maru-hleda-byt-validation ls /app/data/
```
Expected files:
```
byty_cityhome.json byty_idnes.json byty_merged.json
byty_realingo.json byty_sreality.json
mapa_bytu.html
scraper_history.json
stats_bezrealitky.json stats_cityhome.json stats_idnes.json
stats_realingo.json stats_sreality.json
status.json
```
### 5. Run a second limited scrape (run 2)
```bash
docker exec maru-hleda-byt-validation bash /app/run_all.sh --max-pages 1 --max-properties 10
```
Expected last line: `Historie uložena: /app/data/scraper_history.json (2 záznamů)`
### 6. Verify history via API
```bash
curl -s http://localhost:8081/api/status/history | python3 -c "
import json, sys
h = json.load(sys.stdin)
print(f'{len(h)} entries:')
for i, e in enumerate(h):
print(f' [{i}] {e[\"timestamp\"]} total={e[\"total_accepted\"]}')
"
```
Expected: 2 entries with different timestamps.
```bash
curl -s http://localhost:8081/api/status | python3 -c "
import json, sys; s=json.load(sys.stdin)
print(f'status={s[\"status\"]} total={s[\"total_accepted\"]} ts={s[\"timestamp\"]}')
"
```
Expected: `status=done total=<N> ts=<latest timestamp>`
### 7. Check the status page
Open http://localhost:8081/scrapers-status in a browser (or `curl -s http://localhost:8081/scrapers-status | grep -c "clickable-row"` — should print `2`).
### 8. Clean up
```bash
docker stop maru-hleda-byt-validation && docker rm maru-hleda-byt-validation
docker volume rm maru-hleda-byt-validation-data
```
Or use the Makefile shortcut:
```bash
make validation-stop
```
## Notes
- PSN scraper does not support `--max-pages` and will always fail with this command; `success=False` in history is expected during validation.
- Bezrealitky may return 0 results with a 1-page limit; `byty_bezrealitky.json` will be absent from `/app/data/` in that case — this is normal.
- `make validation` (the Makefile target) runs the same limited scrape but does not suppress the background startup scrape, so two concurrent runs may occur. Use the manual steps above for a clean controlled test.

View File

@@ -1,16 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate status.json from scraper JSON outputs and run log.""" """Generate status.json from scraper JSON outputs and per-scraper stats files."""
from __future__ import annotations from __future__ import annotations
import argparse
import json import json
import os import os
import re
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional
HERE = Path(__file__).parent HERE = Path(__file__).parent
DATA_DIR = Path(os.environ.get("DATA_DIR", HERE))
SOURCE_FILES = { SOURCE_FILES = {
"Sreality": "byty_sreality.json", "Sreality": "byty_sreality.json",
@@ -21,7 +20,17 @@ SOURCE_FILES = {
"CityHome": "byty_cityhome.json", "CityHome": "byty_cityhome.json",
} }
STATS_FILES = {
"Sreality": "stats_sreality.json",
"Realingo": "stats_realingo.json",
"Bezrealitky": "stats_bezrealitky.json",
"iDNES": "stats_idnes.json",
"PSN": "stats_psn.json",
"CityHome": "stats_cityhome.json",
}
MERGED_FILE = "byty_merged.json" MERGED_FILE = "byty_merged.json"
HISTORY_FILE = "scraper_history.json"
def count_source(path: Path) -> dict: def count_source(path: Path) -> dict:
@@ -36,105 +45,51 @@ def count_source(path: Path) -> dict:
return {"accepted": 0, "error": str(e)} return {"accepted": 0, "error": str(e)}
def parse_log(log_path: str) -> dict[str, dict]: def read_scraper_stats(path: Path) -> dict:
"""Parse scraper run log and extract per-source statistics. """Load a per-scraper stats JSON. Returns {} on missing or corrupt file."""
if not path.exists():
Scrapers log summary lines like: return {}
✓ Vyhovující byty: 12 try:
Vyloučeno (prodáno): 5 data = json.loads(path.read_text(encoding="utf-8"))
Staženo stránek: 3 return data if isinstance(data, dict) else {}
Staženo inzerátů: 48 except Exception:
Celkem bytů v cache: 120
and section headers like:
[2/6] Realingo
"""
if not log_path or not os.path.exists(log_path):
return {} return {}
with open(log_path, encoding="utf-8") as f:
content = f.read()
# Split into per-source sections by the [N/6] Step header def append_to_history(status: dict, keep: int) -> None:
# Each section header looks like "[2/6] Realingo\n----..." """Append the current status entry to scraper_history.json, keeping only `keep` latest."""
section_pattern = re.compile(r'\[(\d+)/\d+\]\s+(.+)\n-+', re.MULTILINE) history_path = DATA_DIR / HISTORY_FILE
sections_found = list(section_pattern.finditer(content)) history: list = []
if history_path.exists():
try:
history = json.loads(history_path.read_text(encoding="utf-8"))
if not isinstance(history, list):
history = []
except Exception:
history = []
if not sections_found: history.append(status)
return {}
stats = {} # Keep only the N most recent entries
for i, match in enumerate(sections_found): if keep > 0 and len(history) > keep:
step_name = match.group(2).strip() history = history[-keep:]
start = match.end()
end = sections_found[i + 1].start() if i + 1 < len(sections_found) else len(content)
section_text = content[start:end]
# Identify which sources this section covers history_path.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
# "PSN + CityHome" covers both print(f"Historie uložena: {history_path} ({len(history)} záznamů)")
source_names = []
for name in SOURCE_FILES:
if name.lower() in step_name.lower():
source_names.append(name)
if not source_names:
continue
# Parse numeric summary lines
def extract(pattern: str) -> Optional[int]:
m = re.search(pattern, section_text)
return int(m.group(1)) if m else None
# Lines present in all/most scrapers
accepted = extract(r'Vyhovující byty[:\s]+(\d+)')
fetched = extract(r'Staženo inzerátů[:\s]+(\d+)')
pages = extract(r'Staženo stránek[:\s]+(\d+)')
cached = extract(r'Celkem bytů v cache[:\s]+(\d+)')
cache_hits = extract(r'Cache hit[:\s]+(\d+)')
# Rejection reasons — collect all into a dict
excluded = {}
for m in re.finditer(r'Vyloučeno\s+\(([^)]+)\)[:\s]+(\d+)', section_text):
excluded[m.group(1)] = int(m.group(2))
# Also PSN-style "Vyloučeno (prodáno): N"
total_excluded = sum(excluded.values()) if excluded else extract(r'Vyloučen\w*[:\s]+(\d+)')
entry = {}
if accepted is not None:
entry["accepted"] = accepted
if fetched is not None:
entry["fetched"] = fetched
if pages is not None:
entry["pages"] = pages
if cached is not None:
entry["cached"] = cached
if cache_hits is not None:
entry["cache_hits"] = cache_hits
if excluded:
entry["excluded"] = excluded
elif total_excluded is not None:
entry["excluded_total"] = total_excluded
for name in source_names:
stats[name] = entry
return stats
def main(): def main():
start_time = None parser = argparse.ArgumentParser(description="Generate status.json from scraper outputs.")
duration_sec = None parser.add_argument("--start-time", dest="start_time", default=None,
help="ISO timestamp of scrape start (default: now)")
parser.add_argument("--duration", dest="duration", type=int, default=None,
help="Run duration in seconds")
parser.add_argument("--keep", dest="keep", type=int, default=20,
help="Number of history entries to keep (default: 20, 0=unlimited)")
args = parser.parse_args()
if len(sys.argv) >= 3: start_time = args.start_time or datetime.now().isoformat(timespec="seconds")
start_time = sys.argv[1] duration_sec = args.duration
try:
duration_sec = int(sys.argv[2])
except ValueError:
pass
if not start_time:
start_time = datetime.now().isoformat(timespec="seconds")
log_path = sys.argv[3] if len(sys.argv) >= 4 else None
log_stats = parse_log(log_path)
sources = [] sources = []
for name, filename in SOURCE_FILES.items(): for name, filename in SOURCE_FILES.items():
@@ -142,14 +97,12 @@ def main():
info = count_source(path) info = count_source(path)
info["name"] = name info["name"] = name
# Merge log stats # Merge in stats from the per-scraper stats file (authoritative for run data)
ls = log_stats.get(name, {}) stats = read_scraper_stats(DATA_DIR / STATS_FILES[name])
for k in ("fetched", "pages", "cached", "cache_hits", "excluded", "excluded_total"): for key in ("accepted", "fetched", "pages", "cache_hits", "excluded", "excluded_total",
if k in ls: "success", "duration_sec", "error"):
info[k] = ls[k] if key in stats:
# Override accepted from log if available (log is authoritative for latest run) info[key] = stats[key]
if "accepted" in ls:
info["accepted"] = ls["accepted"]
sources.append(info) sources.append(info)
@@ -168,17 +121,21 @@ def main():
duplicates_removed = total_accepted - deduplicated if deduplicated else 0 duplicates_removed = total_accepted - deduplicated if deduplicated else 0
# Top-level success: True if no source has an error
success = not any("error" in s for s in sources)
status = { status = {
"status": "done", "status": "done",
"timestamp": start_time, "timestamp": start_time,
"duration_sec": duration_sec, "duration_sec": duration_sec,
"success": success,
"total_accepted": total_accepted, "total_accepted": total_accepted,
"deduplicated": deduplicated, "deduplicated": deduplicated,
"duplicates_removed": duplicates_removed, "duplicates_removed": duplicates_removed,
"sources": sources, "sources": sources,
} }
out = HERE / "status.json" out = DATA_DIR / "status.json"
out.write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8") out.write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Status uložen: {out}") print(f"Status uložen: {out}")
print(f" Celkem bytů (před dedup): {total_accepted}") print(f" Celkem bytů (před dedup): {total_accepted}")
@@ -197,6 +154,8 @@ def main():
parts.append(f"[CHYBA: {err}]") parts.append(f"[CHYBA: {err}]")
print(" " + " ".join(parts)) print(" " + " ".join(parts))
append_to_history(status, args.keep)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,601 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Byty v Praze — mapa (56 bytů)</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; }
#map { width: 100%; height: 100vh; }
.heart-icon { background: none !important; border: none !important; }
.star-icon { background: none !important; border: none !important; }
.rate-btn:hover { background: #f0f0f0 !important; }
.rate-btn.active-fav { background: #FFF9C4 !important; border-color: #FFC107 !important; }
.rate-btn.active-rej { background: #FFEBEE !important; border-color: #F44336 !important; }
@keyframes pulse-glow {
0% { box-shadow: 0 0 4px 2px rgba(255, 193, 7, 0.4); }
50% { box-shadow: 0 0 10px 5px rgba(255, 193, 7, 0.7); }
100% { box-shadow: 0 0 4px 2px rgba(255, 193, 7, 0.4); }
}
.marker-favorite { animation: pulse-glow 2s ease-in-out infinite; border-radius: 50%; }
.heart-icon-fav svg path { stroke: gold !important; stroke-width: 2.5 !important; filter: drop-shadow(0 0 4px rgba(255,193,7,0.7)); }
.heart-icon-rej { opacity: 0.4 !important; filter: grayscale(1); }
.reject-overlay { background: none !important; border: none !important; pointer-events: none !important; }
@keyframes pulse-new {
0% { stroke-opacity: 1; stroke-width: 3px; r: 11; }
50% { stroke-opacity: 0.4; stroke-width: 6px; r: 12; }
100% { stroke-opacity: 1; stroke-width: 3px; r: 11; }
}
.marker-new { animation: pulse-new 2s ease-in-out infinite; }
.info-panel {
position: absolute; top: 10px; right: 10px; z-index: 1000;
background: white; padding: 16px; border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15); max-width: 260px;
font-size: 13px; line-height: 1.5;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.info-panel.collapsed {
transform: translateX(calc(100% + 20px));
opacity: 0; pointer-events: none;
}
.panel-open-btn {
position: absolute; top: 10px; right: 10px; z-index: 1001;
width: 40px; height: 40px; border-radius: 8px;
background: white; border: none; cursor: pointer;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
font-size: 20px; display: flex; align-items: center; justify-content: center;
transition: opacity 0.3s ease;
}
.panel-open-btn.hidden { opacity: 0; pointer-events: none; }
.panel-close-btn {
position: absolute; top: 8px; right: 8px;
width: 28px; height: 28px; border-radius: 6px;
background: none; border: 1px solid #ddd; cursor: pointer;
font-size: 16px; display: flex; align-items: center; justify-content: center;
color: #888;
}
.panel-close-btn:hover { background: #f0f0f0; color: #333; }
.info-panel h2 { font-size: 16px; margin-bottom: 8px; }
.info-panel .stats { color: #666; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
.filter-section { margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; }
.filter-section label { display: flex; align-items: center; gap: 6px; margin: 3px 0; cursor: pointer; }
.filter-section input[type="checkbox"] { accent-color: #1976D2; }
#floor-filter { margin-top: 8px; }
#floor-filter select { width: 100%; padding: 4px; border-radius: 4px; border: 1px solid #ccc; }
.status-link { display: block; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; text-align: center; }
.status-link a { color: #1976D2; text-decoration: none; font-size: 12px; }
@media (max-width: 600px) {
.info-panel { max-width: calc(100vw - 60px); right: 10px; }
.info-panel.collapsed { transform: translateX(calc(100% + 20px)); }
.panel-close-btn { top: 6px; right: 6px; }
}
</style>
</head>
<body>
<div id="map"></div>
<button class="panel-open-btn hidden" id="panel-open-btn" onclick="togglePanel()"></button>
<div class="info-panel" id="info-panel">
<button class="panel-close-btn" id="panel-close-btn" onclick="togglePanel()"></button>
<h2>Byty v Praze</h2>
<div class="stats">
<div>Celkem: <b id="visible-count">56</b> bytů</div>
<div>Cena: 3 641 309 Kč — 13 994 000 Kč</div>
<div>Průměr: 11 279 033 Kč</div>
</div>
<div style="margin-bottom:4px;font-size:12px;color:#555;font-weight:600;">Cena / m²:</div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#1565C0;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>< 110 000 /m²</span></div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#42A5F5;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>110 130 000 Kč/m²</span></div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#66BB6A;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>130 150 000 Kč/m²</span></div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#EF6C00;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>150 165 000 Kč/m²</span></div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#C62828;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>> 165 000 Kč/m²</span></div><div style="display:flex;align-items:center;gap:6px;margin:2px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#9E9E9E;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span><span>cena/plocha neuvedena</span></div><div style="display:flex;align-items:center;gap:6px;margin:6px 0 0 0;padding-top:6px;border-top:1px solid #eee;"><span style="width:18px;height:18px;border-radius:50%;background:#66BB6A;display:inline-block;box-shadow:0 1px 4px rgba(0,0,0,0.35);flex-shrink:0;"></span><span>Nové (z dnešního scrapu) — větší</span></div><div style="margin-top:8px;padding-top:6px;border-top:1px solid #eee;font-size:12px;color:#666;">3+kk (42), 3+1 (5), 4+kk (2)</div><div style="display:flex;align-items:center;gap:6px;margin:8px 0 3px 0;padding-top:6px;border-top:1px solid #eee;"><svg width="14" height="14" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 C2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#D32F2F"/></svg><span>PSN / CityHome (3)</span></div>
<div class="filter-section">
<b>Filtry:</b>
<div id="floor-filter">
<label>Patro od:
<select id="min-floor" onchange="applyFilters()">
<option value="2">2. NP (vše)</option>
<option value="3">3. NP+</option>
<option value="4">4. NP+</option>
<option value="5">5. NP+</option>
</select>
</label>
</div>
<div style="margin-top:6px;">
<label>Max cena:
<select id="max-price" onchange="applyFilters()">
<option value="13500000">13 500 000 Kč</option>
<option value="12000000">12 000 000 Kč</option>
<option value="10000000">10 000 000 Kč</option>
<option value="8000000">8 000 000 Kč</option>
</select>
</label>
</div>
</div>
<div class="filter-section">
<div id="rating-counts" style="margin-bottom:6px;font-size:12px;color:#666;">
⭐ 0 oblíbených, 🚫 0 zamítnutých
</div>
<label>
<input type="checkbox" id="hide-rejected" onchange="applyFilters()">
Skrýt zamítnuté
</label>
</div>
<div class="status-link"><a href="status.html">Scraper status</a></div>
</div>
<script>
// Prague center
var map = L.map('map').setView([50.075, 14.437], 12);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
maxZoom: 19,
subdomains: 'abcd',
}).addTo(map);
// Labels as separate layer on top (so markers sit between background and labels)
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
subdomains: 'abcd',
pane: 'shadowPane',
}).addTo(map);
var allMarkers = [];
function addMarker(lat, lon, color, popup, hashId) {
var marker = L.circleMarker([lat, lon], {
radius: 8,
fillColor: color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.85,
}).bindPopup(popup);
marker._data = { lat: lat, lon: lon, color: color, hashId: hashId };
allMarkers.push(marker);
marker.addTo(map);
}
function addNewMarker(lat, lon, color, popup, hashId) {
var marker = L.circleMarker([lat, lon], {
radius: 12,
fillColor: color,
color: color,
weight: 4,
opacity: 0.35,
fillOpacity: 0.95,
}).bindPopup(popup);
marker._data = { lat: lat, lon: lon, color: color, hashId: hashId, isNew: true };
allMarkers.push(marker);
marker.addTo(map);
marker.on('add', function() {
if (marker._path) marker._path.classList.add('marker-new');
});
}
function heartIcon(color) {
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">'
+ '<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 '
+ 'C2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 '
+ '14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 '
+ '11.54L12 21.35z" fill="' + color + '" stroke="white" stroke-width="1.5"/></svg>';
return L.divIcon({
html: svg,
className: 'heart-icon',
iconSize: [24, 24],
iconAnchor: [12, 22],
popupAnchor: [0, -18],
});
}
function starIcon() {
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24">'
+ '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 '
+ '5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" '
+ 'fill="#FFC107" stroke="#F57F17" stroke-width="1" '
+ 'filter="drop-shadow(0 1px 3px rgba(0,0,0,0.3))"/></svg>';
return L.divIcon({
html: svg,
className: 'star-icon',
iconSize: [28, 28],
iconAnchor: [14, 14],
popupAnchor: [0, -14],
});
}
function addHeartMarker(lat, lon, color, popup, hashId) {
var marker = L.marker([lat, lon], {
icon: heartIcon(color),
}).bindPopup(popup);
marker._data = { lat: lat, lon: lon, color: color, hashId: hashId, isHeart: true };
allMarkers.push(marker);
marker.addTo(map);
}
addHeartMarker(50.0729, 14.4767, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="8941"><b style="font-size:14px;">13 994 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#D32F2F;color:white;padding:1px 6px;border-radius:3px;">PSN</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 102.7 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Litevská 1174/8, Praha 10</b><br>Stavba: neuvedeno<br>Vlastnictví: osobní<br><br><a href="https://psn.cz/prodej/ubytovaci-jednotka-3-kk-litevska-praha-10-vrsovice-lit4219" target="_blank" style="color:#D32F2F;text-decoration:none;font-weight:bold;">→ Otevřít na PSN</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '8941');
addHeartMarker(50.0652858, 14.3931318, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="cityhome_na-vaclavce-34_Byt A2.3"><b style="font-size:14px;">13 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#D32F2F;color:white;padding:1px 6px;border-radius:3px;">CityHome</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 99.1 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Na Václavce 34, Praha 5</b><br>Stavba: Cihlová<br>Vlastnictví: neuvedeno<br><br><a href="https://www.city-home.cz/projekty/na-vaclavce-34/nabidka-nemovitosti/byt-a23" target="_blank" style="color:#D32F2F;text-decoration:none;font-weight:bold;">→ Otevřít na CityHome</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', 'cityhome_na-vaclavce-34_Byt A2.3');
addHeartMarker(50.0652858, 14.3931318, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="cityhome_na-vaclavce-34_Byt A3.2"><b style="font-size:14px;">13 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#D32F2F;color:white;padding:1px 6px;border-radius:3px;">CityHome</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 95.6 m² | 3. NP</span><br><br><b>Na Václavce 34, Praha 5</b><br>Stavba: Cihlová<br>Vlastnictví: neuvedeno<br><br><a href="https://www.city-home.cz/projekty/na-vaclavce-34/nabidka-nemovitosti/byt-a32" target="_blank" style="color:#D32F2F;text-decoration:none;font-weight:bold;">→ Otevřít na CityHome</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', 'cityhome_na-vaclavce-34_Byt A3.2');
addNewMarker(50.069641, 14.470198, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="938877772"><b style="font-size:14px;">12 990 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 99 m² | 3. NP</span><br><br><b>Čeljabinská, Praha 10 - Vršovice</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-celjabinska/938877772" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '938877772');
addNewMarker(50.039608, 14.316702, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2036855628"><b style="font-size:14px;">10 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 83 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Na Výrovně, Praha 5 - Stodůlky</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-na-vyrovne/2036855628" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2036855628');
addNewMarker(50.084381, 14.372257, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2148991820"><b style="font-size:14px;">10 990 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 72 m² | 4. NP</span><br><br><b>Pod Marjánkou, Praha 6 - Břevnov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-brevnov-pod-marjankou/2148991820" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2148991820');
addMarker(50.060715, 14.401836, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3226313292"><b style="font-size:14px;">13 500 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><br><span style="color:#666;">3+kk | 83 m² | 4. NP</span><br><br><b>Na Neklance, Praha 5 - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-na-neklance/3226313292" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3226313292');
addNewMarker(50.039043, 14.314881, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="475530060"><b style="font-size:14px;">12 250 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 83 m² | 3. NP</span><br><br><b>Radouňova, Praha 5 - Stodůlky</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-radounova/475530060" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '475530060');
addNewMarker(50.100174, 14.492079, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2303799884"><b style="font-size:14px;">12 860 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 88 m² | 3. NP</span><br><br><b>Spojovací, Praha 9 - Vysočany</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vysocany-spojovaci/2303799884" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2303799884');
addNewMarker(50.125145, 14.507703, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3493290828"><b style="font-size:14px;">11 390 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 83 m² | 4. NP</span><br><br><b>Kytlická, Praha 9 - Prosek</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-prosek-kytlicka/3493290828" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3493290828');
addNewMarker(50.101852, 14.486118, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="151528268"><b style="font-size:14px;">11 390 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 86 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Spojovací, Praha</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha--spojovaci/151528268" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '151528268');
addNewMarker(50.071224, 14.407872, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1837527884"><b style="font-size:14px;">12 790 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 73 m² | 3. NP</span><br><br><b>Vrázova, Praha - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-vrazova/1837527884" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '1837527884');
addNewMarker(50.03138, 14.391757, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3330433868"><b style="font-size:14px;">11 890 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 93 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Kříženeckého náměstí, Praha 5 - Hlubočepy</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hlubocepy-krizeneckeho-namesti/3330433868" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3330433868');
addMarker(50.122192, 14.57646, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2053579340"><b style="font-size:14px;">11 858 981 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><br><span style="color:#666;">3+kk | 76 m² | 3. NP</span><br><br><b>Za Novákovou zahradou, Praha - Satalice</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-satalice-za-novakovou-zahradou/2053579340" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2053579340');
addNewMarker(50.084606, 14.482681, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3651539788"><b style="font-size:14px;">13 500 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 69 m² | 12. NP</span><br><br><b>Zvěřinova, Praha 3 - Strašnice</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-strasnice-zverinova/3651539788" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3651539788');
addNewMarker(50.086601, 14.5636, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4005061452"><b style="font-size:14px;">12 875 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 101 m² | 5. NP</span><br><br><b>U Hostavického potoka, Praha 9 - Hostavice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hostavice-u-hostavickeho-potoka/4005061452" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '4005061452');
addNewMarker(50.004192, 14.355805, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="589460300"><b style="font-size:14px;">13 126 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 75 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Ke Slivenci, Praha - Lochkov</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-lochkov-ke-slivenci/589460300" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '589460300');
addNewMarker(50.072403, 14.410302, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2926625612"><b style="font-size:14px;">13 499 900 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 85 m² | 3. NP</span><br><br><b>Hořejší nábřeží, Praha 5 - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-horejsi-nabrezi/2926625612" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2926625612');
addNewMarker(50.082985, 14.311815, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3672994636"><b style="font-size:14px;">12 390 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 81 m² | 3. NP</span><br><br><b>Stochovská, Praha 6 - Ruzyně</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-ruzyne-stochovska/3672994636" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3672994636');
addNewMarker(50.157696, 14.519159, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4070581580"><b style="font-size:14px;">12 207 113 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 77 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/4070581580" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '4070581580');
addNewMarker(50.141739, 14.522086, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2772919116"><b style="font-size:14px;">13 000 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 81 m² | 5. NP</span><br><br><b>Hlučkova, Praha 9 - Letňany</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-letnany-hluckova/2772919116" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2772919116');
addNewMarker(50.13076, 14.423249, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2242032460"><b style="font-size:14px;">12 762 764 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 98 m² | 5. NP</span><br><br><b>Lodžská, Praha 8 - Bohnice</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-bohnice-lodzska/2242032460" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2242032460');
addNewMarker(50.036095, 14.48035, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3617202764"><b style="font-size:14px;">12 959 520 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 79 m² | 3. NP</span><br><br><b>Komárkova, Praha 4 - Chodov</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-chodov-komarkova/3617202764" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3617202764');
addNewMarker(50.082317, 14.450463, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2860663372"><b style="font-size:14px;">12 463 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 78 m² | 4. NP</span><br><br><b>Kubelíkova, Praha</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha--kubelikova/2860663372" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2860663372');
addNewMarker(50.157696, 14.519159, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="158065228"><b style="font-size:14px;">10 947 779 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 73 m² | 4. NP</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/158065228" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '158065228');
addNewMarker(50.065208, 14.450711, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4227625804"><b style="font-size:14px;">10 990 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 81 m² | 5. NP</span><br><br><b>Ukrajinská, Praha 10 - Vršovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-ukrajinska/4227625804" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '4227625804');
addNewMarker(50.106956, 14.510207, '#1565C0', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1313456972"><b style="font-size:14px;">3 641 309 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 87 m² | 5. NP</span><br><br><b>Praha 9</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-praha-9-/1313456972" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '1313456972');
addNewMarker(50.157696, 14.519159, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1671439692"><b style="font-size:14px;">12 556 524 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 77 m² | 3. NP</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/1671439692" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '1671439692');
addNewMarker(50.04636, 14.310556, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="576226124"><b style="font-size:14px;">12 026 912 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 71 m² | 3. NP</span><br><br><b>Hábova, Praha 5 - Stodůlky</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-habova/576226124" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '576226124');
addNewMarker(50.04636, 14.310556, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="473465676"><b style="font-size:14px;">12 349 349 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 73 m² | 3. NP</span><br><br><b>Hábova, Praha 5 - Stodůlky</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-habova/473465676" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '473465676');
addNewMarker(50.074284, 14.405826, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2185458508"><b style="font-size:14px;">11 978 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Matoušova, Praha - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-smichov-matousova/2185458508" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '2185458508');
addNewMarker(50.053101, 14.507191, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3988325196"><b style="font-size:14px;">13 190 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 83 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Práčská, Praha</b><br>Stavba: Kamenná<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha--pracska/3988325196" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3988325196');
addNewMarker(50.13237, 14.53639, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3019572044"><b style="font-size:14px;">10 790 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 3. NP</span><br><br><b>Plzákova, Praha - Kbely</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-kbely-plzakova/3019572044" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3019572044');
addNewMarker(50.072536, 14.476557, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3704697676"><b style="font-size:14px;">8 000 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 72 m² | 5. NP</span><br><br><b>Litevská, Praha 10 - Vršovice</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vrsovice-litevska/3704697676" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '3704697676');
addNewMarker(50.106956, 14.510207, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1137623884"><b style="font-size:14px;">12 318 349 Kč</b><span style="margin-left:8px;font-size:11px;background:#1976D2;color:white;padding:1px 6px;border-radius:3px;">Sreality</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 71 m² | 4. NP</span><br><br><b>Praha 9</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<br><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-praha-9-/1137623884" target="_blank" style="color:#1976D2;text-decoration:none;font-weight:bold;">→ Otevřít na Sreality</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '1137623884');
addNewMarker(50.142303781599, 14.522362316941, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24515884"><b style="font-size:14px;">13 000 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 81 m² | 5. NP</span><br><br><b>Hlučkova 869, Praha</b><br>Stavba: OTHER<br>Vlastnictví: Osobní<br><br><a href="https://www.realingo.cz/prodej/byt-3+kk-hluckova-869-praha/24515884" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24515884');
addNewMarker(50.106598, 14.506245, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24515669"><b style="font-size:14px;">8 487 297 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 190 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-190-00/24515669" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24515669');
addNewMarker(50.087602, 14.470882, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24515653"><b style="font-size:14px;">8 890 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 130 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-130-00/24515653" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24515653');
addNewMarker(50.045786, 14.470711, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24515514"><b style="font-size:14px;">7 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 141 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-141-00/24515514" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24515514');
addNewMarker(50.076449, 14.435263, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24514922"><b style="font-size:14px;">12 132 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 120 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-2+kk-slezska-praha/24514922" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24514922');
addNewMarker(50.074273, 14.493284, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24514813"><b style="font-size:14px;">8 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 100 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-100-00/24514813" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24514813');
addNewMarker(50.010056, 14.353809, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24514769"><b style="font-size:14px;">6 980 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 154 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-154-00/24514769" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24514769');
addNewMarker(50.030571, 14.308491, '#9E9E9E', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24514708"><b style="font-size:14px;">5 362 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#00897B;color:white;padding:1px 6px;border-radius:3px;">Realingo</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 155 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<br><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-155-00/24514708" target="_blank" style="color:#00897B;text-decoration:none;font-weight:bold;">→ Otevřít na Realingo</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '24514708');
addMarker(50.1026043, 14.4435365, '#C62828', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="981278"><b style="font-size:14px;">11 890 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#E91E63;color:white;padding:1px 6px;border-radius:3px;">Bezrealitky</span><br><span style="color:#666;">3+kk | 70 m² | 3. NP</span><br><br><b>Argentinská, Praha - Holešovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.bezrealitky.cz/nemovitosti-byty-domy/981278-nabidka-prodej-bytu-argentinska-praha" target="_blank" style="color:#E91E63;text-decoration:none;font-weight:bold;">→ Otevřít na Bezrealitky</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '981278');
addMarker(50.1113213, 14.5106858, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="991217"><b style="font-size:14px;">11 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#E91E63;color:white;padding:1px 6px;border-radius:3px;">Bezrealitky</span><br><span style="color:#666;">3+kk | 71 m² | 3. NP</span><br><br><b>Kolbenova, Praha - Vysočany</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<br><br><a href="https://www.bezrealitky.cz/nemovitosti-byty-domy/991217-nabidka-prodej-bytu-kolbenova-praha" target="_blank" style="color:#E91E63;text-decoration:none;font-weight:bold;">→ Otevřít na Bezrealitky</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '991217');
addNewMarker(50.049168412058556, 14.302095927878957, '#1565C0', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="699ed0af74468ff4c2079aa1"><b style="font-size:14px;">4 600 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 86 m² | 8. NP</span><br><br><b>Hynka Puce, Praha 5 - Stodůlky</b><br>Stavba: Cihlová<br>Vlastnictví: družstevní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-13-hynka-puce/699ed0af74468ff4c2079aa1/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '699ed0af74468ff4c2079aa1');
addNewMarker(50.009743674736, 14.460835345662, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="693690e98418631b48025208"><b style="font-size:14px;">11 323 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 87 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Libušská, Praha 4 - Libuš</b><br>Stavba: Cihlová<br>Vlastnictví: družstevní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-12-libusska/693690e98418631b48025208/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '693690e98418631b48025208');
addNewMarker(50.0652882346, 14.3931192571, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="699487a84abe8029bd065570"><b style="font-size:14px;">13 490 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 96 m² | 4. NP</span><br><br><b>Na Václavce, Praha 5 - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-5-na-vaclavce/699487a84abe8029bd065570/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '699487a84abe8029bd065570');
addNewMarker(50.04710645755815, 14.473057214055794, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="697c7e54d08e16f19902d777"><b style="font-size:14px;">11 590 040 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 5. NP</span><br><br><b>Žilinská, Praha 4 - Záběhlice</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-4-zilinska/697c7e54d08e16f19902d777/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '697c7e54d08e16f19902d777');
addNewMarker(50.0579944444, 14.4682905556, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="6941cf632ff10124be08ce19"><b style="font-size:14px;">13 249 900 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 94 m² | 14. NP</span><br><br><b>V dolině, Praha 10 - Michle</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-10-v-doline/6941cf632ff10124be08ce19/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '6941cf632ff10124be08ce19');
addNewMarker(50.0290438889, 14.3641566667, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="690c2cba1c264f9f43027912"><b style="font-size:14px;">10 631 123 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 74 m² | 6. NP</span><br><br><b>Voskovcova, Praha 5 - Hlubočepy</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-5-voskovcova/690c2cba1c264f9f43027912/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '690c2cba1c264f9f43027912');
addNewMarker(50.026899, 14.613713, '#EF6C00', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="68404b3d8178bbed020f1742"><b style="font-size:14px;">10 990 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 71 m² | 5. NP</span><br><br><b>Praha 10 - Uhříněves</b><br>Stavba: Skeletová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-22/68404b3d8178bbed020f1742/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '68404b3d8178bbed020f1742');
addNewMarker(50.1297302, 14.4286652, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="6932bf9dc9442dc194054416"><b style="font-size:14px;">8 100 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 71 m² | 5. NP</span><br><br><b>Štětínská, Praha 8 - Bohnice, okres Praha</b><br>Stavba: 1974<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-8-stetinska/6932bf9dc9442dc194054416/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '6932bf9dc9442dc194054416');
addNewMarker(50.030382258, 14.5931238354, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="68f0b7b4263df471cb050df9"><b style="font-size:14px;">10 363 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 75 m² | 4. NP</span><br><br><b>Karla Guta, Praha 10 - Uhříněves</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-22-karla-guta/68f0b7b4263df471cb050df9/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '68f0b7b4263df471cb050df9');
addNewMarker(50.132835725, 14.5613326001, '#66BB6A', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="695cdf5113e97880200d9e62"><b style="font-size:14px;">11 133 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 82 m² | 3. NP</span><br><br><b>K Vinoři, Praha 9 - Kbely</b><br>Stavba: 2026<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-19-k-vinori/695cdf5113e97880200d9e62/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '695cdf5113e97880200d9e62');
addNewMarker(50.0114383, 14.5469, '#42A5F5', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="69930de7098209b20e066a6c"><b style="font-size:14px;">11 000 000 Kč</b><span style="margin-left:8px;font-size:11px;background:#FF6F00;color:white;padding:1px 6px;border-radius:3px;">iDNES</span><span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 91 m² | 3. NP</span><br><br><b>Formanská, Praha 4 - Újezd u Průhonic, okres Praha</b><br>Stavba: 2017<br>Vlastnictví: osobní<br><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-11-formanska/69930de7098209b20e066a6c/" target="_blank" style="color:#FF6F00;text-decoration:none;font-weight:bold;">→ Otevřít na iDNES</a><div style="margin-top:10px;padding-top:8px;border-top:1px solid #eee;"><div style="display:flex;gap:6px;align-items:center;"><button class="rate-btn fav-btn" data-action="fav" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">⭐</button><button class="rate-btn rej-btn" data-action="reject" style="padding:4px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:16px;background:#fff;">🚫</button><span class="rating-status" style="margin-left:6px;font-size:12px;color:#999;"></span></div><textarea class="rating-note" placeholder="Poznámka..." style="width:100%;margin-top:6px;padding:4px;border:1px solid #ddd;border-radius:4px;font-size:12px;resize:vertical;min-height:32px;display:none;font-family:system-ui,sans-serif;"></textarea></div></div>', '69930de7098209b20e066a6c');
// ── Rating system ──────────────────────────────────────────────
var RATINGS_KEY = 'byty_ratings';
function loadRatings() {
try {
var data = localStorage.getItem(RATINGS_KEY);
return data ? JSON.parse(data) : {};
} catch(e) { return {}; }
}
function saveRatings(ratings) {
localStorage.setItem(RATINGS_KEY, JSON.stringify(ratings));
fetch('/api/ratings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ratings)
});
}
function addRejectStrike(marker) {
removeRejectStrike(marker);
var color = marker._data.color || '#999';
// SVG "no entry" icon — circle with diagonal line, colored to match marker
var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">'
+ '<circle cx="12" cy="12" r="10" fill="none" stroke="' + color + '" stroke-width="2.5" opacity="0.85"/>'
+ '<line x1="5.5" y1="5.5" x2="18.5" y2="18.5" stroke="' + color + '" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>'
+ '</svg>';
var icon = L.divIcon({
className: 'reject-overlay',
html: svg,
iconSize: [20, 20],
iconAnchor: [10, 10],
});
var m = L.marker([marker._data.lat, marker._data.lon], {
icon: icon,
interactive: false,
pane: 'markerPane',
});
m.addTo(map);
marker._rejectStrike = m;
}
function removeRejectStrike(marker) {
if (marker._rejectStrike) {
map.removeLayer(marker._rejectStrike);
marker._rejectStrike = null;
}
}
function applyMarkerStyle(marker, status) {
if (marker._data.isHeart) {
var el = marker._icon;
if (!el) return;
el.classList.remove('heart-icon-fav', 'heart-icon-rej');
el.style.transform = el.style.transform.replace(/scale\([^)]*\)/, '');
if (status === 'fav') {
el.classList.add('heart-icon-fav');
el.style.transform += ' scale(1.3)';
} else if (status === 'reject') {
el.classList.add('heart-icon-rej');
}
} else {
if (status === 'fav') {
removeRejectStrike(marker);
if (!marker._data._origCircle) marker._data._origCircle = true;
var popup = marker.getPopup();
var popupContent = popup ? popup.getContent() : '';
var wasOnMap = map.hasLayer(marker);
if (wasOnMap) map.removeLayer(marker);
var starMarker = L.marker([marker._data.lat, marker._data.lon], {
icon: starIcon(),
}).bindPopup(popupContent);
starMarker._data = marker._data;
var idx = allMarkers.indexOf(marker);
if (idx !== -1) allMarkers[idx] = starMarker;
if (wasOnMap) starMarker.addTo(map);
} else if (status === 'reject') {
if (marker._data._origCircle && !(marker instanceof L.CircleMarker)) {
revertToCircle(marker, { radius: 6, fillOpacity: 0.35, fillColor: marker._data.color, color: '#fff', weight: 1 });
} else {
marker.setStyle({
radius: 6, fillOpacity: 0.35, fillColor: marker._data.color, color: '#fff', weight: 1,
});
if (marker._path) marker._path.classList.remove('marker-favorite');
}
// Add strikethrough line over the marker
addRejectStrike(marker);
} else {
if (marker._data._origCircle && !(marker instanceof L.CircleMarker)) {
revertToCircle(marker, { radius: 8, fillColor: marker._data.color, color: '#fff', weight: 2, fillOpacity: 0.85 });
} else {
marker.setStyle({
radius: 8, fillColor: marker._data.color, color: '#fff',
weight: 2, fillOpacity: 0.85,
});
if (marker._path) marker._path.classList.remove('marker-favorite');
}
if (marker._path) marker._path.classList.remove('marker-rejected');
removeRejectStrike(marker);
}
}
}
function revertToCircle(marker, style) {
var popup = marker.getPopup();
var popupContent = popup ? popup.getContent() : '';
var wasOnMap = map.hasLayer(marker);
if (wasOnMap) map.removeLayer(marker);
var cm = L.circleMarker([marker._data.lat, marker._data.lon], style).bindPopup(popupContent);
cm._data = marker._data;
delete cm._data._starRef;
var idx = allMarkers.indexOf(marker);
if (idx !== -1) allMarkers[idx] = cm;
if (wasOnMap) cm.addTo(map);
}
function rateMarker(marker, action) {
var hashId = marker._data.hashId;
var ratings = loadRatings();
var current = ratings[hashId];
if (current && current.status === action) {
delete ratings[hashId];
saveRatings(ratings);
applyMarkerStyle(marker, null);
updateRatingCounts();
applyFilters();
return null;
} else {
var note = (current && current.note) || '';
ratings[hashId] = { status: action, note: note };
saveRatings(ratings);
applyMarkerStyle(marker, action);
updateRatingCounts();
applyFilters();
return action;
}
}
function setNote(hashId, note) {
var ratings = loadRatings();
if (ratings[hashId]) {
ratings[hashId].note = note;
saveRatings(ratings);
}
}
function updateRatingCounts() {
var ratings = loadRatings();
var favCount = 0, rejCount = 0;
for (var id in ratings) {
if (ratings[id].status === 'fav') favCount++;
if (ratings[id].status === 'reject') rejCount++;
}
var el = document.getElementById('rating-counts');
if (el) el.innerHTML = '⭐ ' + favCount + ' oblíbených, 🚫 ' + rejCount + ' zamítnutých';
}
function restoreRatings() {
var ratings = loadRatings();
allMarkers.forEach(function(m) {
var r = ratings[m._data.hashId];
if (r) applyMarkerStyle(m, r.status);
});
updateRatingCounts();
}
// ── Popup rating handler ───────────────────────────────────────
map.on('popupopen', function(e) {
var container = e.popup.getElement();
if (!container) return;
var wrapper = container.querySelector('[data-hashid]');
if (!wrapper) return;
var hashId = wrapper.getAttribute('data-hashid');
var marker = null;
for (var i = 0; i < allMarkers.length; i++) {
if (String(allMarkers[i]._data.hashId) === String(hashId)) {
marker = allMarkers[i]; break;
}
}
if (!marker) return;
var ratings = loadRatings();
var current = ratings[hashId];
var favBtn = container.querySelector('.fav-btn');
var rejBtn = container.querySelector('.rej-btn');
var statusEl = container.querySelector('.rating-status');
var noteEl = container.querySelector('.rating-note');
// Restore state in popup
favBtn.classList.remove('active-fav');
rejBtn.classList.remove('active-rej');
statusEl.textContent = '';
noteEl.style.display = 'none';
noteEl.value = '';
if (current) {
if (current.status === 'fav') {
favBtn.classList.add('active-fav');
statusEl.textContent = 'Oblíbený';
statusEl.style.color = '#F9A825';
} else if (current.status === 'reject') {
rejBtn.classList.add('active-rej');
statusEl.textContent = 'Zamítnutý';
statusEl.style.color = '#E53935';
}
noteEl.style.display = 'block';
noteEl.value = current.note || '';
}
favBtn.onclick = function() {
var result = rateMarker(marker, 'fav');
favBtn.classList.remove('active-fav');
rejBtn.classList.remove('active-rej');
if (result === 'fav') {
favBtn.classList.add('active-fav');
statusEl.textContent = 'Oblíbený';
statusEl.style.color = '#F9A825';
noteEl.style.display = 'block';
} else {
statusEl.textContent = '';
noteEl.style.display = 'none';
noteEl.value = '';
}
};
rejBtn.onclick = function() {
var result = rateMarker(marker, 'reject');
favBtn.classList.remove('active-fav');
rejBtn.classList.remove('active-rej');
if (result === 'reject') {
rejBtn.classList.add('active-rej');
statusEl.textContent = 'Zamítnutý';
statusEl.style.color = '#E53935';
noteEl.style.display = 'block';
} else {
statusEl.textContent = '';
noteEl.style.display = 'none';
noteEl.value = '';
}
};
var noteTimer = null;
noteEl.oninput = function() {
clearTimeout(noteTimer);
noteTimer = setTimeout(function() {
setNote(hashId, noteEl.value);
}, 500);
};
});
// ── Filters ────────────────────────────────────────────────────
function applyFilters() {
var minFloor = parseInt(document.getElementById('min-floor').value);
var maxPrice = parseInt(document.getElementById('max-price').value);
var hideRejected = document.getElementById('hide-rejected').checked;
var ratings = loadRatings();
var visible = 0;
allMarkers.forEach(function(m) {
var popup = m.getPopup().getContent();
var floorMatch = popup.match(/(\d+)\. NP/);
var priceMatch = popup.match(/([\d\s]+)\sKč/);
var floor = floorMatch ? parseInt(floorMatch[1]) : null;
var price = priceMatch ? parseInt(priceMatch[1].replace(/\s/g, '')) : 0;
var show = true;
if (floor !== null && floor < minFloor) show = false;
if (price > maxPrice) show = false;
var r = ratings[m._data.hashId];
if (hideRejected && r && r.status === 'reject') show = false;
if (show) {
if (!map.hasLayer(m)) m.addTo(map);
visible++;
// Show strike line if rejected and visible
if (m._rejectStrike && !map.hasLayer(m._rejectStrike)) m._rejectStrike.addTo(map);
} else {
if (map.hasLayer(m)) map.removeLayer(m);
// Hide strike line when marker hidden
if (m._rejectStrike && map.hasLayer(m._rejectStrike)) map.removeLayer(m._rejectStrike);
}
});
// Re-apply styles for markers that were just added to map
allMarkers.forEach(function(m) {
if (map.hasLayer(m)) {
var r = ratings[m._data.hashId];
if (r) applyMarkerStyle(m, r.status);
}
});
document.getElementById('visible-count').textContent = visible;
}
// Initialize ratings on load — fetch from API (source of truth), seed cache, then restore
fetch('/api/ratings')
.then(function(r) { return r.ok ? r.json() : {}; })
.then(function(data) {
localStorage.setItem(RATINGS_KEY, JSON.stringify(data));
restoreRatings();
})
.catch(function() {
restoreRatings();
});
// ── Panel toggle ──────────────────────────────────────────────
function togglePanel() {
var panel = document.getElementById('info-panel');
var openBtn = document.getElementById('panel-open-btn');
var isOpen = !panel.classList.contains('collapsed');
if (isOpen) {
panel.classList.add('collapsed');
openBtn.classList.remove('hidden');
} else {
panel.classList.remove('collapsed');
openBtn.classList.add('hidden');
}
}
// On mobile, start with panel collapsed
if (window.innerWidth <= 600) {
document.getElementById('info-panel').classList.add('collapsed');
document.getElementById('panel-open-btn').classList.remove('hidden');
}
</script>
</body>
</html>

View File

@@ -11,9 +11,4 @@
## documentation ## documentation
- precisely document original intent of the app (Maru has to provide this) - precisely document original intent of the app (Maru has to provide this)
##
- prepare production run
- probably in home kubernetes
- maru-hleda-byt.lab.home.hrajfrisbee.cz

View File

@@ -20,8 +20,10 @@ START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S")
START_EPOCH=$(date +%s) START_EPOCH=$(date +%s)
LOG_FILE="$(pwd)/scrape_run.log" LOG_FILE="$(pwd)/scrape_run.log"
# Mark status as running # Mark scraper as running; cleaned up on exit (even on error/kill)
echo '{"status":"running"}' > status.json LOCK_FILE="${DATA_DIR:-.}/scraper_running.json"
echo '{"running":true,"started_at":"'"$START_TIME"'"}' > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
show_help() { show_help() {
echo "Usage: ./run_all.sh [OPTIONS]" echo "Usage: ./run_all.sh [OPTIONS]"
@@ -32,16 +34,19 @@ show_help() {
echo " --max-pages N Maximální počet stránek ke stažení z každého zdroje" echo " --max-pages N Maximální počet stránek ke stažení z každého zdroje"
echo " --max-properties N Maximální počet nemovitostí ke stažení z každého zdroje" echo " --max-properties N Maximální počet nemovitostí ke stažení z každého zdroje"
echo " --log-level LEVEL Úroveň logování (DEBUG, INFO, WARNING, ERROR)" echo " --log-level LEVEL Úroveň logování (DEBUG, INFO, WARNING, ERROR)"
echo " --keep N Počet běhů v historii (výchozí: 5, 0=neomezeno)"
echo " -h, --help Zobrazí tuto nápovědu" echo " -h, --help Zobrazí tuto nápovědu"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " ./run_all.sh # plný běh" echo " ./run_all.sh # plný běh"
echo " ./run_all.sh --max-pages 1 --max-properties 10 # rychlý test" echo " ./run_all.sh --max-pages 1 --max-properties 10 # rychlý test"
echo " ./run_all.sh --log-level DEBUG # s debug logováním" echo " ./run_all.sh --log-level DEBUG # s debug logováním"
echo " ./run_all.sh --keep 10 # uchovej 10 běhů v historii"
} }
# Parse arguments # Parse arguments
SCRAPER_ARGS="" SCRAPER_ARGS=""
KEEP_ARG=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
-h|--help) -h|--help)
@@ -52,6 +57,10 @@ while [[ $# -gt 0 ]]; do
SCRAPER_ARGS="$SCRAPER_ARGS $1 $2" SCRAPER_ARGS="$SCRAPER_ARGS $1 $2"
shift 2 shift 2
;; ;;
--keep)
KEEP_ARG="--keep $2"
shift 2
;;
*) *)
echo "Unknown argument: $1" echo "Unknown argument: $1"
echo "" echo ""
@@ -103,7 +112,7 @@ python3 merge_and_map.py || { echo -e "${RED}✗ Merge selhal${NC}"; FAILED=$((F
END_EPOCH=$(date +%s) END_EPOCH=$(date +%s)
DURATION=$((END_EPOCH - START_EPOCH)) DURATION=$((END_EPOCH - START_EPOCH))
python3 generate_status.py "$START_TIME" "$DURATION" "$LOG_FILE" python3 generate_status.py --start-time "$START_TIME" --duration "$DURATION" $KEEP_ARG
echo "" echo ""
echo "============================================================" echo "============================================================"

View File

@@ -15,6 +15,9 @@ import urllib.request
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats
STATS_FILE = "stats_sreality.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -209,6 +212,8 @@ def load_cache(json_path: str = "byty_sreality.json") -> dict[int, dict]:
def scrape(max_pages: int | None = None, max_properties: int | None = None): def scrape(max_pages: int | None = None, max_properties: int | None = None):
"""Main scraping function. Returns list of filtered estates.""" """Main scraping function. Returns list of filtered estates."""
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
all_estates_raw = [] all_estates_raw = []
cache = load_cache() cache = load_cache()
@@ -366,6 +371,21 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "Sreality",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(unique_estates),
"cache_hits": cache_hits,
"excluded": {
"panel/síd": excluded_panel,
"<69 m²": excluded_small,
"bez GPS": excluded_no_gps,
"bez detailu": excluded_no_detail,
},
})
return results return results
@@ -653,7 +673,7 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
Skrýt zamítnuté Skrýt zamítnuté
</label> </label>
</div> </div>
<div class="status-link"><a href="status.html">Scraper status</a></div> <div class="status-link"><a href="/scrapers-status">Scraper status</a></div>
</div> </div>
<script> <script>
@@ -1089,8 +1109,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "Sreality",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
# Save raw data as JSON backup # Save raw data as JSON backup

View File

@@ -15,6 +15,9 @@ import re
import time import time
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats
STATS_FILE = "stats_bezrealitky.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -171,6 +174,8 @@ def load_cache(json_path: str = "byty_bezrealitky.json") -> dict[int, dict]:
def scrape(max_pages: int | None = None, max_properties: int | None = None): def scrape(max_pages: int | None = None, max_properties: int | None = None):
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
cache = load_cache() cache = load_cache()
logger.info("=" * 60) logger.info("=" * 60)
@@ -374,6 +379,25 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "Bezrealitky",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(all_adverts),
"pages": page - 1,
"cache_hits": cache_hits,
"excluded": {
"dispozice": excluded_disp,
"cena": excluded_price,
"plocha": excluded_area,
"bez GPS": excluded_no_gps,
"panel/síd": excluded_panel,
"patro": excluded_floor,
"bez detailu": excluded_detail,
},
})
return results return results
@@ -394,8 +418,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "Bezrealitky",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
json_path = Path("byty_bezrealitky.json") json_path = Path("byty_bezrealitky.json")

View File

@@ -14,6 +14,9 @@ import time
import urllib.request import urllib.request
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats
STATS_FILE = "stats_cityhome.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -203,6 +206,8 @@ def extract_project_gps(html: str) -> tuple[float, float] | None:
def scrape(max_pages: int | None = None, max_properties: int | None = None): def scrape(max_pages: int | None = None, max_properties: int | None = None):
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Stahuji inzeráty z CityHome (city-home.cz)") logger.info("Stahuji inzeráty z CityHome (city-home.cz)")
logger.info(f"Cena: do {format_price(MAX_PRICE)}") logger.info(f"Cena: do {format_price(MAX_PRICE)}")
@@ -374,6 +379,23 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "CityHome",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(all_listings),
"excluded": {
"prodáno": excluded_sold,
"typ": excluded_type,
"dispozice": excluded_disp,
"cena": excluded_price,
"plocha": excluded_area,
"patro": excluded_floor,
"bez GPS": excluded_no_gps,
},
})
return results return results
@@ -394,8 +416,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "CityHome",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
json_path = Path("byty_cityhome.json") json_path = Path("byty_cityhome.json")

View File

@@ -17,6 +17,9 @@ import urllib.request
import urllib.parse import urllib.parse
from html.parser import HTMLParser from html.parser import HTMLParser
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats
STATS_FILE = "stats_idnes.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -279,6 +282,8 @@ def load_cache(json_path: str = "byty_idnes.json") -> dict[str, dict]:
def scrape(max_pages: int | None = None, max_properties: int | None = None): def scrape(max_pages: int | None = None, max_properties: int | None = None):
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
cache = load_cache() cache = load_cache()
logger.info("=" * 60) logger.info("=" * 60)
@@ -478,6 +483,25 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "iDNES",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(all_listings),
"pages": page,
"cache_hits": cache_hits,
"excluded": {
"cena": excluded_price,
"plocha": excluded_area,
"dispozice": excluded_disp,
"panel/síd": excluded_panel,
"patro": excluded_floor,
"bez GPS": excluded_no_gps,
"bez detailu": excluded_detail,
},
})
return results return results
@@ -498,8 +522,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "iDNES",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
json_path = Path("byty_idnes.json") json_path = Path("byty_idnes.json")

View File

@@ -15,6 +15,9 @@ import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode from urllib.parse import urlencode
from scraper_stats import write_stats
STATS_FILE = "stats_psn.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -67,6 +70,8 @@ def format_price(price: int) -> str:
def scrape(max_properties: int | None = None): def scrape(max_properties: int | None = None):
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
logger.info("=" * 60) logger.info("=" * 60)
logger.info("Stahuji inzeráty z PSN.cz") logger.info("Stahuji inzeráty z PSN.cz")
logger.info(f"Cena: do {format_price(MAX_PRICE)}") logger.info(f"Cena: do {format_price(MAX_PRICE)}")
@@ -93,6 +98,15 @@ def scrape(max_properties: int | None = None):
data = fetch_json(url) data = fetch_json(url)
except Exception as e: except Exception as e:
logger.error(f"Chyba při stahování: {e}", exc_info=True) logger.error(f"Chyba při stahování: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "PSN",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
return [] return []
all_units = data.get("units", {}).get("data", []) all_units = data.get("units", {}).get("data", [])
@@ -234,20 +248,27 @@ def scrape(max_properties: int | None = None):
logger.info(f"\n{'=' * 60}") logger.info(f"\n{'=' * 60}")
logger.info(f"Výsledky PSN:") logger.info(f"Výsledky PSN:")
logger.info(f" Staženo inzerátů: {len(all_units)}") logger.info(f" Staženo jednotek: {len(all_units)}")
for reason, count in excluded.items(): for reason, count in excluded.items():
if count: if count:
logger.info(f" Vyloučeno ({reason}): {count}") logger.info(f" Vyloučeno ({reason}): {count}")
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "PSN",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(all_units),
"excluded": excluded,
})
return results return results
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Scrape apartments from PSN.cz") parser = argparse.ArgumentParser(description="Scrape apartments from PSN.cz")
parser.add_argument("--max-pages", type=int, default=None,
help="Ignored — PSN uses a single API call, no pagination")
parser.add_argument("--max-properties", type=int, default=None, parser.add_argument("--max-properties", type=int, default=None,
help="Maximum number of properties to include in results") help="Maximum number of properties to include in results")
parser.add_argument("--log-level", type=str, default="INFO", parser.add_argument("--log-level", type=str, default="INFO",
@@ -261,8 +282,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_properties=args.max_properties) try:
estates = scrape(max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "PSN",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
json_path = Path("byty_psn.json") json_path = Path("byty_psn.json")

View File

@@ -15,6 +15,9 @@ import re
import time import time
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats
STATS_FILE = "stats_realingo.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -136,6 +139,8 @@ def load_cache(json_path: str = "byty_realingo.json") -> dict[int, dict]:
def scrape(max_pages: int | None = None, max_properties: int | None = None): def scrape(max_pages: int | None = None, max_properties: int | None = None):
_run_start = time.time()
_run_ts = datetime.now().isoformat(timespec="seconds")
cache = load_cache() cache = load_cache()
logger.info("=" * 60) logger.info("=" * 60)
@@ -333,6 +338,25 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.info(f" ✓ Vyhovující byty: {len(results)}") logger.info(f" ✓ Vyhovující byty: {len(results)}")
logger.info(f"{'=' * 60}") logger.info(f"{'=' * 60}")
write_stats(STATS_FILE, {
"source": "Realingo",
"timestamp": _run_ts,
"duration_sec": round(time.time() - _run_start, 1),
"success": True,
"accepted": len(results),
"fetched": len(all_listings),
"pages": page - 1,
"cache_hits": cache_hits,
"excluded": {
"dispozice": excluded_category,
"cena": excluded_price,
"plocha": excluded_area,
"bez GPS": excluded_no_gps,
"panel/síd": excluded_panel,
"patro": excluded_floor,
"bez detailu": excluded_detail,
},
})
return results return results
@@ -353,8 +377,22 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
except Exception as e:
logger.error(f"Scraper failed: {e}", exc_info=True)
write_stats(STATS_FILE, {
"source": "Realingo",
"timestamp": _run_ts,
"duration_sec": round(time.time() - start, 1),
"success": False,
"accepted": 0,
"fetched": 0,
"error": str(e),
})
raise
if estates: if estates:
json_path = Path("byty_realingo.json") json_path = Path("byty_realingo.json")

15
scraper_stats.py Normal file
View File

@@ -0,0 +1,15 @@
"""Shared utility for writing per-scraper run statistics to JSON."""
from __future__ import annotations
import json
import os
from pathlib import Path
HERE = Path(__file__).parent
DATA_DIR = Path(os.environ.get("DATA_DIR", HERE))
def write_stats(filename: str, stats: dict) -> None:
"""Write scraper run stats dict to the data directory."""
path = DATA_DIR / filename
path.write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8")

502
server.py
View File

@@ -1,60 +1,78 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Combined HTTP server: serves static files from DATA_DIR and General-purpose HTTP server for maru-hleda-byt.
provides the ratings API at /api/ratings.
GET /api/ratings → returns ratings.json contents Serves static files from DATA_DIR and additionally handles:
POST /api/ratings → saves entire ratings object GET /scrapers-status → SSR scraper status page
GET /api/ratings/export → same as GET, with Content-Disposition: attachment GET /api/ratings → ratings.json contents
GET /<path> → serves static file from DATA_DIR POST /api/ratings → save entire ratings object
GET /api/ratings/export → same as GET, with download header
GET /api/status → status.json contents (JSON)
GET /api/status/history → scraper_history.json contents (JSON)
""" """
import argparse from __future__ import annotations
import functools
import json import json
import logging import logging
import os import os
import sys import sys
from datetime import datetime
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path from pathlib import Path
from flask import Flask, jsonify, request, send_from_directory PORT = int(os.environ.get("SERVER_PORT", 8080))
DATA_DIR = Path(os.environ.get("DATA_DIR", "."))
parser = argparse.ArgumentParser(description="Flat-search map server")
parser.add_argument("--log-level", "-l", default=None, choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log level (default: INFO)")
parser.add_argument("--verbose", "-v", action="store_true", help="Shorthand for --log-level DEBUG")
args = parser.parse_args()
log_level = logging.DEBUG if args.verbose else getattr(logging, args.log_level or "INFO")
PORT = int(os.environ.get("PORT", 8080))
DATA_DIR = Path(os.environ.get("DATA_DIR", ".")).resolve()
RATINGS_FILE = DATA_DIR / "ratings.json" RATINGS_FILE = DATA_DIR / "ratings.json"
_LOG_LEVEL = getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO)
logging.basicConfig( logging.basicConfig(
level=log_level, level=_LOG_LEVEL,
format="%(asctime)s [server] %(levelname)s %(message)s", format="%(asctime)s [server] %(levelname)s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S", datefmt="%Y-%m-%dT%H:%M:%S",
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
app = Flask(__name__, static_folder=None) # ── Helpers ──────────────────────────────────────────────────────────────────
app.json.ensure_ascii = False
COLORS = {
"sreality": "#1976D2",
"realingo": "#7B1FA2",
"bezrealitky": "#E65100",
"idnes": "#C62828",
"psn": "#2E7D32",
"cityhome": "#00838F",
}
MONTHS_CZ = [
"ledna", "února", "března", "dubna", "května", "června",
"července", "srpna", "září", "října", "listopadu", "prosince",
]
@app.after_request def _load_json(path: Path, default=None):
def add_cors(response): """Read and parse JSON file; return default on missing or parse error."""
response.headers["Access-Control-Allow-Origin"] = "*" log.debug("_load_json: %s", path.resolve())
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" try:
response.headers["Access-Control-Allow-Headers"] = "Content-Type" if path.exists():
return response return json.loads(path.read_text(encoding="utf-8"))
except Exception as e:
log.warning("Failed to load %s: %s", path, e)
return default
def _fmt_date(iso_str: str) -> str:
"""Format ISO timestamp as Czech date string."""
try:
d = datetime.fromisoformat(iso_str)
return f"{d.day}. {MONTHS_CZ[d.month - 1]} {d.year}, {d.hour:02d}:{d.minute:02d}"
except Exception:
return iso_str
def load_ratings() -> dict: def load_ratings() -> dict:
try: return _load_json(RATINGS_FILE, default={})
if RATINGS_FILE.exists():
return json.loads(RATINGS_FILE.read_text(encoding="utf-8"))
except Exception as e:
log.error("Failed to load ratings: %s", e)
return {}
def save_ratings(data: dict) -> None: def save_ratings(data: dict) -> None:
@@ -64,56 +82,396 @@ def save_ratings(data: dict) -> None:
) )
@app.route("/api/ratings", methods=["OPTIONS"]) # ── SSR status page ──────────────────────────────────────────────────────────
@app.route("/api/ratings/export", methods=["OPTIONS"])
def ratings_options(): _CSS = """\
return ("", 204) * { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5; color: #333;
padding: 24px; max-width: 640px; margin: 0 auto;
}
h1 { font-size: 22px; margin-bottom: 4px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; }
.card {
background: white; border-radius: 12px; padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08); margin-bottom: 16px;
}
.card h2 { font-size: 15px; margin-bottom: 12px; color: #555; }
.timestamp { font-size: 28px; font-weight: 700; color: #1976D2; }
.timestamp-sub { font-size: 13px; color: #999; margin-top: 2px; }
.summary-row {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid #f0f0f0;
}
.summary-row:last-child { border-bottom: none; }
.summary-label { font-size: 13px; color: #666; }
.summary-value { font-size: 18px; font-weight: 700; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 11px; font-weight: 600; color: white;
}
.badge-ok { background: #4CAF50; }
.badge-err { background: #F44336; }
.badge-skip { background: #FF9800; }
.bar-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.bar-track { flex: 1; height: 20px; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; }
.bar-count { font-size: 12px; width: 36px; font-variant-numeric: tabular-nums; }
.loader-wrap {
display: flex; flex-direction: column; align-items: center;
justify-content: center; padding: 60px 0;
}
.spinner {
width: 40px; height: 40px; border: 4px solid #e0e0e0;
border-top-color: #1976D2; border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-text { margin-top: 16px; color: #999; font-size: 14px; }
.link-row { text-align: center; margin-top: 8px; }
.link-row a { color: #1976D2; text-decoration: none; font-size: 14px; }
.history-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.history-table th {
text-align: left; font-weight: 600; color: #999; font-size: 11px;
padding: 4px 6px 8px 6px; border-bottom: 2px solid #f0f0f0;
}
.history-table td { padding: 7px 6px; border-bottom: 1px solid #f5f5f5; vertical-align: middle; }
.history-table tr:last-child td { border-bottom: none; }
.history-table tr.latest td { background: #f8fbff; font-weight: 600; }
.src-nums { display: flex; gap: 4px; flex-wrap: wrap; }
.src-chip {
display: inline-block; padding: 1px 5px; border-radius: 3px;
font-size: 10px; color: white; font-variant-numeric: tabular-nums;
}
.clickable-row { cursor: pointer; }
.clickable-row:hover td { background: #f0f7ff !important; }
/* Modal */
#md-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.45);
display: flex; align-items: flex-start; justify-content: center;
z-index: 1000; padding: 40px 16px; overflow-y: auto;
}
#md-box {
background: white; border-radius: 12px; padding: 24px;
width: 100%; max-width: 620px; position: relative;
box-shadow: 0 8px 32px rgba(0,0,0,0.24); margin: auto;
}
#md-close {
position: absolute; top: 10px; right: 14px;
background: none; border: none; font-size: 26px; cursor: pointer;
color: #aaa; line-height: 1;
}
#md-close:hover { color: #333; }
#md-box h3 { font-size: 15px; margin-bottom: 14px; padding-right: 24px; }
.md-summary { display: flex; gap: 20px; flex-wrap: wrap; font-size: 13px; margin-bottom: 16px; color: #555; }
.md-summary b { color: #333; }
.detail-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.detail-table th {
text-align: left; color: #999; font-size: 11px; font-weight: 600;
padding: 4px 8px 6px 0; border-bottom: 2px solid #f0f0f0; white-space: nowrap;
}
.detail-table td { padding: 6px 8px 6px 0; border-bottom: 1px solid #f5f5f5; vertical-align: top; }
.detail-table tr:last-child td { border-bottom: none; }
"""
_SOURCE_ORDER = ["Sreality", "Realingo", "Bezrealitky", "iDNES", "PSN", "CityHome"]
_SOURCE_ABBR = ["Sre", "Rea", "Bez", "iDN", "PSN", "CH"]
@app.route("/api/ratings", methods=["GET"]) def _sources_html(sources: list) -> str:
def get_ratings(): if not sources:
ratings = load_ratings() return ""
log.info("GET /api/ratings → %d ratings", len(ratings)) max_count = max((s.get("accepted", 0) for s in sources), default=1) or 1
return jsonify(ratings) parts = ['<div class="card"><h2>Zdroje</h2>']
for s in sources:
name = s.get("name", "?")
accepted = s.get("accepted", 0)
error = s.get("error")
exc = s.get("excluded", {})
excluded_total = sum(exc.values()) if isinstance(exc, dict) else s.get("excluded_total", 0)
color = COLORS.get(name.lower(), "#999")
pct = round(accepted / max_count * 100) if max_count else 0
if error:
badge = '<span class="badge badge-err">chyba</span>'
elif accepted == 0:
badge = '<span class="badge badge-skip">0</span>'
else:
badge = '<span class="badge badge-ok">OK</span>'
parts.append(
f'<div style="margin-bottom:12px;">'
f'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">'
f'<span style="font-weight:600;font-size:14px;">{name} {badge}</span>'
f'<span style="font-size:12px;color:#999;">{excluded_total} vyloučených</span>'
f'</div>'
f'<div class="bar-row">'
f'<div class="bar-track"><div class="bar-fill" style="width:{pct}%;background:{color};"></div></div>'
f'<span class="bar-count">{accepted}</span>'
f'</div></div>'
)
parts.append("</div>")
return "".join(parts)
@app.route("/api/ratings/export", methods=["GET"]) def _history_html(history: list) -> str:
def export_ratings(): if not history:
ratings = load_ratings() return ""
log.info("GET /api/ratings/export → %d ratings", len(ratings)) rows = list(reversed(history))
response = jsonify(ratings) parts = [
response.headers["Content-Disposition"] = 'attachment; filename="ratings.json"' '<div class="card">'
return response '<h2>Historie běhů <span style="font-size:11px;font-weight:400;color:#bbb;"> klikni pro detaily</span></h2>',
'<table class="history-table"><thead><tr>',
'<th>Datum</th><th>Trvání</th><th>Přijato&nbsp;/&nbsp;Dedup</th><th>Zdroje</th><th>OK</th>',
'</tr></thead><tbody>',
]
for i, entry in enumerate(rows):
row_class = ' class="latest clickable-row"' if i == 0 else ' class="clickable-row"'
src_map = {s["name"]: s for s in entry.get("sources", []) if "name" in s}
chips = "".join(
f'<span class="src-chip" style="background:{"#F44336" if (src_map.get(name) or {}).get("error") else COLORS.get(name.lower(), "#999")}" title="{name}">'
f'{abbr}&nbsp;{src_map[name].get("accepted", 0) if name in src_map else "-"}</span>'
for name, abbr in zip(_SOURCE_ORDER, _SOURCE_ABBR)
)
ok_badge = (
'<span class="badge badge-err">chyba</span>'
if entry.get("success") is False
else '<span class="badge badge-ok">OK</span>'
)
dur = f'{entry["duration_sec"]}s' if entry.get("duration_sec") is not None else "-"
parts.append(
f'<tr{row_class} data-idx="{i}">'
f'<td>{_fmt_date(entry.get("timestamp", ""))}</td>'
f'<td>{dur}</td>'
f'<td>{entry.get("total_accepted", "-")}&nbsp;/&nbsp;{entry.get("deduplicated", "-")}</td>'
f'<td><div class="src-nums">{chips}</div></td>'
f'<td>{ok_badge}</td>'
f'</tr>'
)
parts.append("</tbody></table></div>")
return "".join(parts)
@app.route("/api/ratings", methods=["POST"]) def _modal_script(rows_json: str) -> str:
def post_ratings(): """Return the modal overlay HTML + JS for the history detail popup."""
length = request.content_length return (
if not length: '<div id="md-overlay" style="display:none">'
return jsonify({"error": "empty body"}), 400 '<div id="md-box"><button id="md-close">&times;</button>'
try: '<div id="md-body"></div></div></div>\n'
data = request.get_json(force=True, silent=False) '<script>\n(function(){\n'
except Exception as e: f'var H={rows_json};\n'
log.warning("Bad request body: %s", e) 'var C={"sreality":"#1976D2","realingo":"#7B1FA2","bezrealitky":"#E65100","idnes":"#C62828","psn":"#2E7D32","cityhome":"#00838F"};\n'
return jsonify({"error": "invalid JSON"}), 400 'var MN=["ledna","února","března","dubna","května","června","července","srpna","září","října","listopadu","prosince"];\n'
if not isinstance(data, dict): 'function fd(s){var d=new Date(s);return d.getDate()+". "+MN[d.getMonth()]+" "+d.getFullYear()+", "+String(d.getHours()).padStart(2,"0")+":"+String(d.getMinutes()).padStart(2,"0");}\n'
return jsonify({"error": "expected JSON object"}), 400 'function openModal(idx){\n'
save_ratings(data) ' var e=H[idx],src=e.sources||[];\n'
log.info("POST /api/ratings → saved %d ratings", len(data)) ' var h="<h3>Detaily b\u011bhu \u2013 "+fd(e.timestamp)+"</h3>";\n'
return jsonify({"ok": True, "count": len(data)}) ' h+="<div class=\\"md-summary\\">";\n'
' if(e.duration_sec!=null) h+="<span><b>Trvání:</b> "+e.duration_sec+"s</span>";\n'
' if(e.total_accepted!=null) h+="<span><b>Přijato:</b> "+e.total_accepted+"</span>";\n'
' if(e.deduplicated!=null) h+="<span><b>Po dedup:</b> "+e.deduplicated+"</span>";\n'
' h+="</div>";\n'
' h+="<table class=\\"detail-table\\"><thead><tr>";\n'
' h+="<th>Zdroj</th><th>Přijato</th><th>Staženo</th><th>Stránky</th><th>Cache</th><th>Vyloučeno</th><th>Čas</th><th>OK</th>";\n'
' h+="</tr></thead><tbody>";\n'
' src.forEach(function(s){\n'
' var nm=s.name||"?",col=C[nm.toLowerCase()]||"#999";\n'
' var exc=s.excluded||{};\n'
' var excStr=Object.entries(exc).filter(function(kv){return kv[1]>0;}).map(function(kv){return kv[0]+":&nbsp;"+kv[1];}).join(", ")||"\u2013";\n'
' var ok=s.error?"<span class=\\"badge badge-err\\" title=\\""+s.error+"\\">chyba</span>":"<span class=\\"badge badge-ok\\">OK</span>";\n'
' var dot="<span style=\\"display:inline-block;width:8px;height:8px;border-radius:50%;background:"+col+";margin-right:5px;\\"></span>";\n'
' h+="<tr>";\n'
' h+="<td>"+dot+nm+"</td>";\n'
' h+="<td>"+(s.accepted!=null?s.accepted:"\u2013")+"</td>";\n'
' h+="<td>"+(s.fetched!=null?s.fetched:"\u2013")+"</td>";\n'
' h+="<td>"+(s.pages!=null?s.pages:"\u2013")+"</td>";\n'
' h+="<td>"+(s.cache_hits!=null?s.cache_hits:"\u2013")+"</td>";\n'
' h+="<td style=\\"font-size:11px;color:#666;\\">"+excStr+"</td>";\n'
' h+="<td>"+(s.duration_sec!=null?s.duration_sec+"s":"\u2013")+"</td>";\n'
' h+="<td>"+ok+"</td></tr>";\n'
' });\n'
' h+="</tbody></table>";\n'
' document.getElementById("md-body").innerHTML=h;\n'
' document.getElementById("md-overlay").style.display="flex";\n'
'}\n'
'function closeModal(){document.getElementById("md-overlay").style.display="none";}\n'
'var tb=document.querySelector(".history-table tbody");\n'
'if(tb)tb.addEventListener("click",function(e){var tr=e.target.closest("tr[data-idx]");if(tr)openModal(parseInt(tr.dataset.idx,10));});\n'
'document.getElementById("md-close").addEventListener("click",closeModal);\n'
'document.getElementById("md-overlay").addEventListener("click",function(e){if(e.target===this)closeModal();});\n'
'document.addEventListener("keydown",function(e){if(e.key==="Escape")closeModal();});\n'
'})();\n</script>'
)
@app.route("/") def _render_status_html(status: dict | None, history: list, is_running: bool = False) -> str:
def index(): """Generate the complete HTML page for /scrapers-status."""
return send_from_directory(str(DATA_DIR), "mapa_bytu.html") head_open = (
'<!DOCTYPE html>\n<html lang="cs">\n<head>\n'
'<meta charset="UTF-8">\n'
'<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
f'<title>Scraper status</title>\n<style>{_CSS}</style>\n'
)
page_header = '<h1>Scraper status</h1>\n<div class="subtitle">maru-hleda-byt</div>\n'
footer = '<div class="link-row"><a href="/mapa_bytu.html">Otevřít mapu</a></div>'
if status is None:
return (
head_open + '</head>\n<body>\n' + page_header
+ '<div class="card"><p style="color:#F44336">Status není k dispozici.</p></div>\n'
+ footer + '\n</body>\n</html>'
)
if is_running:
return (
head_open
+ '<meta http-equiv="refresh" content="30">\n'
+ '</head>\n<body>\n' + page_header
+ '<div class="loader-wrap"><div class="spinner"></div>'
+ '<div class="loader-text">Scraper právě běží…</div></div>\n'
+ footer + '\n</body>\n</html>'
)
# ── Done state ────────────────────────────────────────────────────────────
ts = status.get("timestamp", "")
duration = status.get("duration_sec")
total_accepted = status.get("total_accepted", 0)
deduplicated = status.get("deduplicated")
ts_card = (
'<div class="card"><h2>Poslední scrape</h2>'
f'<div class="timestamp">{_fmt_date(ts)}</div>'
+ (f'<div class="timestamp-sub">Trvání: {round(duration)}s</div>' if duration is not None else "")
+ '</div>'
)
sum_card = (
'<div class="card"><h2>Souhrn</h2>'
f'<div class="summary-row"><span class="summary-label">Vyhovujících bytů</span>'
f'<span class="summary-value" style="color:#4CAF50">{total_accepted}</span></div>'
+ (
f'<div class="summary-row"><span class="summary-label">Po deduplikaci (v mapě)</span>'
f'<span class="summary-value" style="color:#1976D2">{deduplicated}</span></div>'
if deduplicated is not None else ""
)
+ '</div>'
)
rows_for_js = list(reversed(history))
body = (
page_header
+ ts_card + "\n"
+ sum_card + "\n"
+ _sources_html(status.get("sources", [])) + "\n"
+ _history_html(history) + "\n"
+ footer
)
modal = _modal_script(json.dumps(rows_for_js, ensure_ascii=False))
return head_open + '</head>\n<body>\n' + body + '\n' + modal + '\n</body>\n</html>'
@app.route("/<path:filename>") # ── HTTP handler ──────────────────────────────────────────────────────────────
def static_files(filename):
return send_from_directory(str(DATA_DIR), filename) class Handler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass # suppress default access log; use our own where needed
def _send_json(self, status: int, body, extra_headers=None):
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
if extra_headers:
for k, v in extra_headers.items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(payload)
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_GET(self):
if self.path.startswith("/api/"):
self._handle_api_get()
elif self.path.rstrip("/") == "/scrapers-status":
self._serve_status_page()
else:
log.debug("GET %s → static file: %s", self.path, self.translate_path(self.path))
super().do_GET()
def _handle_api_get(self):
if self.path in ("/api/ratings", "/api/ratings/export"):
ratings = load_ratings()
extra = None
if self.path == "/api/ratings/export":
extra = {"Content-Disposition": 'attachment; filename="ratings.json"'}
log.info("GET %s%d ratings", self.path, len(ratings))
self._send_json(200, ratings, extra)
elif self.path == "/api/status":
data = _load_json(DATA_DIR / "status.json")
if data is None:
self._send_json(404, {"error": "status not available"})
return
log.info("GET /api/status → ok")
self._send_json(200, data)
elif self.path == "/api/status/history":
data = _load_json(DATA_DIR / "scraper_history.json", default=[])
if not isinstance(data, list):
data = []
log.info("GET /api/status/history → %d entries", len(data))
self._send_json(200, data)
else:
self._send_json(404, {"error": "not found"})
def _serve_status_page(self):
status = _load_json(DATA_DIR / "status.json")
history = _load_json(DATA_DIR / "scraper_history.json", default=[])
if not isinstance(history, list):
history = []
is_running = (DATA_DIR / "scraper_running.json").exists()
html = _render_status_html(status, history, is_running)
payload = html.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_POST(self):
if self.path == "/api/ratings":
length = int(self.headers.get("Content-Length", 0))
if length == 0:
self._send_json(400, {"error": "empty body"})
return
try:
raw = self.rfile.read(length)
data = json.loads(raw.decode("utf-8"))
except Exception as e:
log.warning("Bad request body: %s", e)
self._send_json(400, {"error": "invalid JSON"})
return
if not isinstance(data, dict):
self._send_json(400, {"error": "expected JSON object"})
return
save_ratings(data)
log.info("POST /api/ratings → saved %d ratings", len(data))
self._send_json(200, {"ok": True, "count": len(data)})
else:
self._send_json(404, {"error": "not found"})
if __name__ == "__main__": if __name__ == "__main__":
log.info("Server starting on port %d, data dir: %s", PORT, DATA_DIR) log.info("Server starting on port %d, data dir: %s", PORT, DATA_DIR)
log.info("Ratings file: %s", RATINGS_FILE) handler = functools.partial(Handler, directory=str(DATA_DIR))
app.run(host="0.0.0.0", port=PORT) server = HTTPServer(("0.0.0.0", PORT), handler)
try:
server.serve_forever()
except KeyboardInterrupt:
log.info("Stopped.")
sys.exit(0)

View File

@@ -1,204 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scraper status</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5; color: #333;
padding: 24px; max-width: 640px; margin: 0 auto;
}
h1 { font-size: 22px; margin-bottom: 4px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; }
.card {
background: white; border-radius: 12px; padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08); margin-bottom: 16px;
}
.card h2 { font-size: 15px; margin-bottom: 12px; color: #555; }
.timestamp {
font-size: 28px; font-weight: 700; color: #1976D2;
}
.timestamp-ago { font-size: 13px; color: #999; margin-top: 2px; }
/* Source table */
.source-table { width: 100%; border-collapse: collapse; }
.source-table td { padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
.source-table tr:last-child td { border-bottom: none; }
.source-table .name { font-weight: 600; }
.source-table .count { text-align: right; font-variant-numeric: tabular-nums; }
.source-table .rejected { text-align: right; color: #999; font-size: 12px; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 11px; font-weight: 600; color: white;
}
.badge-ok { background: #4CAF50; }
.badge-err { background: #F44336; }
.badge-skip { background: #FF9800; }
/* Summary bar */
.summary-row {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid #f0f0f0;
}
.summary-row:last-child { border-bottom: none; }
.summary-label { font-size: 13px; color: #666; }
.summary-value { font-size: 18px; font-weight: 700; }
/* Source bar chart */
.bar-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.bar-label { width: 90px; font-size: 12px; text-align: right; color: #666; }
.bar-track { flex: 1; height: 20px; background: #f0f0f0; border-radius: 4px; overflow: hidden; position: relative; }
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
.bar-count { font-size: 12px; width: 36px; font-variant-numeric: tabular-nums; }
/* Loader */
.loader-wrap {
display: flex; flex-direction: column; align-items: center;
justify-content: center; padding: 60px 0;
}
.spinner {
width: 40px; height: 40px; border: 4px solid #e0e0e0;
border-top-color: #1976D2; border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-text { margin-top: 16px; color: #999; font-size: 14px; }
.error-msg { color: #F44336; padding: 40px 0; text-align: center; }
.link-row { text-align: center; margin-top: 8px; }
.link-row a { color: #1976D2; text-decoration: none; font-size: 14px; }
</style>
</head>
<body>
<h1>Scraper status</h1>
<div class="subtitle">maru-hleda-byt</div>
<div id="content">
<div class="loader-wrap">
<div class="spinner"></div>
<div class="loader-text">Nacitam status...</div>
</div>
</div>
<div class="link-row"><a href="mapa_bytu.html">Otevrit mapu</a></div>
<script>
var COLORS = {
sreality: '#1976D2',
realingo: '#7B1FA2',
bezrealitky: '#E65100',
idnes: '#C62828',
psn: '#2E7D32',
cityhome: '#00838F',
};
function timeAgo(dateStr) {
var d = new Date(dateStr);
var now = new Date();
var diff = Math.floor((now - d) / 1000);
if (diff < 60) return 'prave ted';
if (diff < 3600) return Math.floor(diff / 60) + ' min zpet';
if (diff < 86400) return Math.floor(diff / 3600) + ' hod zpet';
return Math.floor(diff / 86400) + ' dni zpet';
}
function formatDate(dateStr) {
var d = new Date(dateStr);
var day = d.getDate();
var months = ['ledna','unora','brezna','dubna','kvetna','cervna',
'cervence','srpna','zari','rijna','listopadu','prosince'];
var hh = String(d.getHours()).padStart(2, '0');
var mm = String(d.getMinutes()).padStart(2, '0');
return day + '. ' + months[d.getMonth()] + ' ' + d.getFullYear() + ', ' + hh + ':' + mm;
}
function render(data) {
// Check if scrape is currently running
if (data.status === 'running') {
document.getElementById('content').innerHTML =
'<div class="loader-wrap">' +
'<div class="spinner"></div>' +
'<div class="loader-text">Scraper prave bezi...</div>' +
'</div>';
setTimeout(loadStatus, 30000);
return;
}
var sources = data.sources || [];
var totalOk = 0, totalRej = 0;
var maxCount = 0;
sources.forEach(function(s) {
totalOk += s.accepted || 0;
totalRej += s.rejected || 0;
if (s.accepted > maxCount) maxCount = s.accepted;
});
var html = '';
// Timestamp card
html += '<div class="card">';
html += '<h2>Posledni scrape</h2>';
html += '<div class="timestamp">' + formatDate(data.timestamp) + '</div>';
html += '<div class="timestamp-ago">' + timeAgo(data.timestamp) + '</div>';
if (data.duration_sec) {
html += '<div class="timestamp-ago">Trvani: ' + Math.round(data.duration_sec) + 's</div>';
}
html += '</div>';
// Summary card
html += '<div class="card">';
html += '<h2>Souhrn</h2>';
html += '<div class="summary-row"><span class="summary-label">Vyhovujicich bytu</span><span class="summary-value" style="color:#4CAF50">' + totalOk + '</span></div>';
html += '<div class="summary-row"><span class="summary-label">Vyloucenych</span><span class="summary-value" style="color:#999">' + totalRej + '</span></div>';
if (data.deduplicated !== undefined) {
html += '<div class="summary-row"><span class="summary-label">Po deduplikaci (v mape)</span><span class="summary-value" style="color:#1976D2">' + data.deduplicated + '</span></div>';
}
html += '</div>';
// Sources card
html += '<div class="card">';
html += '<h2>Zdroje</h2>';
sources.forEach(function(s) {
var color = COLORS[s.name.toLowerCase()] || '#999';
var pct = maxCount > 0 ? Math.round((s.accepted / maxCount) * 100) : 0;
var badge = s.error
? '<span class="badge badge-err">chyba</span>'
: (s.accepted === 0 ? '<span class="badge badge-skip">0</span>' : '<span class="badge badge-ok">OK</span>');
html += '<div style="margin-bottom:12px;">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">';
html += '<span style="font-weight:600;font-size:14px;">' + s.name + ' ' + badge + '</span>';
html += '<span style="font-size:12px;color:#999;">' + (s.rejected || 0) + ' vyloucenych</span>';
html += '</div>';
html += '<div class="bar-row">';
html += '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:' + color + ';"></div></div>';
html += '<span class="bar-count">' + (s.accepted || 0) + '</span>';
html += '</div>';
html += '</div>';
});
html += '</div>';
document.getElementById('content').innerHTML = html;
}
function loadStatus() {
fetch('status.json?t=' + Date.now())
.then(function(r) {
if (!r.ok) throw new Error(r.status);
return r.json();
})
.then(render)
.catch(function(err) {
document.getElementById('content').innerHTML =
'<div class="error-msg">Status zatim neni k dispozici.<br><small>(' + err.message + ')</small></div>';
});
}
loadStatus();
</script>
</body>
</html>