docs(duckdns): Update status and sync to Notion

- Added analysis of latest DNS monitor logs (06.01.2026).

- Refactored sync_docs_to_notion.py to be generic and support multiple files.
This commit is contained in:
2026-01-06 20:40:04 +00:00
parent 1742b1c83b
commit bc1cff825a
2 changed files with 51 additions and 65 deletions

View File

@@ -60,6 +60,23 @@ docker logs dns-monitor
``` ```
Häufig verursacht durch DSL-Resyncs oder Paketverlust wegen der Stichleitung. Häufig verursacht durch DSL-Resyncs oder Paketverlust wegen der Stichleitung.
## Aktueller Status (06.01.2026)
Basierend auf den Logs des `dns-monitor` (Stand 20:34 Uhr) ergibt sich folgendes Bild:
### 1. Analyse der Log-Phasen
* **Vormittag/Nachmittag:** Sporadische `[ALERT]`-Meldungen. Die IP bei DuckDNS sprang kurzzeitig auf die veraltete Endung `.49` zurück, stabilisierte sich aber meist nach 5-10 Minuten wieder auf `.252`.
* **Kritische Phase (19:38 - 19:54 Uhr):** Ein 20-minütiger anhaltender Alert. Cloudflare (`CF`) lieferte hartnäckig die alte IP `.49`, obwohl die öffentliche IP (`Pub`) bereits `.252` war. Dies deutet auf eine verzögerte TTL-Aktualisierung oder einen "Zombie-Updater" im Netzwerk hin, der kurzzeitig erfolgreich war.
* **Stabilisierung (seit 19:59 Uhr):** Das System ist seit über 30 Minuten durchgehend im Status `[OK]`. Sowohl Cloudflare als auch die lokale Auflösung zeigen stabil auf die `.252`.
### 2. Erkenntnisse
* **Netzwerk-Instabilität:** Die `[NETWORK_WAIT]` Einträge korrelieren mit den DSL-Sync-Problemen der FritzBox. In diesen Momenten ist keine DNS-Abfrage möglich (Paketverlust).
* **Local Cache Lag:** Die Spalte `Loc` (Lokale Auflösung) zeigt oft noch `Unresolved` oder die alte IP, wenn Cloudflare bereits korrekt ist. Dies bestätigt, dass der interne DNS-Cache der Synology/Docker-Umgebung deutlich träger reagiert als externe Server.
### 3. Empfehlung
* **Beobachtung:** Da die Stabilisierung seit 19:59 Uhr anhält, scheint der neue DuckDNS-Token nun die Oberhand gewonnen zu haben.
* **Hardware:** Die physische DSL-Beeinträchtigung (Stichleitung 17m) bleibt das primäre Risiko für die `NETWORK_WAIT` Timeouts.
## Dateien ## Dateien
- `/app/docker-compose.yml`: Definition der Services. - `/app/docker-compose.yml`: Definition der Services.
- `/app/dns-monitor/monitor.sh`: Das Shell-Skript für den Monitor-Container. - `/app/dns-monitor/monitor.sh`: Das Shell-Skript für den Monitor-Container.

View File

