2 Commits
0.09 ... 0.01

Author SHA1 Message Date
Jan Novak
a1212c6312 Tag Docker images with both git tag and latest
All checks were successful
Build and Push / build (push) Successful in 8s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:05:51 +01:00
Jan Novak
0b95c847c4 Add first_seen/last_updated timestamps to track property freshness
Each property record now carries two date fields:
- first_seen: date the listing first appeared (preserved across runs)
- last_updated: date of the most recent scrape that included it

All 6 scrapers (Sreality, Realingo, Bezrealitky, iDNES, PSN, CityHome)
set these fields during scraping. Cached results preserve first_seen and
refresh last_updated. PSN and CityHome gain a load_previous() helper to
track first_seen across runs (they lacked caching before).

The merge script keeps the earliest first_seen and latest last_updated
when deduplicating listings across sources.

The HTML map now shows dates in popups ("Přidáno: DD.MM.YYYY"), displays
a green "NOVÉ" badge on newly discovered listings, and adds a "Přidáno"
dropdown filter (24h / 3 days / 7 days / 14 days) for spotting new ones.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:03:08 +01:00
30 changed files with 22128 additions and 1895 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,31 @@
{
"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:*)"
]
}
}

View File

@@ -30,6 +30,7 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG=${{ inputs.tag }} TAG=${{ inputs.tag }}
fi fi
IMAGE=gitea.home.hrajfrisbee.cz/${{ github.repository }}:$TAG REPO=gitea.home.hrajfrisbee.cz/${{ github.repository }}
docker build -f build/Dockerfile -t $IMAGE . docker build -f build/Dockerfile -t $REPO:$TAG -t $REPO:latest .
docker push $IMAGE docker push $REPO:$TAG
docker push $REPO:latest

5
.gitignore vendored
View File

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

View File

@@ -3,13 +3,9 @@ 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 serve validation validation-local validation-stop validation-local-debug debug debug-stop .PHONY: build run stop logs scrape restart clean help validation validation-local validation-stop validation-local-debug
help: help:
@echo "Available targets:" @echo "Available targets:"
@@ -24,9 +20,6 @@ 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 - Start server.py locally on port 8080"
@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:
@@ -66,27 +59,6 @@ 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

View File

@@ -151,7 +151,7 @@ The project includes a Docker setup for unattended operation with a cron-based s
│ PID 1: python3 -m http.server :8080 │ │ PID 1: python3 -m http.server :8080 │
│ serves /app/data/ │ │ serves /app/data/ │
│ │ │ │
│ crond: runs run_all.sh every 4 hours │ crond: runs run_all.sh at 06:00/18:00
│ Europe/Prague timezone │ │ Europe/Prague timezone │
│ │ │ │
│ /app/ -- scripts (.py, .sh) │ │ /app/ -- scripts (.py, .sh) │
@@ -160,7 +160,7 @@ The project includes a Docker setup for unattended operation with a cron-based s
└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
``` ```
On startup, the HTTP server starts immediately. The initial scrape runs in the background. Subsequent cron runs update data in-place every 4 hours. On startup, the HTTP server starts immediately. The initial scrape runs in the background. Subsequent cron runs update data in-place twice daily at 06:00 and 18:00 CET/CEST.
### Quick start ### Quick start
@@ -208,7 +208,7 @@ Validation targets run scrapers with `--max-pages 1 --max-properties 10` for a f
├── build/ ├── build/
│ ├── Dockerfile # Container image definition (python:3.13-alpine) │ ├── Dockerfile # Container image definition (python:3.13-alpine)
│ ├── entrypoint.sh # Container entrypoint (HTTP server + cron + initial scrape) │ ├── entrypoint.sh # Container entrypoint (HTTP server + cron + initial scrape)
│ ├── crontab # Cron schedule (every 4 hours) │ ├── crontab # Cron schedule (06:00 and 18:00 CET)
│ └── CONTAINER.md # Container-specific documentation │ └── CONTAINER.md # Container-specific documentation
└── .gitignore # Ignores byty_*.json, __pycache__, .vscode └── .gitignore # Ignores byty_*.json, __pycache__, .vscode
``` ```

View File

@@ -5,14 +5,12 @@ 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
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 generate_status.py scraper_stats.py \ merge_and_map.py regen_map.py run_all.sh ./
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 */4 * * * cd /app && bash /app/run_all.sh >> /proc/1/fd/1 2>> /proc/1/fd/2 0 6,18 * * * cd /app && bash /app/run_all.sh >> /proc/1/fd/1 2>> /proc/1/fd/2

View File

@@ -1,12 +1,12 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
export DATA_DIR="/app/data" 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 \
byty_idnes.json byty_psn.json byty_cityhome.json byty_merged.json \ byty_idnes.json byty_psn.json byty_cityhome.json byty_merged.json \
mapa_bytu.html ratings.json; do mapa_bytu.html; do
# Remove real file if it exists (e.g. baked into image) # Remove real file if it exists (e.g. baked into image)
[ -f "/app/$f" ] && [ ! -L "/app/$f" ] && rm -f "/app/$f" [ -f "/app/$f" ] && [ ! -L "/app/$f" ] && rm -f "/app/$f"
ln -sf "$DATA_DIR/$f" "/app/$f" ln -sf "$DATA_DIR/$f" "/app/$f"
@@ -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 server on port 8080..." echo "[entrypoint] Starting HTTP server on port 8080..."
exec python3 /app/server.py exec python3 -m http.server 8080 --directory "$DATA_DIR"

427
byty_bezrealitky.json Normal file
View File

@@ -0,0 +1,427 @@
[
{
"hash_id": 990183,
"name": "Prodej bytu 3+kk 86 m²",
"price": 10385000,
"price_formatted": "10 385 000 Kč",
"locality": "Ke Tvrzi, Praha - Královice",
"lat": 50.0390519,
"lon": 14.63862,
"disposition": "3+kk",
"floor": 2,
"area": 86,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/990183-nabidka-prodej-bytu-ke-tvrzi-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 989862,
"name": "Prodej bytu 3+kk 73 m²",
"price": 12790000,
"price_formatted": "12 790 000 Kč",
"locality": "Vrázova, Praha - Smíchov",
"lat": 50.0711312,
"lon": 14.4076652,
"disposition": "3+kk",
"floor": 3,
"area": 73,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/989862-nabidka-prodej-bytu-vrazova-praha",
"source": "bezrealitky",
"image": ""
},
{
"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": ""
},
{
"hash_id": 989817,
"name": "Prodej bytu 3+kk 88 m²",
"price": 13490000,
"price_formatted": "13 490 000 Kč",
"locality": "Miroslava Hajna, Praha - Letňany",
"lat": 50.1406487,
"lon": 14.5207541,
"disposition": "3+kk",
"floor": 2,
"area": 88,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/989817-nabidka-prodej-bytu-miroslava-hajna-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 970257,
"name": "Prodej bytu 3+1 106 m²",
"price": 12950000,
"price_formatted": "12 950 000 Kč",
"locality": "Novákových, Praha - Libeň",
"lat": 50.1034771,
"lon": 14.4758735,
"disposition": "3+1",
"floor": 5,
"area": 106,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/970257-nabidka-prodej-bytu-novakovych-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 972406,
"name": "Prodej bytu 3+kk 83 m²",
"price": 10490000,
"price_formatted": "10 490 000 Kč",
"locality": "Na Výrovně, Praha - Stodůlky",
"lat": 50.0396067,
"lon": 14.3167022,
"disposition": "3+kk",
"floor": 2,
"area": 83,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/972406-nabidka-prodej-bytu-na-vyrovne",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 967142,
"name": "Prodej bytu 3+kk 78 m²",
"price": 11648000,
"price_formatted": "11 648 000 Kč",
"locality": "Na Míčánkách, Praha - Vršovice",
"lat": 50.0713284,
"lon": 14.4638722,
"disposition": "3+kk",
"floor": 6,
"area": 78,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/967142-nabidka-prodej-bytu-na-micankach",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 955977,
"name": "Prodej bytu 4+kk 75 m²",
"price": 10363000,
"price_formatted": "10 363 000 Kč",
"locality": "Karla Guta, Praha - Uhříněves",
"lat": 50.03017,
"lon": 14.5940072,
"disposition": "4+kk",
"floor": 4,
"area": 75,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/955977-nabidka-prodej-bytu-karla-guta",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 974557,
"name": "Prodej bytu 4+kk 94 m²",
"price": 13499900,
"price_formatted": "13 499 900 Kč",
"locality": "V Dolině, Praha - Michle",
"lat": 50.0579963,
"lon": 14.4682887,
"disposition": "4+kk",
"floor": 8,
"area": 94,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/974557-nabidka-prodej-bytu-v-doline-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 988498,
"name": "Prodej bytu 3+1 75 m²",
"price": 11400000,
"price_formatted": "11 400 000 Kč",
"locality": "5. května, Praha - Nusle",
"lat": 50.0604096,
"lon": 14.4326302,
"disposition": "3+1",
"floor": 4,
"area": 75,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/988498-nabidka-prodej-bytu-5-kvetna-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 985285,
"name": "Prodej bytu 3+kk 70 m²",
"price": 12200000,
"price_formatted": "12 200 000 Kč",
"locality": "Klausova, Praha - Stodůlky",
"lat": 50.0370204,
"lon": 14.3432643,
"disposition": "3+kk",
"floor": 5,
"area": 70,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/985285-nabidka-prodej-bytu-klausova-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 965526,
"name": "Prodej bytu 3+kk 77 m²",
"price": 11890000,
"price_formatted": "11 890 000 Kč",
"locality": "Vinohradská, Praha - Strašnice",
"lat": 50.0776726,
"lon": 14.4870072,
"disposition": "3+kk",
"floor": 16,
"area": 77,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/965526-nabidka-prodej-bytu-vinohradska-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 924811,
"name": "Prodej bytu 3+kk 75 m²",
"price": 13390000,
"price_formatted": "13 390 000 Kč",
"locality": "Waltariho, Praha - Hloubětín",
"lat": 50.1076717,
"lon": 14.5248559,
"disposition": "3+kk",
"floor": 4,
"area": 75,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/924811-nabidka-prodej-bytu-waltariho-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 985859,
"name": "Prodej bytu 3+1 80 m²",
"price": 9000000,
"price_formatted": "9 000 000 Kč",
"locality": "Staňkova, Praha - Háje",
"lat": 50.0377128,
"lon": 14.5311557,
"disposition": "3+1",
"floor": 2,
"area": 80,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/985859-nabidka-prodej-bytu-stankova-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 985583,
"name": "Prodej bytu 3+kk 76 m²",
"price": 10850000,
"price_formatted": "10 850 000 Kč",
"locality": "Boloňská, Praha - Horní Měcholupy",
"lat": 50.047328,
"lon": 14.5565277,
"disposition": "3+kk",
"floor": 4,
"area": 76,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/985583-nabidka-prodej-bytu-bolonska-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 981178,
"name": "Prodej bytu 4+kk 86 m²",
"price": 11990000,
"price_formatted": "11 990 000 Kč",
"locality": "Sušilova, Praha - Uhříněves",
"lat": 50.032081,
"lon": 14.5885148,
"disposition": "4+kk",
"floor": 2,
"area": 86,
"building_type": "SKELET",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/981178-nabidka-prodej-bytu-susilova-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 973216,
"name": "Prodej bytu 4+1 82 m²",
"price": 11357000,
"price_formatted": "11 357 000 Kč",
"locality": "Nad Kapličkou, Praha - Strašnice",
"lat": 50.0839509,
"lon": 14.4904493,
"disposition": "4+1",
"floor": 2,
"area": 82,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/973216-nabidka-prodej-bytu-nad-kaplickou-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 868801,
"name": "Prodej bytu 3+kk 109 m²",
"price": 7299000,
"price_formatted": "7 299 000 Kč",
"locality": "Pod Karlovem, Praha - Vinohrady",
"lat": 50.0676313,
"lon": 14.432498,
"disposition": "3+kk",
"floor": 5,
"area": 109,
"building_type": "Cihlová",
"ownership": "Družstevní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/868801-nabidka-prodej-bytu-pod-karlovem-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 868795,
"name": "Prodej bytu 3+kk 106 m²",
"price": 6299000,
"price_formatted": "6 299 000 Kč",
"locality": "Pod Karlovem, Praha - Vinohrady",
"lat": 50.0676313,
"lon": 14.432498,
"disposition": "3+kk",
"floor": 2,
"area": 106,
"building_type": "Cihlová",
"ownership": "Družstevní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/868795-nabidka-prodej-bytu-pod-karlovem-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 981890,
"name": "Prodej bytu 3+1 84 m²",
"price": 12980000,
"price_formatted": "12 980 000 Kč",
"locality": "Novákových, Praha - Libeň",
"lat": 50.103273,
"lon": 14.4746894,
"disposition": "3+1",
"floor": 2,
"area": 84,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/981890-nabidka-prodej-bytu-novakovych-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 976276,
"name": "Prodej bytu 3+kk 75 m²",
"price": 13490000,
"price_formatted": "13 490 000 Kč",
"locality": "Svornosti, Praha - Smíchov",
"lat": 50.0673284,
"lon": 14.4095087,
"disposition": "3+kk",
"floor": 2,
"area": 75,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/976276-nabidka-prodej-bytu-svornosti-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 950787,
"name": "Prodej bytu 3+kk 70 m²",
"price": 9999000,
"price_formatted": "9 999 000 Kč",
"locality": "Sečská, Praha - Strašnice",
"lat": 50.071191,
"lon": 14.5035501,
"disposition": "3+kk",
"floor": 3,
"area": 70,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/950787-nabidka-prodej-bytu-secska-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 978045,
"name": "Prodej bytu 3+kk 76 m²",
"price": 11133000,
"price_formatted": "11 133 000 Kč",
"locality": "K Vinoři, Praha - Kbely",
"lat": 50.1329656,
"lon": 14.5618499,
"disposition": "3+kk",
"floor": 2,
"area": 76,
"building_type": "Smíšená",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/978045-nabidka-prodej-bytu-k-vinori",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 974552,
"name": "Prodej bytu 3+1 75 m²",
"price": 11000000,
"price_formatted": "11 000 000 Kč",
"locality": "Vejražkova, Praha - Košíře",
"lat": 50.0637808,
"lon": 14.3612275,
"disposition": "3+1",
"floor": 2,
"area": 75,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/974552-nabidka-prodej-bytu-vejrazkova-praha",
"source": "bezrealitky",
"image": ""
},
{
"hash_id": 955010,
"name": "Prodej bytu 3+kk 70 m²",
"price": 12290000,
"price_formatted": "12 290 000 Kč",
"locality": "Břeclavská, Praha - Kyje",
"lat": 50.0951045,
"lon": 14.5454237,
"disposition": "3+kk",
"floor": 2,
"area": 70,
"building_type": "Cihlová",
"ownership": "Osobní",
"url": "https://www.bezrealitky.cz/nemovitosti-byty-domy/955010-nabidka-prodej-bytu-breclavska-hlavni-mesto-praha",
"source": "bezrealitky",
"image": ""
}
]

1
byty_cityhome.json Normal file
View File

@@ -0,0 +1 @@
[]

5867
byty_idnes.json Normal file

File diff suppressed because it is too large Load Diff

1940
byty_merged.json Normal file

File diff suppressed because it is too large Load Diff

1
byty_psn.json Normal file
View File

@@ -0,0 +1 @@
[]

7091
byty_realingo.json Normal file

File diff suppressed because it is too large Load Diff

