Files
portfolio/n8n-workflows/portfolio-status.json
denshooter a958008add fix: remove review truncation and show full reviews; fix telegram-cms workflow bugs
- ReadBooks.tsx: remove line-clamp-3, readMore button, and review modal
- Show full review text inline instead of truncated snippets
- Remove unused AnimatePresence, X import, selectedReview state
- Fix  typo in 6 handler nodes
- Fix Markdown/HTML mix (*text*</b> → <b>text</b>)
- Fix Switch condition syntax (.action → .action)
- Fix position collision (Review Info Handler)
- Hardcode Telegram bot token, fix response handling in Publish Handler
- Add AI-generated questions for .review flow (was .review HC_ID TEXT)
- New .answer command for submitting review answers
- Create/Refine Review: POST new translations if missing instead of skipping
- Remove all substring truncations from Telegram messages
2026-04-09 17:22:23 +02:00

258 lines
10 KiB
JSON

{
"name": "portfolio-website",
"nodes": [
{
"parameters": {
"path": "/denshooter-71242/status",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
96
],
"id": "44d27fdc-49e7-4f86-a917-10781d81104f",
"name": "Webhook",
"webhookId": "4c292bc7-41f2-423d-86cf-a0384924b539"
},
{
"parameters": {
"url": "https://wakapi.dk0.dev/api/summary",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "interval",
"value": "today"
},
{
"name": "api_key",
"value": "2158fa72-e7fa-4dbd-9627-4235f241105e"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
240,
176
],
"id": "4e7559f3-85dc-43b6-b0c2-31313db15fbf",
"name": "Wakapi",
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// --------------------------------------------------------\n// DATEN AUS DEN VORHERIGEN NODES HOLEN\n// --------------------------------------------------------\n\n// 1. Spotify Node\nlet spotifyData = null;\ntry {\n spotifyData = $('Spotify').first().json;\n} catch (e) {}\n\n// 2. Lanyard Node (Discord)\nlet lanyardData = null;\ntry {\n lanyardData = $('Lanyard').first().json.data;\n} catch (e) {}\n\n// 3. Wakapi Summary (Tages-Statistik)\nlet wakapiStats = null;\ntry {\n const wRaw = $('Wakapi').first().json;\n // Manchmal ist es direkt im Root, manchmal unter data\n wakapiStats = wRaw.grand_total ? wRaw : (wRaw.data ? wRaw.data : null);\n} catch (e) {}\n\n// 4. Wakapi Heartbeats (Live Check)\nlet heartbeatsList = [];\ntry {\n // Deine API liefert ein Array mit einem Objekt, das \"data\" enthält\n // Struktur: [ { \"data\": [...] } ]\n const response = $('WakapiLast').last().json;\n if (response.data && Array.isArray(response.data)) {\n heartbeatsList = response.data;\n }\n} catch (e) {}\n\n\n// --------------------------------------------------------\n// LOGIK & FORMATIERUNG\n// --------------------------------------------------------\n\n// --- A. SPOTIFY / MUSIC ---\nlet music = null;\n\nif (spotifyData && spotifyData.item && spotifyData.is_playing) {\n music = {\n isPlaying: true,\n track: spotifyData.item.name,\n artist: spotifyData.item.artists.map(a => a.name).join(', '),\n album: spotifyData.item.album.name,\n albumArt: spotifyData.item.album.images[0]?.url,\n url: spotifyData.item.external_urls.spotify\n };\n} else if (lanyardData?.listening_to_spotify && lanyardData.spotify) {\n music = {\n isPlaying: true,\n track: lanyardData.spotify.song,\n artist: lanyardData.spotify.artist.replace(/;/g, \", \"),\n album: lanyardData.spotify.album,\n albumArt: lanyardData.spotify.album_art_url,\n url: `https://open.spotify.com/track/${lanyardData.spotify.track_id}`\n };\n}\n\n// --- B. GAMING & STATUS ---\nlet gaming = null;\nlet status = {\n text: lanyardData?.discord_status || \"offline\",\n color: 'gray'\n};\n\n// Farben mapping\nif (status.text === 'online') status.color = 'green';\nif (status.text === 'idle') status.color = 'yellow';\nif (status.text === 'dnd') status.color = 'red';\n\nif (lanyardData?.activities) {\n lanyardData.activities.forEach(act => {\n // Type 0 = Game (Spotify ignorieren)\n if (act.type === 0 && act.name !== \"Spotify\") {\n let image = null;\n if (act.assets?.large_image) {\n if (act.assets.large_image.startsWith(\"mp:external\")) {\n image = act.assets.large_image.replace(/mp:external\\/([^\\/]*)\\/(https?)\\/([^\\/]*)\\/(.*)/, \"$2://$3/$4\");\n } else {\n image = `https://cdn.discordapp.com/app-assets/${act.application_id}/${act.assets.large_image}.png`;\n }\n }\n gaming = {\n isPlaying: true,\n name: act.name,\n details: act.details,\n state: act.state,\n image: image\n };\n }\n });\n}\n\n\n// --- C. CODING (Wakapi Logic) ---\nlet coding = null;\n\n// 1. Basis-Stats von heute (Fallback)\nif (wakapiStats && wakapiStats.grand_total) {\n coding = {\n isActive: false, \n stats: {\n time: wakapiStats.grand_total.text, // \"2 hrs 10 mins\"\n topLang: wakapiStats.languages?.[0]?.name || \"Code\",\n topProject: wakapiStats.projects?.[0]?.name || \"Project\"\n }\n };\n}\n\n// 2. Live Check via Heartbeats\nif (heartbeatsList.length > 0) {\n // Nimm den allerletzten Eintrag aus der Liste (das ist der neuste)\n const latestBeat = heartbeatsList[heartbeatsList.length - 1];\n\n if (latestBeat && latestBeat.time) {\n // Zeitstempel vergleichen \n // latestBeat.time ist Unix Seconds (z.B. 1767829137) -> mal 1000 für Millisekunden\n const beatTime = new Date(latestBeat.time * 1000).getTime();\n const now = new Date().getTime();\n const diffMinutes = (now - beatTime) / 1000 / 60;\n\n // Debugging (optional, kannst du in n8n Console sehen)\n // console.log(`Letzter Beat: ${new Date(beatTime).toISOString()} (${diffMinutes.toFixed(1)} min her)`);\n\n // Wenn jünger als 15 Minuten -> AKTIV\n if (diffMinutes < 1) {\n // Falls Summary leer war, erstellen wir ein Dummy\n if (!coding) coding = { stats: { time: \"Just started\" } };\n\n coding.isActive = true;\n \n // Projekt Name\n coding.project = latestBeat.project || coding.stats?.topProject;\n \n // Dateiname extrahieren (funktioniert für Windows \\ und Unix /)\n if (latestBeat.entity) {\n const parts = latestBeat.entity.split(/[/\\\\]/);\n coding.file = parts[parts.length - 1]; // Nimmt \"ActivityFeed.tsx\"\n }\n \n coding.language = latestBeat.language;\n }\n }\n}\n\n// --------------------------------------------------------\n// OUTPUT\n// --------------------------------------------------------\nreturn {\n json: {\n status,\n music,\n gaming,\n coding,\n timestamp: new Date().toISOString()\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
656,
48
],
"id": "103c6314-c48f-46a9-b986-20f94814bcae",
"name": "Code in JavaScript"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
848,
48
],
"id": "d2ef27ea-34b5-4686-8f24-41205636fc82",
"name": "Respond to Webhook"
},
{
"parameters": {
"operation": "currentlyPlaying"
},
"type": "n8n-nodes-base.spotify",
"typeVersion": 1,
"position": [
240,
0
],
"id": "cac25f50-c8bf-47d3-8812-ef56cd8110df",
"name": "Spotify",
"credentials": {
"spotifyOAuth2Api": {
"id": "2bHFkmHiwTxZQsK3",
"name": "Spotify account"
}
}
},
{
"parameters": {
"url": "https://api.lanyard.rest/v1/users/172037532370862080",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
240,
-176
],
"id": "febe9caf-2cc2-4bd6-8289-cf4f34249e20",
"name": "Lanyard",
"onError": "continueErrorOutput"
},
{
"parameters": {
"url": "https://wakapi.dk0.dev/api/compat/wakatime/v1/users/current/heartbeats",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "2158fa72-e7fa-4dbd-9627-4235f241105e"
},
{
"name": "date",
"value": "={{ new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Berlin' }) }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
256,
368
],
"id": "8d70ed7a-455d-4961-b735-2df86a8b5c04",
"name": "WakapiLast",
"onError": "continueErrorOutput"
},
{
"parameters": {
"numberInputs": 4
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
480,
16
],
"id": "c4a1957a-9863-4dea-95c4-4e55637b403e",
"name": "Merge"
}
],
"pinData": {},
"connections": {
"Webhook": {
"main": [
[
{
"node": "Spotify",
"type": "main",
"index": 0
},
{
"node": "Lanyard",
"type": "main",
"index": 0
},
{
"node": "Wakapi",
"type": "main",
"index": 0
},
{
"node": "WakapiLast",
"type": "main",
"index": 0
}
]
]
},
"Wakapi": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 2
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Spotify": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Lanyard": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"WakapiLast": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 3
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "842c4910-5935-4788-aede-b290af8cb96e",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
},
"id": "M6sq0mVBmRYt4sia",
"tags": []
}