From cd74264bb5ed5ee73f19fd78756db40a9e073a10 Mon Sep 17 00:00:00 2001 From: blasebast Date: Sat, 11 Apr 2026 10:34:50 +0200 Subject: [PATCH] Add pre-commit with detect-secrets, ignore gitea runtime data --- .gitignore | 3 + .pre-commit-config.yaml | 14 + .secrets.baseline | 308 +++++++++++++++ crowdsec/bin/daily-summary.sh | 64 ++++ crowdsec/conf/profiles.yaml | 4 - docker-compose.yaml | 49 ++- esphome/bathroom-shelly-1l-01.yaml | 13 +- esphome/esp_mini_office.yaml | 12 +- esphome/esp_woox_dining.yaml | 12 +- esphome/esp_woox_fan.yaml | 14 +- esphome/light_driveway.yaml | 17 +- esphome/packages/common_core.yaml | 11 + esphome/sonoff-mini-digestor.yaml | 12 +- esphome/sonoff-mini-kitchen-led.yaml | 12 +- esphome/sonoff-mini-r2-1.yaml | 12 +- esphome/sonoff-mini-r2-2.yaml | 17 +- esphome/sonoff_mini_bedroom.yaml | 17 +- esphome/sonoff_mini_hall.yaml | 17 +- esphome/sonoff_mini_stefi.yaml | 17 +- esphome/sonofft13guk.yaml | 19 +- ha-dashboard/app.py | 149 +++++++- ha-dashboard/templates/ansible.html | 256 +++++++++++++ ha-dashboard/templates/index.html | 106 +++++- ha-dashboard/templates/niepalenie.html | 360 ++++++++++++++++-- .../templates/partials/nav_user_chips.html | 7 + mosquitto/README.md | 6 +- mosquitto/config/mosquitto.conf | 5 +- pa | 1 + postgresql-ssl/server.crt | 77 ++++ prowlarr/Definitions/torrentqq.yml | 4 +- prowlarr/Definitions/totheglory.yml | 3 +- prowlarr/Definitions/totheglory2fa.yml | 3 +- 32 files changed, 1418 insertions(+), 203 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline create mode 100755 crowdsec/bin/daily-summary.sh create mode 100644 esphome/packages/common_core.yaml create mode 100644 ha-dashboard/templates/ansible.html create mode 100644 ha-dashboard/templates/partials/nav_user_chips.html create mode 100644 pa create mode 100644 postgresql-ssl/server.crt diff --git a/.gitignore b/.gitignore index 2ce926b..57e4aff 100644 --- a/.gitignore +++ b/.gitignore @@ -277,5 +277,8 @@ side-agent/manifest/ .claude/ CLAUDE.md +# Gitea runtime data (sessions, SSH keys, databases) +gitea/ + # OTHERS log-ingester/state/state.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..51a119f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: ^\.env.* + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..9c32ede --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,308 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "crowdsec/conf/dev.yaml": [ + { + "type": "Secret Keyword", + "filename": "crowdsec/conf/dev.yaml", + "hashed_secret": "3d6a86d0b53584491e13b12173441f0b945307d2", + "is_verified": false, + "line_number": 24 + } + ], + "crowdsec/conf/user.yaml": [ + { + "type": "Secret Keyword", + "filename": "crowdsec/conf/user.yaml", + "hashed_secret": "3d6a86d0b53584491e13b12173441f0b945307d2", + "is_verified": false, + "line_number": 21 + } + ], + "esphome/archive/sonoff-mini-kennel.yaml": [ + { + "type": "Secret Keyword", + "filename": "esphome/archive/sonoff-mini-kennel.yaml", + "hashed_secret": "9d53c2aa0d14616ca56c762d72201eadf0f67ab2", + "is_verified": false, + "line_number": 21 + } + ], + "esphome/archive/spare-sonoff-mini-r2.yaml": [ + { + "type": "Base64 High Entropy String", + "filename": "esphome/archive/spare-sonoff-mini-r2.yaml", + "hashed_secret": "efa88fa62d45a21ecc1f21f91058be2100917e29", + "is_verified": false, + "line_number": 14 + }, + { + "type": "Hex High Entropy String", + "filename": "esphome/archive/spare-sonoff-mini-r2.yaml", + "hashed_secret": "a16d394c06ef4e3ac934f918bcdc05c50c2e0607", + "is_verified": false, + "line_number": 18 + }, + { + "type": "Secret Keyword", + "filename": "esphome/archive/spare-sonoff-mini-r2.yaml", + "hashed_secret": "a16d394c06ef4e3ac934f918bcdc05c50c2e0607", + "is_verified": false, + "line_number": 18 + }, + { + "type": "Secret Keyword", + "filename": "esphome/archive/spare-sonoff-mini-r2.yaml", + "hashed_secret": "09e1ed8cf851cd6b3584644185f9ae97658a1b32", + "is_verified": false, + "line_number": 27 + } + ], + "esphome/esp_woox_dining.yaml": [ + { + "type": "Secret Keyword", + "filename": "esphome/esp_woox_dining.yaml", + "hashed_secret": "6411188dcabc641aea51512f70ae89afa8d01386", + "is_verified": false, + "line_number": 24 + } + ], + "frigate/backup_config.yaml": [ + { + "type": "Secret Keyword", + "filename": "frigate/backup_config.yaml", + "hashed_secret": "6411188dcabc641aea51512f70ae89afa8d01386", + "is_verified": false, + "line_number": 11 + } + ], + "gitea/gitea/conf/app.ini": [ + { + "type": "Secret Keyword", + "filename": "gitea/gitea/conf/app.ini", + "hashed_secret": "3f13034fb0b1edc6ef85ad01e4417bfac87d6d4a", + "is_verified": false, + "line_number": 27 + }, + { + "type": "Secret Keyword", + "filename": "gitea/gitea/conf/app.ini", + "hashed_secret": "15b4e547b78c9fb7f88e7fd8aa44ff3d2c43101e", + "is_verified": false, + "line_number": 36 + }, + { + "type": "JSON Web Token", + "filename": "gitea/gitea/conf/app.ini", + "hashed_secret": "cfbe756d20abb814f15d2168821d1b98532fffa2", + "is_verified": false, + "line_number": 66 + }, + { + "type": "Secret Keyword", + "filename": "gitea/gitea/conf/app.ini", + "hashed_secret": "f42f039263b8bdcb318abca422615b3decd4a1d0", + "is_verified": false, + "line_number": 67 + }, + { + "type": "Secret Keyword", + "filename": "gitea/gitea/conf/app.ini", + "hashed_secret": "163ab7bb3489bf9090792cac65aca6ad5ba69486", + "is_verified": false, + "line_number": 101 + } + ], + "ha-dashboard/auth/pending/craver.json": [ + { + "type": "Secret Keyword", + "filename": "ha-dashboard/auth/pending/craver.json", + "hashed_secret": "68eac51b678708b47b959ad9507eea778457c2af", + "is_verified": false, + "line_number": 1 + } + ], + "ha-dashboard/auth/pending/testuser.json": [ + { + "type": "Secret Keyword", + "filename": "ha-dashboard/auth/pending/testuser.json", + "hashed_secret": "f27f25023ad16502f41618cb8d15492c141a35fb", + "is_verified": false, + "line_number": 1 + } + ], + "homeassistant/config/automations.yaml": [ + { + "type": "Hex High Entropy String", + "filename": "homeassistant/config/automations.yaml", + "hashed_secret": "06013aa4c86643f92488f86790b0305800fe67c6", + "is_verified": false, + "line_number": 962 + } + ], + "mktxp/mktxp.conf": [ + { + "type": "Secret Keyword", + "filename": "mktxp/mktxp.conf", + "hashed_secret": "2a5c4352756bc8f740ee8c4cec36bb50601424a6", + "is_verified": false, + "line_number": 6 + }, + { + "type": "Secret Keyword", + "filename": "mktxp/mktxp.conf", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "is_verified": false, + "line_number": 90 + } + ], + "prometheus/prometheus.yml": [ + { + "type": "Secret Keyword", + "filename": "prometheus/prometheus.yml", + "hashed_secret": "4f7f28f949082724ce21e94114ee60ce285a954b", + "is_verified": false, + "line_number": 34 + }, + { + "type": "JSON Web Token", + "filename": "prometheus/prometheus.yml", + "hashed_secret": "af98dd665ee30b325ad16d71581a2a99719b7e9b", + "is_verified": false, + "line_number": 54 + } + ], + "qbit-vpn/config/qBittorrent/qBittorrent.conf": [ + { + "type": "Secret Keyword", + "filename": "qbit-vpn/config/qBittorrent/qBittorrent.conf", + "hashed_secret": "3c64501f31a2ed3a77a9f0d307ce6092a4ceb6d2", + "is_verified": false, + "line_number": 51 + } + ] + }, + "generated_at": "2026-04-11T08:30:02Z" +} diff --git a/crowdsec/bin/daily-summary.sh b/crowdsec/bin/daily-summary.sh new file mode 100755 index 0000000..e510fe8 --- /dev/null +++ b/crowdsec/bin/daily-summary.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Daily CrowdSec ban summary - Telegram + +BOT_TOKEN=$(grep -m1 '^TELEGRAM_BOT_TOKEN=.\+' /home/seba/mydocker/.env | cut -d= -f2-) +CHAT_ID=$(grep -m1 '^TELEGRAM_CHAT_ID=.\+' /home/seba/mydocker/.env | cut -d= -f2-) +TMPFILE=$(mktemp) + +docker exec crowdsec cscli decisions list --since 24h -o json > "$TMPFILE" 2>/dev/null + +python3 - "$TMPFILE" "$BOT_TOKEN" "$CHAT_ID" << 'PYEOF' +import sys, json, urllib.request, urllib.parse + +tmpfile, bot_token, chat_id = sys.argv[1], sys.argv[2], sys.argv[3] + +with open(tmpfile) as f: + raw = f.read().strip() + +data = json.loads(raw) if raw and raw != 'null' else [] + +def send(text): + params = urllib.parse.urlencode({ + 'chat_id': chat_id, + 'text': text, + 'parse_mode': 'HTML', + 'disable_web_page_preview': 'true' + }).encode() + urllib.request.urlopen(f'https://api.telegram.org/bot{bot_token}/sendMessage', params) + +if not data: + send('πŸ›‘ CrowdSec - raport dobowy\nBrak banΓ³w w ostatnich 24h.') + sys.exit(0) + +header = f'🚨 CrowdSec β€” {len(data)} ban(y) z ostatnich 24h\n\n' +entries = [] +for alert in data: + src = alert.get('source', {}) + decs = alert.get('decisions') or [{}] + dec = decs[0] + ip = src.get('ip', '?') + cn = src.get('cn', '?') + asn = src.get('as_name', '?') + scenario = alert.get('scenario', dec.get('scenario', '?')) + events = alert.get('events_count', '?') + start = alert.get('start_at', '?')[:16].replace('T', ' ') + entries.append( + f'🌍 {ip} [{cn}] {asn}\n' + f'πŸ”’ {scenario}\n' + f'πŸ“Š zdarzeΕ„: {events} ⏱ {start}\n' + f'πŸ”— CTI Β· ' + f'whois\n' + ) + +msg = header +for entry in entries: + if len(msg) + len(entry) + 1 > 4000: + send(msg.rstrip()) + msg = entry + '\n' + else: + msg += entry + '\n' +if msg.strip(): + send(msg.rstrip()) +PYEOF + +rm -f "$TMPFILE" diff --git a/crowdsec/conf/profiles.yaml b/crowdsec/conf/profiles.yaml index 22474fe..ba7659c 100644 --- a/crowdsec/conf/profiles.yaml +++ b/crowdsec/conf/profiles.yaml @@ -6,8 +6,6 @@ decisions: - type: ban duration: 4h #duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4) -notifications: - - http_default on_success: break --- name: default_range_remediation @@ -16,6 +14,4 @@ filters: decisions: - type: ban duration: 4h -notifications: - - http_default on_success: break diff --git a/docker-compose.yaml b/docker-compose.yaml index 24768a7..345780f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -232,6 +232,7 @@ services: postgresql18: container_name: postgres18 image: postgres:18 + command: ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/etc/postgresql/ssl/server.crt", "-c", "ssl_key_file=/etc/postgresql/ssl/server.key"] restart: always ports: - "5432:5432" @@ -241,6 +242,36 @@ services: POSTGRES_DB: homeassistant volumes: - ${PWD}/postgres18/data:/var/lib/postgresql + - ${PWD}/postgresql-ssl:/etc/postgresql/ssl:ro + gitea: + image: gitea/gitea:1.21 + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=postgresql18:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=homeassistant + - GITEA__database__PASS=${DB_PASS} + - GITEA__database__SSL_MODE=disable + volumes: + - ./gitea:/data + - /home/seba/git.repos:/import/git.repos:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3003:3000" + - "2222:22" + labels: + - "traefik.enable=true" + - "traefik.http.routers.gitea.rule=Host(`git.sebson.space`)" + - "traefik.http.routers.gitea.entrypoints=websecure" + - "traefik.http.routers.gitea.tls.certresolver=mytlschallenge" + - "traefik.http.services.gitea.loadbalancer.server.port=3000" + depends_on: + - postgresql18 + postgres-exporter: image: prometheuscommunity/postgres-exporter @@ -387,11 +418,16 @@ services: volumes: - ${PWD}/esphome:/config:rw restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "20m" + max-file: "3" mosquitto: container_name: mosquitto restart: unless-stopped - image: eclipse-mosquitto:latest + image: eclipse-mosquitto:2 volumes: - ${PWD}/mosquitto/config:/mosquitto/config - ${PWD}/mosquitto/log:/mosquitto/log @@ -400,6 +436,11 @@ services: network_mode: host environment: - TZ=Europe/Prague + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" # ============================================ # MEDIA @@ -518,7 +559,11 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro - ${PWD}/backup.log:/backup.log:ro - ${PWD}/ha-dashboard/auth:/auth + - ${PWD}/ha-dashboard/app.py:/app/app.py:ro + - ${PWD}/ha-dashboard/templates:/app/templates:ro + - ${PWD}/ha-dashboard/static:/app/static:ro - ${PWD}/homeassistant/config:/ha_config:ro + - /home/seba/git.repos/myansible:/myansible:ro labels: - "traefik.enable=true" - "traefik.http.routers.ha-dashboard.rule=Host(`hadash.sebson.space`)" @@ -538,7 +583,7 @@ services: ports: - '9989:80' environment: - ADMIN_TOKEN: ${ENCRYTED_BITWARDEN_ADMIN_TOKEN} + ADMIN_TOKEN: ${ENCRYPTED_BITWARDEN_ADMIN_TOKEN} DATABASE_URL: postgresql://homeassistant:${DB_PASS}@192.168.1.132:5432/bitwarden YUBICO_CLIENT_ID: ${YUBICO_CLIENT_ID} YUBICO_SECRET_KEY: ${YUBICO_SECRET_KEY} diff --git a/esphome/bathroom-shelly-1l-01.yaml b/esphome/bathroom-shelly-1l-01.yaml index 7c9c173..3145192 100644 --- a/esphome/bathroom-shelly-1l-01.yaml +++ b/esphome/bathroom-shelly-1l-01.yaml @@ -21,17 +21,10 @@ wifi: ap: ssid: ${plug_name} - password: 'GulaLikesMcDrive' + password: !secret fallback_ap_password -captive_portal: - -ota: - platform: esphome - -api: - -logger: - level: VERBOSE +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 diff --git a/esphome/esp_mini_office.yaml b/esphome/esp_mini_office.yaml index 3836b46..7b34582 100644 --- a/esphome/esp_mini_office.yaml +++ b/esphome/esp_mini_office.yaml @@ -25,11 +25,10 @@ wifi: ap: ssid: 'sonoff-office' - password: 'GulaLikesMcDrive' + password: !secret fallback_ap_password -captive_portal: - -logger: +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 @@ -38,11 +37,6 @@ api: # Opcjonalnie: JeΕ›li masz problemy z kluczami szyfrowania, # na czas migracji moΕΌna to zostawiΔ‡ puste lub dodaΔ‡ encryption key. -ota: - platform: esphome - # Bezpiecznik: jeΕ›li OTA przez HA zawiedzie, zawsze moΕΌesz wejΕ›Δ‡ przez WWW - # bo wΕ‚Δ…czyliΕ›my web_server powyΕΌej. - status_led: pin: number: GPIO13 diff --git a/esphome/esp_woox_dining.yaml b/esphome/esp_woox_dining.yaml index bf0019e..34f230c 100644 --- a/esphome/esp_woox_dining.yaml +++ b/esphome/esp_woox_dining.yaml @@ -23,18 +23,12 @@ wifi: ssid: "dining-light-fallback" password: "guilermo3" -captive_portal: - -logger: - -api: - -ota: - platform: esphome +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 - + status_led: pin: number: GPIO13 diff --git a/esphome/esp_woox_fan.yaml b/esphome/esp_woox_fan.yaml index 3853545..837b36b 100644 --- a/esphome/esp_woox_fan.yaml +++ b/esphome/esp_woox_fan.yaml @@ -18,18 +18,8 @@ wifi: - ssid: !secret wifi_ssid_iot password: !secret wifi_pw_iot - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome +packages: + common_core: !include packages/common_core.yaml status_led: pin: diff --git a/esphome/light_driveway.yaml b/esphome/light_driveway.yaml index 7c6505f..1232247 100644 --- a/esphome/light_driveway.yaml +++ b/esphome/light_driveway.yaml @@ -26,22 +26,13 @@ wifi: ap: ssid: 'sonoff-driveway' - password: 'GulaLikesMcDrive' + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome status_led: pin: diff --git a/esphome/packages/common_core.yaml b/esphome/packages/common_core.yaml new file mode 100644 index 0000000..1631b2c --- /dev/null +++ b/esphome/packages/common_core.yaml @@ -0,0 +1,11 @@ +# Shared: HA native API, OTA, captive portal, sane log level. +# Per-device YAML wins on duplicate keys (ESPHome merge rules). +captive_portal: + +logger: + level: INFO + +api: + +ota: + platform: esphome diff --git a/esphome/sonoff-mini-digestor.yaml b/esphome/sonoff-mini-digestor.yaml index 51796f7..9cd87d8 100644 --- a/esphome/sonoff-mini-digestor.yaml +++ b/esphome/sonoff-mini-digestor.yaml @@ -18,16 +18,10 @@ wifi: ap: ssid: "kitchen-hood-fallback" - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password -captive_portal: - -logger: - -api: - -ota: - platform: esphome +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 diff --git a/esphome/sonoff-mini-kitchen-led.yaml b/esphome/sonoff-mini-kitchen-led.yaml index 25de959..524b6d0 100644 --- a/esphome/sonoff-mini-kitchen-led.yaml +++ b/esphome/sonoff-mini-kitchen-led.yaml @@ -19,16 +19,10 @@ wifi: # Fallback AP ap: ssid: "kitchen-led-fallback" - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password -captive_portal: - -logger: - -api: - -ota: - platform: esphome +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 diff --git a/esphome/sonoff-mini-r2-1.yaml b/esphome/sonoff-mini-r2-1.yaml index 009cffc..e6f59ed 100644 --- a/esphome/sonoff-mini-r2-1.yaml +++ b/esphome/sonoff-mini-r2-1.yaml @@ -24,20 +24,14 @@ wifi: ap: ssid: 'garden-pump-fallback' - password: 'GulaLikesMcDrive' + password: !secret fallback_ap_password -captive_portal: - -logger: +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 -api: - -ota: - platform: esphome - status_led: pin: number: GPIO13 diff --git a/esphome/sonoff-mini-r2-2.yaml b/esphome/sonoff-mini-r2-2.yaml index 0c5b584..968b635 100644 --- a/esphome/sonoff-mini-r2-2.yaml +++ b/esphome/sonoff-mini-r2-2.yaml @@ -23,22 +23,13 @@ wifi: # Fallback AP ap: ssid: ${dev_name} - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome # --- HARDWARE CONFIGURATION (SONOFF MINI) --- diff --git a/esphome/sonoff_mini_bedroom.yaml b/esphome/sonoff_mini_bedroom.yaml index 27a25a4..f2ac8a9 100644 --- a/esphome/sonoff_mini_bedroom.yaml +++ b/esphome/sonoff_mini_bedroom.yaml @@ -23,22 +23,13 @@ wifi: # Fallback AP ap: ssid: ${dev_name} - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome # --- HARDWARE CONFIGURATION (SONOFF MINI) --- diff --git a/esphome/sonoff_mini_hall.yaml b/esphome/sonoff_mini_hall.yaml index db937bc..70ab945 100644 --- a/esphome/sonoff_mini_hall.yaml +++ b/esphome/sonoff_mini_hall.yaml @@ -23,22 +23,13 @@ wifi: # Fallback AP ap: ssid: ${dev_name} - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome # --- HARDWARE CONFIGURATION (SONOFF MINI) --- diff --git a/esphome/sonoff_mini_stefi.yaml b/esphome/sonoff_mini_stefi.yaml index c24db3c..7adae38 100644 --- a/esphome/sonoff_mini_stefi.yaml +++ b/esphome/sonoff_mini_stefi.yaml @@ -24,23 +24,14 @@ wifi: # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: 'sff-mni-stef' - password: 'GulaLikesMcDrive' + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: port: 80 -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome - status_led: pin: number: GPIO13 diff --git a/esphome/sonofft13guk.yaml b/esphome/sonofft13guk.yaml index daff110..76da921 100644 --- a/esphome/sonofft13guk.yaml +++ b/esphome/sonofft13guk.yaml @@ -23,22 +23,13 @@ wifi: # Fallback AP ap: ssid: ${dev_name} - password: "GulaLikesMcDrive" + password: !secret fallback_ap_password + +packages: + common_core: !include packages/common_core.yaml web_server: - port: 80 - -captive_portal: - -# Enable logging -logger: - -# Enable Home Assistant API -api: - -# Enable over-the-air updates -ota: - platform: esphome + port: 80 # --- HARDWARE CONFIGURATION (3-GANG) --- diff --git a/ha-dashboard/app.py b/ha-dashboard/app.py index 1a8cc7b..7475c87 100644 --- a/ha-dashboard/app.py +++ b/ha-dashboard/app.py @@ -11,10 +11,22 @@ from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY", "change-me-in-production") +# Volume-mounted templates must be picked up without restarting the process +app.config["TEMPLATES_AUTO_RELOAD"] = True AUTH_DIR = os.environ.get("AUTH_DIR", "/auth") +@app.after_request +def _no_store_html(res): + """Avoid stale dashboard HTML behind proxies / browsers (templates change often).""" + ct = res.headers.get("Content-Type", "") + if ct.startswith("text/html"): + res.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + res.headers["Pragma"] = "no-cache" + return res + + # ── Auth helpers ────────────────────────────────────────────────────────────── def _pending_path(username): @@ -33,6 +45,59 @@ def _get_pending(username): def _is_approved(username): return os.path.exists(_approved_path(username)) + +def _list_dashboard_users(): + """Approved ha-dashboard logins (filenames under auth/approved/).""" + d = os.path.join(AUTH_DIR, "approved") + if not os.path.isdir(d): + return [] + names = [] + for name in os.listdir(d): + if name.startswith("."): + continue + p = os.path.join(d, name) + if os.path.isfile(p) or os.path.isdir(p): + names.append(name) + return sorted(names, key=str.lower) + + +def _list_pending_usernames(): + """Registered accounts awaiting approval (pending/*.json, stem = username).""" + d = os.path.join(AUTH_DIR, "pending") + if not os.path.isdir(d): + return [] + out = [] + for fn in os.listdir(d): + if fn.startswith(".") or not fn.endswith(".json"): + continue + out.append(fn[:-5]) + return sorted(out, key=str.lower) + + +def _nav_user_chips(): + """Footer avatars: current session, then approved, then pending-only (deduped).""" + seen = set() + chips = [] + + def push(name, role): + key = (name or "").strip().lower() + if not key or key in seen: + return + seen.add(key) + chips.append({"name": name.strip(), "role": role}) + + cu = session.get("user") + if cu: + push(str(cu), "current") + + for u in _list_dashboard_users(): + push(u, "approved") + + for u in _list_pending_usernames(): + push(u, "pending") + + return chips + def _save_pending(username, password): os.makedirs(os.path.join(AUTH_DIR, "pending"), exist_ok=True) with open(_pending_path(username), "w") as f: @@ -840,7 +905,89 @@ def rzucanie_update(token): @app.route("/") @login_required def index(): - return render_template("index.html") + return render_template("index.html", nav_user_chips=_nav_user_chips()) + + +# ── Ansible runs ────────────────────────────────────────────────────────────── + +_ANSIBLE_LOG = "/myansible/ansible_runs.jsonl" + + +@app.route("/api/ansible") +@login_required +def api_ansible(): + try: + with open(_ANSIBLE_LOG) as f: + lines = [l.strip() for l in f if l.strip()] + except FileNotFoundError: + return jsonify({"error": "Log not found"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + runs_raw = [] + for line in lines: + try: + runs_raw.append(json.loads(line)) + except Exception: + continue + + runs_raw.sort(key=lambda r: r.get("ts", ""), reverse=True) + + runs = [] + for r in runs_raw[:50]: + hosts = r.get("hosts", {}) + total_ok = sum(h.get("ok", 0) for h in hosts.values()) + total_changed = sum(h.get("changed", 0) for h in hosts.values()) + total_failed = sum(h.get("failed", 0) for h in hosts.values()) + runs.append({ + "ts": r.get("ts", ""), + "playbook": r.get("playbook", ""), + "user": r.get("user", ""), + "duration": r.get("duration", 0), + "status": r.get("status", "unknown"), + "hosts": r.get("hosts", {}), + "tasks_count": len(r.get("tasks", [])), + "ok": total_ok, + "changed": total_changed, + "failed": total_failed, + }) + + total = len(runs_raw) + success_count = sum(1 for r in runs_raw if r.get("status") == "success") + failed_count = total - success_count + success_rate = round(success_count / total * 100, 1) if total else 0.0 + durations = [r.get("duration", 0) for r in runs_raw if r.get("duration") is not None] + avg_duration = round(sum(durations) / len(durations), 1) if durations else 0.0 + + by_playbook = {} + for r in runs_raw: + pb = r.get("playbook", "unknown") + if pb not in by_playbook: + by_playbook[pb] = {"count": 0, "failures": 0, "last_run": ""} + by_playbook[pb]["count"] += 1 + if r.get("status") == "failed": + by_playbook[pb]["failures"] += 1 + ts = r.get("ts", "") + if ts > by_playbook[pb]["last_run"]: + by_playbook[pb]["last_run"] = ts + + return jsonify({ + "runs": runs, + "stats": { + "total": total, + "success_count": success_count, + "failed_count": failed_count, + "success_rate": success_rate, + "avg_duration": avg_duration, + }, + "by_playbook": by_playbook, + }) + + +@app.route("/ansible") +@login_required +def ansible_page(): + return render_template("ansible.html", nav_user_chips=_nav_user_chips()) if __name__ == "__main__": diff --git a/ha-dashboard/templates/ansible.html b/ha-dashboard/templates/ansible.html new file mode 100644 index 0000000..282e6bc --- /dev/null +++ b/ha-dashboard/templates/ansible.html @@ -0,0 +1,256 @@ + + + + + + Ansible Runs + + + + + +
+ ← Dashboard +