5570
byty_sreality.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,123 +0,0 @@
# 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,161 +0,0 @@
#!/usr/bin/env python3
"""Generate status.json from scraper JSON outputs and per-scraper stats files."""
from __future__ import annotations
import argparse
import json
import os
from datetime import datetime
from pathlib import Path
HERE = Path(__file__).parent
DATA_DIR = Path(os.environ.get("DATA_DIR", HERE))
SOURCE_FILES = {
"Sreality": "byty_sreality.json",
"Realingo": "byty_realingo.json",
"Bezrealitky": "byty_bezrealitky.json",
"iDNES": "byty_idnes.json",
"PSN": "byty_psn.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"
HISTORY_FILE = "scraper_history.json"
def count_source(path: Path) -> dict:
"""Read a scraper JSON and return accepted count + file mtime."""
if not path.exists():
return {"accepted": 0, "error": "soubor nenalezen"}
try:
data = json.loads(path.read_text(encoding="utf-8"))
mtime = datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds")
return {"accepted": len(data), "updated_at": mtime}
except Exception as e:
return {"accepted": 0, "error": str(e)}
def read_scraper_stats(path: Path) -> dict:
"""Load a per-scraper stats JSON. Returns {} on missing or corrupt file."""
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def append_to_history(status: dict, keep: int) -> None:
"""Append the current status entry to scraper_history.json, keeping only `keep` latest."""
history_path = DATA_DIR / HISTORY_FILE
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 = []
history.append(status)
# Keep only the N most recent entries
if keep > 0 and len(history) > keep:
history = history[-keep:]
history_path.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Historie uložena: {history_path} ({len(history)} záznamů)")
def main():
parser = argparse.ArgumentParser(description="Generate status.json from scraper outputs.")
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()
start_time = args.start_time or datetime.now().isoformat(timespec="seconds")
duration_sec = args.duration
sources = []
for name, filename in SOURCE_FILES.items():
path = HERE / filename
info = count_source(path)
info["name"] = name
# Merge in stats from the per-scraper stats file (authoritative for run data)
stats = read_scraper_stats(DATA_DIR / STATS_FILES[name])
for key in ("accepted", "fetched", "pages", "cache_hits", "excluded", "excluded_total",
"success", "duration_sec", "error"):
if key in stats:
info[key] = stats[key]
sources.append(info)
# Total accepted before dedup
total_accepted = sum(s.get("accepted", 0) for s in sources)
# Merged / deduplicated count
merged_path = HERE / MERGED_FILE
deduplicated = 0
if merged_path.exists():
try:
merged = json.loads(merged_path.read_text(encoding="utf-8"))
deduplicated = len(merged)
except Exception:
pass
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": "done",
"timestamp": start_time,
"duration_sec": duration_sec,
"success": success,
"total_accepted": total_accepted,
"deduplicated": deduplicated,
"duplicates_removed": duplicates_removed,
"sources": sources,
}
out = DATA_DIR / "status.json"
out.write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Status uložen: {out}")
print(f" Celkem bytů (před dedup): {total_accepted}")
print(f" Po deduplikaci: {deduplicated}")
if duplicates_removed:
print(f" Odstraněno duplikátů: {duplicates_removed}")
for s in sources:
acc = s.get("accepted", 0)
err = s.get("error", "")
exc = s.get("excluded", {})
exc_total = sum(exc.values()) if exc else s.get("excluded_total", 0)
parts = [f"{s['name']:12s}: {acc} bytů"]
if exc_total:
parts.append(f"({exc_total} vyloučeno)")
if err:
parts.append(f"[CHYBA: {err}]")
print(" " + " ".join(parts))
append_to_history(status, args.keep)
if __name__ == "__main__":
main()

504
mapa_bytu.html Normal file
View File

