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 @@
+
+
+