Ansible Runs

+
+ 🚭 Niepalenie + {% include 'partials/nav_user_chips.html' %} +
+
+ +
+ + +
+
Total Runs
+
+
-
+
runs
+
+
+ +
+
Success Rate
+
+
-
+
percent
+
+
+ +
+
Failed Runs
+
+
-
+
runs
+
+
+ +
+
Avg Duration
+
+
-
+
seconds
+
+
+ + +
+
Recent Runs (last 50)
+
+
Loading...
+
+
+ + +
+
Per-Playbook Breakdown
+
+
Loading...
+
+
+ +
+ + + + + diff --git a/ha-dashboard/templates/index.html b/ha-dashboard/templates/index.html index 11cf817..5d3c38b 100644 --- a/ha-dashboard/templates/index.html +++ b/ha-dashboard/templates/index.html @@ -13,8 +13,13 @@ header { padding: 13px 20px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; gap: 10px 16px; + } + header h1 { font-size: 16px; font-weight: 600; color: #f1f5f9; flex: 1 1 200px; min-width: 0; } + .header-links { + display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; + flex-shrink: 0; } - header h1 { font-size: 16px; font-weight: 600; color: #f1f5f9; } #refresh-info { color: #64748b; font-size: 12px; } main { @@ -260,16 +265,68 @@ .light-name { font-size:10px; color:#fcd34d; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:64px; } .lights-empty { color:#475569; font-size:12px; padding:6px 0; } + + /* ── Footer (discreet but always visible) ── */ + .site-footer { + max-width: 1600px; margin: 0 auto; padding: 12px 20px 22px; + display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 14px 18px; + border-top: 1px solid #1e293b; + opacity: 0.72; + transition: opacity 0.2s; + } + .site-footer:hover { opacity: 1; } + .footer-niepalenie { + color: #94a3b8; text-decoration: none; font-size: 12px; font-weight: 600; + padding: 5px 12px; border-radius: 8px; border: 1px solid #334155; white-space: nowrap; + } + .footer-niepalenie:hover { color: #e2e8f0; border-color: #475569; background: #1e293b; } + .footer-users { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } + .user-chip { + width: 24px; height: 24px; border-radius: 50%; + background: #1e293b; border: 1px solid #334155; + display: inline-flex; align-items: center; justify-content: center; + font-size: 10px; font-weight: 700; color: #94a3b8; + letter-spacing: 0; + cursor: default; + user-select: none; + } + .user-chip-current { border-color: #2563eb; color: #93c5fd; box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.35); } + .user-chip-approved { color: #94a3b8; } + .user-chip-pending { border-style: dashed; opacity: 0.75; color: #64748b; } + + .quick-links-card { + padding: 10px 14px; + display: flex; align-items: center; flex-wrap: wrap; gap: 12px 16px; + border-color: #334155; + background: linear-gradient(180deg, #1e293b 0%, #172033 100%); + } + .quick-links-card .quick-niepal { + color: #93c5fd; font-weight: 600; font-size: 13px; text-decoration: none; + padding: 4px 10px; border-radius: 6px; border: 1px solid #2563eb; + } + .quick-links-card .quick-niepal:hover { background: #1e3a5f; color: #e0f2fe; } + .quick-links-card .quick-lbl { font-size: 10px; font-weight: 700; text-transform: uppercase; + letter-spacing: .08em; color: #64748b; margin-right: 4px; }

🏠 HA Dashboard β€” sebson.space

- Refresh in 60s +
+ +
πŸ‘€ Who's home
@@ -300,22 +357,27 @@
- -
+ +
🌑 Room temperatures
-
-
⚑ Power consumption
+
+
⚑ Power
-
+
πŸ›‘ Crowdsec
+
+
πŸš€ Ansible last runs
+
+
+
πŸ“· Frigate β€” last detections
@@ -388,6 +450,11 @@
+ +