@@ -0,0 +1,504 @@
<!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 (102 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; }
.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.2 !important; }
.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;
}
.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; }
</style>
</head>
<body>
<div id="map"></div>
<div class="info-panel">
<h2>Byty v Praze</h2>
<div class="stats">
<div>Celkem: <b id="visible-count">102</b> bytů</div>
<div>Cena: 1 000 000 Kč — 13 500 000 Kč</div>
<div>Průměr: 10 819 535 Kč</div>
</div>
<div><b>Dispozice:</b></div>
<div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#2196F3;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span><span>3+kk (42)</span></div><div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#4CAF50;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span><span>3+1 (18)</span></div><div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#FF9800;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span><span>4+kk (32)</span></div><div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#F44336;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span><span>4+1 (2)</span></div><div style="display:flex;align-items:center;gap:6px;margin:3px 0;"><span style="width:14px;height:14px;border-radius:50%;background:#9C27B0;display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span><span>5+kk (1)</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 style="margin-top:6px;">
<label>Přidáno:
<select id="first-seen-filter" onchange="applyFilters()">
<option value="all">Vše</option>
<option value="1">Posledních 24h</option>
<option value="3">Poslední 3 dny</option>
<option value="7">Poslední týden</option>
<option value="14">Posledních 14 dní</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>
<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, firstSeen) {
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, firstSeen: firstSeen };
allMarkers.push(marker);
marker.addTo(map);
}
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 addHeartMarker(lat, lon, color, popup, hashId, firstSeen) {
var marker = L.marker([lat, lon], {
icon: heartIcon(color),
}).bindPopup(popup);
marker._data = { lat: lat, lon: lon, color: color, hashId: hashId, isHeart: true, firstSeen: firstSeen };
allMarkers.push(marker);
marker.addTo(map);
}
addMarker(50.122192, 14.57646, '#2196F3', '<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><span style="margin-left:6px;font-size:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.077869, 14.301823, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1754669900"><b style="font-size:14px;">12 897 101 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 96 m² | 3. NP</span><br><br><b>Na Jivinách, Praha 6 - Ruzyně</b><br>Stavba: Montovaná<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-ruzyne-na-jivinach/1754669900" 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>', '1754669900', '2026-02-15');
addMarker(50.039219, 14.63833, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3483726668"><b style="font-size:14px;">10 385 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>Ke Tvrzi, Praha - Královice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-kralovice-ke-tvrzi/3483726668" 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>', '3483726668', '2026-02-15');
addMarker(50.057732, 14.561535, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="65700684"><b style="font-size:14px;">12 391 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>Kutnohorská, Praha 10 - Dolní Měcholupy</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-dolni-mecholupy-kutnohorska/65700684" 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>', '65700684', '2026-02-15');
addMarker(50.003006, 14.355603, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3224503116"><b style="font-size:14px;">12 620 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>K Lahovské, Praha 5 - Lochkov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-lochkov-k-lahovske/3224503116" 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>', '3224503116', '2026-02-15');
addMarker(50.003479, 14.362226, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="975307596"><b style="font-size:14px;">11 550 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>K Lahovské, Praha 5 - Lochkov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-lochkov-k-lahovske/975307596" 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>', '975307596', '2026-02-15');
addMarker(50.042961, 14.312009, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3450082124"><b style="font-size:14px;">12 800 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 72 m² | 3. NP</span><br><br><b>Toufarova, Praha - Stodůlky</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-stodulky-toufarova/3450082124" 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>', '3450082124', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.106956, 14.510207, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.103539, 14.52475, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="593134412"><b style="font-size:14px;">12 690 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>Poděbradská, Praha 9 - Hloubětín</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hloubetin-podebradska/593134412" 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>', '593134412', '2026-02-15');
addMarker(50.082985, 14.311815, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1726158156"><b style="font-size:14px;">11 917 901 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 72 m² | 3. NP</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/1726158156" 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>', '1726158156', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2212697420"><b style="font-size:14px;">11 597 281 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 72 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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/2212697420" 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>', '2212697420', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="832578892"><b style="font-size:14px;">12 509 412 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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-marie-podvalove/832578892" 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>', '832578892', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.106956, 14.510207, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.033131, 14.502145, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3361420108"><b style="font-size:14px;">13 290 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 83 m² | 3. NP</span><br><br><b>Květnového vítězství, Praha 4 - Chodov</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-chodov-kvetnoveho-vitezstvi/3361420108" 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>', '3361420108', '2026-02-15');
addMarker(50.047672, 14.55894, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2550690636"><b style="font-size:14px;">10 850 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 4. NP</span><br><br><b>Boloňská, Praha - Horní Měcholupy</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-horni-mecholupy-bolonska/2550690636" 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>', '2550690636', '2026-02-15');
addMarker(50.157696, 14.519159, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.103756, 14.515195, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1005851468"><b style="font-size:14px;">13 100 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 81 m² | 4. NP</span><br><br><b>V Předním Hloubětíně, Praha 9 - Vysočany</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-vysocany-v-prednim-hloubetine/1005851468" 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>', '1005851468', '2026-02-15');
addMarker(50.0396, 14.569008, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1192653644"><b style="font-size:14px;">12 999 990 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 80 m² | 5. NP</span><br><br><b>Mantovská, Praha - Horní Měcholupy</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-horni-mecholupy-mantovska/1192653644" 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>', '1192653644', '2026-02-15');
addMarker(50.069641, 14.470198, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.10976, 14.448239, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4143002444"><b style="font-size:14px;">12 290 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>Ortenovo náměstí, Praha 7 - Holešovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-holesovice-ortenovo-namesti/4143002444" 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>', '4143002444', '2026-02-15');
addMarker(50.101852, 14.486118, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="151528268"><b style="font-size:14px;">11 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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.060715, 14.401836, '#2196F3', '<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><span style="margin-left:6px;font-size:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.082317, 14.450463, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.027798, 14.365065, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1021510476"><b style="font-size:14px;">12 690 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 80 m² | 5. NP</span><br><br><b>Novotného, Praha 5 - Hlubočepy</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hlubocepy-novotneho/1021510476" 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>', '1021510476', '2026-02-15');
addMarker(50.156288, 14.516864, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2101179212"><b style="font-size:14px;">9 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 74 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Bermanova, Praha 9 - Čakovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-bermanova/2101179212" 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>', '2101179212', '2026-02-15');
addMarker(50.153698, 14.530779, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3939726156"><b style="font-size:14px;">10 480 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 91 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Danielova, Praha 9 - Čakovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-cakovice-danielova/3939726156" 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>', '3939726156', '2026-02-15');
addMarker(50.112217, 14.475707, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1098429260"><b style="font-size:14px;">7 834 047 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 86 m² | 5. NP</span><br><br><b>Zenklova, Praha 8 - Libeň</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-liben-zenklova/1098429260" 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>', '1098429260', '2026-02-15');
addMarker(50.112217, 14.475707, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="443069260"><b style="font-size:14px;">8 103 728 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 89 m² | 5. NP</span><br><br><b>Zenklova, Praha 8 - Libeň</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-liben-zenklova/443069260" 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>', '443069260', '2026-02-15');
addMarker(50.124489, 14.453209, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="961934156"><b style="font-size:14px;">13 220 749 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 71 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Vršní, Praha 8 - Kobylisy</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-kobylisy-vrsni/961934156" 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>', '961934156', '2026-02-15');
addMarker(50.065208, 14.450711, '#2196F3', '<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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.03138, 14.391757, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="276456268"><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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-hlubocepy-krizeneckeho-namesti/276456268" 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>', '276456268', '2026-02-15');
addMarker(50.128586, 14.483824, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="871002956"><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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 8. NP</span><br><br><b>Březenská, Praha 8 - Libeň</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+kk/praha-liben-brezenska/871002956" 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>', '871002956', '2026-02-15');
addMarker(50.030979, 14.392273, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3121242956"><b style="font-size:14px;">11 900 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 88 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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-hlubocepy-krizeneckeho-namesti/3121242956" 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>', '3121242956', '2026-02-15');
addMarker(50.046608, 14.492217, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1403183948"><b style="font-size:14px;">8 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 76 m² | 4. NP</span><br><br><b>Bělčická, Praha 4 - Záběhlice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-zabehlice-belcicka/1403183948" 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>', '1403183948', '2026-02-15');
addMarker(50.065617, 14.428337, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="948552524"><b style="font-size:14px;">9 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 97 m² | 5. NP</span><br><br><b>Oldřichova, Praha 2 - Nusle</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-nusle-oldrichova/948552524" 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>', '948552524', '2026-02-15');
addMarker(50.041092, 14.457349, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2023191372"><b style="font-size:14px;">12 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 84 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Valtínovská, Praha 4 - Krč</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-krc-valtinovska/2023191372" 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>', '2023191372', '2026-02-15');
addMarker(50.103786, 14.476581, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1556706124"><b style="font-size:14px;">13 385 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 84 m² | 3. NP</span><br><br><b>Novákových, Praha - Libeň</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-liben-novakovych/1556706124" 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>', '1556706124', '2026-02-15');
addMarker(50.070919, 14.482954, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1180791628"><b style="font-size:14px;">8 880 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 70 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>V předpolí, Praha 10 - Strašnice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-strasnice-v-predpoli/1180791628" 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>', '1180791628', '2026-02-15');
addMarker(50.069969, 14.469748, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4060156748"><b style="font-size:14px;">12 380 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 77 m² | 6. NP</span><br><br><b>Čeljabinská, Praha 10 - Vršovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-vrsovice-celjabinska/4060156748" 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>', '4060156748', '2026-02-15');
addMarker(49.968777, 14.394235, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3475837772"><b style="font-size:14px;">9 400 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 77 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Žitavského, Praha - Zbraslav</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-zbraslav-zitavskeho/3475837772" 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>', '3475837772', '2026-02-15');
addMarker(50.009743, 14.460835, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2628260684"><b style="font-size:14px;">11 323 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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-libus-libusska/2628260684" 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>', '2628260684', '2026-02-15');
addMarker(50.086834, 14.445434, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4058571596"><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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 121 m² | 5. NP</span><br><br><b>Husitská, Praha 3 - Žižkov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-zizkov-husitska/4058571596" 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>', '4058571596', '2026-02-15');
addMarker(50.089504, 14.463739, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3972887372"><b style="font-size:14px;">11 349 087 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 84 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Hartigova, Praha 3 - Žižkov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-zizkov-hartigova/3972887372" 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>', '3972887372', '2026-02-15');
addMarker(50.098339, 14.447381, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2609607500"><b style="font-size:14px;">13 300 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 84 m² | 3. NP</span><br><br><b>Bubenské nábřeží, Praha 7 - Holešovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-holesovice-bubenske-nabrezi/2609607500" 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>', '2609607500', '2026-02-15');
addMarker(50.042667, 14.457544, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3224830796"><b style="font-size:14px;">11 900 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 78 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Budějovická, Praha - Praha 4</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-praha-4-budejovicka/3224830796" 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>', '3224830796', '2026-02-15');
addMarker(49.964233, 14.395434, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2792387404"><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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 94 m² | 3. NP</span><br><br><b>Spojařů, Praha 5 - Zbraslav</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-zbraslav-spojaru/2792387404" 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>', '2792387404', '2026-02-15');
addMarker(50.037449, 14.413369, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3469083468"><b style="font-size:14px;">11 399 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 97 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Školní, Praha 4 - Braník</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-branik-skolni/3469083468" 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>', '3469083468', '2026-02-15');
addMarker(50.070713, 14.37893, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2119304012"><b style="font-size:14px;">8 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 70 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Plzeňská, Praha 5 - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-smichov-plzenska/2119304012" 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>', '2119304012', '2026-02-15');
addMarker(50.07045, 14.378873, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1455780684"><b style="font-size:14px;">12 600 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 146 m² | 6. NP</span><br><br><b>Plzeňská, Praha 5 - Košíře</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/3+1/praha-kosire-plzenska/1455780684" 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>', '1455780684', '2026-02-15');
addMarker(50.157696, 14.519159, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4041016140"><b style="font-size:14px;">13 300 500 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 88 m² | 5. NP</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-cakovice-marie-podvalove/4041016140" 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>', '4041016140', '2026-02-15');
addMarker(50.157696, 14.519159, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="330105676"><b style="font-size:14px;">12 752 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 88 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í: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-cakovice-marie-podvalove/330105676" 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>', '330105676', '2026-02-15');
addMarker(50.141624, 14.514146, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1717773132"><b style="font-size:14px;">12 690 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 84 m² | 3. NP</span><br><br><b>Letovská, Praha 9 - Letňany</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-letnany-letovska/1717773132" 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>', '1717773132', '2026-02-15');
addMarker(50.037685, 14.591111, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="222794572"><b style="font-size:14px;">13 435 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 76 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Bečovská, Praha 10 - Uhříněves</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-uhrineves-becovska/222794572" 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>', '222794572', '2026-02-15');
addMarker(50.106956, 14.510207, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2591810124"><b style="font-size:14px;">12 618 295 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 77 m² | 3. NP</span><br><br><b>Praha 9</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-praha-9-/2591810124" 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>', '2591810124', '2026-02-15');
addMarker(50.157696, 14.519159, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3681342028"><b style="font-size:14px;">12 188 188 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 76 m² | 4. NP</span><br><br><b>Marie Podvalové, Praha - Čakovice</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-cakovice-marie-podvalove/3681342028" 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>', '3681342028', '2026-02-15');
addMarker(50.102959, 14.478184, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="230212172"><b style="font-size:14px;">12 650 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 79 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Sokolovská, Praha 8 - Libeň</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-liben-sokolovska/230212172" 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>', '230212172', '2026-02-15');
addMarker(50.047947, 14.484759, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2141807436"><b style="font-size:14px;">10 299 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 80 m² | 5. NP</span><br><br><b>Hrusická, Praha 4 - Záběhlice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-zabehlice-hrusicka/2141807436" 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>', '2141807436', '2026-02-15');
addMarker(50.057995, 14.468289, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2712802124"><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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 94 m² | 8. NP</span><br><br><b>V Dolině, Praha 10 - Michle</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-michle-v-doline/2712802124" 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>', '2712802124', '2026-02-15');
addMarker(50.154964, 14.538545, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3195237196"><b style="font-size:14px;">11 912 880 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 79 m² | 3. NP</span><br><br><b>Polabská, Praha 9 - Miškovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-miskovice-polabska/3195237196" 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>', '3195237196', '2026-02-15');
addMarker(50.073978, 14.428468, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3344900940"><b style="font-size:14px;">12 980 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 108 m² | 5. NP</span><br><br><b>Na Bojišti, Praha 2 - Nové Město</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-nove-mesto-na-bojisti/3344900940" 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>', '3344900940', '2026-02-15');
addMarker(50.03017, 14.594007, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="586400588"><b style="font-size:14px;">10 363 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:10px;background:#4CAF50;color:white;padding:1px 5px;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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-uhrineves-karla-guta/586400588" 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>', '586400588', '2026-02-15');
addMarker(50.067547, 14.554193, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="971801420"><b style="font-size:14px;">5 033 600 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 113 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Honzíkova, Praha 10 - Dolní Měcholupy</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-dolni-mecholupy-honzikova/971801420" 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>', '971801420', '2026-02-15');
addMarker(50.08839, 14.463692, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2825986892"><b style="font-size:14px;">13 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 77 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Domažlická, Praha 3 - Žižkov</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-zizkov-domazlicka/2825986892" 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>', '2825986892', '2026-02-15');
addMarker(50.154964, 14.538545, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="4059067212"><b style="font-size:14px;">12 026 070 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 79 m² | 3. NP</span><br><br><b>Polabská, Praha 9 - Miškovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-miskovice-polabska/4059067212" 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>', '4059067212', '2026-02-15');
addMarker(50.154964, 14.538545, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2716889932"><b style="font-size:14px;">12 281 850 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 79 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Polabská, Praha 9 - Miškovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-miskovice-polabska/2716889932" 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>', '2716889932', '2026-02-15');
addMarker(50.154964, 14.538545, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1374712652"><b style="font-size:14px;">12 054 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 80 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Polabská, Praha 9 - Miškovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-miskovice-polabska/1374712652" 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>', '1374712652', '2026-02-15');
addMarker(50.032063, 14.588518, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3116368716"><b style="font-size:14px;">11 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 78 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Sušilova, Praha 10 - Uhříněves</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-uhrineves-susilova/3116368716" 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>', '3116368716', '2026-02-15');
addMarker(50.140385, 14.519495, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1430709068"><b style="font-size:14px;">12 450 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 80 m² | 6. NP</span><br><br><b>Škrábkových, Praha 9 - Letňany</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-letnany-skrabkovych/1430709068" 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>', '1430709068', '2026-02-15');
addMarker(50.069508, 14.389555, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1327956812"><b style="font-size:14px;">11 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 83 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Fráni Šrámka, Praha 5 - Smíchov</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-smichov-frani-sramka/1327956812" 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>', '1327956812', '2026-02-15');
addMarker(50.0299, 14.36202, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="954037068"><b style="font-size:14px;">3 999 999 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 104 m² | 3. NP</span><br><br><b>náměstí Olgy Scheinpflugové, Praha 5 - Hlubočepy</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-hlubocepy-namesti-olgy-scheinpflugove/954037068" 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>', '954037068', '2026-02-15');
addMarker(50.035149, 14.505833, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3472032588"><b style="font-size:14px;">4 486 462 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 96 m² | 7. NP</span><br><br><b>Praha 4 - Chodov</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-chodov-/3472032588" 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>', '3472032588', '2026-02-15');
addMarker(50.146542, 14.532428, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1005912908"><b style="font-size:14px;">11 700 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 90 m² | 3. NP</span><br><br><b>Ke stadionu, Praha 9 - Čakovice</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-cakovice-ke-stadionu/1005912908" 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>', '1005912908', '2026-02-15');
addMarker(50.130093, 14.535296, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2301477708"><b style="font-size:14px;">4 595 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 97 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Sedlářova, Praha 9 - Kbely</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-kbely-sedlarova/2301477708" 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>', '2301477708', '2026-02-15');
addMarker(50.067547, 14.554193, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1797391180"><b style="font-size:14px;">3 799 037 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 125 m² | 3. NP</span><br><br><b>Honzíkova, Praha 10 - Dolní Měcholupy</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-dolni-mecholupy-honzikova/1797391180" 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>', '1797391180', '2026-02-15');
addMarker(50.069912, 14.431077, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1099359052"><b style="font-size:14px;">12 360 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 116 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Legerova, Praha 2 - Vinohrady</b><br>Stavba: Cihlová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-vinohrady-legerova/1099359052" 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>', '1099359052', '2026-02-15');
addMarker(50.030186, 14.361911, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="3106485068"><b style="font-size:14px;">4 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 95 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Gollové, Praha - Hlubočepy</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-hlubocepy-gollove/3106485068" 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>', '3106485068', '2026-02-15');
addMarker(50.0439, 14.515306, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="242774860"><b style="font-size:14px;">13 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 94 m² | 3. NP</span><br><br><b>Bratislavská, Praha - Hostivař</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-hostivar-bratislavska/242774860" 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>', '242774860', '2026-02-15');
addMarker(50.026546, 14.416888, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1058415436"><b style="font-size:14px;">13 299 328 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 81 m² | 4. NP</span><br><br><b>Údolní, Praha 4 - Braník</b><br>Stavba: Skeletová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-branik-udolni/1058415436" 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>', '1058415436', '2026-02-15');
addMarker(50.030125, 14.59409, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2868101964"><b style="font-size:14px;">11 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 84 m² | 5. NP</span><br><br><b>Karla Guta, Praha 10 - Uhříněves</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-uhrineves-karla-guta/2868101964" 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>', '2868101964', '2026-02-15');
addMarker(50.028706, 14.361368, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="772551244"><b style="font-size:14px;">4 982 498 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 110 m² | 6. NP</span><br><br><b>Vítové, Praha 5 - Hlubočepy</b><br>Stavba: Skeletová<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+kk/praha-hlubocepy-vitove/772551244" 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>', '772551244', '2026-02-15');
addMarker(50.107712, 14.620706, '#F44336', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1436115788"><b style="font-size:14px;">8 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+1 | 79 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Markupova, Praha 9 - Horní Počernice</b><br>Stavba: Smíšená<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+1/praha-horni-pocernice-markupova/1436115788" 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>', '1436115788', '2026-02-15');
addMarker(50.141563, 14.576679, '#F44336', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="1765593932"><b style="font-size:14px;">12 700 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+1 | 90 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Semtínská, Praha - Vinoř</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/4+1/praha-vinor-semtinska/1765593932" 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>', '1765593932', '2026-02-15');
addMarker(50.035149, 14.505833, '#9C27B0', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="2676609868"><b style="font-size:14px;">5 189 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">5+kk | 113 m² | 2. NP</span><br><span style="color:#FF9800;font-weight:bold;">⚠ 2. NP — zvážit klidnost lokality</span><br><br><b>Praha 4 - Chodov</b><br>Stavba: Smíšená<br>Vlastnictví: Družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.sreality.cz/detail/prodej/byt/5+kk/praha-chodov-/2676609868" 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>', '2676609868', '2026-02-15');
addMarker(50.039103596591, 14.638737128969, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24508078"><b style="font-size:14px;">10 385 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:10px;background:#4CAF50;color:white;padding:1px 5px;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>Ke Tvrzi 183, Praha</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-3+kk-ke-tvrzi-183-praha/24508078" 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>', '24508078', '2026-02-15');
addMarker(50.05769, 14.5362, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507848"><b style="font-size:14px;">8 850 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 102 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-102-00/24507848" 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>', '24507848', '2026-02-15');
addMarker(50.087602, 14.346085, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507820"><b style="font-size:14px;">7 190 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 162 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-162-00/24507820" 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>', '24507820', '2026-02-15');
addMarker(50.1088, 14.467449, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507812"><b style="font-size:14px;">1 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 180 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-180-00/24507812" 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>', '24507812', '2026-02-15');
addMarker(50.087602, 14.470882, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507750"><b style="font-size:14px;">8 500 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:10px;background:#4CAF50;color:white;padding:1px 5px;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<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-ostatni-byty-praha-130-00/24507750" 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>', '24507750', '2026-02-15');
addMarker(50.05769, 14.5362, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507718"><b style="font-size:14px;">6 990 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 102 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-2+kk-hostivar-praha/24507718" 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>', '24507718', '2026-02-15');
addMarker(50.135545, 14.422903, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507600"><b style="font-size:14px;">6 835 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 181 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-1+kk-v-zamcich-51-praha/24507600" 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>', '24507600', '2026-02-15');
addMarker(50.050799756286, 14.489296034161, '#FF9800', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507556"><b style="font-size:14px;">10 299 900 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">4+kk | 80 m² | 6. NP</span><br><br><b>Záběhlice, Praha</b><br>Stavba: neuvedeno<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-4+kk-zabehlice-praha/24507556" 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>', '24507556', '2026-02-15');
addMarker(50.071092222222, 14.407692777778, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507469"><b style="font-size:14px;">12 790 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:10px;background:#4CAF50;color:white;padding:1px 5px;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 2243, Praha</b><br>Stavba: Cihlová<br>Vlastnictví: Osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-3+kk-vrazova-2243-praha/24507469" 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>', '24507469', '2026-02-15');
addMarker(50.132684, 14.549675, '#999999', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="24507450"><b style="font-size:14px;">7 590 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">Atypický | neuvedeno | neuvedeno</span><br><br><b>Praha, 197 00</b><br>Stavba: neuvedeno<br>Vlastnictví: neuvedeno<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://www.realingo.cz/prodej/byt-2+kk-hulkova-302-praha/24507450" 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>', '24507450', '2026-02-15');
addMarker(50.1026043, 14.4435365, '#2196F3', '<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><span style="margin-left:6px;font-size:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.1113213, 14.5106858, '#2196F3', '<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><span style="margin-left:6px;font-size:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</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í<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><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', '2026-02-15');
addMarker(50.132835725, 14.5613326001, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="695cdeb1034d53b244074016"><b style="font-size:14px;">11 682 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 76 m² | 3. NP</span><br><br><b>K Vinoři, Praha 9 - Kbely</b><br>Stavba: 2026<br>Vlastnictví: osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-19-k-vinori/695cdeb1034d53b244074016/" 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>', '695cdeb1034d53b244074016', '2026-02-15');
addMarker(50.03524802, 14.31407393, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="64edfa5ce31ee12cdb097cf9"><b style="font-size:14px;">11 634 400 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 74 m² | 6. NP</span><br><br><b>Jáchymovská, Praha 5 - Řeporyje</b><br>Stavba: Smíšená<br>Vlastnictví: osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-13-jachymovska/64edfa5ce31ee12cdb097cf9/" 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>', '64edfa5ce31ee12cdb097cf9', '2026-02-15');
addMarker(50.1041841, 14.5751611, '#2196F3', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="69918c5e817db4038e0dd6b5"><b style="font-size:14px;">5 780 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+kk | 87 m² | 5. NP</span><br><br><b>náměstí Plukovníka Vlčka, Praha 9 - Černý Most, okres Praha</b><br>Stavba: 2002<br>Vlastnictví: družstevní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-14-namesti-plukovnika-vlcka/69918c5e817db4038e0dd6b5/" 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>', '69918c5e817db4038e0dd6b5', '2026-02-15');
addMarker(50.089264479, 14.669036349, '#4CAF50', '<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="6932a772e5e50b08340fdf3e"><b style="font-size:14px;">13 493 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:10px;background:#4CAF50;color:white;padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span><br><span style="color:#666;">3+1 | 81 m² | 3. NP</span><br><br><b>Medinská, Praha 9 - Klánovice</b><br>Stavba: Cihlová<br>Vlastnictví: osobní<div style="margin-top:4px;font-size:11px;color:#888;">Přidáno: 15.02.2026</div><br><a href="https://reality.idnes.cz/detail/prodej/byt/praha-21-medinska/6932a772e5e50b08340fdf3e/" 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>', '6932a772e5e50b08340fdf3e', '2026-02-15');
// ── 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));
}
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') {
marker.setStyle({
radius: 12, fillOpacity: 1, weight: 3,
fillColor: marker._data.color, color: '#fff',
});
if (marker._path) marker._path.classList.add('marker-favorite');
} else if (status === 'reject') {
marker.setStyle({
radius: 6, fillOpacity: 0.15, fillColor: '#999', color: '#bbb', weight: 1,
});
if (marker._path) marker._path.classList.remove('marker-favorite');
} 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');
}
}
}
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 daysAgoDate(days) {
var d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
}
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 firstSeenVal = document.getElementById('first-seen-filter').value;
var ratings = loadRatings();
var visible = 0;
var minFirstSeen = '';
if (firstSeenVal !== 'all') {
minFirstSeen = daysAgoDate(parseInt(firstSeenVal));
}
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;
// Date filter
if (minFirstSeen && m._data.firstSeen) {
if (m._data.firstSeen < minFirstSeen) 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++;
} else {
if (map.hasLayer(m)) map.removeLayer(m);
}
});
// 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
restoreRatings();
</script>
</body>
</html>

View File

@@ -9,7 +9,6 @@ from __future__ import annotations
import json import json
import re import re
import unicodedata
from pathlib import Path from pathlib import Path
from scrape_and_map import generate_map, format_price from scrape_and_map import generate_map, format_price
@@ -20,8 +19,14 @@ def normalize_street(locality: str) -> str:
# "Studentská, Praha 6 - Dejvice" → "studentska" # "Studentská, Praha 6 - Dejvice" → "studentska"
# "Rýnská, Praha" → "rynska" # "Rýnská, Praha" → "rynska"
street = locality.split(",")[0].strip().lower() street = locality.split(",")[0].strip().lower()
# Remove diacritics using Unicode decomposition (handles all Czech characters) # Remove diacritics (simple Czech)
street = unicodedata.normalize("NFKD", street).encode("ascii", "ignore").decode("ascii") replacements = {
"á": "a", "č": "c", "ď": "d", "é": "e", "ě": "e",
"í": "i", "ň": "n", "ó": "o", "ř": "r", "š": "s",
"ť": "t", "ú": "u", "ů": "u", "ý": "y", "ž": "z",
}
for src, dst in replacements.items():
street = street.replace(src, dst)
# Remove non-alphanumeric # Remove non-alphanumeric
street = re.sub(r"[^a-z0-9]", "", street) street = re.sub(r"[^a-z0-9]", "", street)
return street return street
@@ -74,10 +79,19 @@ def main():
if key in seen_keys: if key in seen_keys:
dupes += 1 dupes += 1
existing = seen_keys[key] existing = seen_keys[key]
# Preserve earliest first_seen across sources # Merge timestamps: keep earliest first_seen, latest last_updated
dup_fs = e.get("first_seen", "") e_first = e.get("first_seen", "")
if dup_fs and (not existing.get("first_seen") or dup_fs < existing["first_seen"]): ex_first = existing.get("first_seen", "")
existing["first_seen"] = dup_fs if e_first and ex_first:
existing["first_seen"] = min(e_first, ex_first)
elif e_first:
existing["first_seen"] = e_first
e_updated = e.get("last_updated", "")
ex_updated = existing.get("last_updated", "")
if e_updated and ex_updated:
existing["last_updated"] = max(e_updated, ex_updated)
elif e_updated:
existing["last_updated"] = e_updated
# Log it # Log it
print(f" Duplikát: {e['locality']} | {format_price(e['price'])} | {e.get('area', '?')}" print(f" Duplikát: {e['locality']} | {format_price(e['price'])} | {e.get('area', '?')}"
f"({e.get('source', '?')} vs {existing.get('source', '?')})") f"({e.get('source', '?')} vs {existing.get('source', '?')})")

114
regen_map.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Přegeneruje mapu z již stažených dat (byty_sreality.json).
Doplní chybějící plochy ze Sreality API, opraví URL, aplikuje filtry.
"""
from __future__ import annotations
import json
import time
import urllib.request
from pathlib import Path
from scrape_and_map import (
generate_map, format_price, MIN_AREA, HEADERS, DETAIL_API
)
def api_get(url: str) -> dict:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def fix_sreality_url(estate: dict) -> str:
"""Fix the Sreality URL to include disposition segment (only if missing)."""
disp = estate.get("disposition", "")
slug_map = {
"1+kk": "1+kk", "1+1": "1+1", "2+kk": "2+kk", "2+1": "2+1",
"3+kk": "3+kk", "3+1": "3+1", "4+kk": "4+kk", "4+1": "4+1",
"5+kk": "5+kk", "5+1": "5+1", "6+": "6-a-vice", "Atypický": "atypicky",
}
slug = slug_map.get(disp, "byt")
old_url = estate.get("url", "")
parts = old_url.split("/")
try:
byt_idx = parts.index("byt")
# Only insert if disposition slug is not already there
if byt_idx + 1 < len(parts) and parts[byt_idx + 1] == slug:
return old_url # already correct
parts.insert(byt_idx + 1, slug)
return "/".join(parts)
except ValueError:
return old_url
def fetch_area(hash_id: int) -> int | None:
"""Fetch area from detail API."""
try:
url = DETAIL_API.format(hash_id)
detail = api_get(url)
for item in detail.get("items", []):
name = item.get("name", "")
if "žitná ploch" in name or "zitna ploch" in name.lower():
return int(item["value"])
except Exception:
pass
return None
def main():
json_path = Path("byty_sreality.json")
if not json_path.exists():
print("Soubor byty_sreality.json nenalezen. Nejprve spusť scrape_and_map.py")
return
estates = json.loads(json_path.read_text(encoding="utf-8"))
print(f"Načteno {len(estates)} bytů z byty_sreality.json")
# Step 1: Fetch missing areas
missing_area = [e for e in estates if e.get("area") is None]
print(f"Doplňuji plochu u {len(missing_area)} bytů...")
for i, e in enumerate(missing_area):
time.sleep(0.3)
area = fetch_area(e["hash_id"])
if area is not None:
e["area"] = area
if (i + 1) % 50 == 0:
print(f" {i + 1}/{len(missing_area)} ...")
# Count results
with_area = sum(1 for e in estates if e.get("area") is not None)
print(f"Plocha doplněna: {with_area}/{len(estates)}")
# Step 2: Fix URLs
for e in estates:
e["url"] = fix_sreality_url(e)
# Step 3: Filter by min area
filtered = []
excluded = 0
for e in estates:
area = e.get("area")
if area is not None and area < MIN_AREA:
excluded += 1
continue
filtered.append(e)
print(f"Vyloučeno (< {MIN_AREA} m²): {excluded}")
print(f"Zbývá: {len(filtered)} bytů")
# Save updated data
filtered_path = Path("byty_sreality.json")
filtered_path.write_text(
json.dumps(filtered, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# Generate map
generate_map(filtered)
if __name__ == "__main__":
main()

View File

@@ -16,14 +16,6 @@ NC='\033[0m'
TOTAL=6 TOTAL=6
CURRENT=0 CURRENT=0
FAILED=0 FAILED=0
START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S")
START_EPOCH=$(date +%s)
LOG_FILE="$(pwd)/scrape_run.log"
# Mark scraper as running; cleaned up on exit (even on error/kill)
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]"
@@ -34,19 +26,16 @@ 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)
@@ -57,10 +46,6 @@ 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 ""
@@ -78,8 +63,6 @@ step() {
} }
# ── Scrapery (paralelně kde to jde) ───────────────────────── # ── Scrapery (paralelně kde to jde) ─────────────────────────
# Tee all output to log file for status generation
exec > >(tee -a "$LOG_FILE") 2>&1
step "Sreality" step "Sreality"
python3 scrape_and_map.py $SCRAPER_ARGS || { echo -e "${RED}✗ Sreality selhalo${NC}"; FAILED=$((FAILED + 1)); } python3 scrape_and_map.py $SCRAPER_ARGS || { echo -e "${RED}✗ Sreality selhalo${NC}"; FAILED=$((FAILED + 1)); }
@@ -108,12 +91,6 @@ python3 merge_and_map.py || { echo -e "${RED}✗ Merge selhal${NC}"; FAILED=$((F
# ── Otevření mapy ──────────────────────────────────────────── # ── Otevření mapy ────────────────────────────────────────────
# ── Generování statusu ─────────────────────────────────────
END_EPOCH=$(date +%s)
DURATION=$((END_EPOCH - START_EPOCH))
python3 generate_status.py --start-time "$START_TIME" --duration "$DURATION" $KEEP_ARG
echo "" echo ""
echo "============================================================" echo "============================================================"
if [ $FAILED -eq 0 ]; then if [ $FAILED -eq 0 ]; then

View File

@@ -13,11 +13,8 @@ import math
import time import time
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from datetime import datetime, timedelta from datetime import datetime
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats, validate_listing
STATS_FILE = "stats_sreality.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,9 +42,9 @@ HEADERS = {
def api_get(url: str) -> dict: def api_get(url: str) -> dict:
"""Fetch JSON from Sreality API with retry.""" """Fetch JSON from Sreality API."""
for attempt in range(3): logger.debug(f"HTTP GET request: {url}")
logger.debug(f"HTTP GET request (attempt {attempt + 1}/3): {url}") logger.debug(f"Headers: {HEADERS}")
req = urllib.request.Request(url, headers=HEADERS) req = urllib.request.Request(url, headers=HEADERS)
try: try:
with urllib.request.urlopen(req, timeout=30) as resp: with urllib.request.urlopen(req, timeout=30) as resp:
@@ -55,15 +52,8 @@ def api_get(url: str) -> dict:
logger.debug(f"HTTP response: status={resp.status}, size={len(response_data)} bytes") logger.debug(f"HTTP response: status={resp.status}, size={len(response_data)} bytes")
logger.debug(f"Response preview: {response_data[:200]}") logger.debug(f"Response preview: {response_data[:200]}")
return json.loads(response_data) return json.loads(response_data)
except urllib.error.HTTPError:
raise
except (urllib.error.URLError, ConnectionError, OSError) as e: except (urllib.error.URLError, ConnectionError, OSError) as e:
if attempt < 2: logger.error(f"HTTP request failed for {url}: {e}", exc_info=True)
wait = (attempt + 1) * 2
logger.warning(f"Connection error (retry {attempt + 1}/3 after {wait}s): {e}")
time.sleep(wait)
else:
logger.error(f"HTTP request failed after 3 attempts: {e}", exc_info=True)
raise raise
@@ -219,8 +209,6 @@ 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()
@@ -284,9 +272,13 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
# Check cache — if hash_id exists and price unchanged, reuse # Check cache — if hash_id exists and price unchanged, reuse
cached = cache.get(hash_id) cached = cache.get(hash_id)
today = datetime.now().strftime("%Y-%m-%d")
if cached and cached.get("price") == estate.get("price", 0): if cached and cached.get("price") == estate.get("price", 0):
cache_hits += 1 cache_hits += 1
logger.debug(f"Cache hit for hash_id={hash_id}") logger.debug(f"Cache hit for hash_id={hash_id}")
cached["last_updated"] = today
if "first_seen" not in cached:
cached["first_seen"] = today
results.append(cached) results.append(cached)
continue continue
@@ -344,6 +336,11 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
disp_cb = estate.get("_disposition_cb") or estate.get("seo", {}).get("category_sub_cb") disp_cb = estate.get("_disposition_cb") or estate.get("seo", {}).get("category_sub_cb")
seo = estate.get("seo", {}) seo = estate.get("seo", {})
# Preserve first_seen from cache if this is a price-changed re-fetch
first_seen = today
if cached and "first_seen" in cached:
first_seen = cached["first_seen"]
result = { result = {
"hash_id": hash_id, "hash_id": hash_id,
"name": estate.get("name", ""), "name": estate.get("name", ""),
@@ -359,12 +356,9 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
"ownership": ownership, "ownership": ownership,
"url": sreality_url(hash_id, seo), "url": sreality_url(hash_id, seo),
"image": (estate.get("_links", {}).get("images", [{}])[0].get("href", "") if estate.get("_links", {}).get("images") else ""), "image": (estate.get("_links", {}).get("images", [{}])[0].get("href", "") if estate.get("_links", {}).get("images") else ""),
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": cached.get("first_seen", datetime.now().strftime("%Y-%m-%d")) if cached else datetime.now().strftime("%Y-%m-%d"), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d"),
} }
if not validate_listing(result, "sreality"):
continue
results.append(result) results.append(result)
details_fetched += 1 details_fetched += 1
@@ -382,21 +376,6 @@ 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
@@ -405,70 +384,26 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"): def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
"""Generate an interactive Leaflet.js HTML map.""" """Generate an interactive Leaflet.js HTML map."""
# Color by price per m² — cool blue→warm red scale, no yellow # Color by disposition
# Thresholds based on Prague market distribution (p25=120k, p50=144k, p75=162k) color_map = {
price_color_scale = [ "3+kk": "#2196F3", # blue
(110_000, "#1565C0"), # < 110k/m² → deep blue (levné) "3+1": "#4CAF50", # green
(130_000, "#42A5F5"), # 110130k → light blue "4+kk": "#FF9800", # orange
(150_000, "#66BB6A"), # 130150k → green (střed) "4+1": "#F44336", # red
(165_000, "#EF6C00"), # 150165k → dark orange "5+kk": "#9C27B0", # purple
(float("inf"), "#C62828"), # > 165k → dark red (drahé) "5+1": "#795548", # brown
] "6+": "#607D8B", # grey-blue
}
def price_color(estate: dict) -> str: def fmt_date(d):
price = estate.get("price") or 0 """Format ISO date (YYYY-MM-DD) to Czech format (DD.MM.YYYY)."""
area = estate.get("area") or 0 if d and len(d) == 10:
if not area: return f"{d[8:10]}.{d[5:7]}.{d[:4]}"
return "#9E9E9E" return ""
ppm2 = price / area
for threshold, color in price_color_scale:
if ppm2 < threshold:
return color
return "#E53935"
# Legend bands for info panel (built once)
price_legend_items = (
'<div style="margin-bottom:4px;font-size:12px;color:#555;font-weight:600;">Cena / m²:</div>'
)
bands = [
("#1565C0", "< 110 000 Kč/m²"),
("#42A5F5", "110 130 000 Kč/m²"),
("#66BB6A", "130 150 000 Kč/m²"),
("#EF6C00", "150 165 000 Kč/m²"),
("#C62828", "> 165 000 Kč/m²"),
("#9E9E9E", "cena/plocha neuvedena"),
]
for bcolor, blabel in bands:
price_legend_items += (
f'<div class="price-band" data-color="{bcolor}" onclick="toggleColorFilter(\'{bcolor}\')" '
f'style="display:flex;align-items:center;gap:6px;margin:2px 0;padding:2px 4px;'
f'border-radius:4px;border:2px solid transparent;">'
f'<span style="width:14px;height:14px;border-radius:50%;background:{bcolor};'
f'display:inline-block;border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.3);flex-shrink:0;"></span>'
f'<span>{blabel}</span></div>'
)
price_legend_items += (
'<div id="price-filter-reset" style="display:none;margin:3px 0 0 4px;">'
'<a href="#" onclick="resetColorFilter();return false;" '
'style="font-size:11px;color:#1976D2;text-decoration:none;">✕ Zobrazit všechny ceny</a>'
'</div>'
)
# New marker indicator — bigger dot, no extra border
price_legend_items += (
'<div style="display:flex;align-items:center;gap:6px;margin:6px 0 0 0;'
'padding-top:6px;border-top:1px solid #eee;">'
'<span style="display:inline-flex;align-items:center;gap:3px;flex-shrink:0;">'
'<span style="width:14px;height:14px;border-radius:50%;background:#66BB6A;'
'display:inline-block;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span>'
'<span style="font-size:8px;font-weight:700;background:#FFD600;color:#333;'
'padding:1px 3px;border-radius:2px;">NEW</span>'
'</span>'
'<span>Nové (≤ 1 den)</span></div>'
)
markers_js = "" markers_js = ""
for e in estates: for e in estates:
color = price_color(e) color = color_map.get(e["disposition"], "#999999")
floor_text = f'{e["floor"]}. NP' if e["floor"] else "neuvedeno" floor_text = f'{e["floor"]}. NP' if e["floor"] else "neuvedeno"
area_text = f'{e["area"]}' if e["area"] else "neuvedeno" area_text = f'{e["area"]}' if e["area"] else "neuvedeno"
building_text = e["building_type"] or "neuvedeno" building_text = e["building_type"] or "neuvedeno"
@@ -485,32 +420,34 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
source_label = source_labels.get(source, source) source_label = source_labels.get(source, source)
source_color = source_colors.get(source, "#999") source_color = source_colors.get(source, "#999")
hash_id = f"{source}_{e.get('hash_id', '')}" hash_id = e.get("hash_id", "")
first_seen = e.get("first_seen", "") first_seen = e.get("first_seen", "")
last_changed = e.get("last_changed", "") last_updated = e.get("last_updated", "")
today = datetime.now().strftime("%Y-%m-%d")
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
is_new = first_seen in (today, yesterday)
first_seen_fmt = fmt_date(first_seen)
last_updated_fmt = fmt_date(last_updated)
# "NOVÉ" badge if first_seen equals latest scrape date
new_badge = ""
if first_seen and first_seen == last_updated:
new_badge = ( new_badge = (
'<span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;' '<span style="margin-left:6px;font-size:10px;background:#4CAF50;color:white;'
'padding:1px 6px;border-radius:3px;font-weight:bold;">NOVÉ</span>' 'padding:1px 5px;border-radius:3px;font-weight:bold;">NOVÉ</span>'
if is_new else ""
) )
date_parts = [] # Date info line
if first_seen: date_line = ""
date_parts.append(f'Přidáno: {first_seen}') if first_seen_fmt:
if last_changed and last_changed != first_seen: date_line = (
date_parts.append(f'Změněno: {last_changed}') f'<div style="margin-top:4px;font-size:11px;color:#888;">'
date_row = ( f'Přidáno: {first_seen_fmt}'
f'<span style="font-size:11px;color:#888;">{"&nbsp;·&nbsp;".join(date_parts)}</span><br>'
if date_parts else ""
) )
if last_updated_fmt and last_updated != first_seen:
date_line += f' · Aktualizace: {last_updated_fmt}'
date_line += '</div>'
popup = ( popup = (
f'<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="{hash_id}" data-first-seen="{first_seen}" data-last-changed="{last_changed}">' f'<div style="min-width:280px;font-family:system-ui,sans-serif;" data-hashid="{hash_id}">'
f'<b style="font-size:14px;">{format_price(e["price"])}</b>' f'<b style="font-size:14px;">{format_price(e["price"])}</b>'
f'<span style="margin-left:8px;font-size:11px;background:{source_color};color:white;' f'<span style="margin-left:8px;font-size:11px;background:{source_color};color:white;'
f'padding:1px 6px;border-radius:3px;">{source_label}</span>{new_badge}<br>' f'padding:1px 6px;border-radius:3px;">{source_label}</span>{new_badge}<br>'
@@ -518,9 +455,8 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
f'{floor_note}<br><br>' f'{floor_note}<br><br>'
f'<b>{e["locality"]}</b><br>' f'<b>{e["locality"]}</b><br>'
f'Stavba: {building_text}<br>' f'Stavba: {building_text}<br>'
f'Vlastnictví: {ownership_text}<br>' f'Vlastnictví: {ownership_text}'
f'{date_row}' f'{date_line}<br>'
f'<br>'
f'<a href="{e["url"]}" target="_blank" ' f'<a href="{e["url"]}" target="_blank" '
f'style="color:{source_color};text-decoration:none;font-weight:bold;">' f'style="color:{source_color};text-decoration:none;font-weight:bold;">'
f'→ Otevřít na {source_label}</a>' f'→ Otevřít na {source_label}</a>'
@@ -544,32 +480,26 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
popup = popup.replace("'", "\\'").replace("\n", "") popup = popup.replace("'", "\\'").replace("\n", "")
is_fav = source in ("psn", "cityhome") is_fav = source in ("psn", "cityhome")
marker_fn = "addHeartMarker" if is_fav else "addMarker"
if is_fav:
marker_fn = "addHeartMarker"
elif is_new:
marker_fn = "addNewMarker"
else:
marker_fn = "addMarker"
markers_js += ( markers_js += (
f" {marker_fn}({e['lat']}, {e['lon']}, '{color}', '{popup}', '{hash_id}', '{first_seen}', '{last_changed}');\n" f" {marker_fn}({e['lat']}, {e['lon']}, '{color}', '{popup}', '{hash_id}', '{first_seen}');\n"
) )
# Build legend — price per m² bands + disposition counts # Build legend
legend_items = price_legend_items legend_items = ""
# Disposition counts below the color legend
disp_counts = {} disp_counts = {}
for e in estates: for e in estates:
d = e["disposition"] d = e["disposition"]
disp_counts[d] = disp_counts.get(d, 0) + 1 disp_counts[d] = disp_counts.get(d, 0) + 1
disp_order = ["3+kk", "3+1", "4+kk", "4+1", "5+kk", "5+1", "6+"] for disp, color in color_map.items():
disp_summary = ", ".join( count = disp_counts.get(disp, 0)
f"{d} ({disp_counts[d]})" for d in disp_order if d in disp_counts if count > 0:
)
legend_items += ( legend_items += (
f'<div style="margin-top:8px;padding-top:6px;border-top:1px solid #eee;' f'<div style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
f'font-size:12px;color:#666;">{disp_summary}</div>' f'<span style="width:14px;height:14px;border-radius:50%;'
f'background:{color};display:inline-block;border:2px solid white;'
f'box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span>'
f'<span>{disp} ({count})</span></div>'
) )
# Heart marker legend for PSN/CityHome # Heart marker legend for PSN/CityHome
@@ -605,7 +535,6 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
body {{ font-family: system-ui, -apple-system, sans-serif; }} body {{ font-family: system-ui, -apple-system, sans-serif; }}
#map {{ width: 100%; height: 100vh; }} #map {{ width: 100%; height: 100vh; }}
.heart-icon {{ background: none !important; border: none !important; }} .heart-icon {{ background: none !important; border: none !important; }}
.star-icon {{ background: none !important; border: none !important; }}
.rate-btn:hover {{ background: #f0f0f0 !important; }} .rate-btn:hover {{ background: #f0f0f0 !important; }}
.rate-btn.active-fav {{ background: #FFF9C4 !important; border-color: #FFC107 !important; }} .rate-btn.active-fav {{ background: #FFF9C4 !important; border-color: #FFC107 !important; }}
.rate-btn.active-rej {{ background: #FFEBEE !important; border-color: #F44336 !important; }} .rate-btn.active-rej {{ background: #FFEBEE !important; border-color: #F44336 !important; }}
@@ -616,73 +545,32 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
}} }}
.marker-favorite {{ animation: pulse-glow 2s ease-in-out infinite; border-radius: 50%; }} .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-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); }} .heart-icon-rej {{ opacity: 0.2 !important; }}
.reject-overlay {{ background: none !important; border: none !important; pointer-events: none !important; }}
.new-badge-icon {{ background: none !important; border: none !important; pointer-events: none !important; }}
.new-badge {{
font-size: 9px; font-weight: 700; color: #333; background: #FFD600;
padding: 1px 4px; border-radius: 3px; white-space: nowrap;
box-shadow: 0 1px 3px rgba(0,0,0,0.3); letter-spacing: 0.5px;
}}
.info-panel {{ .info-panel {{
position: absolute; top: 10px; right: 10px; z-index: 1000; position: absolute; top: 10px; right: 10px; z-index: 1000;
background: white; padding: 16px; border-radius: 10px; background: white; padding: 16px; border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15); max-width: 260px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); max-width: 260px;
font-size: 13px; line-height: 1.5; 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 h2 {{ font-size: 16px; margin-bottom: 8px; }}
.info-panel .stats {{ color: #666; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; }} .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 {{ 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 label {{ display: flex; align-items: center; gap: 6px; margin: 3px 0; cursor: pointer; }}
.price-band {{ cursor: pointer; transition: background 0.12s; }}
.price-band:hover {{ background: #f0f0f0; }}
.price-band.active {{ border-color: #333 !important; background: #e8f0fe; }}
.price-band.dimmed {{ opacity: 0.35; }}
.filter-section input[type="checkbox"] {{ accent-color: #1976D2; }} .filter-section input[type="checkbox"] {{ accent-color: #1976D2; }}
#floor-filter {{ margin-top: 8px; }} #floor-filter {{ margin-top: 8px; }}
#floor-filter select {{ width: 100%; padding: 4px; border-radius: 4px; border: 1px solid #ccc; }} #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> </style>
</head> </head>
<body> <body>
<div id="map"></div> <div id="map"></div>
<button class="panel-open-btn hidden" id="panel-open-btn" onclick="togglePanel()">☰</button> <div class="info-panel">
<div class="info-panel" id="info-panel">
<button class="panel-close-btn" id="panel-close-btn" onclick="togglePanel()">✕</button>
<h2>Byty v Praze</h2> <h2>Byty v Praze</h2>
<div class="stats"> <div class="stats">
<div>Celkem: <b id="visible-count">{len(estates)}</b> bytů</div> <div>Celkem: <b id="visible-count">{len(estates)}</b> bytů</div>
<div>Cena: {min_price}{max_price}</div> <div>Cena: {min_price}{max_price}</div>
<div>Průměr: {avg_price}</div> <div>Průměr: {avg_price}</div>
</div> </div>
<div><b>Dispozice:</b></div>
{legend_items} {legend_items}
<div class="filter-section"> <div class="filter-section">
<b>Filtry:</b> <b>Filtry:</b>
@@ -698,23 +586,22 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
</div> </div>
<div style="margin-top:6px;"> <div style="margin-top:6px;">
<label>Max cena: <label>Max cena:
<input type="number" id="max-price" value="13500000" max="14000000" step="500000" <select id="max-price" onchange="applyFilters()">
style="width:130px;padding:2px 4px;border:1px solid #ccc;border-radius:3px;" <option value="13500000">13 500 000 Kč</option>
onchange="applyFilters()" onkeyup="applyFilters()"> Kč <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> </label>
</div> </div>
<div style="margin-top:6px;"> <div style="margin-top:6px;">
<label>Přidáno / změněno: <label>Přidáno:
<select id="days-filter" onchange="applyFilters()" style="width:100%;padding:4px;border-radius:4px;border:1px solid #ccc;"> <select id="first-seen-filter" onchange="applyFilters()">
<option value="0">Vše</option> <option value="all">Vše</option>
<option value="1">za 1 den</option> <option value="1">Posledních 24h</option>
<option value="2">za 2 dny</option> <option value="3">Poslední 3 dny</option>
<option value="3">za 3 dny</option> <option value="7">Poslední týden</option>
<option value="4">za 4 dny</option> <option value="14">Posledních 14 dní</option>
<option value="5">za 5 dní</option>
<option value="7">za 7 dní</option>
<option value="14">za 14 dní</option>
<option value="30">za 30 dní</option>
</select> </select>
</label> </label>
</div> </div>
@@ -728,7 +615,6 @@ 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="/scrapers-status">Scraper status</a></div>
</div> </div>
<script> <script>
@@ -748,39 +634,9 @@ L.tileLayer('https://{{s}}.basemaps.cartocdn.com/light_only_labels/{{z}}/{{x}}/{
pane: 'shadowPane', pane: 'shadowPane',
}}).addTo(map); }}).addTo(map);
var selectedColors = [];
function toggleColorFilter(color) {{
var idx = selectedColors.indexOf(color);
if (idx >= 0) selectedColors.splice(idx, 1);
else selectedColors.push(color);
document.querySelectorAll('.price-band').forEach(function(el) {{
var c = el.getAttribute('data-color');
if (selectedColors.length === 0) {{
el.classList.remove('active', 'dimmed');
}} else if (selectedColors.indexOf(c) >= 0) {{
el.classList.add('active'); el.classList.remove('dimmed');
}} else {{
el.classList.add('dimmed'); el.classList.remove('active');
}}
}});
document.getElementById('price-filter-reset').style.display =
selectedColors.length > 0 ? 'block' : 'none';
applyFilters();
}}
function resetColorFilter() {{
selectedColors = [];
document.querySelectorAll('.price-band').forEach(function(el) {{
el.classList.remove('active', 'dimmed');
}});
document.getElementById('price-filter-reset').style.display = 'none';
applyFilters();
}}
var allMarkers = []; var allMarkers = [];
function addMarker(lat, lon, color, popup, hashId, firstSeen, lastChanged) {{ function addMarker(lat, lon, color, popup, hashId, firstSeen) {{
var marker = L.circleMarker([lat, lon], {{ var marker = L.circleMarker([lat, lon], {{
radius: 8, radius: 8,
fillColor: color, fillColor: color,
@@ -789,37 +645,11 @@ function addMarker(lat, lon, color, popup, hashId, firstSeen, lastChanged) {{
opacity: 1, opacity: 1,
fillOpacity: 0.85, fillOpacity: 0.85,
}}).bindPopup(popup); }}).bindPopup(popup);
marker._data = {{ lat: lat, lon: lon, color: color, hashId: hashId, firstSeen: firstSeen || '', lastChanged: lastChanged || '' }}; marker._data = {{ lat: lat, lon: lon, color: color, hashId: hashId, firstSeen: firstSeen }};
allMarkers.push(marker); allMarkers.push(marker);
marker.addTo(map); marker.addTo(map);
}} }}
function addNewMarker(lat, lon, color, popup, hashId, firstSeen, lastChanged) {{
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, isNew: true, firstSeen: firstSeen || '', lastChanged: lastChanged || '' }};
allMarkers.push(marker);
marker.addTo(map);
var badge = L.marker([lat, lon], {{
icon: L.divIcon({{
className: 'new-badge-icon',
html: '<span class="new-badge">NEW</span>',
iconSize: [32, 14],
iconAnchor: [-6, 7],
}}),
interactive: false,
pane: 'markerPane',
}});
badge.addTo(map);
marker._newBadge = badge;
}}
function heartIcon(color) {{ function heartIcon(color) {{
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">' 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 ' + '<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 '
@@ -835,26 +665,11 @@ function heartIcon(color) {{
}}); }});
}} }}
function starIcon() {{ function addHeartMarker(lat, lon, color, popup, hashId, firstSeen) {{
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, firstSeen, lastChanged) {{
var marker = L.marker([lat, lon], {{ var marker = L.marker([lat, lon], {{
icon: heartIcon(color), icon: heartIcon(color),
}}).bindPopup(popup); }}).bindPopup(popup);
marker._data = {{ lat: lat, lon: lon, color: color, hashId: hashId, isHeart: true, firstSeen: firstSeen || '', lastChanged: lastChanged || '' }}; marker._data = {{ lat: lat, lon: lon, color: color, hashId: hashId, isHeart: true, firstSeen: firstSeen }};
allMarkers.push(marker); allMarkers.push(marker);
marker.addTo(map); marker.addTo(map);
}} }}
@@ -873,41 +688,6 @@ function loadRatings() {{
function saveRatings(ratings) {{ function saveRatings(ratings) {{
localStorage.setItem(RATINGS_KEY, JSON.stringify(ratings)); localStorage.setItem(RATINGS_KEY, JSON.stringify(ratings));
fetch('/api/ratings', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify(ratings)
}}).catch(function() {{}});
}}
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) {{ function applyMarkerStyle(marker, status) {{
@@ -924,35 +704,16 @@ function applyMarkerStyle(marker, status) {{
}} }}
}} else {{ }} else {{
if (status === 'fav') {{ if (status === 'fav') {{
removeRejectStrike(marker);
if (marker._newBadge && map.hasLayer(marker._newBadge)) map.removeLayer(marker._newBadge);
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({{ marker.setStyle({{
radius: 6, fillOpacity: 0.35, fillColor: marker._data.color, color: '#fff', weight: 1, radius: 12, fillOpacity: 1, weight: 3,
fillColor: marker._data.color, color: '#fff',
}});
if (marker._path) marker._path.classList.add('marker-favorite');
}} else if (status === 'reject') {{
marker.setStyle({{
radius: 6, fillOpacity: 0.15, fillColor: '#999', color: '#bbb', weight: 1,
}}); }});
if (marker._path) marker._path.classList.remove('marker-favorite'); if (marker._path) marker._path.classList.remove('marker-favorite');
}}
// Add strikethrough line over the marker
addRejectStrike(marker);
if (marker._newBadge && map.hasLayer(marker._newBadge)) map.removeLayer(marker._newBadge);
}} 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 {{ }} else {{
marker.setStyle({{ marker.setStyle({{
radius: 8, fillColor: marker._data.color, color: '#fff', radius: 8, fillColor: marker._data.color, color: '#fff',
@@ -960,24 +721,7 @@ function applyMarkerStyle(marker, status) {{
}}); }});
if (marker._path) marker._path.classList.remove('marker-favorite'); if (marker._path) marker._path.classList.remove('marker-favorite');
}} }}
if (marker._path) marker._path.classList.remove('marker-rejected');
removeRejectStrike(marker);
if (marker._newBadge && !map.hasLayer(marker._newBadge)) marker._newBadge.addTo(map);
}} }}
}}
}}
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) {{ function rateMarker(marker, action) {{
@@ -1116,21 +860,23 @@ map.on('popupopen', function(e) {{
}}); }});
// ── Filters ──────────────────────────────────────────────────── // ── Filters ────────────────────────────────────────────────────
function daysAgoDate(days) {{
var d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
}}
function applyFilters() {{ function applyFilters() {{
var minFloor = parseInt(document.getElementById('min-floor').value); var minFloor = parseInt(document.getElementById('min-floor').value);
var maxPriceEl = document.getElementById('max-price'); var maxPrice = parseInt(document.getElementById('max-price').value);
var maxPrice = parseInt(maxPriceEl.value) || 14000000;
if (maxPrice > 14000000) {{ maxPrice = 14000000; maxPriceEl.value = 14000000; }}
var hideRejected = document.getElementById('hide-rejected').checked; var hideRejected = document.getElementById('hide-rejected').checked;
var daysFilter = parseInt(document.getElementById('days-filter').value) || 0; var firstSeenVal = document.getElementById('first-seen-filter').value;
var ratings = loadRatings(); var ratings = loadRatings();
var visible = 0; var visible = 0;
var cutoff = null; var minFirstSeen = '';
if (daysFilter > 0) {{ if (firstSeenVal !== 'all') {{
cutoff = new Date(); minFirstSeen = daysAgoDate(parseInt(firstSeenVal));
cutoff.setDate(cutoff.getDate() - daysFilter);
cutoff.setHours(0, 0, 0, 0);
}} }}
allMarkers.forEach(function(m) {{ allMarkers.forEach(function(m) {{
@@ -1145,28 +891,19 @@ function applyFilters() {{
if (floor !== null && floor < minFloor) show = false; if (floor !== null && floor < minFloor) show = false;
if (price > maxPrice) show = false; if (price > maxPrice) show = false;
if (cutoff) {{ // Date filter
var fs = m._data.firstSeen ? new Date(m._data.firstSeen) : null; if (minFirstSeen && m._data.firstSeen) {{
var lc = m._data.lastChanged ? new Date(m._data.lastChanged) : null; if (m._data.firstSeen < minFirstSeen) show = false;
if (!((fs && fs >= cutoff) || (lc && lc >= cutoff))) show = false;
}} }}
if (selectedColors.length > 0 && selectedColors.indexOf(m._data.color) < 0) show = false;
var r = ratings[m._data.hashId]; var r = ratings[m._data.hashId];
if (hideRejected && r && r.status === 'reject') show = false; if (hideRejected && r && r.status === 'reject') show = false;
if (show) {{ if (show) {{
if (!map.hasLayer(m)) m.addTo(map); if (!map.hasLayer(m)) m.addTo(map);
visible++; visible++;
// Show strike line if rejected and visible
if (m._rejectStrike && !map.hasLayer(m._rejectStrike)) m._rejectStrike.addTo(map);
if (m._newBadge && !map.hasLayer(m._newBadge)) m._newBadge.addTo(map);
}} else {{ }} else {{
if (map.hasLayer(m)) map.removeLayer(m); if (map.hasLayer(m)) map.removeLayer(m);
// Hide strike line when marker hidden
if (m._rejectStrike && map.hasLayer(m._rejectStrike)) map.removeLayer(m._rejectStrike);
if (m._newBadge && map.hasLayer(m._newBadge)) map.removeLayer(m._newBadge);
}} }}
}}); }});
@@ -1181,45 +918,8 @@ function applyFilters() {{
document.getElementById('visible-count').textContent = visible; document.getElementById('visible-count').textContent = visible;
}} }}
// Initialize ratings: load from server, merge with localStorage, then restore // Initialize ratings on load
function initRatings() {{ restoreRatings();
var local = loadRatings();
fetch('/api/ratings')
.then(function(r) {{ return r.ok ? r.json() : null; }})
.then(function(server) {{
if (server && typeof server === 'object') {{
var merged = Object.assign({{}}, local, server);
localStorage.setItem(RATINGS_KEY, JSON.stringify(merged));
}}
restoreRatings();
updateRatingCounts();
}})
.catch(function() {{
restoreRatings();
updateRatingCounts();
}});
}}
initRatings();
// ── 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> </script>
</body> </body>
@@ -1248,22 +948,8 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) 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

@@ -7,17 +7,14 @@ Výstup: byty_bezrealitky.json
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from datetime import datetime
import json import json
import logging import logging
import math import math
import re import re
import time import time
import urllib.request import urllib.request
from datetime import datetime
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats, validate_listing
STATS_FILE = "stats_bezrealitky.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,35 +68,19 @@ HEADERS = {
BASE_URL = "https://www.bezrealitky.cz" BASE_URL = "https://www.bezrealitky.cz"
def fetch_url(url: str, retries: int = 3) -> str:
"""Fetch URL and return HTML string with retry on transient errors."""
for attempt in range(retries):
try:
logger.debug(f"HTTP GET request (attempt {attempt + 1}/{retries}): {url}")
req = urllib.request.Request(url, headers=HEADERS)
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
return html
except urllib.error.HTTPError:
raise
except (ConnectionResetError, ConnectionError, urllib.error.URLError, OSError) as e:
if attempt < retries - 1:
wait = (attempt + 1) * 2
logger.warning(f"Connection error (retry {attempt + 1}/{retries} after {wait}s): {e}")
time.sleep(wait)
else:
logger.error(f"HTTP request failed after {retries} attempts: {e}", exc_info=True)
raise
def fetch_page(page: int) -> tuple[list[dict], int]: def fetch_page(page: int) -> tuple[list[dict], int]:
""" """
Fetch a listing page from Bezrealitky. Fetch a listing page from Bezrealitky.
Returns (list of advert dicts from Apollo cache, total count). Returns (list of advert dicts from Apollo cache, total count).
""" """
url = f"{BASE_URL}/vypis/nabidka-prodej/byt/praha?page={page}" url = f"{BASE_URL}/vypis/nabidka-prodej/byt/praha?page={page}"
html = fetch_url(url) logger.debug(f"HTTP GET request: {url}")
logger.debug(f"Headers: {HEADERS}")
req = urllib.request.Request(url, headers=HEADERS)
try:
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
match = re.search( match = re.search(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
@@ -129,13 +110,20 @@ def fetch_page(page: int) -> tuple[list[dict], int]:
logger.debug(f"Page {page}: found {len(adverts)} adverts, total={total}") logger.debug(f"Page {page}: found {len(adverts)} adverts, total={total}")
return adverts, total return adverts, total
except (urllib.error.URLError, ConnectionError, OSError) as e:
logger.error(f"HTTP request failed for {url}: {e}", exc_info=True)
raise
def fetch_detail(uri: str) -> dict | None: def fetch_detail(uri: str) -> dict | None:
"""Fetch detail page for a listing.""" """Fetch detail page for a listing."""
try: try:
url = f"{BASE_URL}/nemovitosti-byty-domy/{uri}" url = f"{BASE_URL}/nemovitosti-byty-domy/{uri}"
html = fetch_url(url) logger.debug(f"HTTP GET request: {url}")
req = urllib.request.Request(url, headers=HEADERS)
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
match = re.search( match = re.search(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
@@ -183,8 +171,6 @@ 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)
@@ -299,10 +285,14 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
# Check cache — if hash_id exists and price unchanged, reuse # Check cache — if hash_id exists and price unchanged, reuse
adv_id = int(adv["id"]) adv_id = int(adv["id"])
adv_price = adv.get("price", 0) or 0 adv_price = adv.get("price", 0) or 0
today = datetime.now().strftime("%Y-%m-%d")
cached = cache.get(adv_id) cached = cache.get(adv_id)
if cached and cached.get("price") == adv_price: if cached and cached.get("price") == adv_price:
cache_hits += 1 cache_hits += 1
logger.debug(f"Cache hit for id={adv_id}") logger.debug(f"Cache hit for id={adv_id}")
cached["last_updated"] = today
if "first_seen" not in cached:
cached["first_seen"] = today
results.append(cached) results.append(cached)
continue continue
@@ -354,6 +344,11 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
if not address: if not address:
address = adv.get('address({"locale":"CS"})', "Praha") address = adv.get('address({"locale":"CS"})', "Praha")
# Preserve first_seen from cache if this is a price-changed re-fetch
first_seen = today
if cached and "first_seen" in cached:
first_seen = cached["first_seen"]
result = { result = {
"hash_id": int(adv["id"]), "hash_id": int(adv["id"]),
"name": f"Prodej bytu {DISPOSITION_LABELS.get(disp, '?')} {adv.get('surface', '?')}", "name": f"Prodej bytu {DISPOSITION_LABELS.get(disp, '?')} {adv.get('surface', '?')}",
@@ -370,12 +365,9 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
"url": f"{BASE_URL}/nemovitosti-byty-domy/{uri}", "url": f"{BASE_URL}/nemovitosti-byty-domy/{uri}",
"source": "bezrealitky", "source": "bezrealitky",
"image": "", "image": "",
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": cached.get("first_seen", datetime.now().strftime("%Y-%m-%d")) if cached else datetime.now().strftime("%Y-%m-%d"), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d"),
} }
if not validate_listing(result, "bezrealitky"):
continue
results.append(result) results.append(result)
properties_fetched += 1 properties_fetched += 1
@@ -392,25 +384,6 @@ 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
@@ -431,22 +404,8 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) 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,9 +14,6 @@ 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, validate_listing
STATS_FILE = "stats_cityhome.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -37,26 +34,24 @@ HEADERS = {
BASE_URL = "https://www.city-home.cz" BASE_URL = "https://www.city-home.cz"
def fetch_url(url: str, retries: int = 3) -> str: def fetch_url(url: str) -> str:
"""Fetch URL and return HTML string. Raises HTTPError on 4xx/5xx.""" """Fetch URL and return HTML string."""
for attempt in range(retries): for attempt in range(3):
try: try:
logger.debug(f"HTTP GET request (attempt {attempt + 1}/{retries}): {url}") logger.debug(f"HTTP GET request (attempt {attempt + 1}/3): {url}")
logger.debug(f"Headers: {HEADERS}")
req = urllib.request.Request(url, headers=HEADERS) req = urllib.request.Request(url, headers=HEADERS)
resp = urllib.request.urlopen(req, timeout=30) resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8") html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes") logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
return html return html
except urllib.error.HTTPError:
# Don't retry on HTTP errors (404, 403, etc.) — re-raise immediately
raise
except (ConnectionResetError, ConnectionError, urllib.error.URLError) as e: except (ConnectionResetError, ConnectionError, urllib.error.URLError) as e:
if attempt < retries - 1: if attempt < 2:
wait = (attempt + 1) * 2 wait = (attempt + 1) * 2
logger.warning(f"Connection error (retry {attempt + 1}/{retries} after {wait}s): {e}") logger.warning(f"Connection error (retry {attempt + 1}/3 after {wait}s): {e}")
time.sleep(wait) time.sleep(wait)
else: else:
logger.error(f"HTTP request failed after {retries} attempts: {e}", exc_info=True) logger.error(f"HTTP request failed after 3 attempts: {e}", exc_info=True)
raise raise
@@ -130,21 +125,31 @@ def parse_filter_page(html: str) -> list[dict]:
if detail_url and not detail_url.startswith("http"): if detail_url and not detail_url.startswith("http"):
detail_url = BASE_URL + detail_url detail_url = BASE_URL + detail_url
# Parse table cells: [unit_name, unit_type_label, address, floor, disposition, area, transaction, price] # Extract floor from cells — look for pattern like "3.NP" or "2.PP"
cells = re.findall(r'<td[^>]*>(.*?)</td>', row_content, re.DOTALL) cells = re.findall(r'<td[^>]*>(.*?)</td>', row_content, re.DOTALL)
cell_texts = [re.sub(r'<[^>]+>', '', c).strip() for c in cells]
# Cell[2] = address (e.g. "Žateckých 14"), cell[3] = floor (e.g. "3.NP")
project_address = cell_texts[2] if len(cell_texts) > 2 else ""
floor = None floor = None
if len(cell_texts) > 3: floor_text = ""
np_match = re.search(r'(\d+)\.\s*NP', cell_texts[3]) project_name = ""
pp_match = re.search(r'(\d+)\.\s*PP', cell_texts[3])
for cell in cells:
cell_text = re.sub(r'<[^>]+>', '', cell).strip()
# Floor pattern
np_match = re.search(r'(\d+)\.\s*NP', cell_text)
pp_match = re.search(r'(\d+)\.\s*PP', cell_text)
if np_match: if np_match:
floor = int(np_match.group(1)) floor = int(np_match.group(1))
floor_text = cell_text
elif pp_match: elif pp_match:
floor = -int(pp_match.group(1)) floor = -int(pp_match.group(1)) # Underground
floor_text = cell_text
# Extract project name — usually in a cell that's not a number/price/floor
for cell in cells:
cell_text = re.sub(r'<[^>]+>', '', cell).strip()
if cell_text and not re.match(r'^[\d\s.,]+$', cell_text) and "NP" not in cell_text and "PP" not in cell_text and "" not in cell_text and "" not in cell_text and "EUR" not in cell_text and "CZK" not in cell_text:
if len(cell_text) > 3 and cell_text != unit_name:
project_name = cell_text
break
listing = { listing = {
"price": int(cena.group(1)), "price": int(cena.group(1)),
@@ -154,60 +159,43 @@ def parse_filter_page(html: str) -> list[dict]:
"project_id": project.group(1) if project else "", "project_id": project.group(1) if project else "",
"transaction": transaction.group(1) if transaction else "", "transaction": transaction.group(1) if transaction else "",
"disposition": dispozition.group(1) if dispozition else "", "disposition": dispozition.group(1) if dispozition else "",
"location": location.group(1) if location else "",
"url": detail_url, "url": detail_url,
"unit_name": unit_name, "unit_name": unit_name,
"floor": floor, "floor": floor,
"project_address": project_address, "project_name": project_name,
} }
listings.append(listing) listings.append(listing)
return listings return listings
def get_lokalita_urls(slug: str) -> list[str]: def extract_project_gps(html: str) -> dict[str, tuple[float, float]]:
"""Return candidate lokalita URLs to try in order.""" """Extract GPS coordinates for projects from locality pages."""
return [ # Pattern in JS: ['<h4>Project Name</h4>...', 'LAT', 'LON', '1', 'Name']
f"{BASE_URL}/projekty/{slug}/lokalita", gps_data = {}
f"{BASE_URL}/bytove-domy/{slug}/lokalita", for match in re.finditer(r"\['[^']*<h4>([^<]+)</h4>[^']*',\s*'([\d.]+)',\s*'([\d.]+)'", html):
f"{BASE_URL}/bytove-domy/{slug}/lokalita1", name = match.group(1).strip()
] lat = float(match.group(2))
lon = float(match.group(3))
gps_data[name] = (lat, lon)
return gps_data
def extract_project_gps(html: str) -> tuple[float, float] | None: def load_previous(json_path: str = "byty_cityhome.json") -> dict[str, str]:
"""Extract project GPS from lokalita page JS variable. """Load first_seen dates from previous run, keyed by hash_id."""
path = Path(json_path)
The page contains: var locations = [['<h4>Name</h4>...', 'LAT', 'LNG', 'CATEGORY', 'Label'], ...] if not path.exists():
Category '1' = the project's own marker. Some projects have two cat-1 entries (data error); return {}
in that case we pick the one whose name contains a digit and is not a transit landmark. try:
""" data = json.loads(path.read_text(encoding="utf-8"))
block = re.search(r'var locations\s*=\s*\[(.*?)\];', html, re.DOTALL) return {str(e["hash_id"]): e.get("first_seen", "") for e in data if "hash_id" in e}
if not block: except (json.JSONDecodeError, KeyError):
return None return {}
entries = re.findall(
r"'<h4>(.*?)</h4>.*?',\s*'([\d.]+)',\s*'([\d.]+)',\s*'1'",
block.group(0),
re.DOTALL,
)
if not entries:
return None
if len(entries) == 1:
return float(entries[0][1]), float(entries[0][2])
# Multiple cat-1 entries: pick the real project marker
transit_re = re.compile(r'nádraží|park|metro|tramvaj|autobus|zastávka', re.IGNORECASE)
for name, lat, lng in entries:
if re.search(r'\d', name) and not transit_re.search(name):
return float(lat), float(lng)
# Fallback: first entry
return float(entries[0][1]), float(entries[0][2])
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() previous_first_seen = load_previous()
_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)}")
@@ -236,34 +224,22 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
# Fetch GPS for each project from locality pages # Fetch GPS for each project from locality pages
project_gps = {} project_gps = {}
for slug in sorted(project_slugs): for slug in sorted(project_slugs):
time.sleep(0.3) time.sleep(0.5)
gps = None
for url in get_lokalita_urls(slug):
try: try:
logger.debug(f"Fetching project GPS: {url}") locality_url = f"{BASE_URL}/projekty/{slug}/lokalita"
loc_html = fetch_url(url) logger.debug(f"Fetching project GPS: {locality_url}")
loc_html = fetch_url(locality_url)
gps = extract_project_gps(loc_html) gps = extract_project_gps(loc_html)
if gps: if gps:
break # Take first entry (the project itself)
except Exception as e: first_name, (lat, lon) = next(iter(gps.items()))
logger.debug(f"GPS fetch failed for {url}: {e}") project_gps[slug] = (lat, lon)
continue logger.info(f"{slug}: {lat}, {lon}")
if gps:
project_gps[slug] = gps
logger.info(f"{slug}: {gps[0]}, {gps[1]}")
else: else:
logger.info(f"{slug}: GPS nenalezeno") logger.info(f"{slug}: GPS nenalezeno")
except Exception as e:
# Load previous output for first_seen/last_changed tracking logger.warning(f"Error fetching GPS for {slug}: {e}", exc_info=True)
_prev_cache: dict[str, dict] = {} logger.info(f"{slug}: chyba ({e})")
_prev_path = Path("byty_cityhome.json")
if _prev_path.exists():
try:
for _item in json.loads(_prev_path.read_text(encoding="utf-8")):
_prev_cache[str(_item["hash_id"])] = _item
except Exception:
pass
# Step 3: Filter listings # Step 3: Filter listings
logger.info(f"\nFáze 3: Filtrování...") logger.info(f"\nFáze 3: Filtrování...")
@@ -341,42 +317,29 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
lat, lon = gps lat, lon = gps
# locality: use project address from cell (e.g. "Žateckých 14") + city from GPS lookup today = datetime.now().strftime("%Y-%m-%d")
project_address = listing.get("project_address", "") hash_id = f"cityhome_{slug}_{listing['unit_name']}"
# derive city from slug (GPS lookup key) first_seen = previous_first_seen.get(str(hash_id), "") or today
city_map = {
"karlinske-namesti-5": "Praha 8",
"melnicka-12": "Praha 7",
"na-vaclavce-34": "Praha 5",
"nad-kajetankou-12": "Praha 6",
"vosmikovych-3": "Praha 9",
"zateckych-14": "Praha 2",
}
city_str = city_map.get(slug, "Praha")
locality_str = f"{project_address}, {city_str}" if project_address else city_str
result = { result = {
"hash_id": f"cityhome_{slug}_{listing['unit_name']}", "hash_id": hash_id,
"name": f"Prodej bytu {disp}, {int(area)} m² — {project_address}", "name": f"Prodej bytu {disp} {area} m² — {listing['project_name']}",
"price": price, "price": price,
"price_formatted": format_price(price), "price_formatted": format_price(price),
"locality": locality_str, "locality": f"{listing['project_name']}, Praha",
"lat": lat, "lat": lat,
"lon": lon, "lon": lon,
"disposition": disp, "disposition": disp,
"floor": floor, "floor": floor,
"area": float(area), "area": area,
"building_type": "Cihlová", # CityHome renovuje cihlové domy "building_type": "Cihlová", # CityHome renovuje cihlové domy
"ownership": "neuvedeno", "ownership": "neuvedeno",
"url": url, "url": url,
"source": "cityhome", "source": "cityhome",
"image": "", "image": "",
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": _prev_cache.get(f"cityhome_{slug}_{listing['unit_name']}", {}).get("first_seen", datetime.now().strftime("%Y-%m-%d")), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d") if _prev_cache.get(f"cityhome_{slug}_{listing['unit_name']}", {}).get("price") != price else _prev_cache[f"cityhome_{slug}_{listing['unit_name']}"].get("last_changed", datetime.now().strftime("%Y-%m-%d")),
} }
if not validate_listing(result, "cityhome"):
continue
results.append(result) results.append(result)
properties_fetched += 1 properties_fetched += 1
@@ -393,23 +356,6 @@ 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
@@ -430,22 +376,8 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) 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

@@ -7,7 +7,6 @@ Výstup: byty_idnes.json
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from datetime import datetime
import json import json
import logging import logging
import math import math
@@ -15,10 +14,9 @@ import re
import time import time
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from datetime import datetime
from html.parser import HTMLParser
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats, validate_listing
STATS_FILE = "stats_idnes.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -281,8 +279,6 @@ 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)
@@ -383,10 +379,14 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
logger.debug(f"Max properties limit reached: {max_properties}") logger.debug(f"Max properties limit reached: {max_properties}")
break break
# Check cache — if hash_id exists and price unchanged, reuse # Check cache — if hash_id exists and price unchanged, reuse
today = datetime.now().strftime("%Y-%m-%d")
cached = cache.get(str(item["id"])) cached = cache.get(str(item["id"]))
if cached and cached.get("price") == item["price"]: if cached and cached.get("price") == item["price"]:
cache_hits += 1 cache_hits += 1
logger.debug(f"Cache hit for id={item['id']}") logger.debug(f"Cache hit for id={item['id']}")
cached["last_updated"] = today
if "first_seen" not in cached:
cached["first_seen"] = today
results.append(cached) results.append(cached)
continue continue
@@ -447,6 +447,11 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
else: else:
building_type = construction.capitalize() building_type = construction.capitalize()
# Preserve first_seen from cache if this is a price-changed re-fetch
first_seen = today
if cached and "first_seen" in cached:
first_seen = cached["first_seen"]
result = { result = {
"hash_id": item["id"], "hash_id": item["id"],
"name": f"Prodej bytu {item['disposition']} {item.get('area', '?')}", "name": f"Prodej bytu {item['disposition']} {item.get('area', '?')}",
@@ -463,12 +468,9 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
"url": item["url"], "url": item["url"],
"source": "idnes", "source": "idnes",
"image": "", "image": "",
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": cached.get("first_seen", datetime.now().strftime("%Y-%m-%d")) if cached else datetime.now().strftime("%Y-%m-%d"), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d"),
} }
if not validate_listing(result, "idnes"):
continue
results.append(result) results.append(result)
properties_fetched += 1 properties_fetched += 1
@@ -486,25 +488,6 @@ 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
@@ -525,22 +508,8 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) 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

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
PSN.cz scraper. PSN.cz scraper.
Stáhne byty na prodej z API /api/units-list — jeden požadavek, žádné stránkování. Stáhne byty na prodej v Praze z projektů PSN a vyfiltruje podle kritérií.
Výstup: byty_psn.json Výstup: byty_psn.json
""" """
from __future__ import annotations from __future__ import annotations
@@ -14,10 +14,6 @@ import subprocess
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode
from scraper_stats import write_stats, validate_listing
STATS_FILE = "stats_psn.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -27,43 +23,82 @@ MAX_PRICE = 14_000_000
MIN_AREA = 69 MIN_AREA = 69
MIN_FLOOR = 2 MIN_FLOOR = 2
WANTED_DISPOSITIONS = {"3+kk", "3+1", "4+kk", "4+1", "5+kk", "5+1", "6+kk", "6+1", "5+kk a větší"} WANTED_DISPOSITIONS = {"3+kk", "3+1", "4+kk", "4+1", "5+kk", "5+1", "6+kk", "6+1"}
# Pouze Praha — ostatní města (Brno, Pardubice, Špindlerův Mlýn) přeskočit
WANTED_CITIES = {"Praha"}
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
BASE_URL = "https://psn.cz" BASE_URL = "https://psn.cz"
UNITS_API = f"{BASE_URL}/api/units-list"
# Known Prague project slugs with GPS (from research)
PRAGUE_PROJECTS = [
{"slug": "zit-branik", "name": "Žít Braník", "lat": 50.0353, "lon": 14.4125},
{"slug": "rostislavova-4", "name": "Rostislavova 4", "lat": 50.0620, "lon": 14.4463},
{"slug": "pod-drinopolem", "name": "Pod Drinopolem", "lat": 50.0851, "lon": 14.3720},
{"slug": "skyline-chodov", "name": "Skyline Chodov", "lat": 50.0418, "lon": 14.4990},
{"slug": "jitro", "name": "Jitro", "lat": 50.0729, "lon": 14.4768},
{"slug": "maroldka", "name": "Maroldka", "lat": 50.0614, "lon": 14.4517},
{"slug": "belehradska-29", "name": "Bělehradská 29", "lat": 50.0682, "lon": 14.4348},
{"slug": "jeseniova-93", "name": "Jeseniova 93", "lat": 50.0887, "lon": 14.4692},
{"slug": "vanguard", "name": "Vanguard", "lat": 50.0164, "lon": 14.4036},
{"slug": "vinohradska-160", "name": "Vinohradská 160", "lat": 50.0780, "lon": 14.4653},
{"slug": "hermanova24", "name": "Heřmanova 24", "lat": 50.1009, "lon": 14.4313},
{"slug": "vinohradska-8", "name": "Vinohradská 8", "lat": 50.0787, "lon": 14.4342},
{"slug": "bydleni-na-vysinach", "name": "Bydlení Na Výšinách", "lat": 50.1003, "lon": 14.4187},
{"slug": "bydleni-u-pekaren", "name": "Bydlení U Pekáren", "lat": 50.0555, "lon": 14.5414},
{"slug": "pechackova-6", "name": "Pechackova 6", "lat": 50.0734, "lon": 14.4063},
{"slug": "ahoj-vanguard", "name": "Ahoj Vanguard", "lat": 50.0164, "lon": 14.4033},
]
def fetch_json(url: str, retries: int = 3) -> dict: def fetch_url(url: str) -> str:
"""Fetch JSON via curl (urllib SSL may fail on Cloudflare) with retry.""" """Fetch URL via curl (urllib SSL too old for Cloudflare)."""
for attempt in range(retries): logger.debug(f"HTTP GET request (via curl): {url}")
logger.debug(f"HTTP GET (attempt {attempt + 1}/{retries}): {url}") logger.debug(f"User-Agent: {UA}")
result = subprocess.run( result = subprocess.run(
["curl", "-s", "-L", "--max-time", "30", ["curl", "-s", "-L", "--max-time", "30",
"-H", f"User-Agent: {UA}", "-H", f"User-Agent: {UA}",
"-H", "Accept: application/json", "-H", "Accept: text/html",
url], url],
capture_output=True, text=True, timeout=60 capture_output=True, text=True, timeout=60
) )
if result.returncode == 0: if result.returncode != 0:
return json.loads(result.stdout) logger.error(f"curl failed (return code {result.returncode}): {result.stderr[:200]}")
if attempt < retries - 1: raise RuntimeError(f"curl failed ({result.returncode}): {result.stderr[:200]}")
wait = (attempt + 1) * 2 logger.debug(f"HTTP response: size={len(result.stdout)} bytes")
logger.warning(f"curl failed (retry {attempt + 1}/{retries} after {wait}s): {result.stderr[:200]}") return result.stdout
time.sleep(wait)
else:
raise RuntimeError(f"curl failed after {retries} attempts ({result.returncode}): {result.stderr[:200]}")
def fix_gps(lat, lng): def extract_units_from_html(html: str) -> list[dict]:
"""PSN má u některých projektů prohozené lat/lng — opravíme.""" """Extract unit JSON objects from raw HTML with escaped quotes."""
if lat is not None and lng is not None and lat < 20 and lng > 20: # The HTML contains RSC data with escaped JSON: \\"key\\":\\"value\\"
return lng, lat # Step 1: Unescape the double-backslash-quotes to regular quotes
return lat, lng cleaned = html.replace('\\"', '"')
# Step 2: Find each unit by looking for "title":"Byt and walking back to {
units = []
decoder = json.JSONDecoder()
for m in re.finditer(r'"title":"Byt', cleaned):
pos = m.start()
# Walk backwards to find the opening brace
depth = 0
found = False
for i in range(pos - 1, max(pos - 3000, 0), -1):
if cleaned[i] == '}':
depth += 1
elif cleaned[i] == '{':
if depth == 0:
try:
obj, end = decoder.raw_decode(cleaned, i)
if isinstance(obj, dict) and 'price_czk' in obj:
units.append(obj)
found = True
except (json.JSONDecodeError, ValueError):
pass
break
depth -= 1
return units
def format_price(price: int) -> str: def format_price(price: int) -> str:
@@ -75,249 +110,253 @@ def format_price(price: int) -> str:
return " ".join(reversed(parts)) + "" return " ".join(reversed(parts)) + ""
def scrape(max_properties: int | None = None): def load_previous(json_path: str = "byty_psn.json") -> dict[str, str]:
_run_start = time.time() """Load first_seen dates from previous run, keyed by hash_id."""
_run_ts = datetime.now().isoformat(timespec="seconds") path = Path(json_path)
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return {str(e["hash_id"]): e.get("first_seen", "") for e in data if "hash_id" in e}
except (json.JSONDecodeError, KeyError):
return {}
def scrape(max_pages: int | None = None, max_properties: int | None = None):
previous_first_seen = load_previous()
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)}")
logger.info(f"Min. plocha: {MIN_AREA}") logger.info(f"Min. plocha: {MIN_AREA}")
logger.info(f"Patro: od {MIN_FLOOR}. NP") logger.info(f"Patro: od {MIN_FLOOR}. NP")
logger.info(f"Region: Praha") logger.info(f"Region: Praha ({len(PRAGUE_PROJECTS)} projektů)")
if max_pages:
logger.info(f"Max. stran: {max_pages}")
if max_properties: if max_properties:
logger.info(f"Max. bytů: {max_properties}") logger.info(f"Max. bytů: {max_properties}")
logger.info("=" * 60) logger.info("=" * 60)
# Jediný API požadavek — vrátí všechny jednotky (cca 236) # Fetch units from each Prague project
params = urlencode({ all_units = []
"locale": "cs",
"filters": "{}", for proj in PRAGUE_PROJECTS:
"type": "list", page = 1
"order": "price-asc", project_units = []
"offset": 0,
"limit": 500, while True:
}) if max_pages and page > max_pages:
url = f"{UNITS_API}?{params}" logger.debug(f"Max pages limit reached: {max_pages}")
logger.info("Stahuji jednotky z API ...") break
url = f"{BASE_URL}/projekt/{proj['slug']}?page={page}"
logger.info(f"{proj['name']} — strana {page} ...")
time.sleep(0.5)
try: try:
data = fetch_json(url) html = fetch_url(url)
except Exception as e: except Exception as e:
logger.error(f"Chyba při stahování: {e}", exc_info=True) logger.error(f"Fetch error for {proj['name']}: {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 []
all_units = data.get("units", {}).get("data", [])
logger.info(f"Staženo jednotek celkem: {len(all_units)}")
# Load previous output for first_seen/last_changed tracking
_prev_cache: dict[str, dict] = {}
_prev_path = Path("byty_psn.json")
if _prev_path.exists():
try:
for _item in json.loads(_prev_path.read_text(encoding="utf-8")):
_prev_cache[str(_item["hash_id"])] = _item
except Exception:
pass
# Filtrování
results = []
excluded = {
"prodáno": 0,
"typ": 0,
"město": 0,
"dispozice": 0,
"cena": 0,
"plocha": 0,
"patro": 0,
}
properties_fetched = 0
for unit in all_units:
if max_properties and properties_fetched >= max_properties:
break break
unit_id = unit.get("id", "?") units = extract_units_from_html(html)
logger.debug(f"Project {proj['slug']} page {page}: extracted {len(units)} units")
# Pouze prodej bytů (type_id=0) if not units:
if unit.get("type_id") != 0: if page == 1:
excluded["typ"] += 1 logger.info(f"→ 0 jednotek")
logger.debug(f"id={unit_id}: přeskočen (type_id={unit.get('type_id')}, není prodej bytu)") break
continue
# Pouze volné (ne rezervované, prodané, v přípravě) # Add project info to each unit
sale_status = unit.get("sale_status", "") for unit in units:
if not unit.get("latitude") or not unit.get("longitude"):
unit["latitude"] = proj["lat"]
unit["longitude"] = proj["lon"]
unit["_project_name"] = proj["name"]
unit["_project_slug"] = proj["slug"]
project_units.extend(units)
if page == 1:
logger.info(f"{len(units)} jednotek na stránce")
# Check if there might be more pages
# If we got fewer than expected or same units, stop
if len(units) < 10:
break
page += 1
if page > 10: # Safety limit
break
all_units.extend(project_units)
# Deduplicate by slug
seen_slugs = set()
unique_units = []
for u in all_units:
slug = u.get("slug", "")
if slug and slug not in seen_slugs:
seen_slugs.add(slug)
unique_units.append(u)
elif not slug:
unique_units.append(u)
logger.info(f"\nStaženo celkem: {len(unique_units)} unikátních jednotek")
# Filter
logger.info(f"\nFiltrování...")
results = []
excluded_sold = 0
excluded_type = 0
excluded_disp = 0
excluded_price = 0
excluded_area = 0
excluded_floor = 0
excluded_panel = 0
properties_fetched = 0
for unit in unique_units:
if max_properties and properties_fetched >= max_properties:
logger.debug(f"Max properties limit reached: {max_properties}")
break
unit_id = unit.get("id", unit.get("slug", "unknown"))
# Only free units
is_free = unit.get("is_free", False) is_free = unit.get("is_free", False)
is_sold = unit.get("is_sold", False) is_sold = unit.get("is_sold", False)
if is_sold or not is_free: if is_sold or not is_free:
excluded["prodáno"] += 1 excluded_sold += 1
logger.debug(f"id={unit_id}: přeskočen (status={sale_status})") logger.debug(f"Filter: id={unit_id} - excluded (sold/not free)")
continue continue
# Pouze Praha # Only apartments
city = (unit.get("location") or unit.get("address", {}).get("city") or "").strip() category = str(unit.get("category", "")).lower()
# location field je typicky "Praha 4", "Praha 7" atd. if "byt" not in category and "ateliér" not in category:
city_base = city.split(" ")[0] if city else "" excluded_type += 1
if city_base not in WANTED_CITIES: logger.debug(f"Filter: id={unit_id} - excluded (not apartment, category={category})")
excluded["město"] += 1
logger.debug(f"id={unit_id}: přeskočen (město={city})")
continue continue
# Dispozice # Disposition
disp = unit.get("disposition", "") disp = unit.get("disposition", "")
if disp not in WANTED_DISPOSITIONS: if disp not in WANTED_DISPOSITIONS:
excluded["dispozice"] += 1 excluded_disp += 1
logger.debug(f"id={unit_id}: přeskočen (dispozice={disp})") logger.debug(f"Filter: id={unit_id} - excluded (disposition {disp})")
continue continue
# Cena # Price
price = unit.get("action_price_czk") or unit.get("price_czk") or 0 price = unit.get("price_czk") or unit.get("action_price_czk") or 0
if not price or price <= 0 or price > MAX_PRICE: if price <= 0 or price > MAX_PRICE:
excluded["cena"] += 1 excluded_price += 1
logger.debug(f"id={unit_id}: přeskočen (cena={price})") logger.debug(f"Filter: id={unit_id} - excluded (price {price})")
continue continue
# Plocha # Area
area = unit.get("total_area") or unit.get("floor_area") or 0 area = unit.get("total_area") or unit.get("floor_area") or 0
if area < MIN_AREA: if area < MIN_AREA:
excluded["plocha"] += 1 excluded_area += 1
logger.debug(f"id={unit_id}: přeskočen (plocha={area} m²)") logger.debug(f"Filter: id={unit_id} - excluded (area {area} m²)")
continue continue
# Patro # Floor
floor_str = str(unit.get("floor", "")) floor_str = str(unit.get("floor", ""))
floor = None floor = None
if floor_str: if floor_str:
try: try:
floor = int(floor_str) floor = int(floor_str)
except ValueError: except ValueError:
m = re.search(r'(-?\d+)', floor_str) floor_match = re.search(r'(-?\d+)', floor_str)
if m: if floor_match:
floor = int(m.group(1)) floor = int(floor_match.group(1))
if floor is not None and floor < MIN_FLOOR: if floor is not None and floor < MIN_FLOOR:
excluded["patro"] += 1 excluded_floor += 1
logger.debug(f"id={unit_id}: přeskočen (patro={floor})") logger.debug(f"Filter: id={unit_id} - excluded (floor {floor})")
continue continue
# GPS — opravit prohozené souřadnice # Construction — check for panel
lat_raw = unit.get("latitude") build_type = str(unit.get("build_type", "")).lower()
lng_raw = unit.get("longitude") if "panel" in build_type:
lat, lng = fix_gps(lat_raw, lng_raw) excluded_panel += 1
if not lat or not lng: logger.debug(f"Filter: id={unit_id} - excluded (panel construction)")
logger.warning(f"id={unit_id}: chybí GPS souřadnice, přeskakuji") logger.info(f"✗ Vyloučen: panel ({build_type})")
continue continue
# Sestavit adresu pro locality # Build construction label
addr = unit.get("address") or {} building_type = "neuvedeno"
street = addr.get("street", "") if build_type and build_type != "nevybráno":
street_no = addr.get("street_no", "") if "cihlo" in build_type or "cihla" in build_type:
if street and street_no: building_type = "Cihlová"
locality_str = f"{street} {street_no}, {city}" elif "skelet" in build_type:
elif street: building_type = "Skeletová"
locality_str = f"{street}, {city}"
else: else:
project_name = unit.get("project", "") building_type = build_type.capitalize()
locality_str = f"{project_name}, {city}" if project_name else city
# URL na detail jednotky lat = unit.get("latitude", 0)
unit_slug = unit.get("slug", "") lon = unit.get("longitude", 0)
project_slug = ""
# project_slug lze odvodit z projektu nebo z reference_no slug = unit.get("slug", "")
# API nevrací project_slug přímo — použijeme reference_no nebo jen ID project_slug = unit.get("_project_slug", "")
reference_no = unit.get("reference_no", "") detail_url = f"{BASE_URL}/projekt/{project_slug}/{slug}" if slug else f"{BASE_URL}/projekt/{project_slug}"
if unit_slug:
detail_url = f"{BASE_URL}/prodej/{unit_slug}" today = datetime.now().strftime("%Y-%m-%d")
elif reference_no: hash_id = unit.get("id", slug)
detail_url = f"{BASE_URL}/prodej/{reference_no}" first_seen = previous_first_seen.get(str(hash_id), "") or today
else:
detail_url = BASE_URL
result = { result = {
"hash_id": str(unit_id), "hash_id": hash_id,
"name": f"Prodej bytu {disp}, {int(area)} m² — {unit.get('project', locality_str)}", "name": f"Prodej bytu {disp} {area} m² — {unit.get('_project_name', '')}",
"price": int(price), "price": int(price),
"price_formatted": format_price(int(price)), "price_formatted": format_price(int(price)),
"locality": locality_str, "locality": f"{unit.get('street', unit.get('_project_name', ''))}, Praha",
"lat": lat, "lat": lat,
"lon": lng, "lon": lon,
"disposition": disp, "disposition": disp,
"floor": floor, "floor": floor,
"area": float(area), "area": area,
"building_type": "neuvedeno", "building_type": building_type,
"ownership": "osobní", "ownership": unit.get("ownership", "neuvedeno") or "neuvedeno",
"url": detail_url, "url": detail_url,
"source": "psn", "source": "psn",
"image": "", "image": "",
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": _prev_cache.get(str(unit_id), {}).get("first_seen", datetime.now().strftime("%Y-%m-%d")), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d") if _prev_cache.get(str(unit_id), {}).get("price") != int(price) else _prev_cache[str(unit_id)].get("last_changed", datetime.now().strftime("%Y-%m-%d")),
} }
if not validate_listing(result, "psn"):
continue
results.append(result) results.append(result)
properties_fetched += 1 properties_fetched += 1
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 jednotek: {len(all_units)}") logger.info(f" Celkem jednotek: {len(unique_units)}")
for reason, count in excluded.items(): logger.info(f" Vyloučeno (prodáno): {excluded_sold}")
if count: logger.info(f" Vyloučeno (typ): {excluded_type}")
logger.info(f" Vyloučeno ({reason}): {count}") logger.info(f" Vyloučeno (dispozice): {excluded_disp}")
logger.info(f" Vyloučeno (cena): {excluded_price}")
logger.info(f" Vyloučeno (plocha): {excluded_area}")
logger.info(f" Vyloučeno (patro): {excluded_floor}")
logger.info(f" Vyloučeno (panel): {excluded_panel}")
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="Maximum number of listing pages per project to scrape")
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", choices=["DEBUG", "INFO", "WARNING", "ERROR"],
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Logging level (default: INFO)") help="Logging level (default: INFO)")
args = parser.parse_args() args = parser.parse_args()
# Configure logging
logging.basicConfig( logging.basicConfig(
level=getattr(logging, args.log_level), level=getattr(logging, args.log_level),
format="[%(levelname)s] %(asctime)s - %(name)s - %(message)s", format="[%(levelname)s] %(asctime)s - %(name)s - %(message)s",
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try: estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties)
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")
@@ -327,6 +366,6 @@ if __name__ == "__main__":
) )
elapsed = time.time() - start elapsed = time.time() - start
logger.info(f"\n✓ Data uložena: {json_path.resolve()}") logger.info(f"\n✓ Data uložena: {json_path.resolve()}")
logger.info(f"⏱ Celkový čas: {elapsed:.1f} s") logger.info(f"⏱ Celkový čas: {elapsed:.0f} s")
else: else:
logger.info("\nŽádné byty z PSN neodpovídají kritériím :(") logger.info("\nŽádné byty z PSN neodpovídají kritériím :(")

View File

@@ -7,17 +7,14 @@ Výstup: byty_realingo.json
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from datetime import datetime
import json import json
import logging import logging
import math import math
import re import re
import time import time
import urllib.request import urllib.request
from datetime import datetime
from pathlib import Path from pathlib import Path
from scraper_stats import write_stats, validate_listing
STATS_FILE = "stats_realingo.json"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -56,28 +53,6 @@ HEADERS = {
BASE_URL = "https://www.realingo.cz" BASE_URL = "https://www.realingo.cz"
def fetch_url(url: str, retries: int = 3) -> str:
"""Fetch URL and return HTML string with retry on transient errors."""
for attempt in range(retries):
try:
logger.debug(f"HTTP GET request (attempt {attempt + 1}/{retries}): {url}")
req = urllib.request.Request(url, headers=HEADERS)
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
return html
except urllib.error.HTTPError:
raise
except (ConnectionResetError, ConnectionError, urllib.error.URLError, OSError) as e:
if attempt < retries - 1:
wait = (attempt + 1) * 2
logger.warning(f"Connection error (retry {attempt + 1}/{retries} after {wait}s): {e}")
time.sleep(wait)
else:
logger.error(f"HTTP request failed after {retries} attempts: {e}", exc_info=True)
raise
def fetch_listing_page(page: int = 1) -> tuple[list[dict], int]: def fetch_listing_page(page: int = 1) -> tuple[list[dict], int]:
"""Fetch a page of Prague listings. Returns (items, total_count).""" """Fetch a page of Prague listings. Returns (items, total_count)."""
if page == 1: if page == 1:
@@ -85,7 +60,14 @@ def fetch_listing_page(page: int = 1) -> tuple[list[dict], int]:
else: else:
url = f"{BASE_URL}/prodej_byty/praha/{page}_strana/" url = f"{BASE_URL}/prodej_byty/praha/{page}_strana/"
html = fetch_url(url) logger.debug(f"HTTP GET request: {url}")
logger.debug(f"Headers: {HEADERS}")
req = urllib.request.Request(url, headers=HEADERS)
try:
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
match = re.search( match = re.search(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
html, re.DOTALL html, re.DOTALL
@@ -98,13 +80,21 @@ def fetch_listing_page(page: int = 1) -> tuple[list[dict], int]:
offer_list = data["props"]["pageProps"]["store"]["offer"]["list"] offer_list = data["props"]["pageProps"]["store"]["offer"]["list"]
logger.debug(f"Page {page}: found {len(offer_list['data'])} items, total={offer_list['total']}") logger.debug(f"Page {page}: found {len(offer_list['data'])} items, total={offer_list['total']}")
return offer_list["data"], offer_list["total"] return offer_list["data"], offer_list["total"]
except (urllib.error.URLError, ConnectionError, OSError) as e:
logger.error(f"HTTP request failed for {url}: {e}", exc_info=True)
raise
def fetch_detail(listing_url: str) -> dict | None: def fetch_detail(listing_url: str) -> dict | None:
"""Fetch detail page for a listing to get floor, building type, etc.""" """Fetch detail page for a listing to get floor, building type, etc."""
try: try:
url = f"{BASE_URL}{listing_url}" url = f"{BASE_URL}{listing_url}"
html = fetch_url(url) logger.debug(f"HTTP GET request: {url}")
req = urllib.request.Request(url, headers=HEADERS)
resp = urllib.request.urlopen(req, timeout=30)
html = resp.read().decode("utf-8")
logger.debug(f"HTTP response: status={resp.status}, size={len(html)} bytes")
match = re.search( match = re.search(
r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
html, re.DOTALL html, re.DOTALL
@@ -146,8 +136,6 @@ 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)
@@ -251,10 +239,14 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
# Check cache — if hash_id exists and price unchanged, reuse # Check cache — if hash_id exists and price unchanged, reuse
item_id = int(item["id"]) item_id = int(item["id"])
item_price = item.get("price", {}).get("total", 0) or 0 item_price = item.get("price", {}).get("total", 0) or 0
today = datetime.now().strftime("%Y-%m-%d")
cached = cache.get(item_id) cached = cache.get(item_id)
if cached and cached.get("price") == item_price: if cached and cached.get("price") == item_price:
cache_hits += 1 cache_hits += 1
logger.debug(f"Cache hit for id={item_id}") logger.debug(f"Cache hit for id={item_id}")
cached["last_updated"] = today
if "first_seen" not in cached:
cached["first_seen"] = today
results.append(cached) results.append(cached)
continue continue
@@ -311,6 +303,11 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
cat = item.get("category", "") cat = item.get("category", "")
loc = item.get("location", {}) loc = item.get("location", {})
# Preserve first_seen from cache if this is a price-changed re-fetch
first_seen = today
if cached and "first_seen" in cached:
first_seen = cached["first_seen"]
result = { result = {
"hash_id": int(item["id"]), "hash_id": int(item["id"]),
"name": f"Prodej bytu {CATEGORY_LABELS.get(cat, '?')} {item.get('area', {}).get('main', '?')}", "name": f"Prodej bytu {CATEGORY_LABELS.get(cat, '?')} {item.get('area', {}).get('main', '?')}",
@@ -327,12 +324,9 @@ def scrape(max_pages: int | None = None, max_properties: int | None = None):
"url": f"{BASE_URL}{item['url']}", "url": f"{BASE_URL}{item['url']}",
"source": "realingo", "source": "realingo",
"image": "", "image": "",
"scraped_at": datetime.now().strftime("%Y-%m-%d"), "first_seen": first_seen,
"first_seen": cached.get("first_seen", datetime.now().strftime("%Y-%m-%d")) if cached else datetime.now().strftime("%Y-%m-%d"), "last_updated": today,
"last_changed": datetime.now().strftime("%Y-%m-%d"),
} }
if not validate_listing(result, "realingo"):
continue
results.append(result) results.append(result)
properties_fetched += 1 properties_fetched += 1
@@ -349,25 +343,6 @@ 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
@@ -388,22 +363,8 @@ if __name__ == "__main__":
handlers=[logging.StreamHandler()] handlers=[logging.StreamHandler()]
) )
_run_ts = datetime.now().isoformat(timespec="seconds")
start = time.time() start = time.time()
try:
estates = scrape(max_pages=args.max_pages, max_properties=args.max_properties) 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")

View File

@@ -1,55 +0,0 @@
"""Shared utilities for scraper run statistics and listing validation."""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
HERE = Path(__file__).parent
DATA_DIR = Path(os.environ.get("DATA_DIR", HERE))
_val_log = logging.getLogger(__name__)
_REQUIRED_FIELDS = ("hash_id", "price", "locality", "lat", "lon", "url", "source")
def validate_listing(listing: dict, context: str = "") -> bool:
"""
Validate a listing dict before it is written to the output JSON.
Returns True if valid, False if the listing should be skipped.
Logs a warning for each invalid listing.
"""
prefix = f"[{context}] " if context else ""
for field in _REQUIRED_FIELDS:
val = listing.get(field)
if val is None or val == "":
_val_log.warning(f"{prefix}Skipping listing — missing field '{field}': {listing.get('hash_id', '?')}")
return False
price = listing.get("price")
if not isinstance(price, (int, float)) or price <= 0:
_val_log.warning(f"{prefix}Skipping listing — invalid price={price!r}: {listing.get('hash_id', '?')}")
return False
lat, lon = listing.get("lat"), listing.get("lon")
if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)):
_val_log.warning(f"{prefix}Skipping listing — non-numeric GPS lat={lat!r} lon={lon!r}: {listing.get('hash_id', '?')}")
return False
if not (47.0 <= lat <= 52.0) or not (12.0 <= lon <= 19.0):
_val_log.warning(f"{prefix}Skipping listing — GPS outside Czech Republic lat={lat} lon={lon}: {listing.get('hash_id', '?')}")
return False
area = listing.get("area")
if area is not None and (not isinstance(area, (int, float)) or area <= 0):
_val_log.warning(f"{prefix}Skipping listing — invalid area={area!r}: {listing.get('hash_id', '?')}")
return False
return True
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")

477
server.py
View File

@@ -1,477 +0,0 @@
#!/usr/bin/env python3
"""
General-purpose HTTP server for maru-hleda-byt.
Serves static files from DATA_DIR and additionally handles:
GET /scrapers-status → SSR scraper status page
GET /api/ratings → ratings.json contents
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)
"""
from __future__ import annotations
import functools
import json
import logging
import os
import sys
from datetime import datetime
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
PORT = int(os.environ.get("SERVER_PORT", 8080))
DATA_DIR = Path(os.environ.get("DATA_DIR", "."))
RATINGS_FILE = DATA_DIR / "ratings.json"
_LOG_LEVEL = getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO)
logging.basicConfig(
level=_LOG_LEVEL,
format="%(asctime)s [server] %(levelname)s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
log = logging.getLogger(__name__)
# ── Helpers ──────────────────────────────────────────────────────────────────
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",
]
def _load_json(path: Path, default=None):
"""Read and parse JSON file; return default on missing or parse error."""
log.debug("_load_json: %s", path.resolve())
try:
if path.exists():
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:
return _load_json(RATINGS_FILE, default={})
def save_ratings(data: dict) -> None:
RATINGS_FILE.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# ── SSR status page ──────────────────────────────────────────────────────────
_CSS = """\
* { 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"]
def _sources_html(sources: list) -> str:
if not sources:
return ""
max_count = max((s.get("accepted", 0) for s in sources), default=1) or 1
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)
def _history_html(history: list) -> str:
if not history:
return ""
rows = list(reversed(history))
parts = [
'<div class="card">'
'<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)
def _modal_script(rows_json: str) -> str:
"""Return the modal overlay HTML + JS for the history detail popup."""
return (
'<div id="md-overlay" style="display:none">'
'<div id="md-box"><button id="md-close">&times;</button>'
'<div id="md-body"></div></div></div>\n'
'<script>\n(function(){\n'
f'var H={rows_json};\n'
'var C={"sreality":"#1976D2","realingo":"#7B1FA2","bezrealitky":"#E65100","idnes":"#C62828","psn":"#2E7D32","cityhome":"#00838F"};\n'
'var MN=["ledna","února","března","dubna","května","června","července","srpna","září","října","listopadu","prosince"];\n'
'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'
'function openModal(idx){\n'
' var e=H[idx],src=e.sources||[];\n'
' var h="<h3>Detaily b\u011bhu \u2013 "+fd(e.timestamp)+"</h3>";\n'
' 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>'
)
def _render_status_html(status: dict | None, history: list, is_running: bool = False) -> str:
"""Generate the complete HTML page for /scrapers-status."""
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>'
# ── HTTP handler ──────────────────────────────────────────────────────────────
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__":
log.info("Server starting on port %d, data dir: %s", PORT, DATA_DIR)
handler = functools.partial(Handler, directory=str(DATA_DIR))
server = HTTPServer(("0.0.0.0", PORT), handler)
try:
server.serve_forever()
except KeyboardInterrupt:
log.info("Stopped.")
sys.exit(0)