Add pre-commit with detect-secrets, ignore gitea runtime data
This commit is contained in:
parent
a80b18bef1
commit
cd74264bb5
|
|
@ -277,5 +277,8 @@ side-agent/manifest/
|
|||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
# Gitea runtime data (sessions, SSH keys, databases)
|
||||
gitea/
|
||||
|
||||
# OTHERS
|
||||
log-ingester/state/state.json
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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('🛡 <b>CrowdSec - raport dobowy</b>\nBrak banów w ostatnich 24h.')
|
||||
sys.exit(0)
|
||||
|
||||
header = f'🚨 <b>CrowdSec — {len(data)} ban(y) z ostatnich 24h</b>\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'🌍 <code>{ip}</code> [<b>{cn}</b>] {asn}\n'
|
||||
f'🔒 <code>{scenario}</code>\n'
|
||||
f'📊 zdarzeń: {events} ⏱ {start}\n'
|
||||
f'🔗 <a href="https://app.crowdsec.net/cti/{ip}">CTI</a> · '
|
||||
f'<a href="https://www.whois.com/whois/{ip}">whois</a>\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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Ansible Runs</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0f172a; color: #e2e8f0; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; }
|
||||
|
||||
header {
|
||||
padding: 13px 20px; background: #1e293b; border-bottom: 1px solid #334155;
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 12px 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header a { color: #64748b; text-decoration: none; font-size: 12px; }
|
||||
header a:hover { color: #e2e8f0; }
|
||||
header h1 { font-size: 16px; font-weight: 600; color: #f1f5f9; flex: 1 1 auto; min-width: 0; }
|
||||
.header-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; flex-shrink: 0; }
|
||||
.header-tools .niepal { color: #94a3b8; font-weight: 600; padding: 4px 10px; border-radius: 6px; border: 1px solid #334155; }
|
||||
.header-tools .niepal:hover { color: #e2e8f0; border-color: #475569; }
|
||||
.footer-users { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.user-chip {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: #0f172a; border: 1px solid #334155;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 10px; font-weight: 700; color: #94a3b8;
|
||||
cursor: default; user-select: none;
|
||||
}
|
||||
.user-chip-current { border-color: #2563eb; color: #93c5fd; }
|
||||
.user-chip-approved { color: #94a3b8; }
|
||||
.user-chip-pending { border-style: dashed; opacity: 0.75; color: #64748b; }
|
||||
|
||||
main {
|
||||
display: grid; grid-template-columns: repeat(12, 1fr);
|
||||
gap: 14px; padding: 16px 20px; max-width: 1600px; margin: 0 auto;
|
||||
}
|
||||
.col-3 { grid-column: span 3; }
|
||||
.col-4 { grid-column: span 4; }
|
||||
.col-6 { grid-column: span 6; }
|
||||
.col-12 { grid-column: span 12; }
|
||||
@media (max-width: 1100px) {
|
||||
.col-3,.col-4,.col-6 { grid-column: span 12; }
|
||||
}
|
||||
|
||||
.card { background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 15px; }
|
||||
.card-title {
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .08em; color: #64748b; margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat { text-align: center; padding: 8px 0; }
|
||||
.stat .val { font-size: 32px; font-weight: 700; line-height: 1.1; }
|
||||
.stat .lbl { font-size: 11px; color: #64748b; margin-top: 5px; }
|
||||
|
||||
.green { color: #34d399; }
|
||||
.red { color: #f87171; }
|
||||
.amber { color: #fbbf24; }
|
||||
.blue { color: #60a5fa; }
|
||||
|
||||
/* table */
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th {
|
||||
text-align: left; padding: 6px 8px; font-size: 10px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: .06em; color: #64748b;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
td { padding: 7px 8px; border-bottom: 1px solid #1e293b; color: #cbd5e1; vertical-align: middle; }
|
||||
tr:hover td { background: #0f172a; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
|
||||
/* badges */
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
|
||||
}
|
||||
.badge-ok { background: #064e3b; color: #6ee7b7; }
|
||||
.badge-fail { background: #4c0519; color: #fca5a5; }
|
||||
|
||||
.empty-msg { color: #475569; font-size: 13px; padding: 20px 0; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="/">← Dashboard</a>
|
||||
<h1>Ansible Runs</h1>
|
||||
<div class="header-tools">
|
||||
<a href="/niepalenie" class="niepal" title="Niepalenie">🚭 Niepalenie</a>
|
||||
{% include 'partials/nav_user_chips.html' %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- stat cards -->
|
||||
<div class="card col-3">
|
||||
<div class="card-title">Total Runs</div>
|
||||
<div class="stat">
|
||||
<div class="val blue" id="st-total">-</div>
|
||||
<div class="lbl">runs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card-title">Success Rate</div>
|
||||
<div class="stat">
|
||||
<div class="val green" id="st-rate">-</div>
|
||||
<div class="lbl">percent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card-title">Failed Runs</div>
|
||||
<div class="stat">
|
||||
<div class="val red" id="st-failed">-</div>
|
||||
<div class="lbl">runs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card-title">Avg Duration</div>
|
||||
<div class="stat">
|
||||
<div class="val amber" id="st-avg">-</div>
|
||||
<div class="lbl">seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- recent runs table -->
|
||||
<div class="card col-12">
|
||||
<div class="card-title">Recent Runs (last 50)</div>
|
||||
<div id="runs-wrap">
|
||||
<div class="empty-msg">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- per-playbook breakdown -->
|
||||
<div class="card col-12">
|
||||
<div class="card-title">Per-Playbook Breakdown</div>
|
||||
<div id="pb-wrap">
|
||||
<div class="empty-msg">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function fmtTs(ts) {
|
||||
if (!ts) return '-';
|
||||
return ts.replace('T', ' ');
|
||||
}
|
||||
|
||||
function fmtDur(s) {
|
||||
if (s == null) return '-';
|
||||
if (s < 60) return s.toFixed(1) + 's';
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = (s % 60).toFixed(0);
|
||||
return m + 'm ' + sec + 's';
|
||||
}
|
||||
|
||||
function renderRuns(runs) {
|
||||
if (!runs || runs.length === 0) {
|
||||
return '<div class="empty-msg">Brak danych - uruchom playbooka</div>';
|
||||
}
|
||||
const rows = runs.map(r => {
|
||||
const hostNames = Object.keys(r.hosts || {}).join(', ') || '-';
|
||||
const badge = r.status === 'success'
|
||||
? '<span class="badge badge-ok">success</span>'
|
||||
: '<span class="badge badge-fail">failed</span>';
|
||||
return `<tr>
|
||||
<td>${fmtTs(r.ts)}</td>
|
||||
<td>${r.playbook}</td>
|
||||
<td>${badge}</td>
|
||||
<td>${fmtDur(r.duration)}</td>
|
||||
<td>${hostNames}</td>
|
||||
<td>${r.ok}</td>
|
||||
<td>${r.changed}</td>
|
||||
<td>${r.failed}</td>
|
||||
<td>${r.tasks_count}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
return `<table>
|
||||
<thead><tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Playbook</th>
|
||||
<th>Status</th>
|
||||
<th>Duration</th>
|
||||
<th>Hosts</th>
|
||||
<th>OK</th>
|
||||
<th>Changed</th>
|
||||
<th>Failed</th>
|
||||
<th>Tasks</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function renderByPlaybook(bp) {
|
||||
const keys = Object.keys(bp || {});
|
||||
if (keys.length === 0) {
|
||||
return '<div class="empty-msg">Brak danych - uruchom playbooka</div>';
|
||||
}
|
||||
const rows = keys.sort().map(pb => {
|
||||
const d = bp[pb];
|
||||
return `<tr>
|
||||
<td>${pb}</td>
|
||||
<td>${d.count}</td>
|
||||
<td>${d.failures}</td>
|
||||
<td>${fmtTs(d.last_run)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
return `<table>
|
||||
<thead><tr>
|
||||
<th>Playbook</th>
|
||||
<th>Runs</th>
|
||||
<th>Failures</th>
|
||||
<th>Last Run</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function load() {
|
||||
fetch('/api/ansible')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
document.getElementById('runs-wrap').innerHTML =
|
||||
'<div class="empty-msg">Brak danych - uruchom playbooka</div>';
|
||||
document.getElementById('pb-wrap').innerHTML =
|
||||
'<div class="empty-msg">Brak danych - uruchom playbooka</div>';
|
||||
return;
|
||||
}
|
||||
const s = data.stats || {};
|
||||
document.getElementById('st-total').textContent = s.total ?? '-';
|
||||
document.getElementById('st-rate').textContent = (s.success_rate ?? '-') + '%';
|
||||
document.getElementById('st-failed').textContent = s.failed_count ?? '-';
|
||||
document.getElementById('st-avg').textContent = s.avg_duration != null ? fmtDur(s.avg_duration) : '-';
|
||||
|
||||
document.getElementById('runs-wrap').innerHTML = renderRuns(data.runs);
|
||||
document.getElementById('pb-wrap').innerHTML = renderByPlaybook(data.by_playbook);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
document.getElementById('runs-wrap').innerHTML =
|
||||
'<div class="empty-msg">Error loading data</div>';
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
setInterval(load, 60000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🏠 HA Dashboard — sebson.space</h1>
|
||||
<span id="refresh-info">Refresh in <span id="countdown">60</span>s</span>
|
||||
<div class="header-links">
|
||||
<a href="/niepalenie" style="color:#94a3b8;text-decoration:none;font-weight:600;font-size:12px;padding:4px 10px;border-radius:6px;border:1px solid #334155;" title="Niepalenie">🚭 Niepalenie</a>
|
||||
<a href="/ansible" style="color:#60a5fa;text-decoration:none;font-weight:600;font-size:12px;background:#1e3a5f;padding:4px 10px;border-radius:6px;border:1px solid #2563eb;">🚀 Ansible</a>
|
||||
<span id="refresh-info">Refresh in <span id="countdown">60</span>s</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<div class="card col-12 quick-links-card" aria-label="Shortcuts">
|
||||
<span class="quick-lbl">Shortcuts</span>
|
||||
<a href="/niepalenie" class="quick-niepal">🚭 Niepalenie</a>
|
||||
{% include 'partials/nav_user_chips.html' %}
|
||||
</div>
|
||||
|
||||
<!-- ═══ ROW 1: Persons / Mazda / Openings / Backup ═══ -->
|
||||
<div class="card col-3">
|
||||
<div class="card-title">👤 Who's home</div>
|
||||
|
|
@ -300,22 +357,27 @@
|
|||
<div class="lights-grid" id="lights-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ ROW 3: Temperatures / Power / Crowdsec ═══ -->
|
||||
<div class="card col-6">
|
||||
<!-- ═══ ROW 3: Temperatures / Power / Crowdsec / Ansible ═══ -->
|
||||
<div class="card col-5">
|
||||
<div class="card-title">🌡 Room temperatures</div>
|
||||
<div class="temp-grid" id="temps-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card-title">⚡ Power consumption</div>
|
||||
<div class="card col-2">
|
||||
<div class="card-title">⚡ Power</div>
|
||||
<div id="power-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card col-2">
|
||||
<div class="card-title">🛡 Crowdsec</div>
|
||||
<div id="crowdsec-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="card col-3">
|
||||
<div class="card-title">🚀 Ansible last runs</div>
|
||||
<div id="ansible-summary-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ ROW 4: Frigate / Containers ═══ -->
|
||||
<div class="card col-8">
|
||||
<div class="card-title">📷 Frigate — last detections</div>
|
||||
|
|
@ -388,6 +450,11 @@
|
|||
|
||||
</main>
|
||||
|
||||
<footer class="site-footer" aria-label="Shortcuts and accounts">
|
||||
<a href="/niepalenie" class="footer-niepalenie" title="Niepalenie">🚭 Niepalenie</a>
|
||||
{% include 'partials/nav_user_chips.html' %}
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const PAL = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ef4444','#06b6d4','#ec4899','#84cc16','#f97316','#a78bfa'];
|
||||
let dbSizeChart, autoChart, bwChart;
|
||||
|
|
@ -821,12 +888,35 @@ async function loadZigbee() {
|
|||
<div class="zb-list">${rows}</div>`;
|
||||
}
|
||||
|
||||
// ── Ansible ──────────────────────────────────────────────────────────────────
|
||||
async function loadAnsibleSummary() {
|
||||
const res = await fetch('/api/ansible');
|
||||
const d = await res.json();
|
||||
if (d.error) { $('ansible-summary-content').innerHTML = `<div style="color:#475569;font-size:12px">${d.error}</div>`; return; }
|
||||
if (!d.runs || !d.runs.length) { $('ansible-summary-content').innerHTML = '<div style="color:#475569;font-size:12px">No runs yet</div>'; return; }
|
||||
|
||||
$('ansible-summary-content').innerHTML = d.runs.slice(0, 4).map(r => {
|
||||
const statusColor = r.status === 'success' ? '#34d399' : '#f87171';
|
||||
return `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid #1e293b;font-size:11px;">
|
||||
<div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;">
|
||||
<span style="color:${statusColor}">●</span>
|
||||
<span style="color:#cbd5e1;font-weight:500;">${r.playbook}</span>
|
||||
</div>
|
||||
<div style="color:#64748b;white-space:nowrap;">${timeAgo(r.ts)}</div>
|
||||
</div>`;
|
||||
}).join('') + `
|
||||
<div style="margin-top:10px;text-align:right;">
|
||||
<a href="/ansible" style="color:#60a5fa;text-decoration:none;font-size:10px;text-transform:uppercase;font-weight:700;letter-spacing:.05em;">View full history →</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Load all ──────────────────────────────────────────────────────────────────
|
||||
async function loadAll() {
|
||||
await Promise.all([
|
||||
loadOverview(), loadHome(), loadPower(), loadFrigate(),
|
||||
loadCrowdsec(), loadContainers(), loadDB(), loadAutomations(), loadBitwarden(),
|
||||
loadZigbee(),
|
||||
loadZigbee(), loadAnsibleSummary(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,30 +39,184 @@
|
|||
letter-spacing: .08em; color: #64748b; margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ── HERO ── */
|
||||
/* ── HERO — akcent BMW R 1250 GSA (Rallye: lód / czerwień / niebieski + złoty widelec) ── */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f2027 50%, #1e293b 100%);
|
||||
border: 1px solid #334155; border-radius: 10px; padding: 28px 24px;
|
||||
text-align: center; position: relative; overflow: hidden;
|
||||
background:
|
||||
linear-gradient(168deg, rgba(26, 29, 34, 0.97) 0%, #0c0f14 48%, #12161c 100%);
|
||||
border: 1px solid #2a313a;
|
||||
border-radius: 12px;
|
||||
padding: 20px 16px 22px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.hero::before {
|
||||
content: '🚭';
|
||||
position: absolute; font-size: 120px; opacity: .04;
|
||||
top: 50%; left: 50%; transform: translate(-50%,-50%);
|
||||
pointer-events: none; user-select: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(115deg, transparent 40%, rgba(227, 6, 19, 0.06) 50%, transparent 60%),
|
||||
linear-gradient(-25deg, transparent 55%, rgba(66, 152, 230, 0.05) 65%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .12em; color: #64748b; margin-bottom: 6px; }
|
||||
.hero-main { font-size: 15px; font-weight: 700; color: #f1f5f9; margin-bottom: 18px; }
|
||||
.hero-main span { color: #f87171; }
|
||||
.gsa-strip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.gsa-strip span {
|
||||
height: 4px;
|
||||
width: 40px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.gsa-strip .gsa-red { background: linear-gradient(90deg, #9a0510, #e30613); }
|
||||
.gsa-strip .gsa-white { background: linear-gradient(90deg, #cbd5e1, #f8fafc); }
|
||||
.gsa-strip .gsa-blue { background: linear-gradient(90deg, #154a86, #4298e6); }
|
||||
|
||||
/* Statystyki tylko drugiego okresu — bardzo małe */
|
||||
.track2-mini {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(66, 152, 230, 0.12);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.track2-mini-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px 10px;
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.track2-mini-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.track2-cell {
|
||||
background: rgba(12, 14, 18, 0.65);
|
||||
border: 1px solid rgba(66, 152, 230, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.track2-k {
|
||||
display: block;
|
||||
font-size: 7px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #475569;
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.track2-num {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.15;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.track2-num.amber { color: #fbbf24; }
|
||||
.track2-num.green { color: #34d399; }
|
||||
.track2-num.violet{ color: #a78bfa; }
|
||||
.track2-sub {
|
||||
display: block;
|
||||
font-size: 8px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
color: #64748b;
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.hero-main {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #e8eef4;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.hero-main span { color: #e30613; text-shadow: 0 0 20px rgba(227, 6, 19, 0.25); }
|
||||
.counter-row {
|
||||
display: flex; justify-content: center; gap: 16px; flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px 10px;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.counter-unit {
|
||||
background: rgba(15,23,42,.7); border: 1px solid #334155; border-radius: 10px;
|
||||
padding: 14px 20px; min-width: 90px;
|
||||
min-width: 54px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, rgba(34, 39, 48, 0.95), rgba(12, 14, 18, 0.92));
|
||||
border: 1px solid rgba(201, 162, 39, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(227, 6, 19, 0.1),
|
||||
0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
transition: border-color 0.25s, box-shadow 0.25s;
|
||||
}
|
||||
.counter-val { font-size: 36px; font-weight: 700; color: #f1f5f9; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.counter-lbl { font-size: 10px; color: #64748b; margin-top: 5px; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.counter-unit:hover {
|
||||
border-color: rgba(201, 162, 39, 0.65);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(66, 152, 230, 0.15),
|
||||
0 8px 22px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.counter-unit--track2 {
|
||||
border-color: rgba(66, 152, 230, 0.38);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(66, 152, 230, 0.12),
|
||||
0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.counter-unit--track2:hover {
|
||||
border-color: rgba(96, 172, 240, 0.55);
|
||||
}
|
||||
.counter-val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #eef2f7;
|
||||
background: linear-gradient(185deg, #ffffff 0%, #c5ced8 42%, #8b98a8 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
|
||||
.counter-val { color: #e8eef4; }
|
||||
}
|
||||
.counter-lbl {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
}
|
||||
|
||||
.hero-split {
|
||||
border-top: 1px solid rgba(66, 152, 230, 0.15);
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.hero-split .hero-title { color: #64748b; }
|
||||
.hero-split .hero-main { font-size: 12px; margin-bottom: 12px; }
|
||||
.hero-split .hero-main span { color: #60a5fa; text-shadow: 0 0 18px rgba(96, 165, 250, 0.2); }
|
||||
|
||||
/* ── STAT CARDS ── */
|
||||
.stat-big { text-align: center; padding: 8px 0; }
|
||||
|
|
@ -119,6 +273,30 @@
|
|||
.savings-row:last-child { border-bottom: none; }
|
||||
.savings-row .sr-label { color: #94a3b8; }
|
||||
.savings-row .sr-val { color: #fbbf24; font-weight: 600; }
|
||||
|
||||
/* Unix (s) — rogi paneli z licznikami */
|
||||
.counter-unix-panel {
|
||||
position: relative;
|
||||
padding: 16px 2px 18px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.counter-unix-panel--track2 .unix-pin { color: #5c7cab; }
|
||||
.unix-pin {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
font-family: ui-monospace, "Cascadia Code", "Consolas", monospace;
|
||||
font-size: 7px;
|
||||
line-height: 1.15;
|
||||
color: #64748b;
|
||||
opacity: 0.55;
|
||||
letter-spacing: 0.02em;
|
||||
max-width: 40%;
|
||||
word-break: break-all;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.unix-pin.unix-tl { top: 0; left: 4px; text-align: left; }
|
||||
.unix-pin.unix-br { bottom: 0; right: 4px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -132,28 +310,88 @@
|
|||
|
||||
<!-- HERO counter -->
|
||||
<div class="hero col-12">
|
||||
<div class="gsa-strip" aria-hidden="true">
|
||||
<span class="gsa-red"></span><span class="gsa-white"></span><span class="gsa-blue"></span>
|
||||
</div>
|
||||
<div class="hero-title">Rzuciłem palenie</div>
|
||||
<div class="hero-main">23 sierpnia 2011 · <span>z przerwą w 2021</span></div>
|
||||
<div class="counter-row">
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-lat">–</div>
|
||||
<div class="counter-lbl">lat</div>
|
||||
<div class="counter-unix-panel" aria-hidden="true">
|
||||
<span class="unix-pin unix-tl" id="uc-box-r1" title=""><span id="uc-r1"></span></span>
|
||||
<span class="unix-pin unix-br" id="uc-box-now1" title=""><span id="uc-now1"></span></span>
|
||||
<div class="counter-row">
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-lat">–</div>
|
||||
<div class="counter-lbl">lat</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-dni">–</div>
|
||||
<div class="counter-lbl">dni</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-godz">–</div>
|
||||
<div class="counter-lbl">godzin</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-min">–</div>
|
||||
<div class="counter-lbl">minut</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-sek">–</div>
|
||||
<div class="counter-lbl">sekund</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-dni">–</div>
|
||||
<div class="counter-lbl">dni</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-split">
|
||||
<div class="hero-title">Od drugiego rzucenia</div>
|
||||
<div class="hero-main">25 grudnia 2021 · <span>tylko czas po drugim rzuceniu</span></div>
|
||||
<div class="counter-unix-panel counter-unix-panel--track2" aria-hidden="true">
|
||||
<span class="unix-pin unix-tl" id="uc-box-r2" title=""><span id="uc-r2"></span></span>
|
||||
<span class="unix-pin unix-br" id="uc-box-now2" title=""><span id="uc-now2"></span></span>
|
||||
<div class="counter-row">
|
||||
<div class="counter-unit counter-unit--track2">
|
||||
<div class="counter-val" id="c2-lat">–</div>
|
||||
<div class="counter-lbl">lat</div>
|
||||
</div>
|
||||
<div class="counter-unit counter-unit--track2">
|
||||
<div class="counter-val" id="c2-dni">–</div>
|
||||
<div class="counter-lbl">dni</div>
|
||||
</div>
|
||||
<div class="counter-unit counter-unit--track2">
|
||||
<div class="counter-val" id="c2-godz">–</div>
|
||||
<div class="counter-lbl">godzin</div>
|
||||
</div>
|
||||
<div class="counter-unit counter-unit--track2">
|
||||
<div class="counter-val" id="c2-min">–</div>
|
||||
<div class="counter-lbl">minut</div>
|
||||
</div>
|
||||
<div class="counter-unit counter-unit--track2">
|
||||
<div class="counter-val" id="c2-sek">–</div>
|
||||
<div class="counter-lbl">sekund</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-godz">–</div>
|
||||
<div class="counter-lbl">godzin</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-min">–</div>
|
||||
<div class="counter-lbl">minut</div>
|
||||
</div>
|
||||
<div class="counter-unit">
|
||||
<div class="counter-val" id="c-sek">–</div>
|
||||
<div class="counter-lbl">sekund</div>
|
||||
<div class="track2-mini" aria-label="Statystyki tylko od drugiego rzucenia">
|
||||
<div class="track2-mini-grid">
|
||||
<div class="track2-cell">
|
||||
<span class="track2-k">Zaoszczędzono</span>
|
||||
<span class="track2-num amber" id="t2-zaosz">–</span>
|
||||
</div>
|
||||
<div class="track2-cell">
|
||||
<span class="track2-k">Nie wypalono</span>
|
||||
<span class="track2-num green" id="t2-pap">–</span>
|
||||
<span class="track2-sub" id="t2-pacz"></span>
|
||||
</div>
|
||||
<div class="track2-cell">
|
||||
<span class="track2-k">Średnio / tydzień</span>
|
||||
<span class="track2-num" id="t2-tydz">–</span>
|
||||
</div>
|
||||
<div class="track2-cell">
|
||||
<span class="track2-k">Następny kamień</span>
|
||||
<span class="track2-num violet" id="t2-jub">–</span>
|
||||
<span class="track2-sub" id="t2-jubza"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -433,8 +671,16 @@ const _TODAY_START = (function() {
|
|||
const d = new Date(); d.setHours(0,0,0,0); return d;
|
||||
})();
|
||||
const _SAVINGS_TO_TODAY = integrateSavings(RZUCONE_DATE, _TODAY_START);
|
||||
const _SAV2_TO_TODAY = integrateSegment(NAWROT_KONIEC, _TODAY_START);
|
||||
const _COST_PER_SEC_NOW = (DZIENNIE / W_PACZCE) * CURRENT_PRICE / SEKUND_DOBY;
|
||||
|
||||
function currentSavingsTrack2(now) {
|
||||
if (now < NAWROT_KONIEC) return 0;
|
||||
const ts = Math.max(_TODAY_START.getTime(), NAWROT_KONIEC.getTime());
|
||||
const secs = (now.getTime() - ts) / 1000;
|
||||
return _SAV2_TO_TODAY + Math.max(0, secs) * _COST_PER_SEC_NOW;
|
||||
}
|
||||
|
||||
function currentSavings() {
|
||||
const now = new Date();
|
||||
// Dzisiaj w nawrocie? (historycznie niemożliwe, ale dla poprawności)
|
||||
|
|
@ -612,6 +858,14 @@ function jubInfo(totalDays) {
|
|||
|
||||
function update() {
|
||||
const now = new Date();
|
||||
const tsNow = Math.floor(now.getTime() / 1000);
|
||||
const nowIso = now.toISOString();
|
||||
const nowTitle = 'Teraz: ' + nowIso + ' → ' + tsNow;
|
||||
document.getElementById('uc-now1').textContent = String(tsNow);
|
||||
document.getElementById('uc-now2').textContent = String(tsNow);
|
||||
document.getElementById('uc-box-now1').title = nowTitle;
|
||||
document.getElementById('uc-box-now2').title = nowTitle;
|
||||
|
||||
const st = calcStats(now);
|
||||
const d = st.d;
|
||||
const s = st.s;
|
||||
|
|
@ -624,6 +878,34 @@ function update() {
|
|||
document.getElementById('c-min').textContent = String(dc.min).padStart(2, '0');
|
||||
document.getElementById('c-sek').textContent = String(dc.sek).padStart(2, '0');
|
||||
|
||||
// Drugie rzucenie — ten sam układ co główny licznik, tylko czas od końca nawrotu (25.12.2021)
|
||||
const s2 = Math.max(0, (now - NAWROT_KONIEC) / 1000);
|
||||
const dc2 = decompose(s2);
|
||||
document.getElementById('c2-lat').textContent = dc2.lat;
|
||||
document.getElementById('c2-dni').textContent = String(dc2.dni).padStart(3, ' ');
|
||||
document.getElementById('c2-godz').textContent = String(dc2.godz).padStart(2, '0');
|
||||
document.getElementById('c2-min').textContent = String(dc2.min).padStart(2, '0');
|
||||
document.getElementById('c2-sek').textContent = String(dc2.sek).padStart(2, '0');
|
||||
|
||||
// Mini statystyki wyłącznie za drugi okres (od końca nawrotu)
|
||||
const d2 = st.o2 / SEKUND_DOBY;
|
||||
const sav2 = currentSavingsTrack2(now);
|
||||
const pap2 = Math.floor(DZIENNIE * d2);
|
||||
document.getElementById('t2-zaosz').textContent = fmtPln(sav2);
|
||||
document.getElementById('t2-pap').textContent = fmt(pap2);
|
||||
const pacz2 = Math.floor(pap2 / W_PACZCE);
|
||||
document.getElementById('t2-pacz').textContent = pacz2 > 0 ? '≈ ' + fmt(pacz2) + ' paczek' : '';
|
||||
const tyg2 = st.o2 / (7 * SEKUND_DOBY);
|
||||
document.getElementById('t2-tydz').textContent =
|
||||
tyg2 >= 1 ? fmtPln(sav2 / tyg2) : '—';
|
||||
const j2 = jubInfo(d2);
|
||||
const lastM = MILESTONES[MILESTONES.length - 1][0];
|
||||
document.getElementById('t2-jub').textContent = j2.label;
|
||||
let jubza = '';
|
||||
if (j2.remDays > 0) jubza = 'za ' + Math.ceil(j2.remDays) + ' dni';
|
||||
else if (d2 >= lastM) jubza = 'kamienie ✓';
|
||||
document.getElementById('t2-jubza').textContent = jubza;
|
||||
|
||||
const zaoszcz = currentSavings();
|
||||
const nieWypalil = Math.floor(DZIENNIE * d);
|
||||
|
||||
|
|
@ -696,6 +978,16 @@ async function fetchCzkRate() {
|
|||
}
|
||||
|
||||
// ── Init
|
||||
(function initUnixCorners() {
|
||||
const ts1 = Math.floor(RZUCONE_DATE.getTime() / 1000);
|
||||
const ts2 = Math.floor(NAWROT_KONIEC.getTime() / 1000);
|
||||
document.getElementById('uc-r1').textContent = String(ts1);
|
||||
document.getElementById('uc-r2').textContent = String(ts2);
|
||||
const iso1 = RZUCONE_DATE.toISOString();
|
||||
const iso2 = NAWROT_KONIEC.toISOString();
|
||||
document.getElementById('uc-box-r1').title = 'Pierwsze rzucenie: ' + iso1 + ' → ' + ts1;
|
||||
document.getElementById('uc-box-r2').title = 'Drugie rzucenie: ' + iso2 + ' → ' + ts2;
|
||||
})();
|
||||
update();
|
||||
buildSavingsChart();
|
||||
fetchCzkRate();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{% if nav_user_chips %}
|
||||
<span class="footer-users" aria-label="Dashboard accounts">
|
||||
{% for chip in nav_user_chips %}
|
||||
<span class="user-chip user-chip-{{ chip.role }}" title="{% if chip.role == 'pending' %}{{ chip.name }} (pending approval){% elif chip.role == 'current' %}{{ chip.name }} (signed in){% else %}{{ chip.name }}{% endif %}">{{ (chip.name[0] if chip.name else '?')|upper }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
@ -10,7 +10,7 @@ Broker MQTT bedacy magistrala komunikacyjna dla calego systemu smart home - lacz
|
|||
|
||||
## Konfiguracja
|
||||
|
||||
- Obraz: `eclipse-mosquitto`
|
||||
- Obraz: `eclipse-mosquitto:2` (seria 2.x; unikamy niejawnego `:latest`)
|
||||
- Siec: `network_mode: host`
|
||||
- Plik konfiguracyjny: `./mosquitto/config/mosquitto.conf`
|
||||
- Hasla: `./mosquitto/config/mosquitto.passwd` (format bcrypt)
|
||||
|
|
@ -19,7 +19,9 @@ Broker MQTT bedacy magistrala komunikacyjna dla calego systemu smart home - lacz
|
|||
|
||||
Kluczowe ustawienia:
|
||||
- `allow_anonymous false` - wymagane uwierzytelnienie
|
||||
- `persistence false` - brak persystencji wiadomosci (wystarczajace dla QoS 0)
|
||||
- `persistence false` - broker nie zapisuje retajnowanych wiadomości ani stanu na dysku (po restarcie znikają retained; dla typowego HA + czujników często OK)
|
||||
- `persistence_location` wskazuje na volume `/mosquitto/data` (gdyby włączyć `persistence true`)
|
||||
- Opcjonalne fragmenty konfiguracji: katalog `config/conf.d/*.conf` (ładowane przez `include_dir`)
|
||||
- `persistent_client_expiration 3m` - klienci offline usuwani po 3 minutach
|
||||
- `max_inflight_messages 40`
|
||||
- Logowane tylko bledy, ostrzezenia i powiadomienia (nie kazde polaczenie)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# Place your local configuration in /config/conf.d/
|
||||
# Place your local configuration in /mosquitto/config/conf.d/
|
||||
include_dir /mosquitto/config/conf.d/
|
||||
sys_interval 10
|
||||
user mosquitto
|
||||
max_inflight_messages 40
|
||||
|
|
@ -11,7 +12,7 @@ autosave_on_changes false
|
|||
|
||||
persistence false
|
||||
persistence_file mosquitto.db
|
||||
persistence_location /config/data/
|
||||
persistence_location /mosquitto/data/
|
||||
|
||||
# Port to use for the default listener.
|
||||
listener 1883
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
25:3f:e2:43:15:b6:48:b4:41:6b:ac:df:96:01:fd:bc:79:28:51:45
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN = postgresql18
|
||||
Validity
|
||||
Not Before: Apr 10 21:59:52 2026 GMT
|
||||
Not After : Apr 7 21:59:52 2036 GMT
|
||||
Subject: CN = postgresql18
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:bf:2a:65:36:f6:81:b7:ff:5a:32:6a:b3:0e:3f:
|
||||
47:73:18:80:9e:0d:db:8c:53:e8:54:1a:45:a9:a5:
|
||||
73:d2:2b:e2:3c:97:90:43:0e:41:18:42:b7:b2:19:
|
||||
b4:da:9e:68:93:b0:35:69:1f:0d:ea:82:5e:bf:b1:
|
||||
b3:de:4a:7f:4e:51:88:1b:12:43:97:01:04:4a:76:
|
||||
0b:a2:69:30:1c:c9:cc:38:9e:f6:4a:95:cc:85:9a:
|
||||
49:34:5f:c9:34:7e:d7:85:90:f9:26:37:e5:0b:e8:
|
||||
03:1a:21:c8:33:e6:81:dd:43:61:12:5c:27:da:b6:
|
||||
fa:26:24:ad:49:8b:a9:9c:a0:b9:6a:e9:51:f8:22:
|
||||
00:74:24:5b:56:11:77:08:f3:e3:56:d9:7c:0a:ac:
|
||||
ee:72:f6:2e:b7:7e:42:59:fa:02:18:f1:6a:1d:8a:
|
||||
52:f8:51:d3:22:c5:5e:81:2a:f9:de:ec:98:82:fd:
|
||||
b3:1a:7f:fb:04:67:9a:95:75:f8:5d:af:41:ff:6a:
|
||||
50:9b:9b:d4:ac:99:f6:0d:0d:83:af:34:26:be:e8:
|
||||
cc:69:09:19:f4:39:7c:06:6e:ee:53:20:15:7e:eb:
|
||||
5d:6e:c7:39:97:f5:7a:99:99:c3:70:98:ce:6b:c5:
|
||||
49:0b:d1:d7:dc:3c:00:30:64:6a:31:5a:9c:9b:51:
|
||||
30:cf
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
90:91:2B:C8:3C:DD:A0:A3:9C:45:D8:ED:08:9E:28:E1:D2:CC:43:AB
|
||||
X509v3 Authority Key Identifier:
|
||||
90:91:2B:C8:3C:DD:A0:A3:9C:45:D8:ED:08:9E:28:E1:D2:CC:43:AB
|
||||
X509v3 Basic Constraints: critical
|
||||
CA:TRUE
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
61:74:11:ff:d8:62:d3:81:54:a3:58:d4:40:57:9c:15:26:72:
|
||||
32:b4:25:01:98:49:14:0f:65:4d:a9:e6:0e:88:6d:d1:5b:2d:
|
||||
a8:04:4d:11:c7:00:d8:20:bd:30:11:73:33:78:1d:e0:e0:98:
|
||||
f8:18:d4:b4:c8:90:ae:9c:cf:70:d0:86:bc:13:2d:61:bb:42:
|
||||
89:e5:95:ca:fd:0b:7b:00:73:1a:f9:ab:84:14:c8:07:9e:dd:
|
||||
9c:aa:70:a7:25:34:27:0e:5e:5a:d6:51:38:3c:66:ae:00:9a:
|
||||
2c:2e:f2:be:14:1d:99:99:8c:c8:98:47:94:0f:72:5e:44:b6:
|
||||
a4:c6:8d:c2:7e:8b:6a:ee:b3:87:73:64:69:ef:78:47:d9:df:
|
||||
1a:fb:8e:0e:a6:60:13:70:46:14:0a:d0:28:69:89:88:a9:77:
|
||||
6e:1d:44:b1:d8:b6:f2:40:62:1a:c8:c1:28:af:30:22:52:87:
|
||||
c2:b6:14:1e:3e:39:bf:e9:25:f5:16:72:b4:db:7b:62:a2:e0:
|
||||
d8:b0:bc:99:bb:fa:8d:8f:08:f9:fa:20:c7:c0:c8:37:b9:07:
|
||||
af:ca:63:04:f6:84:91:b5:82:04:14:99:a9:05:c6:a8:35:72:
|
||||
45:76:72:62:e4:ab:7c:e4:67:c5:d9:db:68:60:fe:ca:a5:a8:
|
||||
a7:b0:8b:d1
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDDzCCAfegAwIBAgIUJT/iQxW2SLRBa6zflgH9vHkoUUUwDQYJKoZIhvcNAQEL
|
||||
BQAwFzEVMBMGA1UEAwwMcG9zdGdyZXNxbDE4MB4XDTI2MDQxMDIxNTk1MloXDTM2
|
||||
MDQwNzIxNTk1MlowFzEVMBMGA1UEAwwMcG9zdGdyZXNxbDE4MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyplNvaBt/9aMmqzDj9HcxiAng3bjFPoVBpF
|
||||
qaVz0iviPJeQQw5BGEK3shm02p5ok7A1aR8N6oJev7Gz3kp/TlGIGxJDlwEESnYL
|
||||
omkwHMnMOJ72SpXMhZpJNF/JNH7XhZD5JjflC+gDGiHIM+aB3UNhElwn2rb6JiSt
|
||||
SYupnKC5aulR+CIAdCRbVhF3CPPjVtl8CqzucvYut35CWfoCGPFqHYpS+FHTIsVe
|
||||
gSr53uyYgv2zGn/7BGealXX4Xa9B/2pQm5vUrJn2DQ2DrzQmvujMaQkZ9Dl8Bm7u
|
||||
UyAVfutdbsc5l/V6mZnDcJjOa8VJC9HX3DwAMGRqMVqcm1EwzwIDAQABo1MwUTAd
|
||||
BgNVHQ4EFgQUkJEryDzdoKOcRdjtCJ4o4dLMQ6swHwYDVR0jBBgwFoAUkJEryDzd
|
||||
oKOcRdjtCJ4o4dLMQ6swDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAYXQR/9hi04FUo1jUQFecFSZyMrQlAZhJFA9lTanmDoht0VstqARNEccA2CC9
|
||||
MBFzM3gd4OCY+BjUtMiQrpzPcNCGvBMtYbtCieWVyv0LewBzGvmrhBTIB57dnKpw
|
||||
pyU0Jw5eWtZRODxmrgCaLC7yvhQdmZmMyJhHlA9yXkS2pMaNwn6Lau6zh3Nkae94
|
||||
R9nfGvuODqZgE3BGFArQKGmJiKl3bh1Esdi28kBiGsjBKK8wIlKHwrYUHj45v+kl
|
||||
9RZytNt7YqLg2LC8mbv6jY8I+fogx8DIN7kHr8pjBPaEkbWCBBSZqQXGqDVyRXZy
|
||||
YuSrfORnxdnbaGD+yqWop7CL0Q==
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -7,9 +7,8 @@ type: public
|
|||
encoding: UTF-8
|
||||
followredirect: true
|
||||
links:
|
||||
- https://torrentqq414.com/
|
||||
- https://torrentqq415.com/
|
||||
legacylinks:
|
||||
- https://torrentqq399.com/
|
||||
- https://torrentqq400.com/
|
||||
- https://torrentqq401.com/
|
||||
- https://torrentqq402.com/
|
||||
|
|
@ -25,6 +24,7 @@ legacylinks:
|
|||
- https://torrentegg87.com/
|
||||
- https://torrentqq412.com/
|
||||
- https://torrentqq413.com/
|
||||
- https://torrentqq414.com/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ caps:
|
|||
MiniVideo: Other # MiniVideo
|
||||
补充音轨: Audio # Supplemental audio tracks
|
||||
iPhone/iPad视频: PC/Mobile-Other # IPhone / iPad video
|
||||
动漫原盘: Movies/Other # Anime BluRay Original disc
|
||||
# 单机游戏
|
||||
PC: PC/Games # Games - PC
|
||||
MAC: PC/Mac # Games - MAC
|
||||
|
|
@ -148,7 +149,7 @@ search:
|
|||
inputs:
|
||||
c: M
|
||||
- path: browse.php
|
||||
categories: [1000, 1010, 1020, 1030, 1040, 1050, 1060, 1070, 1080, 1090, 1100, 1110, 1120, 1130, 1140, 1150, 1160, 1170, 1180, 4000, 4010, 4020, 4030, 4040, 4050, 4060, 4070, 7000, 7010, 7020, 7030, 7040, 7050, 7060]
|
||||
categories: [1000, 1010, 1020, 1030, 1040, 1050, 1080, 1090, 1120, 1130, 1180, 4000, 4030, 4040, 4050, 4060, 7000, 7020]
|
||||
inputs:
|
||||
c: G
|
||||
inputs:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ caps:
|
|||
MiniVideo: Other # MiniVideo
|
||||
补充音轨: Audio # Supplemental audio tracks
|
||||
iPhone/iPad视频: PC/Mobile-Other # IPhone / iPad video
|
||||
动漫原盘: Movies/Other # Anime BluRay Original disc
|
||||
# 单机游戏
|
||||
PC: PC/Games # Games - PC
|
||||
MAC: PC/Mac # Games - MAC
|
||||
|
|
@ -113,7 +114,7 @@ search:
|
|||
inputs:
|
||||
c: M
|
||||
- path: browse.php
|
||||
categories: [1000, 1010, 1020, 1030, 1040, 1050, 1060, 1070, 1080, 1090, 1100, 1110, 1120, 1130, 1140, 1150, 1160, 1170, 1180, 4000, 4010, 4020, 4030, 4040, 4050, 4060, 4070, 7000, 7010, 7020, 7030, 7040, 7050, 7060]
|
||||
categories: [1000, 1010, 1020, 1030, 1040, 1050, 1080, 1090, 1120, 1130, 1180, 4000, 4030, 4040, 4050, 4060, 7000, 7020]
|
||||
inputs:
|
||||
c: G
|
||||
inputs:
|
||||
|
|
|
|||
Loading…
Reference in New Issue