full upgrade to dev
This commit is contained in:
590
docs/N8N_INTEGRATION.md
Normal file
590
docs/N8N_INTEGRATION.md
Normal file
@@ -0,0 +1,590 @@
|
||||
# 🚀 n8n Integration Guide - Complete Setup
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Portfolio nutzt n8n für:
|
||||
- ⚡ **Echtzeit-Aktivitätsanzeige** (Coding, Musik, Gaming, etc.)
|
||||
- 💬 **AI-Chatbot** (mit OpenAI/Anthropic)
|
||||
- 📊 **Aktivitäts-Tracking** (GitHub, Spotify, Netflix, etc.)
|
||||
- 🎮 **Gaming-Status** (Steam, Discord)
|
||||
- 📧 **Automatische Benachrichtigungen**
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Coole Ideen für Integrationen
|
||||
|
||||
### 1. **GitHub Activity Feed** 🔨
|
||||
**Was es zeigt:**
|
||||
- "Currently coding: Portfolio Website"
|
||||
- "Last commit: 5 minutes ago"
|
||||
- "Working on: feature/n8n-integration"
|
||||
- Programming language (TypeScript, Python, etc.)
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
GitHub Webhook → Extract Data → Update Database → Display on Site
|
||||
```
|
||||
|
||||
### 2. **Spotify Now Playing** 🎵
|
||||
**Was es zeigt:**
|
||||
- Aktueller Song + Artist
|
||||
- Album Cover (rotierend animiert!)
|
||||
- Fortschrittsbalken
|
||||
- "Listening to X since Y minutes"
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
Cron (every 30s) → Spotify API → Parse Track Data → Update Database
|
||||
```
|
||||
|
||||
### 3. **Netflix/YouTube/Twitch Watching** 📺
|
||||
**Was es zeigt:**
|
||||
- "Watching: Breaking Bad S05E14"
|
||||
- "Streaming: Coding Tutorial"
|
||||
- Platform badges (Netflix/YouTube/Twitch)
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
Trakt.tv API → Get Current Watching → Update Database
|
||||
Discord Rich Presence → Extract Activity → Update Database
|
||||
```
|
||||
|
||||
### 4. **Gaming Activity** 🎮
|
||||
**Was es zeigt:**
|
||||
- "Playing: Elden Ring"
|
||||
- Platform: Steam/PlayStation/Xbox
|
||||
- Play time
|
||||
- Achievement notifications
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
Steam API → Get Current Game → Update Database
|
||||
Discord Presence → Parse Game → Update Database
|
||||
```
|
||||
|
||||
### 5. **Mood & Custom Status** 😊
|
||||
**Was es zeigt:**
|
||||
- Emoji mood (😊, 💻, 🏃, 🎮, 😴)
|
||||
- Custom message: "Focused on DevOps"
|
||||
- Auto-status based on time/activity
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
Schedule → Determine Status (work hours/break/sleep) → Update Database
|
||||
Manual Webhook → Set Custom Status → Update Database
|
||||
```
|
||||
|
||||
### 6. **Smart Notifications** 📬
|
||||
**Was es zeigt:**
|
||||
- "New email from X"
|
||||
- "GitHub PR needs review"
|
||||
- "Calendar event in 15 min"
|
||||
|
||||
**n8n Workflow:**
|
||||
```
|
||||
Email/Calendar/GitHub → Filter Important → Create Notification → Display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Setup: Datenbank Schema
|
||||
|
||||
### PostgreSQL Table: `activity_status`
|
||||
|
||||
```sql
|
||||
CREATE TABLE activity_status (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Activity
|
||||
activity_type VARCHAR(50), -- 'coding', 'listening', 'watching', 'gaming', 'reading'
|
||||
activity_details TEXT,
|
||||
activity_project VARCHAR(255),
|
||||
activity_language VARCHAR(50),
|
||||
activity_repo VARCHAR(255),
|
||||
|
||||
-- Music
|
||||
music_playing BOOLEAN DEFAULT FALSE,
|
||||
music_track VARCHAR(255),
|
||||
music_artist VARCHAR(255),
|
||||
music_album VARCHAR(255),
|
||||
music_platform VARCHAR(50), -- 'spotify', 'apple'
|
||||
music_progress INTEGER, -- 0-100
|
||||
music_album_art TEXT,
|
||||
|
||||
-- Watching
|
||||
watching_title VARCHAR(255),
|
||||
watching_platform VARCHAR(50), -- 'youtube', 'netflix', 'twitch'
|
||||
watching_type VARCHAR(50), -- 'video', 'stream', 'movie', 'series'
|
||||
|
||||
-- Gaming
|
||||
gaming_game VARCHAR(255),
|
||||
gaming_platform VARCHAR(50), -- 'steam', 'playstation', 'xbox'
|
||||
gaming_status VARCHAR(50), -- 'playing', 'idle'
|
||||
|
||||
-- Status
|
||||
status_mood VARCHAR(10), -- emoji
|
||||
status_message TEXT,
|
||||
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 n8n Workflows
|
||||
|
||||
### Workflow 1: GitHub Activity Tracker
|
||||
|
||||
**Trigger:** Webhook bei Push/Commit
|
||||
**Frequenz:** Echtzeit
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "GitHub Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"parameters": {
|
||||
"path": "github-activity",
|
||||
"method": "POST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Extract Commit Data",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"parameters": {
|
||||
"functionCode": "const commit = items[0].json;\nreturn [\n {\n json: {\n activity_type: 'coding',\n activity_details: commit.head_commit.message,\n activity_project: commit.repository.name,\n activity_language: 'TypeScript',\n activity_repo: commit.repository.html_url,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Database",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "INSERT INTO activity_status (activity_type, activity_details, activity_project, activity_language, activity_repo, updated_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET activity_type = $1, activity_details = $2, activity_project = $3, activity_language = $4, activity_repo = $5, updated_at = $6 WHERE activity_status.id = 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Setup in GitHub:**
|
||||
1. Gehe zu deinem Repository → Settings → Webhooks
|
||||
2. Add webhook: `https://your-n8n-instance.com/webhook/github-activity`
|
||||
3. Content type: `application/json`
|
||||
4. Events: Push events
|
||||
|
||||
---
|
||||
|
||||
### Workflow 2: Spotify Now Playing
|
||||
|
||||
**Trigger:** Cron (alle 30 Sekunden)
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Schedule",
|
||||
"type": "n8n-nodes-base.cron",
|
||||
"parameters": {
|
||||
"cronExpression": "*/30 * * * * *"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Spotify API",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"parameters": {
|
||||
"url": "https://api.spotify.com/v1/me/player/currently-playing",
|
||||
"method": "GET",
|
||||
"authentication": "oAuth2",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{$credentials.spotify.accessToken}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse Track Data",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"parameters": {
|
||||
"functionCode": "const track = items[0].json;\nif (!track || !track.is_playing) {\n return [{ json: { music_playing: false } }];\n}\n\nreturn [\n {\n json: {\n music_playing: true,\n music_track: track.item.name,\n music_artist: track.item.artists[0].name,\n music_album: track.item.album.name,\n music_platform: 'spotify',\n music_progress: Math.round((track.progress_ms / track.item.duration_ms) * 100),\n music_album_art: track.item.album.images[0].url,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Database",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "UPDATE activity_status SET music_playing = $1, music_track = $2, music_artist = $3, music_album = $4, music_platform = $5, music_progress = $6, music_album_art = $7, updated_at = $8 WHERE id = 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Spotify API Setup:**
|
||||
1. Gehe zu https://developer.spotify.com/dashboard
|
||||
2. Create App
|
||||
3. Add Redirect URI: `https://your-n8n-instance.com/oauth/callback`
|
||||
4. Kopiere Client ID & Secret in n8n Credentials
|
||||
5. Scopes: `user-read-currently-playing`, `user-read-playback-state`
|
||||
|
||||
---
|
||||
|
||||
### Workflow 3: AI Chatbot mit OpenAI
|
||||
|
||||
**Trigger:** Webhook bei Chat-Message
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Chat Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"parameters": {
|
||||
"path": "chat",
|
||||
"method": "POST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Build Context",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"parameters": {
|
||||
"functionCode": "const userMessage = items[0].json.message;\n\nconst context = `You are Dennis Konkol's AI assistant. Here's information about Dennis:\n\n- Student in Osnabrück, Germany\n- Passionate self-hoster and DevOps enthusiast\n- Skills: Next.js, Flutter, Docker Swarm, Traefik, CI/CD, n8n\n- Runs own infrastructure on IONOS and OVHcloud\n- Projects: Clarity (Flutter dyslexia app), Self-hosted portfolio with Docker Swarm\n- Hobbies: Gaming, Jogging, Experimenting with tech\n- Fun fact: Uses pen & paper for calendar despite automating everything\n\nAnswer questions about Dennis professionally and friendly. Keep answers concise (2-3 sentences).\n\nUser question: ${userMessage}`;\n\nreturn [{ json: { context, userMessage } }];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OpenAI Chat",
|
||||
"type": "n8n-nodes-base.openAi",
|
||||
"parameters": {
|
||||
"resource": "chat",
|
||||
"operation": "message",
|
||||
"model": "gpt-4",
|
||||
"messages": {
|
||||
"values": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "={{$node[\"Build Context\"].json[\"context\"]}}"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "={{$node[\"Build Context\"].json[\"userMessage\"]}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Return Response",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"parameters": {
|
||||
"responseBody": "={{ { reply: $json.message.content } }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**OpenAI API Setup:**
|
||||
1. Gehe zu https://platform.openai.com/api-keys
|
||||
2. Create API Key
|
||||
3. Add zu n8n Credentials
|
||||
4. Wähle Model: gpt-4 oder gpt-3.5-turbo
|
||||
|
||||
---
|
||||
|
||||
### Workflow 4: Discord/Steam Gaming Status
|
||||
|
||||
**Trigger:** Cron (alle 60 Sekunden)
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Schedule",
|
||||
"type": "n8n-nodes-base.cron",
|
||||
"parameters": {
|
||||
"cronExpression": "0 * * * * *"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Discord API",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"parameters": {
|
||||
"url": "https://discord.com/api/v10/users/@me",
|
||||
"method": "GET",
|
||||
"authentication": "oAuth2",
|
||||
"headers": {
|
||||
"Authorization": "Bot {{$credentials.discord.token}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse Gaming Status",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"parameters": {
|
||||
"functionCode": "const user = items[0].json;\nconst activity = user.activities?.find(a => a.type === 0); // 0 = Playing\n\nif (!activity) {\n return [{ json: { gaming_game: null, gaming_status: 'idle' } }];\n}\n\nreturn [\n {\n json: {\n gaming_game: activity.name,\n gaming_platform: 'discord',\n gaming_status: 'playing',\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Database",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "UPDATE activity_status SET gaming_game = $1, gaming_platform = $2, gaming_status = $3, updated_at = $4 WHERE id = 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Workflow 5: Smart Status (Auto-Detect)
|
||||
|
||||
**Trigger:** Cron (alle 5 Minuten)
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Schedule",
|
||||
"type": "n8n-nodes-base.cron",
|
||||
"parameters": {
|
||||
"cronExpression": "*/5 * * * *"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Determine Status",
|
||||
"type": "n8n-nodes-base.function",
|
||||
"parameters": {
|
||||
"functionCode": "const hour = new Date().getHours();\nconst day = new Date().getDay(); // 0 = Sunday, 6 = Saturday\n\nlet mood = '💻';\nlet message = 'Working on projects';\n\n// Sleep time (0-7 Uhr)\nif (hour >= 0 && hour < 7) {\n mood = '😴';\n message = 'Sleeping (probably dreaming of code)';\n}\n// Morning (7-9 Uhr)\nelse if (hour >= 7 && hour < 9) {\n mood = '☕';\n message = 'Morning coffee & catching up';\n}\n// Work time (9-17 Uhr, Mo-Fr)\nelse if (hour >= 9 && hour < 17 && day >= 1 && day <= 5) {\n mood = '💻';\n message = 'Deep work mode - coding & learning';\n}\n// Evening (17-22 Uhr)\nelse if (hour >= 17 && hour < 22) {\n mood = '🎮';\n message = 'Relaxing - gaming or watching shows';\n}\n// Late night (22-24 Uhr)\nelse if (hour >= 22) {\n mood = '🌙';\n message = 'Late night coding session';\n}\n// Weekend\nif (day === 0 || day === 6) {\n mood = '🏃';\n message = 'Weekend vibes - exploring & experimenting';\n}\n\nreturn [\n {\n json: {\n status_mood: mood,\n status_message: message,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Database",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "UPDATE activity_status SET status_mood = $1, status_message = $2, updated_at = $3 WHERE id = 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Frontend API Integration
|
||||
|
||||
### Update `/app/api/n8n/status/route.ts`
|
||||
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Fetch from your activity_status table
|
||||
const status = await prisma.$queryRaw`
|
||||
SELECT * FROM activity_status WHERE id = 1 LIMIT 1
|
||||
`;
|
||||
|
||||
if (!status || status.length === 0) {
|
||||
return NextResponse.json({
|
||||
activity: null,
|
||||
music: null,
|
||||
watching: null,
|
||||
gaming: null,
|
||||
status: null,
|
||||
});
|
||||
}
|
||||
|
||||
const data = status[0];
|
||||
|
||||
return NextResponse.json({
|
||||
activity: data.activity_type ? {
|
||||
type: data.activity_type,
|
||||
details: data.activity_details,
|
||||
project: data.activity_project,
|
||||
language: data.activity_language,
|
||||
repo: data.activity_repo,
|
||||
timestamp: data.updated_at,
|
||||
} : null,
|
||||
music: data.music_playing ? {
|
||||
isPlaying: data.music_playing,
|
||||
track: data.music_track,
|
||||
artist: data.music_artist,
|
||||
album: data.music_album,
|
||||
platform: data.music_platform,
|
||||
progress: data.music_progress,
|
||||
albumArt: data.music_album_art,
|
||||
} : null,
|
||||
watching: data.watching_title ? {
|
||||
title: data.watching_title,
|
||||
platform: data.watching_platform,
|
||||
type: data.watching_type,
|
||||
} : null,
|
||||
gaming: data.gaming_game ? {
|
||||
game: data.gaming_game,
|
||||
platform: data.gaming_platform,
|
||||
status: data.gaming_status,
|
||||
} : null,
|
||||
status: data.status_mood ? {
|
||||
mood: data.status_mood,
|
||||
customMessage: data.status_message,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching activity status:', error);
|
||||
return NextResponse.json({
|
||||
activity: null,
|
||||
music: null,
|
||||
watching: null,
|
||||
gaming: null,
|
||||
status: null,
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create `/app/api/n8n/chat/route.ts`
|
||||
|
||||
```typescript
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { message } = await request.json();
|
||||
|
||||
// Call your n8n chat webhook
|
||||
const response = await fetch(`${process.env.N8N_WEBHOOK_URL}/webhook/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('n8n webhook failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json({ reply: data.reply });
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
return NextResponse.json(
|
||||
{ reply: 'Sorry, I encountered an error. Please try again later.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Zusätzliche coole Ideen
|
||||
|
||||
### 1. **Live Coding Stats**
|
||||
- Lines of code today
|
||||
- Most used language this week
|
||||
- GitHub contribution graph
|
||||
- Pull requests merged
|
||||
|
||||
### 2. **Coffee Counter** ☕
|
||||
- Button in n8n Dashboard: "I had coffee"
|
||||
- Displays: "3 coffees today"
|
||||
- Funny messages bei > 5 cups
|
||||
|
||||
### 3. **Mood Tracker**
|
||||
- Manual mood updates via Discord Bot
|
||||
- Shows emoji + custom message
|
||||
- Persists über den Tag
|
||||
|
||||
### 4. **Auto-DND Status**
|
||||
- Wenn du in einem Meeting bist (Calendar API)
|
||||
- Wenn du fokussiert arbeitest (Pomodoro Timer)
|
||||
- Custom status: "🔴 In Deep Work - Back at 15:00"
|
||||
|
||||
### 5. **Project Highlights**
|
||||
- "Currently building: X"
|
||||
- "Deployed Y minutes ago"
|
||||
- "Last successful build: Z"
|
||||
|
||||
### 6. **Social Activity**
|
||||
- "New blog post: Title"
|
||||
- "Trending on Twitter: X mentions"
|
||||
- "LinkedIn: Y profile views this week"
|
||||
|
||||
---
|
||||
|
||||
## 📝 Environment Variables
|
||||
|
||||
Add to `.env.local`:
|
||||
|
||||
```bash
|
||||
# n8n
|
||||
N8N_WEBHOOK_URL=https://your-n8n-instance.com
|
||||
N8N_API_KEY=your_n8n_api_key
|
||||
|
||||
# Spotify
|
||||
SPOTIFY_CLIENT_ID=your_spotify_client_id
|
||||
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
|
||||
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
|
||||
# Discord (optional)
|
||||
DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||
|
||||
# GitHub (optional)
|
||||
GITHUB_WEBHOOK_SECRET=your_github_webhook_secret
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Setup Database:**
|
||||
```bash
|
||||
psql -U postgres -d portfolio_dev -f setup_activity_status.sql
|
||||
```
|
||||
|
||||
2. **Create n8n Workflows:**
|
||||
- Import workflows via n8n UI
|
||||
- Configure credentials
|
||||
- Activate workflows
|
||||
|
||||
3. **Update API Routes:**
|
||||
- Add `status/route.ts` and `chat/route.ts`
|
||||
- Set environment variables
|
||||
|
||||
4. **Test:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- Check bottom-right corner for activity bubbles
|
||||
- Click chat button to test AI
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Best Practices
|
||||
|
||||
1. **Caching:** Cache API responses für 30s (nicht bei jedem Request neu fetchen)
|
||||
2. **Error Handling:** Graceful fallbacks wenn n8n down ist
|
||||
3. **Rate Limiting:** Limitiere Chat-Requests (max 10/minute)
|
||||
4. **Privacy:** Zeige nur das, was du teilen willst
|
||||
5. **Performance:** Nutze Webhooks statt Polling wo möglich
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Community Ideas
|
||||
|
||||
Teile deine coolen n8n-Integrationen!
|
||||
- Discord: Zeig deinen Setup
|
||||
- GitHub: Share deine Workflows
|
||||
- Blog: Write-up über dein System
|
||||
|
||||
Happy automating! 🎉
|
||||
Reference in New Issue
Block a user