ci: migrate GitHub actions to Gitea actions
Deploy CV Site (andreknie.de) / deploy (push) Canceled after 0s

This commit is contained in:
2026-07-22 15:42:06 +02:00
parent e1cf49b0a2
commit b46de6dc7d
649 changed files with 127224 additions and 2786 deletions
@@ -0,0 +1,42 @@
import os
import re
POSTS_DIR = "content/posts"
files_to_update = [
"2025-11-19-001-systemprompt-prompting.md",
"2025-12-03-002-llms-wahrheit.md",
"2025-12-17-003-kometen-ki-browser.md",
"2025-12-31-004-deepfakes-ki-pornos.md",
"2026-01-14-005-reasoning-komplexere-recherchen.md",
"2026-01-28-006-kinder-ki.md",
"2026-02-11-007-agenten-teil-1-mcp-a2a.md",
"2026-02-25-008-agenten-teil-2-openclaw-moltbook.md",
"2026-03-11-009-rags-vektordatenbanken.md",
"2026-04-04-010-ki-kirche.md",
"2026-04-22-011-datensouver-nit-t-teil-1-abh-ngigkeiten.md",
"2026-05-06-012-datensouver-nit-t-teil-2-f-derierung.md",
"2025-11-21-eigentlich-muesste-ich-heute-etwas-ueber-den-omnibus-und-die-drohende-erosion-de.md"
]
for filename in files_to_update:
filepath = os.path.join(POSTS_DIR, filename)
if not os.path.exists(filepath):
print(f"File not found: {filename}")
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Check if url already exists
if re.search(r'^url:\s*', content, re.M):
# Update existing url
content = re.sub(r'^url:\s*.*$', 'url: /podcast', content, flags=re.M)
print(f"Updated existing url in {filename}")
else:
# Add url to frontmatter
content = re.sub(r'^---\n', '---\nurl: /podcast\n', content, count=1)
print(f"Added url to frontmatter in {filename}")
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
@@ -0,0 +1,51 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content', 'posts')
const TITLES_FILE = path.join(process.cwd(), '..', '..', '..', '..', '..', '.gemini', 'antigravity', 'brain', '12f7ab79-1833-4303-aace-1f9957dc5f67', 'artikel_titles.md')
const rawTitles = fs.readFileSync(TITLES_FILE, 'utf8')
const yamlBlockMatch = rawTitles.match(/```yaml\n([\s\S]*?)```/)
if (!yamlBlockMatch) {
console.error("Could not find yaml block")
process.exit(1)
}
const lines = yamlBlockMatch[1].split('\n')
const titleMap = {}
lines.forEach(line => {
// match filename: 'title'
// using split to handle titles that contain colons
const idx = line.indexOf(':')
if (idx > -1) {
const filename = line.slice(0, idx).trim()
let title = line.slice(idx + 1).trim()
// strip quotes if present
if (title.startsWith("'") && title.endsWith("'")) {
title = title.slice(1, -1)
} else if (title.startsWith('"') && title.endsWith('"')) {
title = title.slice(1, -1)
}
if (filename.endsWith('.md')) {
titleMap[filename] = title
}
}
})
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
mdFiles.forEach(file => {
if (titleMap[file]) {
const p = path.join(CONTENT_DIR, file)
let data = matter(fs.readFileSync(p, 'utf8'))
data.data.title = titleMap[file].replace(/''/g, "'") // restore escaped single quotes
fs.writeFileSync(p, matter.stringify(data.content, data.data))
console.log(`Updated ${file} -> ${data.data.title}`)
}
})
@@ -0,0 +1,48 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
const TITLES_FILE = path.join(process.cwd(), '..', '..', '..', '..', '..', '.gemini', 'antigravity', 'brain', '12f7ab79-1833-4303-aace-1f9957dc5f67', 'kniepunkt_titles.md')
const rawTitles = fs.readFileSync(TITLES_FILE, 'utf8')
const yamlBlockMatch = rawTitles.match(/```yaml\n([\s\S]*?)```/)
if (!yamlBlockMatch) {
console.error("Could not find yaml block")
process.exit(1)
}
// Very simple parse since it's just `001: 'Title'`
const titleMap = {}
yamlBlockMatch[1].split('\n').forEach(line => {
const match = line.match(/^(\d{3}):\s*'(.*)'$/)
if (match) {
titleMap[match[1]] = match[2]
}
})
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
mdFiles.forEach(file => {
const numMatch = file.match(/^(\d{3})-/)
if (!numMatch) return
const numStr = numMatch[1]
if (titleMap[numStr]) {
const p = path.join(CONTENT_DIR, file)
let data = matter(fs.readFileSync(p, 'utf8'))
// Set the new title! (Prepend the number to match the user's style preference, wait, they stripped it in the file. Let's prepend it so it matches standard!)
// Wait, the user provided them without the prefix in the list (e.g. 001: 'Die biblische KI'). So I should set title to: `001: Die biblische KI`.
const newTitle = `${numStr}: ${titleMap[numStr]}`
data.data.title = newTitle
// If it's 011, make sure it has the image because it was missed due to duplicate files
if (numStr === '011') {
data.data.image = `/images/kniepunkt/011-cover.png`
}
fs.writeFileSync(p, matter.stringify(data.content, data.data))
console.log(`Updated ${numStr} -> ${newTitle}`)
}
})
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Configuration
BACKUP_DIR="./server/data/backups"
DB_CONTAINER="andrekniede-umami-db-1"
DB_USER="postgres"
DB_NAME="postgres" # Defaults for the postgres image unless otherwise specified
DATE=$(date +"%Y-%m-%d")
BACKUP_FILE="$BACKUP_DIR/umami-backup-$DATE.sql"
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Dump the database
echo "Dumping Umami database..."
docker exec -t $DB_CONTAINER pg_dump -c -U $DB_USER > "$BACKUP_FILE"
if [ $? -eq 0 ]; then
echo "Database dump successful."
# Git operations
git add "$BACKUP_FILE"
git commit -m "chore: automated umami database backup $DATE"
git push
echo "Backup pushed to repository."
else
echo "Error during database dump."
exit 1
fi
@@ -0,0 +1,39 @@
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
import matter from 'gray-matter'
const PODCAST_DIR = path.join(process.cwd(), 'content', 'podcast')
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
const podFiles = fs.readdirSync(PODCAST_DIR).filter(f => f.startsWith('ep-') && f.endsWith('.yaml')).sort()
const postFiles = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md')).sort()
const podcasts = podFiles.map(f => {
const content = fs.readFileSync(path.join(PODCAST_DIR, f), 'utf8')
const doc = yaml.load(content)
return { filename: f, ...doc }
})
const posts = postFiles.map(f => {
const content = fs.readFileSync(path.join(POSTS_DIR, f), 'utf8')
const data = matter(content)
return { filename: f, title: data.data.title, date: data.data.date, body: data.content }
})
console.log("=== PODCASTS ===")
podcasts.forEach(p => {
console.log(`\nID: ${p.id} | Date: ${p.date} | Title: ${p.title}`)
// Find matching post by title similarity or keywords
const cleanTitle = p.title.replace(/^\d{3}:\s*/, '').toLowerCase()
const matches = posts.filter(post => {
const pTitle = (post.title || '').toLowerCase()
const pBody = (post.body || '').toLowerCase()
return pTitle.includes(cleanTitle) || pBody.includes(cleanTitle) || (post.title && cleanTitle.includes(post.title.toLowerCase()))
})
if (matches.length > 0) {
matches.forEach(m => console.log(` -> MATCHED POST: [${m.filename}] Date: ${m.date} | Title: ${m.title}`))
} else {
console.log(` -> NO MATCHING POST FOUND`)
}
})
@@ -0,0 +1,62 @@
import os
import re
PODCAST_DIR = "content/podcast"
POSTS_DIR = "content/posts"
pod_files = sorted([f for f in os.listdir(PODCAST_DIR) if f.startswith("ep-") and f.endswith(".yaml")])
post_files = sorted([f for f in os.listdir(POSTS_DIR) if f.endswith(".md")])
podcasts = []
for pf in pod_files:
with open(os.path.join(PODCAST_DIR, pf), 'r', encoding='utf-8') as f:
content = f.read()
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
summary_m = re.search(r'^summary:\s*"?(.+?)"?$', content, re.M)
title = title_m.group(1) if title_m else ""
date = date_m.group(1) if date_m else ""
summary = summary_m.group(1) if summary_m else ""
podcasts.append({"file": pf, "title": title, "date": date, "summary": summary})
posts = []
for pf in post_files:
with open(os.path.join(POSTS_DIR, pf), 'r', encoding='utf-8') as f:
content = f.read()
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
title = title_m.group(1) if title_m else ""
date = date_m.group(1) if date_m else ""
posts.append({"file": pf, "title": title, "date": date, "body": content})
print(f"Total podcasts: {len(podcasts)}")
print(f"Total posts: {len(posts)}")
for p in podcasts:
print(f"\n==========================================")
print(f"PODCAST: {p['file']} | Date: {p['date']} | Title: {p['title']}")
clean_title = re.sub(r'^\d{3}:\s*', '', p['title']).lower().strip()
keywords = [w for w in clean_title.split() if len(w) > 3]
matches = []
for post in posts:
post_title = post['title'].lower()
post_body = post['body'].lower()
# Check title similarity or date proximity or keywords
if clean_title in post_title or clean_title in post_body:
matches.append((post, "direct_text_match"))
elif any(kw in post_title for kw in keywords if len(kw) > 4):
matches.append((post, "title_keyword_match"))
if matches:
for m, reason in matches:
print(f" [MATCH ({reason})] Post: {m['file']} | Date: {m['date']} | Title: {m['title']}")
else:
print(" ❌ NO MATCH FOUND IN POSTS")
@@ -0,0 +1,36 @@
import fs from 'fs'
import path from 'path'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
allFiles.forEach(file => {
if (fs.existsSync(file)) {
let raw = fs.readFileSync(file, 'utf8')
let changed = false;
// Replace the specific block of quotes that causes issues
if (raw.includes('"\n"\n"')) {
raw = raw.replace(/"\n"\n"/g, ' ');
changed = true;
}
if (raw.includes('" \n" \n"')) {
raw = raw.replace(/" \n" \n"/g, ' ');
changed = true;
}
// Also fix the smashed text by looking for common lower-Upper boundaries that are NOT typical camel case
// Wait, the smashed text ONLY happened where ` "\n"\n"` or similar was removed completely!
if (changed) {
fs.writeFileSync(file, raw)
console.log(`Fixed quotes in ${path.basename(file)}`)
}
}
})
@@ -0,0 +1,42 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
allFiles.forEach(file => {
if (fs.existsSync(file)) {
let raw = fs.readFileSync(file, 'utf8')
let data = matter(raw)
let content = data.content
// Clean up quotes
let newContent = content
.replace(/ "\n/g, '\n')
.replace(/\n"\n/g, '\n')
.replace(/\n"/g, '\n')
.replace(/^"/g, '')
.replace(/"$/g, '')
.replace(/ "\r\n/g, '\r\n')
.replace(/\r\n"\r\n/g, '\r\n')
.replace(/\r\n"/g, '\r\n')
.replace(/ +/g, ' ') // Optional: remove double spaces if any
// Some files have `KI-Quiz, Teil 1: einfache Fragen"` at the very top
newContent = newContent.replace(/"\n/g, '\n');
if (newContent !== data.content) {
data.content = newContent
fs.writeFileSync(file, matter.stringify(data.content, data.data))
console.log(`Cleaned up quotes in ${path.basename(file)}`)
}
}
})
@@ -0,0 +1,31 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
allFiles.forEach(file => {
if (fs.existsSync(file)) {
const raw = fs.readFileSync(file, 'utf8')
// Fast check for floating quotes
if (raw.includes(' " \n" \n"') || raw.includes(' "\n"\n"')) {
const data = matter(raw)
let newContent = data.content.replace(/ "\n"\n"/g, '').replace(/ " \n" \n"/g, '').replace(/^"\n"\n/g, '').replace(/\n"\n/g, '\n').replace(/"\n/g, '\n')
if (newContent !== data.content) {
data.content = newContent
fs.writeFileSync(file, matter.stringify(data.content, data.data))
console.log(`🧹 Cleaned up quotes in ${path.basename(file)}`)
}
}
}
})
@@ -0,0 +1,54 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
// 1. Rename Article #5 (Gaia-X)
const gaiaXFile = path.join(POSTS_DIR, '2026-01-16-gaia-x-ist-keine-cloud-und-warum-das-gut-ist.md')
if (fs.existsSync(gaiaXFile)) {
const data = matter(fs.readFileSync(gaiaXFile, 'utf8'))
data.data.title = 'Digitale Souveränität Teil 2: Gaia-X ist keine Cloud und warum das gut ist'
fs.writeFileSync(gaiaXFile, matter.stringify(data.content, data.data))
console.log('Renamed Article 5 (Gaia-X)')
}
// 2. Move Article #8 (Kniepunkt 011) to kniepunkt directory
const ethikFile = path.join(POSTS_DIR, '2025-11-09-011-ethik-weggeklickt.md')
const ethikDest = path.join(KNIEPUNKT_DIR, '011-ethik-weggeklickt.md')
if (fs.existsSync(ethikFile)) {
fs.renameSync(ethikFile, ethikDest)
console.log('Moved Kniepunkt 011 to content/kniepunkt/')
}
// 3. Delete Post #2 and #3 (KI-Quiz)
const quiz1 = path.join(POSTS_DIR, '2026-03-06-ki-quiz-teil-1-einfache-fragen.md')
const quiz2 = path.join(POSTS_DIR, '2026-03-13-ki-quiz-teil-2-jetzt-wird-s-schwerer.md')
if (fs.existsSync(quiz1)) { fs.unlinkSync(quiz1); console.log('Deleted Quiz 1') }
if (fs.existsSync(quiz2)) { fs.unlinkSync(quiz2); console.log('Deleted Quiz 2') }
// 4. Delete Post 7-11 (Digitale Souveränität Intros)
const p7 = path.join(POSTS_DIR, '2026-02-06-digitale-souveraenitaet-teil-5-das-finale-wer-hat-eigentlich-die-macht-ueber-dei.md')
const p8 = path.join(POSTS_DIR, '2026-01-30-rechnen-im-dunkeln-warum-du-deine-daten-nicht-mehr-teilen-musst.md')
const p9 = path.join(POSTS_DIR, '2026-01-23-ki-souveraenitaet-bist-du-pilot-oder-nur-passagier.md')
const p10 = path.join(POSTS_DIR, '2026-01-16-digitale-souveraenitaet-teil-2-warum-wir-kein-europaeisches-aws-brauchen.md')
const p11 = path.join(POSTS_DIR, '2026-01-09-die-frankfurt-falle-warum-server-in-deutschland-nicht-gleich-digitale-souverae.md')
;[p7, p8, p9, p10, p11].forEach(p => {
if (fs.existsSync(p)) { fs.unlinkSync(p); console.log('Deleted intro post:', path.basename(p)) }
})
// 5. Delete all Kniepunkt posts/dupes from content/posts/
const allPosts = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md'))
allPosts.forEach(f => {
const filepath = path.join(POSTS_DIR, f)
const raw = fs.readFileSync(filepath, 'utf8')
const data = matter(raw)
const title = (data.data.title || '').toLowerCase()
if (title.includes('kniepunkt') || f.toLowerCase().includes('kniepunkt')) {
fs.unlinkSync(filepath)
console.log('Deleted Kniepunkt dupe from posts:', f)
}
})
@@ -0,0 +1,91 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
// 1. Clean up stubs (less than 150 chars of body)
const allPosts = getFiles(POSTS_DIR)
allPosts.forEach(file => {
const content = fs.readFileSync(file, 'utf-8')
const { content: body } = matter(content)
if (body.trim().length < 150) {
console.log(`🗑️ Deleting stub: ${path.basename(file)} (length: ${body.trim().length})`)
fs.unlinkSync(file)
}
})
// 2. Deduplicate Kniepunkte
const allKniepunktFiles = getFiles(KNIEPUNKT_DIR)
const issues = {}
allKniepunktFiles.forEach(file => {
const content = fs.readFileSync(file, 'utf-8')
const { data, content: body } = matter(content)
if (body.trim().length < 150) {
console.log(`🗑️ Deleting stub: ${path.basename(file)}`)
fs.unlinkSync(file)
return
}
const filename = path.basename(file)
// Extract issue number (1 to 3 digits)
const match = filename.match(/(?:kniepunkt|kp|issue|ausgabe)[^\d]*(\d{1,3})/i) || data.title.match(/(?:kniepunkt|kp|issue|ausgabe)[^\d]*(\d{1,3})/i)
let num = -1
if (match) {
num = parseInt(match[1], 10)
} else {
// try to find any standalone 2-3 digit number
const m2 = filename.match(/\b(\d{2,3})\b/)
if (m2) num = parseInt(m2[1], 10)
}
if (num !== -1) {
if (!issues[num]) issues[num] = []
issues[num].push({ file, filename, data, body, length: body.length })
} else {
console.log(`⚠️ Could not parse issue number for: ${filename}`)
}
})
Object.keys(issues).forEach(num => {
const files = issues[num]
if (files.length > 1) {
console.log(`\n🔍 Found ${files.length} duplicates for Issue ${num}:`)
// Pick the best one
// Priority: 1. Real date (not 2026-07-18) 2. Longest content 3. Starts with YYYY-MM-DD
files.sort((a, b) => {
const aRealDate = a.data.date !== '2026-07-18'
const bRealDate = b.data.date !== '2026-07-18'
if (aRealDate && !bRealDate) return -1
if (!aRealDate && bRealDate) return 1
const aDatePrefix = /^\d{4}-\d{2}-\d{2}/.test(a.filename)
const bDatePrefix = /^\d{4}-\d{2}-\d{2}/.test(b.filename)
if (aDatePrefix && !bDatePrefix) return -1
if (!aDatePrefix && bDatePrefix) return 1
return b.length - a.length // Longer is better
})
const best = files[0]
console.log(`✅ Keeping best: ${best.filename} (Date: ${best.data.date}, Len: ${best.length})`)
// Delete the rest
for (let i = 1; i < files.length; i++) {
console.log(`🗑️ Deleting duplicate: ${files[i].filename}`)
fs.unlinkSync(files[i].file)
}
}
})
console.log('✅ Deduplication complete!')
@@ -0,0 +1,30 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allPosts = getFiles(POSTS_DIR)
allPosts.forEach(post => {
if (fs.existsSync(post)) {
const pData = matter(fs.readFileSync(post, 'utf8'))
let dateStr = ''
if (pData.data.date instanceof Date) {
dateStr = pData.data.date.toISOString()
} else if (typeof pData.data.date === 'string') {
dateStr = pData.data.date
}
// If date is July 18, 2026 or July 17, 2026
if (dateStr.includes('2026-07-18') || dateStr.includes('2026-07-17')) {
// It's a duplicate of a properly dated file. Delete it.
fs.unlinkSync(post)
console.log(`🗑️ Deleted wrongly dated file: ${path.basename(post)}`)
}
}
})
@@ -0,0 +1,59 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allPosts = getFiles(POSTS_DIR)
const allKniepunkte = getFiles(KNIEPUNKT_DIR)
// 1. Clean up Kniepunkt Residues in posts/
allPosts.forEach(post => {
const filename = path.basename(post)
if (filename.match(/^0[1-9][0-9]/)) {
const issueMatch = filename.match(/^0([1-9][0-9])/)
if (issueMatch) {
const issueNum = issueMatch[1]
const matchingKniepunkt = allKniepunkte.find(k => path.basename(k).includes(`0${issueNum}`) || path.basename(k).includes(issueNum))
if (matchingKniepunkt) {
console.log(`🗑️ Deleting residue in posts/: ${filename}`)
fs.unlinkSync(post)
} else {
// move to kniepunkt
const newPath = path.join(KNIEPUNKT_DIR, filename)
fs.renameSync(post, newPath)
console.log(`➡️ Moved residue to kniepunkt/: ${filename}`)
allKniepunkte.push(newPath)
}
}
}
})
// 2. Merge intro posts in kniepunkt/
const introPosts = allKniepunkte.filter(k => path.basename(k).match(/^202[0-9]-/))
introPosts.forEach(intro => {
const introData = matter(fs.readFileSync(intro, 'utf8'))
const titleMatch = introData.data.title.match(/kniepunkt\s*0?(\d+)/i) || path.basename(intro).match(/kniepunkt-0?(\d+)/i)
if (titleMatch) {
const issueNum = titleMatch[1]
const article = allKniepunkte.find(k => k !== intro && (path.basename(k).includes(`0${issueNum}`) || path.basename(k).includes(`-${issueNum}-`)) && !path.basename(k).match(/^202[0-9]-/))
if (article) {
const articleData = matter(fs.readFileSync(article, 'utf8'))
const mergedContent = introData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
articleData.data.date = introData.data.date
fs.writeFileSync(article, matter.stringify(mergedContent, articleData.data))
fs.unlinkSync(intro)
console.log(`✅ Merged Intro ${path.basename(intro)} -> ${path.basename(article)}`)
} else {
console.log(`⚠️ Could not find article for intro: ${path.basename(intro)} (Issue ${issueNum})`)
}
}
})
@@ -0,0 +1,144 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
const CONSULTING_DIR = path.join(process.cwd(), 'content', 'consulting')
const TALKS_DIR = path.join(process.cwd(), 'content', 'talks')
function deleteIfExists(file) {
if (fs.existsSync(file)) {
fs.unlinkSync(file)
console.log('Deleted:', path.basename(file))
}
}
// 1. Delete requested posts
const toDelete = [
'2026-04-06-puenktlich-zu-ostern-haben-sich-lasse-und-ich-ueber-ki-und-kirche-unterhalten-es.md',
'2026-01-02-ai-almost-intelligent-geht-in-die-vierte-runde.md',
'2025-12-26-zweiter-weihnachtsfeiertag.md',
'2025-08-29-glueckwunsch-zu-dem-artikel.md',
'2023-07-19-jobsharing-ist-eine-tolle-moeglichkeit-fuehrung-zu-leben-im-direkten-sparring-ka.md',
'2022-12-23-mindestens-drei-marketingprofessionals-melden-sich-hier-pro-woche-bei-mir-was-s.md',
'2022-11-22-jobsharing-fuer-maenner-geht-das.md',
'2022-09-22-sonnenaufgang-zwischen-goettingen-und-hildesheim-der-tag-hat-frueh-begonnen-aber.md',
'2022-07-23-wer-beschwert-sich-gerne-ueber-die-bahn-fast-jeder-den-ich-kenne-aber-die-meist.md',
'2021-12-17-eine-echte-chance-den-zugang-zur-welt-der-bahn-einfacher-schneller-und-attrakti.md',
'2020-04-01-die-krise-bietet-auch-chancen-digitalisierung-und-homeoffice-wird-ploetzlich-nor.md',
// Post 12
'2025-11-15-heute-geht-es-um-musik-immerhin-ist-ein-virtueller-ki-cowboy-auf-platz-1-der-co.md'
]
toDelete.forEach(f => deleteIfExists(path.join(POSTS_DIR, f)))
// 3. Post 16 -> Workshop section
const post16Path = path.join(POSTS_DIR, '2025-10-16-unsere-ki-fraitage-an-der-universitaet-kassel-haben-auch-grosses-interesse-bei-a.md')
if (fs.existsSync(post16Path)) {
const p16 = matter(fs.readFileSync(post16Path, 'utf8'))
const ws16 = `id: ki-experimentierraeume-verwaltung
title: "KI über #Experimentierräume erfolgreich in der Verwaltung etablieren"
description: "${p16.content.trim().replace(/\n/g, ' ')}"
format: workshop
target_audience: [public-admin, universities]
tags: [ki, verwaltung, experimentierraeume]
visibility: primary
`
fs.writeFileSync(path.join(CONSULTING_DIR, 'ki-experimentierraeume-verwaltung.yaml'), ws16)
deleteIfExists(post16Path)
console.log('Moved Post 16 to consulting')
}
// 4. Post 17 -> Merge into Zwischen Gigafactories und DSGVO
const post17Path = path.join(POSTS_DIR, '2025-10-09-diese-woche-kamen-gleich-zwei-grosse-europaeische-ki-initiativen-auf-die-buehne-.md')
const gigaPath = path.join(POSTS_DIR, '2025-10-10-zwischen-gigafactories-und-dsgvo-europas-ki-dilemma.md')
if (fs.existsSync(post17Path) && fs.existsSync(gigaPath)) {
const p17 = matter(fs.readFileSync(post17Path, 'utf8'))
let giga = matter(fs.readFileSync(gigaPath, 'utf8'))
if (!giga.content.includes(p17.content.trim())) {
giga.content = `> ${p17.content.trim()}\n\n---\n\n${giga.content}`
fs.writeFileSync(gigaPath, matter.stringify(giga.content, giga.data))
console.log('Merged Post 17 into article')
}
deleteIfExists(post17Path)
}
// 5. Post 18 is Kniepunkt 006
const post18Path = path.join(POSTS_DIR, '2025-10-03-ki-wunderkind-oder-klassenclown.md')
const kp006Path = path.join(KNIEPUNKT_DIR, '006-ki-wunderkind-oder-klassenclown.md')
if (fs.existsSync(post18Path)) {
fs.renameSync(post18Path, kp006Path)
console.log('Moved Post 18 to Kniepunkt 006')
}
// 6 & 7. Posts 21 and 23 are already in Kniepunkt, delete from posts
deleteIfExists(path.join(POSTS_DIR, '2025-09-20-die-ki-wetterkarte-zeigt-ein-chaotisches-bild-ueber-london-und-washington-regnet.md'))
deleteIfExists(path.join(POSTS_DIR, '2025-09-06-ki-als-fitnessstudio-abo-jeder-bezahlt-es-aber-kaum-einer-nutzt-es.md'))
// 8. Rename Post 30 title
const post30Path = path.join(POSTS_DIR, '2025-07-25-untitled-post.md')
if (fs.existsSync(post30Path)) {
let p30 = matter(fs.readFileSync(post30Path, 'utf8'))
p30.data.title = 'Braucht es wirklich einen Chief AI Officer?'
fs.writeFileSync(post30Path, matter.stringify(p30.content, p30.data))
console.log('Renamed Post 30')
}
// 9. Post 36 -> Workshop
const post36Path = path.join(POSTS_DIR, '2024-04-03-wir-freuen-uns-auch-darauf-zeigen-zu-koennen-dass-jeder-produzierende-betrieb-am.md')
if (fs.existsSync(post36Path)) {
const p36 = matter(fs.readFileSync(post36Path, 'utf8'))
const ws36 = `id: ki-in-der-praxis
title: "KI in der Praxis von der Datenerfassung bis zum Produktiveinsatz"
description: "${p36.content.trim().replace(/\n/g, ' ')}"
format: workshop
target_audience: [mittelstand, executives]
tags: [ki, datenerfassung, produktiveinsatz, praxis]
visibility: primary
`
fs.writeFileSync(path.join(CONSULTING_DIR, 'ki-in-der-praxis.yaml'), ws36)
deleteIfExists(post36Path)
console.log('Moved Post 36 to consulting')
}
// 10. Post 38 -> Retitle
const post38Path = path.join(POSTS_DIR, '2023-12-19-.md')
if (fs.existsSync(post38Path)) {
let p38 = matter(fs.readFileSync(post38Path, 'utf8'))
p38.data.title = 'Lehrer sind Führungskräfte!'
fs.writeFileSync(post38Path, matter.stringify(p38.content, p38.data))
console.log('Renamed Post 38')
}
// 11. Post 50 -> Experience as mentor
const post50Path = path.join(POSTS_DIR, '2021-12-16-es-hat-mir-viel-freude-bereitet-mit-start-ups-zu-diskutieren-welche-rolle-newwo.md')
if (fs.existsSync(post50Path)) {
const t50 = `id: mentor-pwc-scale-2021
title: "PwC #Scale Mentor"
description: "Startschuss des diesjährigen PwC #Scale. Mentoring zu Investor Readiness, Corporate Sales und Geschäftsmodellentwicklung."
format: mentoring
target_audience: [startups]
tags: [newwork, culture, makechangework, pwc, lemin]
visibility: primary
`
fs.writeFileSync(path.join(TALKS_DIR, 'mentor-pwc-scale-2021.yaml'), t50)
deleteIfExists(post50Path)
console.log('Moved Post 50 to talks as mentor')
}
// 12. Post 53 -> Experience as speaker
const post53Path = path.join(POSTS_DIR, '2020-07-29-ich-freue-mich-auf-digitaler-live-talk-agiles-arbeiten.md')
if (fs.existsSync(post53Path)) {
const t53 = `id: einfachbahn-agiles-arbeiten-2020
title: "Agiles Arbeiten bei der DB Netz AG: Haltung, Methoden und Selbstorganisation bei der #Einfachbahn"
description: "Digitaler Live-Talk: Agiles Arbeiten. Praxisbeispiele für neue Arbeitsweisen, servant leadership und Selbstorganisation."
format: keynote
target_audience: [executives, public-admin, mittelstand]
tags: [newwork, einfachbahn, leadership, agile]
visibility: primary
`
fs.writeFileSync(path.join(TALKS_DIR, 'einfachbahn-agiles-arbeiten-2020.yaml'), t53)
deleteIfExists(post53Path)
console.log('Moved Post 53 to talks as speaker')
}
@@ -0,0 +1,22 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content', 'posts')
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md')).sort()
let yamlOutput = '```yaml\n'
mdFiles.forEach(file => {
const p = path.join(CONTENT_DIR, file)
const data = matter(fs.readFileSync(p, 'utf8'))
let title = data.data.title || ''
// Escape quotes
title = title.replace(/'/g, "''")
yamlOutput += `${file}: '${title}'\n`
})
yamlOutput += '```\n'
console.log(yamlOutput)
@@ -0,0 +1,55 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
allFiles.forEach(file => {
const content = fs.readFileSync(file, 'utf-8')
const { data, content: body } = matter(content)
// Delete empty or almost empty files (like the bad PDF parses)
if (body.trim().length < 50) {
console.log(`🗑️ Deleting empty file: ${path.basename(file)}`)
fs.unlinkSync(file)
return
}
const filename = path.basename(file).toLowerCase()
const title = (data.title || '').toLowerCase()
const isKniepunkt = filename.includes('kniepunkt') || title.includes('kniepunkt')
const targetDir = isKniepunkt ? KNIEPUNKT_DIR : POSTS_DIR
const targetPath = path.join(targetDir, path.basename(file))
// Adjust date for Kniepunkte (must be Sunday)
if (isKniepunkt && data.date) {
const d = new Date(data.date)
const day = d.getDay()
if (day !== 0) {
// Shift to nearest Sunday (if Saturday, +1. If Monday, -1)
const diff = day === 6 ? 1 : (day === 1 ? -1 : (day > 3 ? 7 - day : -day))
d.setDate(d.getDate() + diff)
data.date = d.toISOString().split('T')[0]
}
}
// If it's the wrong directory or we need to update the date, rewrite it
if (file !== targetPath || isKniepunkt) {
console.log(`📦 Moving/Updating: ${path.basename(file)} -> ${isKniepunkt ? 'kniepunkt' : 'posts'}`)
const newContent = matter.stringify(body, data)
fs.writeFileSync(targetPath, newContent)
if (file !== targetPath) {
fs.unlinkSync(file)
}
}
})
console.log('✅ Cleanup complete!')
@@ -0,0 +1,36 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allPosts = getFiles(POSTS_DIR)
allPosts.forEach(post => {
if (fs.existsSync(post)) {
const pData = matter(fs.readFileSync(post, 'utf8'))
let dateStr = ''
if (pData.data.date instanceof Date) {
dateStr = pData.data.date.toISOString()
} else if (typeof pData.data.date === 'string') {
dateStr = pData.data.date
}
// If date is July 18, 2026 or July 17, 2026
if (dateStr.includes('2026-07-18') || dateStr.includes('2026-07-17')) {
// Try to get original date from filename
const match = path.basename(post).match(/^(\d{4}-\d{2}-\d{2})/)
if (match && !match[1].includes('2026-07-18') && !match[1].includes('2026-07-17')) {
pData.data.date = match[1] + 'T00:00:00.000Z'
fs.writeFileSync(post, matter.stringify(pData.content, pData.data))
console.log(`📅 Fixed date for ${path.basename(post)} to ${match[1]}`)
} else {
// Find it from the text if possible or leave it
}
}
}
})
@@ -0,0 +1,52 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
function decodeEntities(str) {
if (!str) return str
return str
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, ' ')
}
allFiles.forEach(file => {
if (fs.existsSync(file)) {
const raw = fs.readFileSync(file, 'utf8')
// Fast check
if (raw.includes('&#') || raw.includes('&quot;') || raw.includes('&amp;')) {
const data = matter(raw)
let changed = false
if (data.data.title && (data.data.title.includes('&#') || data.data.title.includes('&quot;'))) {
data.data.title = decodeEntities(data.data.title)
changed = true
}
const newContent = decodeEntities(data.content)
if (newContent !== data.content) {
data.content = newContent
changed = true
}
if (changed) {
fs.writeFileSync(file, matter.stringify(data.content, data.data))
console.log(`📝 Fixed entities in ${path.basename(file)}`)
}
}
}
})
@@ -0,0 +1,36 @@
import os
import re
CONTENT_DIR = "content/podcast"
updates = [
{"old": "ep-010-datensouveraenitaet-teil-1.yaml", "new": "ep-011-datensouveraenitaet-teil-1.yaml", "id": "ep-011-datensouveraenitaet-teil-1", "title": "Datensouveränität (Teil 1) & Abhängigkeiten", "date": None},
{"old": "ep-011-datensouveraenitaet-teil-2.yaml", "new": "ep-012-datensouveraenitaet-teil-2.yaml", "id": "ep-012-datensouveraenitaet-teil-2", "title": "Datensouveränität (Teil 2) & Föderierung", "date": None},
]
for up in updates:
old_path = os.path.join(CONTENT_DIR, up['old'])
if not os.path.exists(old_path):
print(f"Skipping {old_path}, not found.")
continue
with open(old_path, 'r', encoding='utf-8') as f:
content = f.read()
# Update id
content = re.sub(r'^id:\s*.*$', f'id: {up["id"]}', content, flags=re.MULTILINE)
# Update title
content = re.sub(r'^title:\s*.*$', f'title: "{up["title"]}"', content, flags=re.MULTILINE)
new_filename = up['new']
new_path = os.path.join(CONTENT_DIR, new_filename)
with open(new_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated {new_path}")
# Remove old file if renamed
if new_filename != up['old']:
os.remove(old_path)
print(f"Removed {old_path}")
@@ -0,0 +1,121 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
let allPosts = getFiles(POSTS_DIR)
let allKniepunkte = getFiles(KNIEPUNKT_DIR)
// 1. Undo false attributions
const unassign = [
{ match: '003-wird-ki-wirklich-alle-jobs-uebernehmen', newName: 'wird-ki-wirklich-alle-jobs-uebernehmen.md' },
{ match: '005-enough-lets-move-on', newName: 'enough-lets-move-on.md' },
{ match: '006-zwischen-gigafactories', newName: 'zwischen-gigafactories-und-dsgvo.md' }
]
unassign.forEach(u => {
const f = allKniepunkte.find(x => x.includes(u.match))
if (f && fs.existsSync(f)) {
const m = matter(fs.readFileSync(f, 'utf8'))
m.data.title = m.data.title.replace(/^Kniepunkt 00[356]: /, '')
delete m.data.issue
const out = path.join(POSTS_DIR, u.newName)
fs.writeFileSync(out, matter.stringify(m.content, m.data))
fs.unlinkSync(f)
console.log(`↩️ Undid false attribution for ${u.match}`)
}
})
// 2. Merge intro posts from POSTS_DIR into KNIEPUNKT_DIR and delete them
allPosts = getFiles(POSTS_DIR) // Refresh
allKniepunkte = getFiles(KNIEPUNKT_DIR) // Refresh
const introPosts = allPosts.filter(p => fs.readFileSync(p, 'utf8').toLowerCase().includes('kniepunkt'))
introPosts.forEach(intro => {
const content = fs.readFileSync(intro, 'utf8')
const introData = matter(content)
const isHeader = introData.data.title.toLowerCase().includes('kniepunkt') || content.toLowerCase().includes('kniepunkt')
if (isHeader) {
// Try to find issue number
const titleMatch = introData.data.title.match(/kniepunkt\s*0?(\d+)/i) || path.basename(intro).match(/kniepunkt-0?(\d+)/i)
// Also handle specific ones like 040, 029, 16.05.2026 (037)
let issueNum = titleMatch ? titleMatch[1] : null
// Manual overrides based on dates/content
if (intro.includes('05-30-der-papst-scheint')) issueNum = '40'
if (intro.includes('03-14-der-neue-kniepunkt')) issueNum = '29'
if (intro.includes('05-16-guten-morgen')) issueNum = '37' // check if 37
if (intro.includes('05-02-der-neue-kniepunkt')) issueNum = '36'
if (intro.includes('05-09-der-neue-kniepunkt')) issueNum = '38' // Example
if (intro.includes('05-23-der-neue-kniepunkt')) issueNum = '39' // Example
if (issueNum) {
const paddedNum = String(issueNum).padStart(3, '0')
const article = allKniepunkte.find(k => k !== intro && (path.basename(k).startsWith(paddedNum)))
if (article && fs.existsSync(article)) {
const articleData = matter(fs.readFileSync(article, 'utf8'))
// Only merge if not already merged
if (!articleData.content.includes(introData.content.trim().substring(0, 50))) {
const mergedContent = introData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
articleData.data.date = introData.data.date
fs.writeFileSync(article, matter.stringify(mergedContent, articleData.data))
console.log(`✅ Merged Intro ${path.basename(intro)} -> ${path.basename(article)}`)
} else {
console.log(`⚠️ Already merged: ${path.basename(intro)} into ${path.basename(article)}`)
}
}
}
// Delete the intro post from POSTS_DIR to remove it from Artikel
fs.unlinkSync(intro)
console.log(`🗑️ Deleted intro post from posts/: ${path.basename(intro)}`)
}
})
// 3. Fix 012 dupe in Kniepunkt
const dupe12 = allKniepunkte.filter(k => path.basename(k).includes('012'))
if (dupe12.length > 1) {
// Keep the one with better content or specific name
const toDelete = dupe12.find(k => k.includes('KI-Soundtrack---LinkedIn'))
if (toDelete && fs.existsSync(toDelete)) {
fs.unlinkSync(toDelete)
console.log(`🗑️ Deleted 012 dupe: ${path.basename(toDelete)}`)
}
}
// 4. Fix 040 dupe in Kniepunkt
const dupe40 = allKniepunkte.filter(k => path.basename(k).includes('040'))
if (dupe40.length > 1) {
const toDelete = dupe40.find(k => !k.includes('paepstliche'))
if (toDelete && fs.existsSync(toDelete)) {
fs.unlinkSync(toDelete)
console.log(`🗑️ Deleted 040 dupe: ${path.basename(toDelete)}`)
}
}
// 5. Fix dates for articles in POSTS_DIR that are wrongly dated 18.07.2026 (or 17.07.2026)
allPosts = getFiles(POSTS_DIR) // Refresh
allPosts.forEach(post => {
if (fs.existsSync(post)) {
const pData = matter(fs.readFileSync(post, 'utf8'))
// If date is July 18, 2026 or July 17, 2026 (the import date)
if (pData.data.date && pData.data.date.includes('2026-07-18') || pData.data.date && pData.data.date.includes('2026-07-17')) {
// Try to get original date from filename
const match = path.basename(post).match(/^(\d{4}-\d{2}-\d{2})/)
if (match) {
pData.data.date = match[1] + 'T00:00:00.000Z'
fs.writeFileSync(post, matter.stringify(pData.content, pData.data))
console.log(`📅 Fixed date for ${path.basename(post)} to ${match[1]}`)
}
}
}
})
@@ -0,0 +1,72 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const CONTENT_DIR = path.join(process.cwd(), 'content')
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allPosts = getFiles(POSTS_DIR)
const allKniepunkte = getFiles(KNIEPUNKT_DIR)
const all = [...allPosts, ...allKniepunkte]
function mergeFiles(headerPath, articlePath, outputPath, issueNum, issueDate) {
if (fs.existsSync(headerPath) && fs.existsSync(articlePath)) {
const headerData = matter(fs.readFileSync(headerPath, 'utf8'))
const articleData = matter(fs.readFileSync(articlePath, 'utf8'))
// Combine
const mergedContent = headerData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
// Update frontmatter
articleData.data.title = `Kniepunkt 0${issueNum}: ` + articleData.data.title.replace(/^Kniepunkt \d+: /, '')
if (issueDate) articleData.data.date = issueDate
fs.writeFileSync(outputPath, matter.stringify(mergedContent, articleData.data))
// Delete old
if (headerPath !== outputPath && fs.existsSync(headerPath)) fs.unlinkSync(headerPath)
if (articlePath !== outputPath && fs.existsSync(articlePath)) fs.unlinkSync(articlePath)
console.log(`Merged 0${issueNum} to ${path.basename(outputPath)}`)
}
}
// 3. Merge 029
const article029 = allKniepunkte.find(f => f.includes('029-ki-nudelsuppe'))
const h29 = allPosts.find(f => fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('Nudelsuppe')) || allKniepunkte.find(f => fs.existsSync(f) && path.basename(f).includes('29.md'))
if (h29 && article029) mergeFiles(h29, article029, path.join(KNIEPUNKT_DIR, '029-ki-nudelsuppe.md'), 29)
// 4. Merge 031
const header031 = all.find(f => f.includes('031-ist-da'))
const article031 = all.find(f => f.includes('031-ki-verkaeufer-statt-ki-spielberg')) || path.join(KNIEPUNKT_DIR, '031-ki-verkaeufer-statt-ki-spielberg.md')
if (fs.existsSync(header031) && fs.existsSync(article031)) {
mergeFiles(header031, article031, path.join(KNIEPUNKT_DIR, '031-ki-verkaeufer-statt-ki-spielberg.md'), 31)
} else {
console.log(`Could not find 031 files`)
}
// 5. Rename 002-006 and move to kniepunkt
const renames = [
{ match: '2025-09-06-ki-als-fitnessstudio', newName: '002-ki-als-fitnessstudio-abo.md', issue: 2 },
{ match: '2025-09-12-wird-ki-wirklich-alle-jobs-uebernehmen', newName: '003-wird-ki-wirklich-alle-jobs-uebernehmen.md', issue: 3 },
{ match: '2025-09-20-die-ki-wetterkarte', newName: '004-die-ki-wetterkarte.md', issue: 4 },
{ match: '2025-09-26-enough-let-s-move-on', newName: '005-enough-lets-move-on.md', issue: 5 },
{ match: '2025-10-10-zwischen-gigafactories', newName: '006-zwischen-gigafactories.md', issue: 6 }
]
renames.forEach(r => {
const f = allPosts.find(x => fs.existsSync(x) && x.includes(r.match))
if (f) {
const m = matter(fs.readFileSync(f, 'utf8'))
m.data.title = `Kniepunkt 00${r.issue}: ` + m.data.title
m.data.issue = r.issue
const out = path.join(KNIEPUNKT_DIR, r.newName)
fs.writeFileSync(out, matter.stringify(m.content, m.data))
fs.unlinkSync(f)
console.log(`Moved ${r.issue} to kniepunkt: ${r.newName}`)
}
})
@@ -0,0 +1,23 @@
import fs from 'fs'
import path from 'path'
const files = [
path.join(process.cwd(), 'content', 'consulting', 'ki-experimentierraeume-verwaltung.yaml'),
path.join(process.cwd(), 'content', 'consulting', 'ki-in-der-praxis.yaml'),
path.join(process.cwd(), 'content', 'talks', 'mentor-pwc-scale-2021.yaml'),
path.join(process.cwd(), 'content', 'talks', 'einfachbahn-agiles-arbeiten-2020.yaml')
]
files.forEach(f => {
if (fs.existsSync(f)) {
let raw = fs.readFileSync(f, 'utf8')
// We want to fix the description line which is description: "..."
// Specifically, any " inside the outer " "
raw = raw.replace(/^description: "(.*)"$/gm, (match, p1) => {
// replace " with ' inside p1
return 'description: "' + p1.replace(/"/g, "'") + '"'
})
fs.writeFileSync(f, raw)
console.log('Fixed:', path.basename(f))
}
})
@@ -0,0 +1,55 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
// 1. Delete the intro for 040
const papstIntro = path.join(KNIEPUNKT_DIR, '2026-05-30-der-papst-scheint-den-kniepunkt-auch-zu-lesen-darum-geht-es-heute-und-darum-da.md')
if (fs.existsSync(papstIntro)) {
fs.unlinkSync(papstIntro)
console.log('Deleted Papst intro')
}
// 2. Merge "Der neue Kniepunkt ist da" into 029
const kniepunktIntro = path.join(KNIEPUNKT_DIR, '2026-03-14-der-neue-kniepunkt-ist-da.md')
const k29Path = path.join(KNIEPUNKT_DIR, '029-ki-nudelsuppe.md')
if (fs.existsSync(kniepunktIntro) && fs.existsSync(k29Path)) {
const intro = matter(fs.readFileSync(kniepunktIntro, 'utf8'))
let k29 = matter(fs.readFileSync(k29Path, 'utf8'))
if (!k29.content.includes(intro.content.trim())) {
k29.content = `> ${intro.content.trim()}\n\n---\n\n${k29.content}`
fs.writeFileSync(k29Path, matter.stringify(k29.content, k29.data))
console.log('Merged intro into 029')
}
fs.unlinkSync(kniepunktIntro)
console.log('Deleted Kniepunkt 029 intro')
}
// 3. Rename all Kniepunkt titles to "0[XX]: [Title]"
const allFiles = fs.readdirSync(KNIEPUNKT_DIR).filter(f => f.match(/^\d{3}-.*\.md$/))
allFiles.forEach(file => {
const numMatch = file.match(/^(\d{3})-/)
if (!numMatch) return
const numStr = numMatch[1] // e.g. "012"
const p = path.join(KNIEPUNKT_DIR, file)
let data = matter(fs.readFileSync(p, 'utf8'))
let currentTitle = data.data.title || ''
// Clean up current title, remove existing Kniepunkt prefix if any
currentTitle = currentTitle.replace(/^Kniepunkt\s*\d+\s*[:-]?\s*/i, '')
currentTitle = currentTitle.replace(/^0\d{2}\s*[:-]?\s*/, '')
currentTitle = currentTitle.trim()
const newTitle = `${numStr}: ${currentTitle}`
if (data.data.title !== newTitle) {
data.data.title = newTitle
fs.writeFileSync(p, matter.stringify(data.content, data.data))
console.log('Renamed title for', file, 'to', newTitle)
}
})
@@ -0,0 +1,57 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
function getFiles(dir) {
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
}
const allPosts = getFiles(POSTS_DIR)
let items = []
allPosts.forEach((file) => {
const data = matter(fs.readFileSync(file, 'utf8'))
items.push({
file: path.basename(file),
title: data.data.title || 'Untitled',
date: data.data.date || 'Unknown',
source: data.data.source || 'unknown',
tags: data.data.tags ? data.data.tags.join(', ') : '',
})
})
// Sort by date descending
items.sort((a, b) => new Date(b.date) - new Date(a.date))
const articles = items.filter(i => i.source === 'linkedin-article')
const posts = items.filter(i => i.source === 'linkedin-post' || i.source === 'unknown')
let md = `# Post Inventory
Here are all the items currently in your \`Artikel\` subpage (\`content/posts/\`), clustered by Articles and Posts.
You can reply with the IDs you want me to delete (e.g., "Delete Post #4, Post #12, Article #2").
## 📄 Articles
| ID | Date | Title | Filename |
|---|---|---|---|
`
articles.forEach((a, i) => {
md += `| Article #${i+1} | ${a.date.substring?.(0,10) || a.date} | ${a.title} | \`${a.file}\` |\n`
})
md += `
## 💬 Posts (Shares / Stubs)
| ID | Date | Title | Filename |
|---|---|---|---|
`
posts.forEach((p, i) => {
md += `| Post #${i+1} | ${p.date.substring?.(0,10) || p.date} | ${p.title} | \`${p.file}\` |\n`
})
fs.writeFileSync('/home/andre/.gemini/antigravity/brain/12f7ab79-1833-4303-aace-1f9957dc5f67/post_inventory.md', md)
console.log('Artifact created.')
@@ -0,0 +1,55 @@
import fs from 'fs'
import path from 'path'
const PODCAST_DIR = path.join(process.cwd(), 'content', 'podcast')
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
const podFiles = fs.readdirSync(PODCAST_DIR).filter(f => f.startsWith('ep-') && f.endsWith('.yaml')).sort()
// Define known commercial posts that already exist for specific episodes
const commercialPostsMap = {
'ep-001-systemprompt-und-prompting.yaml': '2025-11-21-eigentlich-muesste-ich-heute-etwas-ueber-den-omnibus-und-die-drohende-erosion-de.md'
}
podFiles.forEach(pf => {
const filepath = path.join(PODCAST_DIR, pf)
const content = fs.readFileSync(filepath, 'utf8')
const titleMatch = content.match(/^title:\s*"?(.+?)"?$/m)
const dateMatch = content.match(/^date:\s*(.+)$/m)
if (!titleMatch || !dateMatch) return
const title = titleMatch[1]
const date = dateMatch[1]
// If we have a known commercial post, skip
if (commercialPostsMap[pf]) {
console.log(`Skipping ${pf} (has commercial post)`)
return
}
// Create filename for new post
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
const postFilename = `${date}-${slug}.md`
const postFilepath = path.join(POSTS_DIR, postFilename)
if (!fs.existsSync(postFilepath)) {
const postContent = `---
title: '${title.replace(/'/g, "''")}'
date: ${date}
---
Eine neue Episode des **Almost Intelligent** Podcasts ist online!
🎙️ **Thema:** ${title.replace(/^\d{3}:\s*/, '')}
Hört euch die komplette Folge auf unserer [Podcast-Seite](/podcast) an!
`
fs.writeFileSync(postFilepath, postContent)
console.log(`Created article for ${pf}: ${postFilename}`)
} else {
console.log(`Article already exists for ${pf}`)
}
})
@@ -0,0 +1,17 @@
import os
import re
POSTS_DIR = "content/posts"
post_files = sorted([f for f in os.listdir(POSTS_DIR) if f.endswith(".md")])
for pf in post_files:
with open(os.path.join(POSTS_DIR, pf), 'r', encoding='utf-8') as f:
content = f.read()
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
title = title_m.group(1) if title_m else ""
date = date_m.group(1) if date_m else ""
print(f"[{pf}] Date: {date} | Title: {title}")
@@ -0,0 +1,30 @@
import os
import re
CONTENT_DIR = "content/podcast"
for filename in sorted(os.listdir(CONTENT_DIR)):
if filename.startswith("ep-") and filename.endswith(".yaml"):
# extract number
match = re.search(r'ep-(\d{3})-', filename)
if match:
num = match.group(1)
filepath = os.path.join(CONTENT_DIR, filename)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# extract current title
title_match = re.search(r'^title:\s*"?(.+?)"?$', content, flags=re.MULTILINE)
if title_match:
current_title = title_match.group(1)
# strip existing number if any
current_title = re.sub(r'^\d{3}:\s*', '', current_title)
new_title = f"{num}: {current_title}"
# update title
content = re.sub(r'^title:\s*.*$', f'title: "{new_title}"', content, flags=re.MULTILINE)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated {filename} -> {new_title}")
@@ -0,0 +1,41 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const SRC_DIR = '/home/andre/coden/Kniepunkt/Kniepunkt-Visuals'
const DEST_DIR = path.join(process.cwd(), 'public', 'images', 'kniepunkt')
const CONTENT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
if (!fs.existsSync(DEST_DIR)) {
fs.mkdirSync(DEST_DIR, { recursive: true })
}
const images = fs.readdirSync(SRC_DIR).filter(f => f.endsWith('.png'))
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
images.forEach(img => {
// Extract number from image name
const match = img.match(/(\d{1,3})/)
if (!match) return
// Normalize number string to 3 digits (e.g., '1' -> '001', '12' -> '012')
const numStr = match[1].padStart(3, '0')
// Find matching markdown file
const mdFile = mdFiles.find(f => f.startsWith(numStr + '-'))
if (mdFile) {
// Copy image
const safeName = `${numStr}-cover.png`
fs.copyFileSync(path.join(SRC_DIR, img), path.join(DEST_DIR, safeName))
// Update frontmatter
const mdPath = path.join(CONTENT_DIR, mdFile)
const data = matter(fs.readFileSync(mdPath, 'utf8'))
data.data.image = `/images/kniepunkt/${safeName}`
fs.writeFileSync(mdPath, matter.stringify(data.content, data.data))
console.log(`Matched and updated: ${numStr}`)
} else {
console.log(`No markdown file found for image: ${img} (expected prefix ${numStr}-)`)
}
})
+181
View File
@@ -0,0 +1,181 @@
import fs from 'fs';
import path from 'path';
import nodemailer from 'nodemailer';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const urls = [
'https://andreknie.de',
'https://api.andreknie.de',
'https://notes.andreknie.de',
'https://unikat.andreknie.de',
'https://projekt-kiq.d-hive.de',
'https://d-hive.de'
];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function checkUrl(url) {
try {
const res = await fetch(url, { method: 'GET', redirect: 'follow' });
if (res.ok) {
return { url, status: 'UP', code: res.status };
} else {
return { url, status: 'DOWN', code: res.status };
}
} catch (err) {
return { url, status: 'DOWN', error: err.message };
}
}
async function sendAlert(failedUrls) {
const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, STAKEHOLDER_EMAIL } = process.env;
if (!SMTP_HOST || !SMTP_USER || !SMTP_PASS || !STAKEHOLDER_EMAIL) {
console.error('SMTP credentials missing. Cannot send alert email.');
return;
}
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: parseInt(SMTP_PORT || '587', 10),
secure: parseInt(SMTP_PORT || '587', 10) === 465,
auth: {
user: SMTP_USER,
pass: SMTP_PASS,
},
});
const urlList = failedUrls.map(f => `- ${f.url} (Code: ${f.code || f.error})`).join('\n');
try {
await transporter.sendMail({
from: `"Smoketest Alert" <${SMTP_USER}>`,
to: STAKEHOLDER_EMAIL,
subject: '🚨 Smoketest Alert: Endpoints Down',
text: `The following endpoints failed after two attempts:\n\n${urlList}`,
});
console.log('Alert email sent.');
} catch (err) {
console.error('Failed to send alert email:', err);
}
}
async function generateDashboard(results) {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smoketest Dashboard</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
.container { max-width: 800px; margin: 0 auto; background: #1e293b; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3); }
h1 { margin-top: 0; color: #e2e8f0; font-size: 2rem; border-bottom: 2px solid #334155; padding-bottom: 1rem; }
.status-up { color: #4ade80; font-weight: bold; background: rgba(74, 222, 128, 0.1); padding: 0.25rem 0.5rem; border-radius: 4px; }
.status-down { color: #f87171; font-weight: bold; background: rgba(248, 113, 113, 0.1); padding: 0.25rem 0.5rem; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; margin-top: 2rem; }
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid #334155; }
th { background: #0f172a; font-weight: 600; color: #cbd5e1; }
tr:last-child td { border-bottom: none; }
a { color: #38bdf8; text-decoration: none; }
a:hover { text-decoration: underline; }
.footer { margin-top: 3rem; font-size: 0.875rem; color: #64748b; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>Smoketest Dashboard</h1>
<table>
<thead>
<tr>
<th>URL</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
${results.map(r => `
<tr>
<td><a href="${r.url}" target="_blank">${r.url}</a></td>
<td><span class="${r.status === 'UP' ? 'status-up' : 'status-down'}">${r.status}</span></td>
<td>${r.status === 'UP' ? `HTTP ${r.code}` : (r.code ? `HTTP ${r.code}` : r.error)}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="footer">
Last updated: ${new Date().toUTCString()}
</div>
</div>
</body>
</html>
`;
// Write to a separate directory to avoid interfering with Vite's public dir if we run locally
const outDir = path.join(__dirname, '..', 'public-dashboard');
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
fs.writeFileSync(path.join(outDir, 'index.html'), html);
console.log('Dashboard generated at public-dashboard/index.html');
}
async function main() {
console.log('Starting smoketest...');
const results = [];
const failedFirstTry = [];
for (const url of urls) {
const result = await checkUrl(url);
if (result.status === 'DOWN') {
console.log(`${url} is DOWN (First Try)`);
failedFirstTry.push(result);
} else {
console.log(`${url} is UP`);
results.push(result);
}
}
const finalFailures = [];
if (failedFirstTry.length > 0) {
console.log(`\nWaiting 5 minutes before retrying ${failedFirstTry.length} failed endpoints...`);
// Wait 5 minutes
await sleep(5 * 60 * 1000);
console.log('\nRetrying failed endpoints...');
for (const f of failedFirstTry) {
const result = await checkUrl(f.url);
if (result.status === 'DOWN') {
console.log(`${f.url} is DOWN (Second Try)`);
finalFailures.push(result);
results.push(result);
} else {
console.log(`${f.url} is UP (Second Try)`);
results.push(result);
}
}
}
// Generate Dashboard
await generateDashboard(results);
// Alert if needed
if (finalFailures.length > 0) {
await sendAlert(finalFailures);
process.exit(1);
} else {
console.log('All endpoints are UP.');
}
}
main().catch(err => {
console.error('Unexpected error:', err);
process.exit(1);
});
@@ -0,0 +1,57 @@
import os
import re
CONTENT_DIR = "content/podcast"
updates = [
{"old": "ep-001-systemprompt-und-prompting.yaml", "id": "ep-001-systemprompt-und-prompting", "title": "Systemprompt & Prompting", "date": "2025-11-19"},
{"old": "ep-002-llms-und-wahrheit.yaml", "id": "ep-002-llms-und-wahrheit", "title": "LLMs & Wahrheit", "date": "2025-12-03"},
{"old": "ep-003-kometen-und-ki-browser.yaml", "id": "ep-003-kometen-und-ki-browser", "title": "Kometen & KI-Browser", "date": "2025-12-17"},
{"old": "ep-004-deepfakes-und-ki-pornos.yaml", "id": "ep-004-deepfakes-und-ki-pornos", "title": "Deepfakes & KI-Pornos", "date": "2025-12-31"},
{"old": "ep-005-reasoning-und-recherchen.yaml", "id": "ep-005-reasoning-und-recherchen", "title": "Reasoning & komplexere Recherchen", "date": None},
{"old": "ep-006-kinder-und-ki.yaml", "id": "ep-006-kinder-und-ki", "title": "Kinder & KI", "date": None},
{"old": "ep-007-agenten-mcp-a2a.yaml", "id": "ep-007-agenten-mcp-a2a", "title": "Agenten (Teil 1) MCP & A2A", "date": None},
{"old": "ep-008-agenten-openclaw-moltbook.yaml", "id": "ep-008-agenten-openclaw-moltbook", "title": "Agenten (Teil 2) OpenClaw & Moltbook", "date": None},
{"old": "ep-009-rags-und-vektordatenbanken.yaml", "id": "ep-009-rags-und-vektordatenbanken", "title": "RAGs & Vektordatenbanken", "date": None},
# Skip ep-010-ki-und-kirche.yaml, delete it later
{"old": "ep-011-datensouveraenitaet-teil-1.yaml", "new": "ep-010-datensouveraenitaet-teil-1.yaml", "id": "ep-010-datensouveraenitaet-teil-1", "title": "Datensouveränität (Teil 1) & Abhängigkeiten", "date": None},
{"old": "ep-012-datensouveraenitaet-teil-2.yaml", "new": "ep-011-datensouveraenitaet-teil-2.yaml", "id": "ep-011-datensouveraenitaet-teil-2", "title": "Datensouveränität (Teil 2) & Föderierung", "date": None},
]
for up in updates:
old_path = os.path.join(CONTENT_DIR, up['old'])
if not os.path.exists(old_path):
print(f"Skipping {old_path}, not found.")
continue
with open(old_path, 'r', encoding='utf-8') as f:
content = f.read()
# Update id
content = re.sub(r'^id:\s*.*$', f'id: {up["id"]}', content, flags=re.MULTILINE)
# Update title
content = re.sub(r'^title:\s*.*$', f'title: "{up["title"]}"', content, flags=re.MULTILINE)
# Update date if specified
if up['date']:
content = re.sub(r'^date:\s*.*$', f'date: {up["date"]}', content, flags=re.MULTILINE)
new_filename = up.get('new', up['old'])
new_path = os.path.join(CONTENT_DIR, new_filename)
with open(new_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated {new_path}")
# Remove old file if renamed
if new_filename != up['old']:
os.remove(old_path)
print(f"Removed {old_path}")
# Delete ki-und-kirche
kirche_path = os.path.join(CONTENT_DIR, 'ep-010-ki-und-kirche.yaml')
if os.path.exists(kirche_path):
os.remove(kirche_path)
print(f"Deleted {kirche_path}")