@@ -2,10 +2,10 @@ import requests
import json import json
import os import os
import re import re
import sys
TOKEN_FILE = 'notion_api_key.txt' TOKEN_FILE = 'notion_api_key.txt'
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818"
DOC_FILE = "notion_integration.md"
def parse_markdown_to_blocks(md_content): def parse_markdown_to_blocks(md_content):
blocks = [] blocks = []
@@ -15,25 +15,21 @@ def parse_markdown_to_blocks(md_content):
code_content = [] code_content = []
for line in lines: for line in lines:
# Don't strip indentation for code blocks, but do for logic checks
stripped = line.strip() stripped = line.strip()
# Handle Code Blocks
if stripped.startswith("```"): if stripped.startswith("```"):
if in_code_block: if in_code_block:
# End of code block
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "code", "type": "code",
"code": { "code": {
"rich_text": [{"type": "text", "text": {"content": "\n".join(code_content)}}], "rich_text": [{"type": "text", "text": {"content": '\n'.join(code_content)}}],
"language": "plain text" "language": "plain text"
} }
}) })
code_content = [] code_content = []
in_code_block = False in_code_block = False
else: else:
# Start of code block
in_code_block = True in_code_block = True
continue continue
@@ -44,52 +40,38 @@ def parse_markdown_to_blocks(md_content):
if not stripped: if not stripped:
continue continue
# Headings
if line.startswith("# "): if line.startswith("# "):
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "heading_1", "type": "heading_1",
"heading_1": { "heading_1": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}}
"rich_text": [{"type": "text", "text": {"content": line[2:]}}] )
}
})
elif line.startswith("## "): elif line.startswith("## "):
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "heading_2", "type": "heading_2",
"heading_2": { "heading_2": {"rich_text": [{"type": "text", "text": {"content": line[3:]}}]}}
"rich_text": [{"type": "text", "text": {"content": line[3:]}}] )
}
})
elif line.startswith("### "): elif line.startswith("### "):
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "heading_3", "type": "heading_3",
"heading_3": { "heading_3": {"rich_text": [{"type": "text", "text": {"content": line[4:]}}]}}
"rich_text": [{"type": "text", "text": {"content": line[4:]}}] )
}
})
# List Items
elif stripped.startswith("* ") or stripped.startswith("- "): elif stripped.startswith("* ") or stripped.startswith("- "):
content = stripped[2:] content = stripped[2:]
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "bulleted_list_item", "type": "bulleted_list_item",
"bulleted_list_item": { "bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": content}}]}}
"rich_text": [{"type": "text", "text": {"content": content}}] )
}
})
# Numbered List (Simple detection)
elif re.match(r"^\d+\.", stripped): elif re.match(r"^\d+\.", stripped):
content = re.sub(r"^\d+\.\s*", "", stripped) content = re.sub(r"^\d+\.\s*", "", stripped)
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "numbered_list_item", "type": "numbered_list_item",
"numbered_list_item": { "numbered_list_item": {"rich_text": [{"type": "text", "text": {"content": content}}]}}
"rich_text": [{"type": "text", "text": {"content": content}}] )
}
})
# Tables (Very basic handling: convert to code block for preservation)
elif stripped.startswith("|"): elif stripped.startswith("|"):
blocks.append({ blocks.append({
"object": "block", "object": "block",
@@ -99,27 +81,28 @@ def parse_markdown_to_blocks(md_content):
"language": "plain text" "language": "plain text"
} }
}) })
# Paragraphs
else: else:
blocks.append({ blocks.append({
"object": "block", "object": "block",
"type": "paragraph", "type": "paragraph",
"paragraph": { "paragraph": {"rich_text": [{"type": "text", "text": {"content": line}}]}}
"rich_text": [{"type": "text", "text": {"content": line}}] )
}
})
return blocks return blocks
def upload_doc(token): def upload_doc(token, file_path):
try: try:
with open(DOC_FILE, 'r') as f: with open(file_path, 'r') as f:
content = f.read() content = f.read()
except FileNotFoundError: except FileNotFoundError:
print(f"Error: Could not find '{DOC_FILE}'") print(f"Error: Could not find '{file_path}'")
return return
print(f"Parsing '{DOC_FILE}'...") title = os.path.basename(file_path)
if content.startswith("# "):
title = content.split('\n')[0][2:].strip()
print(f"Parsing '{file_path}'...")
children_blocks = parse_markdown_to_blocks(content) children_blocks = parse_markdown_to_blocks(content)
url = "https://api.notion.com/v1/pages" url = "https://api.notion.com/v1/pages"
@@ -132,45 +115,31 @@ def upload_doc(token):
payload = { payload = {
"parent": { "page_id": PARENT_PAGE_ID }, "parent": { "page_id": PARENT_PAGE_ID },
"properties": { "properties": {
"title": [ "title": [{"text": {"content": f"📘 {title}"}}]
{
"text": {
"content": "📘 Notion Integration Dokumentation"
}
}
]
}, },
"children": children_blocks[:100] # Safe limit "children": children_blocks[:100]
} }
print(f"Uploading to Notion under parent {PARENT_PAGE_ID}...") print(f"Uploading '{title}' to Notion...")
try: try:
response = requests.post(url, headers=headers, json=payload) response = requests.post(url, headers=headers, json=payload)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
print("\n=== SUCCESS ===") print(f"SUCCESS: {data.get('url')}")
print(f"Documentation uploaded!")
print(f"URL: {data.get('url')}")
except requests.exceptions.HTTPError as e:
print(f"\n=== ERROR ===")
print(f"HTTP Error: {e}")
print(f"Response: {response.text}")
except Exception as e: except Exception as e:
print(f"\n=== ERROR ===") print(f"ERROR: {e}")
print(f"An error occurred: {e}")
def main(): if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python sync_docs_to_notion.py <filename>")
sys.exit(1)
try: try:
with open(TOKEN_FILE, 'r') as f: with open(TOKEN_FILE, 'r') as f:
token = f.read().strip() token = f.read().strip()
except FileNotFoundError: except FileNotFoundError:
print(f"Error: Could not find '{TOKEN_FILE}'") print(f"Error: Could not find '{TOKEN_FILE}'")
return sys.exit(1)
upload_doc(token) upload_doc(token, sys.argv[1])
if __name__ == "__main__":
main()