diff --git a/app/[locale]/snippets/SnippetsClient.tsx b/app/[locale]/snippets/SnippetsClient.tsx deleted file mode 100644 index 90abfc6..0000000 --- a/app/[locale]/snippets/SnippetsClient.tsx +++ /dev/null @@ -1,294 +0,0 @@ - -"use client"; - -import React, { useState, useEffect, useCallback, useMemo } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { Snippet } from "@/lib/directus"; -import { X, Copy, Check, ChevronLeft, ChevronRight, Search } from "lucide-react"; - -// Color-coded language badges using the liquid design palette -const LANG_STYLES: Record = { - typescript: { bg: "bg-liquid-lavender/40", text: "text-purple-700 dark:text-purple-300", label: "TS" }, - ts: { bg: "bg-liquid-lavender/40", text: "text-purple-700 dark:text-purple-300", label: "TS" }, - javascript: { bg: "bg-liquid-amber/40", text: "text-amber-700 dark:text-amber-300", label: "JS" }, - js: { bg: "bg-liquid-amber/40", text: "text-amber-700 dark:text-amber-300", label: "JS" }, - python: { bg: "bg-liquid-sky/40", text: "text-sky-700 dark:text-sky-300", label: "PY" }, - bash: { bg: "bg-liquid-mint/40", text: "text-emerald-700 dark:text-emerald-300", label: "SH" }, - shell: { bg: "bg-liquid-mint/40", text: "text-emerald-700 dark:text-emerald-300", label: "SH" }, - sh: { bg: "bg-liquid-mint/40", text: "text-emerald-700 dark:text-emerald-300", label: "SH" }, - dockerfile: { bg: "bg-liquid-blue/40", text: "text-blue-700 dark:text-blue-300", label: "🐳" }, - docker: { bg: "bg-liquid-blue/40", text: "text-blue-700 dark:text-blue-300", label: "🐳" }, - css: { bg: "bg-liquid-pink/40", text: "text-pink-700 dark:text-pink-300", label: "CSS" }, - scss: { bg: "bg-liquid-pink/40", text: "text-pink-700 dark:text-pink-300", label: "SCSS" }, - go: { bg: "bg-liquid-teal/40", text: "text-teal-700 dark:text-teal-300", label: "GO" }, - rust: { bg: "bg-liquid-peach/40", text: "text-orange-700 dark:text-orange-300", label: "RS" }, - yaml: { bg: "bg-liquid-lime/40", text: "text-lime-700 dark:text-lime-300", label: "YAML" }, - json: { bg: "bg-liquid-lime/40", text: "text-lime-700 dark:text-lime-300", label: "JSON" }, - sql: { bg: "bg-liquid-coral/40", text: "text-red-700 dark:text-red-300", label: "SQL" }, - nginx: { bg: "bg-liquid-teal/40", text: "text-teal-700 dark:text-teal-300", label: "NGINX" }, -}; - -function getLangStyle(language: string) { - return LANG_STYLES[language?.toLowerCase()] ?? { - bg: "bg-liquid-purple/30", - text: "text-purple-700 dark:text-purple-300", - label: language?.toUpperCase() || "CODE", - }; -} - -function CodePreview({ code }: { code: string }) { - const lines = code.split("\n").slice(0, 4); - return ( -
-      {lines.map((line, i) => (
-        
{line || " "}
- ))} - {code.split("\n").length > 4 && ( -
…
- )} -
- ); -} - -export default function SnippetsClient({ initialSnippets }: { initialSnippets: Snippet[] }) { - const [selectedSnippet, setSelectedSnippet] = useState(null); - const [copied, setCopied] = useState(false); - const [activeCategory, setActiveCategory] = useState("All"); - const [search, setSearch] = useState(""); - - // Derived data - const categories = useMemo(() => { - const cats = Array.from(new Set(initialSnippets.map((s) => s.category))).sort(); - return ["All", ...cats]; - }, [initialSnippets]); - - const filtered = useMemo(() => { - const q = search.toLowerCase(); - return initialSnippets.filter((s) => { - const matchCat = activeCategory === "All" || s.category === activeCategory; - const matchSearch = - !q || - s.title.toLowerCase().includes(q) || - s.description.toLowerCase().includes(q) || - s.category.toLowerCase().includes(q) || - s.language.toLowerCase().includes(q); - return matchCat && matchSearch; - }); - }, [initialSnippets, activeCategory, search]); - - // Language badge for the currently open modal - const modalLang = useMemo( - () => (selectedSnippet ? getLangStyle(selectedSnippet.language) : null), - [selectedSnippet] - ); - - // Keyboard nav: ESC + arrows - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (!selectedSnippet) return; - if (e.key === "Escape") { - setSelectedSnippet(null); - } else if (e.key === "ArrowRight" || e.key === "ArrowDown") { - const idx = filtered.findIndex((s) => s.id === selectedSnippet.id); - if (idx < filtered.length - 1) setSelectedSnippet(filtered[idx + 1]); - } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { - const idx = filtered.findIndex((s) => s.id === selectedSnippet.id); - if (idx > 0) setSelectedSnippet(filtered[idx - 1]); - } - }, - [selectedSnippet, filtered] - ); - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [handleKeyDown]); - - const copyToClipboard = useCallback((code: string) => { - navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }, []); - - const currentIndex = selectedSnippet - ? filtered.findIndex((s) => s.id === selectedSnippet.id) - : -1; - - return ( - <> - {/* ── Filter & Search bar ── */} -
- {/* Search */} -
- - setSearch(e.target.value)} - className="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl text-sm text-stone-900 dark:text-stone-100 placeholder:text-stone-400 focus:outline-none focus:border-liquid-purple transition-colors" - /> -
- - {/* Category chips */} -
- {categories.map((cat) => ( - - ))} -
-
- - {/* ── Empty state ── */} - {filtered.length === 0 && ( -

- No snippets found{search ? ` for "${search}"` : ""}. -

- )} - - {/* ── Snippet Grid ── */} -
- {filtered.map((s, i) => { - const lang = getLangStyle(s.language); - return ( - setSelectedSnippet(s)} - className="text-left bg-white dark:bg-stone-900 rounded-[2.5rem] p-8 border border-stone-200/60 dark:border-stone-800/60 shadow-sm hover:shadow-xl hover:border-liquid-purple/40 transition-all group flex flex-col" - > - {/* Header row: category + language badge */} -
- - {s.category} - - {s.language && ( - - {lang.label} - - )} -
- - {/* Title */} -

- {s.title} -

- - {/* Description */} -

- {s.description} -

- - {/* Mini code preview */} - -
- ); - })} -
- - {/* ── Snippet Modal ── */} - - {selectedSnippet && modalLang && ( -
- setSelectedSnippet(null)} - className="absolute inset-0 bg-stone-950/60 backdrop-blur-md" - /> - -
- {/* Modal header */} -
-
-
-

- {selectedSnippet.category} -

- {selectedSnippet.language && ( - - {modalLang.label} - - )} -
-

- {selectedSnippet.title} -

-
- -
- -

- {selectedSnippet.description} -

- - {/* Code block */} -
-
- -
-
-                    {selectedSnippet.code}
-                  
-
-
- - {/* Modal footer: navigation */} -
- - - {currentIndex + 1} / {filtered.length} - - -
-
-
- )} -
- - ); -} diff --git a/app/[locale]/snippets/page.tsx b/app/[locale]/snippets/page.tsx deleted file mode 100644 index d62e4d7..0000000 --- a/app/[locale]/snippets/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ - -import React from "react"; -import { getSnippets } from "@/lib/directus"; -import { Terminal, ArrowLeft } from "lucide-react"; -import Link from "next/link"; -import SnippetsClient from "./SnippetsClient"; - -export default async function SnippetsPage({ params }: { params: Promise<{ locale: string }> }) { - const { locale } = await params; - const snippets = await getSnippets(100) || []; - - return ( -
-
- - - Back to Portfolio - - -
-
-
- -
-

- The Lab. -

-
-

- A collection of technical snippets, configurations, and mental notes from my daily building process. -

-
- - -
-
- ); -} diff --git a/app/api/snippets/route.ts b/app/api/snippets/route.ts deleted file mode 100644 index 39386f8..0000000 --- a/app/api/snippets/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getSnippets } from '@/lib/directus'; - -const CACHE_TTL = 300; // 5 minutes - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit') || '10'); - const featured = searchParams.get('featured') === 'true' ? true : undefined; - - const snippets = await getSnippets(limit, featured); - - return NextResponse.json( - { snippets: snippets || [] }, - { headers: { 'Cache-Control': `public, s-maxage=${CACHE_TTL}, stale-while-revalidate=${CACHE_TTL * 2}` } } - ); - } catch (_error) { - return NextResponse.json({ error: 'Failed to fetch snippets' }, { status: 500 }); - } -} diff --git a/app/components/About.tsx b/app/components/About.tsx index a6a523f..08ecfbd 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -7,13 +7,13 @@ import dynamic from "next/dynamic"; const RichTextClient = dynamic(() => import("./RichTextClient"), { ssr: false }); import CurrentlyReading from "./CurrentlyReading"; import ReadBooks from "./ReadBooks"; -import { motion, AnimatePresence } from "framer-motion"; -import { TechStackCategory, TechStackItem, Hobby, Snippet } from "@/lib/directus"; +import { motion } from "framer-motion"; +import { TechStackCategory, TechStackItem, Hobby } from "@/lib/directus"; import Link from "next/link"; import ActivityFeed from "./ActivityFeed"; import BentoChat from "./BentoChat"; import { Skeleton } from "./ui/Skeleton"; -import { LucideIcon, X, Copy, Check } from "lucide-react"; +import { LucideIcon } from "lucide-react"; const iconMap: Record = { Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2, BookOpen, Tv, Plane, Camera, Stars, Music, Terminal, Cpu @@ -25,21 +25,17 @@ const About = () => { const [cmsHtml, setCmsHtml] = useState(null); const [techStack, setTechStack] = useState([]); const [hobbies, setHobbies] = useState([]); - const [snippets, setSnippets] = useState([]); - const [selectedSnippet, setSelectedSnippet] = useState(null); - const [copied, setCopied] = useState(false); const [_cmsMessages, setCmsMessages] = useState>({}); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { - const [cmsRes, techRes, hobbiesRes, msgRes, snippetsRes] = await Promise.all([ + const [cmsRes, techRes, hobbiesRes, msgRes] = await Promise.all([ fetch(`/api/content/page?key=home-about&locale=${locale}`), fetch(`/api/tech-stack?locale=${locale}`), fetch(`/api/hobbies?locale=${locale}`), - fetch(`/api/messages?locale=${locale}`), - fetch(`/api/snippets?limit=3&featured=true`) + fetch(`/api/messages?locale=${locale}`) ]); const cmsData = await cmsRes.json(); @@ -53,9 +49,6 @@ const About = () => { const msgData = await msgRes.json(); if (msgData?.messages) setCmsMessages(msgData.messages); - - const snippetsData = await snippetsRes.json(); - if (snippetsData?.snippets) setSnippets(snippetsData.snippets); } catch (error) { console.error("About data fetch failed:", error); } finally { @@ -65,12 +58,6 @@ const About = () => { fetchData(); }, [locale]); - const copyToClipboard = (code: string) => { - navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return (
@@ -169,96 +156,61 @@ const About = () => {
- {/* 5. Library, Gear & Snippets */} -
- {/* Library - Larger Span */} - -
-
-

- Library -

- - View All - -
- -
- + {/* 5. Library */} + +
+
+

+ Library +

+ + View All + +
+ +
+ +
+
+
+ + {/* 6. My Gear */} + +
+
+

+ My Gear +

+
+
+

Main

+

MacBook M4 Pro

+
+
+

PC

+

RTX 3080 / R7

+
+
+

Server

+

IONOS & RPi 4

+
+
+

OS

+

macOS / Linux

+
- - -
- {/* My Gear (Uses) */} - -
-

- My Gear -

-
-
-

Main

-

MacBook M4 Pro

-
-
-

PC

-

RTX 3080 / R7

-
-
-

Server

-

IONOS & RPi 4

-
-
-

OS

-

macOS / Linux

-
-
-
-
- - - -
-

- Snippets -

-
- {isLoading ? ( - Array.from({ length: 2 }).map((_, i) => ) - ) : snippets.length > 0 ? ( - snippets.map((s) => ( - - )) - ) : ( -

No snippets yet.

- )} -
-
- - Enter the Lab - -
+
-
+
- {/* 6. Hobbies */} + {/* 7. Hobbies */} {
- - {/* Snippet Modal */} - - {selectedSnippet && ( -
- setSelectedSnippet(null)} - className="absolute inset-0 bg-stone-950/60 backdrop-blur-md" - /> - -
-
-
-

{selectedSnippet.category}

-

{selectedSnippet.title}

-
- -
- -

- {selectedSnippet.description} -

- -
-
- -
-
-                    {selectedSnippet.code}
-                  
-
-
-
- -
-
-
- )} -
); }; -export default About; +export default About; \ No newline at end of file diff --git a/app/not-found.tsx b/app/not-found.tsx index fc79199..49cf0ac 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,7 +1,7 @@ "use client"; import { motion } from "framer-motion"; -import { ArrowLeft, Search, Terminal } from "lucide-react"; +import { ArrowLeft, Search } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -25,7 +25,7 @@ export default function NotFound() {
@@ -58,52 +58,29 @@ export default function NotFound() {
- {/* Sidebar Cards */} -
- {/* Search/Explore Projects */} - +
+ +

Explore Work

+

Maybe what you need is in my project archive?

+
+ -
- -

Explore Work

-

Maybe what you need is in my project archive?

-
- - View Projects - -
- - - {/* Visit the Lab */} - -
- -

Technical

-

Check out my collection of code snippets and notes.

-
- - Enter the Lab - -
-
+ View Projects + +
+
); -} +} \ No newline at end of file diff --git a/lib/directus.ts b/lib/directus.ts index 429e429..6c42693 100644 --- a/lib/directus.ts +++ b/lib/directus.ts @@ -937,63 +937,6 @@ export async function getProjectBySlug( } } -// Snippets Types -export interface Snippet { - id: string; - title: string; - category: string; - code: string; - description: string; - language: string; -} - -/** - * Get Snippets from Directus - */ -export async function getSnippets(limit = 10, featured?: boolean): Promise { - const filters = ['status: { _eq: "published" }']; - if (featured !== undefined) { - filters.push(`featured: { _eq: ${featured} }`); - } - const filterString = `filter: { _and: [{ ${filters.join(' }, { ')} }] }`; - - const query = ` - query { - snippets( - ${filterString} - limit: ${limit} - ) { - id - title - category - code - description - language - } - } - `; - - try { - const result = await directusRequest( - '', - { body: { query } } - ); - - interface SnippetsResult { - snippets: Snippet[]; - } - const snippets = (result as SnippetsResult | null)?.snippets; - if (!snippets || snippets.length === 0) { - return null; - } - - return snippets; - } catch (_error) { - console.error('Failed to fetch snippets:', _error); - return null; - } -} - // ─── Hardcover β†’ Directus sync helpers ─────────────────────────────────────── export interface BookReviewCreate { diff --git a/n8n-workflows/docker-callback-handler.json b/n8n-workflows/docker-callback-handler.json deleted file mode 100644 index 2537646..0000000 --- a/n8n-workflows/docker-callback-handler.json +++ /dev/null @@ -1,418 +0,0 @@ -{ - "name": "Docker Event - Callback Handler", - "nodes": [ - { - "parameters": { - "updates": [ - "callback_query" - ], - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.2, - "position": [ - -880, - 288 - ], - "id": "a56a5174-3ccf-492f-810b-117be933560c", - "name": "Telegram Trigger", - "webhookId": "6e70b9ab-b76b-48dc-8e4d-5fe1bf0d7e39", - "credentials": { - "telegramApi": { - "id": "ADurvy9EKUDzbDdq", - "name": "DK0_Server" - } - } - }, - { - "parameters": { - "jsCode": "const callback = $input.first().json;\nconst data = callback.callback_query?.data || '';\nconst chatId = callback.callback_query?.from?.id;\nconst messageId = callback.callback_query?.message?.message_id;\n\n// Parse: auto:slug, manual:slug, ignore:slug\nconst [action, slug] = data.split(':');\n\nreturn [{\n json: {\n action,\n slug,\n chatId,\n messageId,\n rawCallback: data\n }\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -656, - 288 - ], - "id": "10e5a475-4194-4919-9186-1eb052fbd79b", - "name": "Parse Callback" - }, - { - "parameters": { - "rules": { - "values": [ - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $json.action }}", - "rightValue": "auto", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Auto" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $json.action }}", - "rightValue": "manual", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Manual" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $json.action }}", - "rightValue": "ignore", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Ignore" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - -448, - 288 - ], - "id": "a533e527-b3c5-4946-9a26-6f499c7dd6c5", - "name": "Switch Action" - }, - { - "parameters": { - "url": "=https://cms.dk0.dev/items/projects?filter[slug][_eq]={{ $json.slug }}&limit=1", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "httpBearerAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - -224, - 80 - ], - "id": "9fc55503-e890-4074-9823-f07001b6948a", - "name": "Get Project from CMS" - }, - { - "parameters": { - "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/commits?limit=3", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 0, - 0 - ], - "id": "a3fda0d9-0cc9-4744-be3e-9a95ef44dfb4", - "name": "Get Commits" - }, - { - "parameters": { - "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/contents/README.md", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 0, - 128 - ], - "id": "7106b8c9-fb20-46d9-9e4e-06882115bf7a", - "name": "Get README" - }, - { - "parameters": { - "model": "openrouter/free", - "options": {} - }, - "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "typeVersion": 1, - "position": [ - 448, - 192 - ], - "id": "9acce2c3-1a26-450f-a263-0dc3a1f1e3cf", - "name": "OpenRouter Chat Model" - }, - { - "parameters": { - "promptType": "define", - "text": "=Du bist ein technischer Autor fΓΌr das Portfolio von Dennis (dk0.dev).\n\nNeues eigenes Projekt deployed:\nRepo: {{ $('Parse Callback').item.json.slug }}\n\nREADME:\n{{ $('Get README').first().json.content ? Buffer.from($('Get README').first().json.content, 'base64').toString('utf8').substring(0, 1000) : 'Kein README' }}\n\nLetzte Commits:\n{{ $('Get Commits').first().json.map(c => '- ' + c.commit.message).join('\\n') }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht das Projekt (Features, Zweck)\n- Tech-Stack und Architektur\n- Highlights aus den Commits\n- Warum ist es cool/interessant\n\nKategorie: webdev (wenn Web-App), automation (wenn Tool/Script), oder selfhosted\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"AussagekrΓ€ftiger Titel\",\n \"title_de\": \"AussagekrΓ€ftiger Titel\",\n \"description_en\": \"4-6 SΓ€tze\",\n \"description_de\": \"4-6 SΓ€tze\",\n \"content_en\": \"2-3 AbsΓ€tze Markdown mit technischen Details\",\n \"content_de\": \"2-3 AbsΓ€tze Markdown mit technischen Details\",\n \"category\": \"webdev|automation|selfhosted\",\n \"technologies\": [\"Next.js\", \"Docker\", \"...\"]\n}", - "batching": {}, - "prompt": "\n Verwende keine Bindestriche, Em-Dashes oder Gedankenstriche (–, β€”, -)." - }, - "type": "@n8n/n8n-nodes-langchain.chainLlm", - "typeVersion": 1.9, - "position": [ - 224, - 80 - ], - "id": "2b011cf8-6ed3-4cb1-ab6f-7727912864fc", - "name": "AI: Generate Description" - }, - { - "parameters": { - "jsCode": "const raw = $input.first().json.text ?? \"\";\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\nconst ai = JSON.parse(match[0]);\nreturn [{ json: ai }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 448, - 80 - ], - "id": "0cbdcf6e-e5d4-460e-b345-b6d47deed051", - "name": "Parse JSON" - }, - { - "parameters": { - "jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Callback').first().json;\n\nconst body = {\n slug: ctx.slug,\n status: \"draft\",\n featured: false,\n title: ai.title_en,\n category: ai.category,\n technologies: ai.technologies,\n tags: ai.technologies,\n date: new Date().toISOString().slice(0, 10),\n translations: {\n create: [\n {\n languages_code: \"en-US\",\n title: ai.title_en,\n description: ai.description_en,\n content: ai.content_en\n },\n {\n languages_code: \"de-DE\",\n title: ai.title_de,\n description: ai.description_de,\n content: ai.content_de\n }\n ]\n }\n};\n\nconst response = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://cms.dk0.dev/items/projects\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body\n});\n\nreturn [{ json: response }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 672, - 80 - ], - "id": "70aecf97-6b70-4f03-99e3-9ee44fc0830b", - "name": "Add to Directus" - }, - { - "parameters": { - "chatId": "={{ $('Parse Callback').item.json.chatId }}", - "text": "={{ \n'βœ… Projekt erstellt: ' + $json.data.title + '\\n\\n' +\n'πŸ“ ' + $('Parse JSON').first().json.description_de.substring(0, 200) + '...\\n\\n' +\n'Status: Draft (ID: ' + $json.data.id + ')\\n\\n' +\n'/publishproject' + $json.data.id + ' β€” VerΓΆffentlichen\\n' + \n'/deleteproject' + $json.data.id + ' β€” LΓΆschen' \n}}", - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 880, - 80 - ], - "id": "9a353247-7d25-4330-9cbf-580599428ae1", - "name": "Notify Success", - "webhookId": "b1d7284d-c2e5-4e87-b65d-272f1b9b8d6d" - }, - { - "parameters": { - "chatId": "={{ $json.chatId }}", - "text": "✍️ OK, schreib mir jetzt was das Projekt macht (4-6 SΓ€tze).\n\nIch formatiere das dann schΓΆn und erstelle einen Draft.", - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - -224, - 288 - ], - "id": "9160b847-5f07-4d64-9488-faeaeca926b9", - "name": "Ask for Manual Input", - "webhookId": "c4cb518d-a2e2-48af-b9b6-c3f645fd37db" - }, - { - "parameters": { - "chatId": "={{ $json.chatId }}", - "text": "❌ OK, ignoriert.", - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - -224, - 480 - ], - "id": "1624b6f1-8202-4fd2-bd0a-52fa039ca696", - "name": "Confirm Ignore", - "webhookId": "4c5248f1-4420-403c-a506-2e1968c5579d", - "credentials": { - "telegramApi": { - "id": "ADurvy9EKUDzbDdq", - "name": "DK0_Server" - } - } - } - ], - "pinData": {}, - "connections": { - "Telegram Trigger": { - "main": [ - [ - { - "node": "Parse Callback", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback": { - "main": [ - [ - { - "node": "Switch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Switch Action": { - "main": [ - [ - { - "node": "Get Project from CMS", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Ask for Manual Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Confirm Ignore", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Project from CMS": { - "main": [ - [ - { - "node": "Get Commits", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Commits": { - "main": [ - [ - { - "node": "Get README", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get README": { - "main": [ - [ - { - "node": "AI: Generate Description", - "type": "main", - "index": 0 - } - ] - ] - }, - "OpenRouter Chat Model": { - "ai_languageModel": [ - [ - { - "node": "AI: Generate Description", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "AI: Generate Description": { - "main": [ - [ - { - "node": "Parse JSON", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse JSON": { - "main": [ - [ - { - "node": "Add to Directus", - "type": "main", - "index": 0 - } - ] - ] - }, - "Add to Directus": { - "main": [ - [ - { - "node": "Notify Success", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": false, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": false - }, - "versionId": "4636a407-7f8e-4833-9345-9d3296ec9b74", - "meta": { - "instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d" - }, - "id": "abnrtUuJ7BAWv9Hm", - "tags": [] -} \ No newline at end of file diff --git a/n8n-workflows/docker-event.json b/n8n-workflows/docker-event.json deleted file mode 100644 index 263bef2..0000000 --- a/n8n-workflows/docker-event.json +++ /dev/null @@ -1,937 +0,0 @@ -{ - "name": "Docker Event (Extended)", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "docker-event", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - 0, - -224 - ], - "id": "870fa550-42f6-4e19-a796-f1f044b0cdc8", - "name": "Webhook", - "webhookId": "e147d70b-79d8-44fd-bbe8-8274cf905b11", - "disabled": true - }, - { - "parameters": { - "jsCode": "const data = $input.first().json;\n\nconst container = data.container ?? data.body?.container ?? '';\nconst image = data.image ?? data.body?.image ?? '';\nconst timestamp = data.timestamp ?? data.body?.timestamp ?? '';\n\nconst slug = container.toLowerCase().replace(/[^a-z0-9]+/g, '-');\n\nconst serviceName = container.replace(/[-_]/g, ' ');\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug \n }\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 224, - -224 - ], - "id": "aaa6a678-1ad3-4f82-9b01-37e21b47b189", - "name": "Kontext aufbereiten", - "disabled": true - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "loose", - "version": 3 - }, - "conditions": [ - { - "id": "ebe26f0c-d5a7-45c9-9747-afc75b57a41c", - "leftValue": "={{ $json.data }}", - "rightValue": "[]", - "operator": { - "type": "string", - "operation": "notEndsWith" - } - } - ], - "combinator": "and" - }, - "looseTypeValidation": true, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 672, - -224 - ], - "id": "62197a33-5169-48e1-9539-57c047efb108", - "name": "If", - "disabled": true - }, - { - "parameters": { - "url": "=https://cms.dk0.dev/items/projects?filter[slug][_eq]={{ $json.slug }}&limit=1", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "httpBearerAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 448, - -224 - ], - "id": "db783886-06b5-4473-8907-dd6c655aa3dd", - "name": "Search for Slug", - "credentials": { - "httpBearerAuth": { - "id": "ZtI5e08iryR9m6FG", - "name": "Directus" - } - }, - "disabled": true - }, - { - "parameters": { - "model": "openrouter/free", - "options": {} - }, - "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "typeVersion": 1, - "position": [ - 976, - 16 - ], - "id": "b9130ff4-359b-4736-9442-1b0ca7d31877", - "name": "OpenRouter Chat Model", - "credentials": { - "openRouterApi": { - "id": "8Kdy4RHHwMZ0Cn6x", - "name": "OpenRouter" - } - }, - "disabled": true - }, - { - "parameters": { - "promptType": "define", - "text": "= Du bist ein technischer Autor fΓΌr das Self-Hosting Portfolio von Dennis auf dk0.dev.\n Ein neuer Service wurde auf dem Server deployed:\n \n Container: {{ $('Kontext aufbereiten').item.json.container }}\n Image: {{ $('Kontext aufbereiten').item.json.image }}\n Service: {{ $('Kontext aufbereiten').item.json.serviceName }}\n \n Aufgabe:\n 1. Erkenne ob es sich um ein EIGENES Projekt (z.B. Image enthΓ€lt \"denshooter\", \"dk0\", \"portfolio\") oder eine \nSELF-HOSTED App handelt.\n 2. Bewerte die \"Coolness\" (1-10) basierend auf:\n - Eigener Code = +3 Punkte\n - Neue/spannende Technologie = +2 Punkte\n - Großes/bekanntes Projekt (Suricata, CrowdStrike-Level) = +3 Punkte\n - Standard Self-Hosted Tool (Nextcloud, Plausible) = +1 Punkt\n - CI/CD Build-Container, Test-Runner = 0 Punkte (ignorieren)\n 3. Erstelle Beschreibung NUR wenn coolness_score >= 6\n \n Antworte NUR als valides JSON:\n {\n \"coolness_score\": 1-10,\n \"notify\": true/false (true wenn >= 7),\n \"reason\": \"Kurze BegrΓΌndung warum cool oder nicht\",\n \"type\": \"own\" oder \"selfhosted\" oder \"ignore\",\n \"title_en\": \"...\",\n \"title_de\": \"...\",\n \"description_en\": \"...\",\n \"description_de\": \"...\",\n \"content_en\": \"...\",\n \"content_de\": \"...\",\n \"category\": \"selfhosted\" oder \"webdev\" oder \"automation\",\n \"technologies\": [\"Docker\", \"...\"]\n }", - "batching": {}, - "prompt": "\n Verwende keine Bindestriche, Em-Dashes oder Gedankenstriche (–, β€”, -)." - }, - "type": "@n8n/n8n-nodes-langchain.chainLlm", - "typeVersion": 1.9, - "position": [ - 896, - -224 - ], - "id": "77d46075-3342-4e93-8806-07087a2389dc", - "name": "Basic LLM Chain", - "disabled": true - }, - { - "parameters": { - "jsCode": "const raw = $input.first().json.text ?? \"\";\n\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\n\nconst ai = JSON.parse(match[0]);\n\nreturn [\n {\n json: ai,\n },\n];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1248, - -224 - ], - "id": "de5ed311-0d46-4677-963c-711a6ad514e9", - "name": "Parse JSON", - "disabled": true - }, - { - "parameters": { - "jsCode": "const ai = $('Parse JSON').first().json;\n const ctx = $('Kontext aufbereiten').first().json;\n\n const body = {\n slug: ctx.slug,\n status: \"draft\",\n featured: false,\n title: ai.title_en,\n category: ai.category,\n technologies: ai.technologies,\n tags: ai.technologies,\n date: new Date().toISOString().slice(0, 10),\n translations: {\n create: [\n {\n languages_code: \"en-US\",\n title: ai.title_en,\n description: ai.description_en,\n content: ai.content_en\n },\n {\n languages_code: \"de-DE\",\n title: ai.title_de,\n description: ai.description_de,\n content: ai.content_de\n }\n ]\n }\n };\n\n const response = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://cms.dk0.dev/items/projects\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body\n });\n\n return [{ json: response }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1680, - -224 - ], - "id": "c47b915d-e4d7-43e9-8ee3-b41389896fa7", - "name": "Add to Directus", - "disabled": true - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "{ \"success\": true }", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 2128, - -224 - ], - "id": "6cf8f30d-1352-466f-9163-9b4f16b972e0", - "name": "Respond to Webhook", - "disabled": true - }, - { - "parameters": { - "chatId": "145931600", - "text": "={{ \n'πŸ†• Neuer Service erkannt!\\n\\n' +\n'πŸ“¦ ' + $('Kontext aufbereiten').first().json.container + '\\n' +\n'🐳 ' + $('Kontext aufbereiten').first().json.image + '\\n\\n' +\n'πŸ“ ' + $('Parse JSON').first().json.title_de + '\\n' + \n$('Parse JSON').first().json.description_de + '\\n\\n' +\n'Status: Draft in Directus erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n('/publishproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' β€” VerΓΆffentlichen\\n' + \n('/deleteproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' β€” LΓΆschen' \n}}", - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1904, - -224 - ], - "id": "b29de3ec-b1ca-40c3-8493-af44e5372fd2", - "name": "Send a text message", - "webhookId": "c02ccf69-16dc-436e-b1cc-f8fa9dd8d33f", - "credentials": { - "telegramApi": { - "id": "ADurvy9EKUDzbDdq", - "name": "DK0_Server" - } - }, - "disabled": true - }, - { - "parameters": { - "rules": { - "values": [ - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "leftValue": "={{ $json.notify }}", - "rightValue": "true", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - }, - "id": "febc397c-b060-4a66-ab9b-1274c8509cc2" - } - ], - "combinator": "and" - } - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.switch", - "typeVersion": 3.4, - "position": [ - 1456, - -224 - ], - "id": "5ade115f-e134-4358-8d95-a144eede8d9a", - "name": "Switch", - "disabled": true - }, - { - "parameters": { - "jsCode": "const data = $input.first().json;\n\nconst container = data.container ?? data.body?.container ?? '';\nconst image = data.image ?? data.body?.image ?? '';\nconst timestamp = data.timestamp ?? data.body?.timestamp ?? '';\n\nconst slug = container.toLowerCase().replace(/[^a-z0-9]+/g, '-');\nconst serviceName = container.replace(/[-_]/g, ' ');\n\n// Detect project type\nlet projectType = 'selfhosted';\nif (image.includes('denshooter') || image.includes('dk0')) {\n projectType = 'own';\n} else if (container.match(/^(act-|gitea-actions-|runner-)/)) {\n projectType = 'cicd';\n}\n\n// Extract repo from image for own projects\nlet repo = null;\nif (projectType === 'own') {\n const match = image.match(/([^/]+):(\\w+)/);\n if (match) repo = match[1];\n}\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug,\n projectType,\n repo\n }\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 896, - 768 - ], - "id": "fb34f047-5c11-4255-9b45-adb9fe169042", - "name": "Parse Context" - }, - { - "parameters": { - "url": "=https://cms.dk0.dev/items/projects?filter[slug][_eq]={{ $json.slug }}&limit=1", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "httpBearerAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 1120, - 768 - ], - "id": "acd7a411-2465-4aa3-a7ee-442a79c500f2", - "name": "Check if Exists", - "credentials": { - "httpBearerAuth": { - "id": "ZtI5e08iryR9m6FG", - "name": "Directus" - } - } - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "loose" - }, - "conditions": [ - { - "leftValue": "={{ $json.data.length }}", - "rightValue": "0", - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 1344, - 768 - ], - "id": "bdcddb94-8676-4467-a370-ad2cf07d09a3", - "name": "If New" - }, - { - "parameters": { - "rules": { - "values": [ - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $('Parse Context').item.json.projectType }}", - "rightValue": "own", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Own Project" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $('Parse Context').item.json.projectType }}", - "rightValue": "cicd", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "CI/CD (Ignore)" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "" - }, - "conditions": [ - { - "leftValue": "={{ $('Parse Context').item.json.projectType }}", - "rightValue": "selfhosted", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Self-Hosted" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1568, - 768 - ], - "id": "00786826-8d6b-4e17-aa7f-1afdca38d7a3", - "name": "Switch Type" - }, - { - "parameters": { - "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/commits?limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 1776, - 560 - ], - "id": "9ef7f66b-3054-4765-b0a8-7ebb6aa353aa", - "name": "Get Last Commit", - "credentials": { - "httpHeaderAuth": { - "id": "YN3oIbok6Fjy5WNW", - "name": "gitea api" - } - } - }, - { - "parameters": { - "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/contents/README.md", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 1840, - 672 - ], - "id": "114fece9-c5f1-4c6b-8272-6f39fb8ce24a", - "name": "Get README", - "credentials": { - "httpHeaderAuth": { - "id": "YN3oIbok6Fjy5WNW", - "name": "gitea api" - } - } - }, - { - "parameters": { - "jsCode": "const ctx = $('Parse Context').first().json;\nconst commit = $('Get Last Commit').first().json[0];\nconst readme = $('Get README').first().json;\n\n// Decode README (base64)\nlet readmeText = '';\ntry {\n readmeText = Buffer.from(readme.content, 'base64').toString('utf8');\n // First 500 chars\n readmeText = readmeText.substring(0, 500).replace(/\\n/g, ' ');\n} catch (e) {\n readmeText = 'No README available';\n}\n\nconst commitMsg = commit?.commit?.message || 'No recent commits';\nconst commitAuthor = commit?.commit?.author?.name || 'Unknown';\n\nreturn [{\n json: {\n container: ctx.container,\n image: ctx.image,\n slug: ctx.slug,\n repo: ctx.repo,\n commitMsg,\n commitAuthor,\n readmeExcerpt: readmeText\n }\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2192, - 480 - ], - "id": "8810426d-c146-42c9-8ec2-5d8f56934a1f", - "name": "Merge Git Data" - }, - { - "parameters": { - "chatId": "145931600", - "text": "={{ \n'πŸš€ Neuer Deploy: ' + $json.container + '\\n' +\n'πŸ“¦ ' + $json.image + '\\n\\n' +\n'πŸ“ Letzter Commit:\\n' + $json.commitMsg + '\\n' +\n'πŸ‘€ ' + $json.commitAuthor + '\\n\\n' +\n'πŸ“„ README:\\n' + $json.readmeExcerpt + '...\\n\\n' +\n'Was ist das Highlight?' \n}}", - "replyMarkup": "inlineKeyboard", - "inlineKeyboard": { - "rows": [ - { - "row": { - "buttons": [ - { - "text": "Selbst beschreiben", - "additionalFields": { - "callback_data": "={{ 'manual:' + $json.slug }}" - } - }, - { - "text": "Auto-generieren", - "additionalFields": { - "callback_data": "={{ 'ignore:' + $json.slug }}" - } - } - ] - } - } - ] - }, - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2544, - 592 - ], - "id": "d4016ea3-7233-4926-af21-c7b07cc5f39d", - "name": "Ask via Telegram", - "webhookId": "313376d7-33a6-4c80-938b-e8ebc7ee2d11", - "credentials": { - "telegramApi": { - "id": "ADurvy9EKUDzbDdq", - "name": "DK0_Server" - } - } - }, - { - "parameters": { - "promptType": "define", - "text": "=Du bist ein technischer Autor fΓΌr dk0.dev.\n\nNeuer Self-Hosted Service:\nContainer: {{ $('Parse Context').item.json.container }}\nImage: {{ $('Parse Context').item.json.image }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht die App\n- Warum Self-Hosting besser ist als Cloud\n- Wie sie in die Infrastruktur integriert ist\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"Titel\",\n \"title_de\": \"Titel\",\n \"description_en\": \"4-6 SΓ€tze\",\n \"description_de\": \"4-6 SΓ€tze\",\n \"content_en\": \"2-3 AbsΓ€tze Markdown\",\n \"content_de\": \"2-3 AbsΓ€tze Markdown\",\n \"category\": \"selfhosted\",\n \"technologies\": [\"Docker\", \"...\"]\n}", - "batching": {}, - "prompt": "\n Verwende keine Bindestriche, Em-Dashes oder Gedankenstriche (–, β€”, -)." - }, - "type": "@n8n/n8n-nodes-langchain.chainLlm", - "typeVersion": 1.9, - "position": [ - 1952, - 864 - ], - "id": "0fd46a9d-40a9-4bb7-be5e-9b32b9a96381", - "name": "AI: Self-Hosted" - }, - { - "parameters": { - "chatId": "145931600", - "text": "={{ \n'πŸ†• Self-Hosted Service: ' + $('Parse Context').first().json.serviceName + '\\n\\n' +\n'πŸ“ ' + $json.data.title + '\\n\\n' +\n'Status: Draft erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n'/publishproject' + $json.data.id + ' β€” VerΓΆffentlichen\\n' + \n'/deleteproject' + $json.data.id + ' β€” LΓΆschen' \n}}", - "additionalFields": {} - }, - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2656, - 848 - ], - "id": "bfaca06b-65ca-41a8-ba8a-1b1aef7ba12d", - "name": "Notify Selfhosted", - "webhookId": "a7d15c96-41e1-4242-9b5f-0382f4f0d31a", - "credentials": { - "telegramApi": { - "id": "ADurvy9EKUDzbDdq", - "name": "DK0_Server" - } - } - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "{ \"success\": true, \"message\": \"CI/CD container ignored\" }", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 1776, - 960 - ], - "id": "d93818d9-64f9-4f57-ae84-c4280eeb50f0", - "name": "Respond (Ignore)" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "{ \"success\": true }", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 2880, - 768 - ], - "id": "4f1ad083-e73a-497c-a724-673205254b34", - "name": "Respond" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "{ \"success\": true, \"message\": \"Project already exists\" }", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 1568, - 960 - ], - "id": "0b93b3c7-c158-4389-af18-b418aa3b2239", - "name": "Respond (Exists)" - }, - { - "parameters": { - "httpMethod": "POST", - "path": "docker-event", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - 688, - 768 - ], - "id": "2b1c77d4-9f7f-4758-9e8e-f88195448ba3", - "name": "Webhook1", - "webhookId": "25d94042-2088-4e09-bfae-645db3d6803f" - }, - { - "parameters": { - "model": "openrouter/free", - "options": {} - }, - "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", - "typeVersion": 1, - "position": [ - 1968, - 1072 - ], - "id": "a450227f-f1e5-44f3-a90e-044420042fc4", - "name": "OpenRouter Chat Model1", - "credentials": { - "openRouterApi": { - "id": "8Kdy4RHHwMZ0Cn6x", - "name": "OpenRouter" - } - } - }, - { - "parameters": { - "jsCode": "const raw = $input.first().json.text ?? \"\";\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\nconst ai = JSON.parse(match[0]);\nreturn [{ json: ai }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2224, - 848 - ], - "id": "ca78ecdd-5520-4540-969b-9e7b77bac3b4", - "name": "Parse JSON1" - }, - { - "parameters": { - "jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Context').first().json;\n\nconst body = {\n slug: ctx.slug,\n status: \"draft\",\n featured: false,\n title: ai.title_en,\n category: ai.category,\n technologies: ai.technologies,\n tags: ai.technologies,\n date: new Date().toISOString().slice(0, 10),\n translations: {\n create: [\n {\n languages_code: \"en-US\",\n title: ai.title_en,\n description: ai.description_en,\n content: ai.content_en\n },\n {\n languages_code: \"de-DE\",\n title: ai.title_de,\n description: ai.description_de,\n content: ai.content_de\n }\n ]\n }\n};\n\nconst response = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://cms.dk0.dev/items/projects\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body\n});\n\nreturn [{ json: response }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2448, - 848 - ], - "id": "1ac0a31c-68a1-44df-a6b3-203698318cbf", - "name": "Add to Directus1" - } - ], - "pinData": {}, - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Kontext aufbereiten", - "type": "main", - "index": 0 - } - ] - ] - }, - "Kontext aufbereiten": { - "main": [ - [ - { - "node": "Search for Slug", - "type": "main", - "index": 0 - } - ] - ] - }, - "If": { - "main": [ - [], - [ - { - "node": "Basic LLM Chain", - "type": "main", - "index": 0 - } - ] - ] - }, - "Search for Slug": { - "main": [ - [ - { - "node": "If", - "type": "main", - "index": 0 - } - ] - ] - }, - "OpenRouter Chat Model": { - "ai_languageModel": [ - [ - { - "node": "Basic LLM Chain", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Basic LLM Chain": { - "main": [ - [ - { - "node": "Parse JSON", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse JSON": { - "main": [ - [ - { - "node": "Switch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Add to Directus": { - "main": [ - [ - { - "node": "Send a text message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send a text message": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - }, - "Switch": { - "main": [ - [ - { - "node": "Add to Directus", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Context": { - "main": [ - [ - { - "node": "Check if Exists", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check if Exists": { - "main": [ - [ - { - "node": "If New", - "type": "main", - "index": 0 - } - ] - ] - }, - "If New": { - "main": [ - [ - { - "node": "Switch Type", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Respond (Exists)", - "type": "main", - "index": 0 - } - ] - ] - }, - "Switch Type": { - "main": [ - [ - { - "node": "Get Last Commit", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Respond (Ignore)", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "AI: Self-Hosted", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Last Commit": { - "main": [ - [ - { - "node": "Get README", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get README": { - "main": [ - [ - { - "node": "Merge Git Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Git Data": { - "main": [ - [ - { - "node": "Ask via Telegram", - "type": "main", - "index": 0 - } - ] - ] - }, - "Ask via Telegram": { - "main": [ - [ - { - "node": "Respond", - "type": "main", - "index": 0 - } - ] - ] - }, - "AI: Self-Hosted": { - "main": [ - [ - { - "node": "Parse JSON1", - "type": "main", - "index": 0 - } - ] - ] - }, - "Notify Selfhosted": { - "main": [ - [ - { - "node": "Respond", - "type": "main", - "index": 0 - } - ] - ] - }, - "Webhook1": { - "main": [ - [ - { - "node": "Parse Context", - "type": "main", - "index": 0 - } - ] - ] - }, - "OpenRouter Chat Model1": { - "ai_languageModel": [ - [ - { - "node": "AI: Self-Hosted", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Parse JSON1": { - "main": [ - [ - { - "node": "Add to Directus1", - "type": "main", - "index": 0 - } - ] - ] - }, - "Add to Directus1": { - "main": [ - [ - { - "node": "Notify Selfhosted", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": false - }, - "versionId": "1e2cf0ca-fe15-4a10-9716-30f85a2c2531", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d" - }, - "id": "RARR6MAlJSHAmBp8", - "tags": [] -} \ No newline at end of file diff --git a/scripts/check-gitea-runner.sh b/scripts/check-gitea-runner.sh deleted file mode 100644 index 8bde2eb..0000000 --- a/scripts/check-gitea-runner.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash - -# Gitea Runner Status Check Script -# PrΓΌft den Status des Gitea Runners - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" -echo -e "${BLUE}β•‘ Gitea Runner Status Check β•‘${NC}" -echo -e "${BLUE}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" -echo "" - -# Check 1: systemd service -echo -e "${CYAN}[1/5] Checking systemd service...${NC}" -if systemctl list-units --type=service --all | grep -q "gitea-runner.service"; then - echo -e "${GREEN}βœ“ systemd service found${NC}" - systemctl status gitea-runner --no-pager -l || true -else - echo -e "${YELLOW}⚠ systemd service not found (runner might be running differently)${NC}" -fi -echo "" - -# Check 2: Running processes -echo -e "${CYAN}[2/5] Checking for running runner processes...${NC}" -RUNNER_PROCESSES=$(ps aux | grep -E "(gitea|act_runner|woodpecker)" | grep -v grep || echo "") -if [ ! -z "$RUNNER_PROCESSES" ]; then - echo -e "${GREEN}βœ“ Found runner processes:${NC}" - echo "$RUNNER_PROCESSES" | while read line; do - echo " $line" - done -else - echo -e "${RED}βœ— No runner processes found${NC}" -fi -echo "" - -# Check 3: Docker containers (if runner runs in Docker) -echo -e "${CYAN}[3/5] Checking for runner Docker containers...${NC}" -RUNNER_CONTAINERS=$(docker ps -a --filter "name=runner" --format "{{.Names}}\t{{.Status}}" 2>/dev/null || echo "") -if [ ! -z "$RUNNER_CONTAINERS" ]; then - echo -e "${GREEN}βœ“ Found runner containers:${NC}" - echo "$RUNNER_CONTAINERS" | while read line; do - echo " $line" - done -else - echo -e "${YELLOW}⚠ No runner containers found${NC}" -fi -echo "" - -# Check 4: Common runner directories -echo -e "${CYAN}[4/5] Checking common runner directories...${NC}" -RUNNER_DIRS=( - "/tmp/gitea-runner" - "/opt/gitea-runner" - "/home/*/gitea-runner" - "~/.gitea-runner" - "/usr/local/gitea-runner" -) - -FOUND_DIRS=0 -for dir in "${RUNNER_DIRS[@]}"; do - # Expand ~ and wildcards - EXPANDED_DIR=$(eval echo "$dir" 2>/dev/null || echo "") - if [ -d "$EXPANDED_DIR" ]; then - echo -e "${GREEN}βœ“ Found runner directory: $EXPANDED_DIR${NC}" - FOUND_DIRS=$((FOUND_DIRS + 1)) - # Check for config files - if [ -f "$EXPANDED_DIR/.runner" ] || [ -f "$EXPANDED_DIR/config.yml" ]; then - echo " β†’ Contains configuration files" - fi - fi -done - -if [ $FOUND_DIRS -eq 0 ]; then - echo -e "${YELLOW}⚠ No runner directories found in common locations${NC}" -fi -echo "" - -# Check 5: Network connections (check if runner is connecting to Gitea) -echo -e "${CYAN}[5/5] Checking network connections to Gitea...${NC}" -GITEA_URL="${GITEA_URL:-https://git.dk0.dev}" -if command -v netstat >/dev/null 2>&1; then - CONNECTIONS=$(netstat -tn 2>/dev/null | grep -E "(git.dk0.dev|3000|3001)" || echo "") -elif command -v ss >/dev/null 2>&1; then - CONNECTIONS=$(ss -tn 2>/dev/null | grep -E "(git.dk0.dev|3000|3001)" || echo "") -fi - -if [ ! -z "$CONNECTIONS" ]; then - echo -e "${GREEN}βœ“ Found connections to Gitea:${NC}" - echo "$CONNECTIONS" | head -5 -else - echo -e "${YELLOW}⚠ No active connections to Gitea found${NC}" -fi -echo "" - -# Summary -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -echo -e "${BLUE}Summary:${NC}" -echo "" - -if [ ! -z "$RUNNER_PROCESSES" ] || [ ! -z "$RUNNER_CONTAINERS" ]; then - echo -e "${GREEN}βœ“ Runner appears to be running${NC}" - echo "" - echo "To check runner status in Gitea:" - echo " 1. Go to: https://git.dk0.dev/denshooter/portfolio/settings/actions/runners" - echo " 2. Check if runner-01 shows as 'online' or 'idle'" - echo "" - echo "To view runner logs:" - if [ ! -z "$RUNNER_PROCESSES" ]; then - echo " - Check process logs or journalctl" - fi - if [ ! -z "$RUNNER_CONTAINERS" ]; then - echo " - docker logs " - fi -else - echo -e "${RED}βœ— Runner does not appear to be running${NC}" - echo "" - echo "To start the runner:" - echo " 1. Find where the runner binary is located" - echo " 2. Check Gitea for registration token" - echo " 3. Run: ./act_runner register --config config.yml" - echo " 4. Run: ./act_runner daemon --config config.yml" -fi - -echo "" -echo -e "${CYAN}For more information, check:${NC}" -echo " - Gitea Runner Docs: https://docs.gitea.com/usage/actions/act-runner" -echo " - Runner Status: https://git.dk0.dev/denshooter/portfolio/settings/actions/runners" -echo "" diff --git a/scripts/gitea-deploy-simple.sh b/scripts/gitea-deploy-simple.sh deleted file mode 100755 index f1fd424..0000000 --- a/scripts/gitea-deploy-simple.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/bash - -# Simplified Gitea deployment script for testing -# This version doesn't require database dependencies - -set -e - -# Configuration -PROJECT_NAME="portfolio" -CONTAINER_NAME="portfolio-app-simple" -IMAGE_NAME="portfolio-app" -PORT=3000 -BACKUP_PORT=3001 -LOG_FILE="./logs/gitea-deploy-simple.log" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Logging function -log() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" -} - -# Check if running as root (skip in CI environments) -if [[ $EUID -eq 0 ]] && [[ -z "$CI" ]]; then - error "This script should not be run as root (use CI=true to override)" - exit 1 -fi - -# Check if Docker is running -if ! docker info > /dev/null 2>&1; then - error "Docker is not running. Please start Docker and try again." - exit 1 -fi - -# Check if we're in the right directory -if [ ! -f "package.json" ] || [ ! -f "Dockerfile" ]; then - error "Please run this script from the project root directory" - exit 1 -fi - -log "πŸš€ Starting simplified Gitea deployment for $PROJECT_NAME" - -# Step 1: Build Application -log "πŸ”¨ Step 1: Building application..." - -# Build Next.js application -log "πŸ“¦ Building Next.js application..." -npm run build || { - error "Build failed" - exit 1 -} - -success "βœ… Application built successfully" - -# Step 2: Docker Operations -log "🐳 Step 2: Docker operations..." - -# Build Docker image -log "πŸ—οΈ Building Docker image..." -docker build -t "$IMAGE_NAME:latest" . || { - error "Docker build failed" - exit 1 -} - -# Tag with timestamp -TIMESTAMP=$(date +%Y%m%d-%H%M%S) -docker tag "$IMAGE_NAME:latest" "$IMAGE_NAME:$TIMESTAMP" - -success "βœ… Docker image built successfully" - -# Step 3: Deployment -log "πŸš€ Step 3: Deploying application..." - -# Export environment variables for docker-compose compatibility -log "πŸ“ Exporting environment variables..." -export NODE_ENV=${NODE_ENV:-production} -export NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://dk0.dev} -export MY_EMAIL=${MY_EMAIL:-contact@dk0.dev} -export MY_INFO_EMAIL=${MY_INFO_EMAIL:-info@dk0.dev} -export MY_PASSWORD="${MY_PASSWORD}" -export MY_INFO_PASSWORD="${MY_INFO_PASSWORD}" -export ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}" -export LOG_LEVEL=${LOG_LEVEL:-info} -export PORT=${PORT:-3000} - -# Log which variables are set (without revealing secrets) -log "Environment variables configured:" -log " - NODE_ENV: ${NODE_ENV}" -log " - NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}" -log " - MY_EMAIL: ${MY_EMAIL}" -log " - MY_INFO_EMAIL: ${MY_INFO_EMAIL}" -log " - MY_PASSWORD: [SET]" -log " - MY_INFO_PASSWORD: [SET]" -log " - ADMIN_BASIC_AUTH: [SET]" -log " - LOG_LEVEL: ${LOG_LEVEL}" -log " - PORT: ${PORT}" - -# Check if container is running -if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; then - log "πŸ“¦ Stopping existing container..." - docker stop "$CONTAINER_NAME" || true - docker rm "$CONTAINER_NAME" || true -fi - -# Check if port is available -if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null ; then - warning "Port $PORT is in use. Trying backup port $BACKUP_PORT" - DEPLOY_PORT=$BACKUP_PORT -else - DEPLOY_PORT=$PORT -fi - -# Start new container with minimal environment variables -log "πŸš€ Starting new container on port $DEPLOY_PORT..." -docker run -d \ - --name "$CONTAINER_NAME" \ - --restart unless-stopped \ - -p "$DEPLOY_PORT:3000" \ - -e NODE_ENV=production \ - -e NEXT_PUBLIC_BASE_URL=https://dk0.dev \ - -e MY_EMAIL=contact@dk0.dev \ - -e MY_INFO_EMAIL=info@dk0.dev \ - -e MY_PASSWORD=test-password \ - -e MY_INFO_PASSWORD=test-password \ - -e ADMIN_BASIC_AUTH=admin:test123 \ - -e LOG_LEVEL=info \ - "$IMAGE_NAME:latest" || { - error "Failed to start container" - exit 1 -} - -# Wait for container to be ready -log "⏳ Waiting for container to be ready..." -sleep 20 - -# Check if container is actually running -if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then - error "Container failed to start or crashed" - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=50 - exit 1 -fi - -# Health check -log "πŸ₯ Performing health check..." -HEALTH_CHECK_TIMEOUT=180 -HEALTH_CHECK_INTERVAL=5 -ELAPSED=0 - -while [ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]; do - # Check if container is still running - if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then - error "Container stopped during health check" - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=50 - exit 1 - fi - - # Try health check endpoint - if curl -f "http://localhost:$DEPLOY_PORT/api/health" > /dev/null 2>&1; then - success "βœ… Application is healthy!" - break - fi - - sleep $HEALTH_CHECK_INTERVAL - ELAPSED=$((ELAPSED + HEALTH_CHECK_INTERVAL)) - echo -n "." -done - -if [ $ELAPSED -ge $HEALTH_CHECK_TIMEOUT ]; then - error "Health check timeout. Application may not be running properly." - log "Container status:" - docker inspect "$CONTAINER_NAME" --format='{{.State.Status}} - {{.State.Health.Status}}' - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=100 - exit 1 -fi - -# Step 4: Verification -log "βœ… Step 4: Verifying deployment..." - -# Test main page -if curl -f "http://localhost:$DEPLOY_PORT/" > /dev/null 2>&1; then - success "βœ… Main page is accessible" -else - error "❌ Main page is not accessible" - exit 1 -fi - -# Show container status -log "πŸ“Š Container status:" -docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - -# Show resource usage -log "πŸ“ˆ Resource usage:" -docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" "$CONTAINER_NAME" - -# Final success message -success "πŸŽ‰ Simplified Gitea deployment completed successfully!" -log "🌐 Application is available at: http://localhost:$DEPLOY_PORT" -log "πŸ₯ Health check endpoint: http://localhost:$DEPLOY_PORT/api/health" -log "πŸ“Š Container name: $CONTAINER_NAME" -log "πŸ“ Logs: docker logs $CONTAINER_NAME" - -# Update deployment log -echo "$(date): Simplified Gitea deployment successful - Port: $DEPLOY_PORT - Image: $IMAGE_NAME:$TIMESTAMP" >> "$LOG_FILE" - -exit 0 diff --git a/scripts/gitea-deploy.sh b/scripts/gitea-deploy.sh deleted file mode 100755 index be11eb9..0000000 --- a/scripts/gitea-deploy.sh +++ /dev/null @@ -1,257 +0,0 @@ -#!/bin/bash - -# Gitea-specific deployment script -# Optimiert fΓΌr lokalen Gitea Runner - -set -e - -# Configuration -PROJECT_NAME="portfolio" -CONTAINER_NAME="portfolio-app" -IMAGE_NAME="portfolio-app" -PORT=3000 -BACKUP_PORT=3001 -LOG_FILE="./logs/gitea-deploy.log" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Logging function -log() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" -} - -# Check if running as root (skip in CI environments) -if [[ $EUID -eq 0 ]] && [[ -z "$CI" ]]; then - error "This script should not be run as root (use CI=true to override)" - exit 1 -fi - -# Check if Docker is running -if ! docker info > /dev/null 2>&1; then - error "Docker is not running. Please start Docker and try again." - exit 1 -fi - -# Check if we're in the right directory -if [ ! -f "package.json" ] || [ ! -f "Dockerfile" ]; then - error "Please run this script from the project root directory" - exit 1 -fi - -log "πŸš€ Starting Gitea deployment for $PROJECT_NAME" - -# Step 1: Code Quality Checks -log "πŸ“‹ Step 1: Running code quality checks..." - -# Run linting -log "πŸ” Running ESLint..." -npm run lint || { - error "ESLint failed. Please fix the issues before deploying." - exit 1 -} - -# Run tests -log "πŸ§ͺ Running tests..." -npm run test:production || { - error "Tests failed. Please fix the issues before deploying." - exit 1 -} - -success "βœ… Code quality checks passed" - -# Step 2: Build Application -log "πŸ”¨ Step 2: Building application..." - -# Build Next.js application -log "πŸ“¦ Building Next.js application..." -npm run build || { - error "Build failed" - exit 1 -} - -success "βœ… Application built successfully" - -# Step 3: Docker Operations -log "🐳 Step 3: Docker operations..." - -# Build Docker image -log "πŸ—οΈ Building Docker image..." -docker build -t "$IMAGE_NAME:latest" . || { - error "Docker build failed" - exit 1 -} - -# Tag with timestamp -TIMESTAMP=$(date +%Y%m%d-%H%M%S) -docker tag "$IMAGE_NAME:latest" "$IMAGE_NAME:$TIMESTAMP" - -success "βœ… Docker image built successfully" - -# Step 4: Deployment -log "πŸš€ Step 4: Deploying application..." - -# Export environment variables for docker-compose compatibility -log "πŸ“ Exporting environment variables..." -export NODE_ENV=${NODE_ENV:-production} -export NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://dk0.dev} -export MY_EMAIL=${MY_EMAIL:-contact@dk0.dev} -export MY_INFO_EMAIL=${MY_INFO_EMAIL:-info@dk0.dev} -export MY_PASSWORD="${MY_PASSWORD}" -export MY_INFO_PASSWORD="${MY_INFO_PASSWORD}" -export ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}" -export LOG_LEVEL=${LOG_LEVEL:-info} -export PORT=${PORT:-3000} - -# Log which variables are set (without revealing secrets) -log "Environment variables configured:" -log " - NODE_ENV: ${NODE_ENV}" -log " - NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}" -log " - MY_EMAIL: ${MY_EMAIL}" -log " - MY_INFO_EMAIL: ${MY_INFO_EMAIL}" -log " - MY_PASSWORD: [SET]" -log " - MY_INFO_PASSWORD: [SET]" -log " - ADMIN_BASIC_AUTH: [SET]" -log " - LOG_LEVEL: ${LOG_LEVEL}" -log " - PORT: ${PORT}" - -# Check if container is running -if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; then - log "πŸ“¦ Stopping existing container..." - docker stop "$CONTAINER_NAME" || true - docker rm "$CONTAINER_NAME" || true -fi - -# Check if port is available -if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null ; then - warning "Port $PORT is in use. Trying backup port $BACKUP_PORT" - DEPLOY_PORT=$BACKUP_PORT -else - DEPLOY_PORT=$PORT -fi - -# Start new container with environment variables -log "πŸš€ Starting new container on port $DEPLOY_PORT..." -docker run -d \ - --name "$CONTAINER_NAME" \ - --restart unless-stopped \ - -p "$DEPLOY_PORT:3000" \ - -e NODE_ENV=production \ - -e NEXT_PUBLIC_BASE_URL=https://dk0.dev \ - -e MY_EMAIL=contact@dk0.dev \ - -e MY_INFO_EMAIL=info@dk0.dev \ - -e MY_PASSWORD="${MY_PASSWORD:-your-email-password}" \ - -e MY_INFO_PASSWORD="${MY_INFO_PASSWORD:-your-info-email-password}" \ - -e ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH:-admin:your_secure_password_here}" \ - -e LOG_LEVEL=info \ - "$IMAGE_NAME:latest" || { - error "Failed to start container" - exit 1 -} - -# Wait for container to be ready -log "⏳ Waiting for container to be ready..." -sleep 15 - -# Check if container is actually running -if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then - error "Container failed to start or crashed" - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=50 - exit 1 -fi - -# Health check -log "πŸ₯ Performing health check..." -HEALTH_CHECK_TIMEOUT=120 -HEALTH_CHECK_INTERVAL=3 -ELAPSED=0 - -while [ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]; do - # Check if container is still running - if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then - error "Container stopped during health check" - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=50 - exit 1 - fi - - # Try health check endpoint - if curl -f "http://localhost:$DEPLOY_PORT/api/health" > /dev/null 2>&1; then - success "βœ… Application is healthy!" - break - fi - - sleep $HEALTH_CHECK_INTERVAL - ELAPSED=$((ELAPSED + HEALTH_CHECK_INTERVAL)) - echo -n "." -done - -if [ $ELAPSED -ge $HEALTH_CHECK_TIMEOUT ]; then - error "Health check timeout. Application may not be running properly." - log "Container status:" - docker inspect "$CONTAINER_NAME" --format='{{.State.Status}} - {{.State.Health.Status}}' - log "Container logs:" - docker logs "$CONTAINER_NAME" --tail=100 - exit 1 -fi - -# Step 5: Verification -log "βœ… Step 5: Verifying deployment..." - -# Test main page -if curl -f "http://localhost:$DEPLOY_PORT/" > /dev/null 2>&1; then - success "βœ… Main page is accessible" -else - error "❌ Main page is not accessible" - exit 1 -fi - -# Show container status -log "πŸ“Š Container status:" -docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - -# Show resource usage -log "πŸ“ˆ Resource usage:" -docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" "$CONTAINER_NAME" - -# Step 6: Cleanup -log "🧹 Step 6: Cleaning up old images..." - -# Remove old images (keep last 3 versions) -docker images "$IMAGE_NAME" --format "table {{.Tag}}\t{{.ID}}" | tail -n +2 | head -n -3 | awk '{print $2}' | xargs -r docker rmi || { - warning "No old images to remove" -} - -# Clean up unused Docker resources -docker system prune -f --volumes || { - warning "Failed to clean up Docker resources" -} - -# Final success message -success "πŸŽ‰ Gitea deployment completed successfully!" -log "🌐 Application is available at: http://localhost:$DEPLOY_PORT" -log "πŸ₯ Health check endpoint: http://localhost:$DEPLOY_PORT/api/health" -log "πŸ“Š Container name: $CONTAINER_NAME" -log "πŸ“ Logs: docker logs $CONTAINER_NAME" - -# Update deployment log -echo "$(date): Gitea deployment successful - Port: $DEPLOY_PORT - Image: $IMAGE_NAME:$TIMESTAMP" >> "$LOG_FILE" - -exit 0 diff --git a/scripts/setup-gitea-runner.sh b/scripts/setup-gitea-runner.sh deleted file mode 100755 index 418655d..0000000 --- a/scripts/setup-gitea-runner.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/bin/bash - -# Gitea Runner Setup Script -# Installiert und konfiguriert einen lokalen Gitea Runner - -set -e - -# Configuration -GITEA_URL="${GITEA_URL:-http://localhost:3000}" -RUNNER_NAME="${RUNNER_NAME:-portfolio-runner}" -RUNNER_LABELS="${RUNNER_LABELS:-ubuntu-latest,self-hosted,portfolio}" -RUNNER_WORK_DIR="${RUNNER_WORK_DIR:-/tmp/gitea-runner}" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Logging function -log() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -# Check if running as root (skip in CI environments) -if [[ $EUID -eq 0 ]] && [[ -z "$CI" ]]; then - error "This script should not be run as root (use CI=true to override)" - exit 1 -fi - -log "πŸš€ Setting up Gitea Runner for Portfolio" - -# Check if Gitea URL is accessible -log "πŸ” Checking Gitea server accessibility..." -if ! curl -f "$GITEA_URL" > /dev/null 2>&1; then - error "Cannot access Gitea server at $GITEA_URL" - error "Please make sure Gitea is running and accessible" - exit 1 -fi -success "βœ… Gitea server is accessible" - -# Create runner directory -log "πŸ“ Creating runner directory..." -mkdir -p "$RUNNER_WORK_DIR" -cd "$RUNNER_WORK_DIR" - -# Download Gitea Runner -log "πŸ“₯ Downloading Gitea Runner..." -RUNNER_VERSION="latest" -RUNNER_ARCH="linux-amd64" - -# Get latest version -if [ "$RUNNER_VERSION" = "latest" ]; then - RUNNER_VERSION=$(curl -s https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') -fi - -RUNNER_URL="https://github.com/woodpecker-ci/woodpecker/releases/download/${RUNNER_VERSION}/woodpecker-agent_${RUNNER_VERSION}_${RUNNER_ARCH}.tar.gz" - -log "Downloading from: $RUNNER_URL" -curl -L -o woodpecker-agent.tar.gz "$RUNNER_URL" - -# Extract runner -log "πŸ“¦ Extracting Gitea Runner..." -tar -xzf woodpecker-agent.tar.gz -chmod +x woodpecker-agent - -success "βœ… Gitea Runner downloaded and extracted" - -# Create systemd service -log "βš™οΈ Creating systemd service..." -sudo tee /etc/systemd/system/gitea-runner.service > /dev/null < "$RUNNER_WORK_DIR/start-runner.sh" << 'EOF' -#!/bin/bash -echo "Starting Gitea Runner..." -sudo systemctl start gitea-runner -sudo systemctl status gitea-runner -EOF - -# Stop script -cat > "$RUNNER_WORK_DIR/stop-runner.sh" << 'EOF' -#!/bin/bash -echo "Stopping Gitea Runner..." -sudo systemctl stop gitea-runner -EOF - -# Status script -cat > "$RUNNER_WORK_DIR/status-runner.sh" << 'EOF' -#!/bin/bash -echo "Gitea Runner Status:" -sudo systemctl status gitea-runner -echo "" -echo "Logs (last 20 lines):" -sudo journalctl -u gitea-runner -n 20 --no-pager -EOF - -# Logs script -cat > "$RUNNER_WORK_DIR/logs-runner.sh" << 'EOF' -#!/bin/bash -echo "Gitea Runner Logs:" -sudo journalctl -u gitea-runner -f -EOF - -chmod +x "$RUNNER_WORK_DIR"/*.sh - -success "βœ… Helper scripts created" - -# Create environment file -cat > "$RUNNER_WORK_DIR/.env" << EOF -# Gitea Runner Configuration -GITEA_URL=$GITEA_URL -RUNNER_NAME=$RUNNER_NAME -RUNNER_LABELS=$RUNNER_LABELS -RUNNER_WORK_DIR=$RUNNER_WORK_DIR -EOF - -log "πŸ“‹ Setup Summary:" -echo " β€’ Runner Directory: $RUNNER_WORK_DIR" -echo " β€’ Gitea URL: $GITEA_URL" -echo " β€’ Runner Name: $RUNNER_NAME" -echo " β€’ Labels: $RUNNER_LABELS" -echo " β€’ Helper Scripts: $RUNNER_WORK_DIR/*.sh" -echo "" - -log "🎯 Next Steps:" -echo "1. Register the runner in Gitea web interface" -echo "2. Enable and start the service" -echo "3. Test with a workflow run" -echo "" - -success "πŸŽ‰ Gitea Runner setup completed!" -log "πŸ“ All files are in: $RUNNER_WORK_DIR" diff --git a/scripts/setup-snippets.js b/scripts/setup-snippets.js deleted file mode 100644 index 7a64791..0000000 --- a/scripts/setup-snippets.js +++ /dev/null @@ -1,79 +0,0 @@ - -/* eslint-disable @typescript-eslint/no-require-imports */ -const fetch = require('node-fetch'); -require('dotenv').config(); - -const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; -const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; - -async function setupSnippets() { - console.log('πŸ“¦ Setting up Snippets collection...'); - - // 1. Create Collection - try { - await fetch(`${DIRECTUS_URL}/collections`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - collection: 'snippets', - meta: { icon: 'terminal', display_template: '{{title}}' }, - schema: { name: 'snippets' } - }) - }); - } catch (_e) {} - - // 2. Add Fields - const fields = [ - { field: 'status', type: 'string', meta: { interface: 'select-dropdown' }, schema: { default_value: 'published' } }, - { field: 'title', type: 'string', meta: { interface: 'input' } }, - { field: 'category', type: 'string', meta: { interface: 'input' } }, - { field: 'code', type: 'text', meta: { interface: 'input-code' } }, - { field: 'description', type: 'text', meta: { interface: 'textarea' } }, - { field: 'language', type: 'string', meta: { interface: 'input' }, schema: { default_value: 'javascript' } }, - { field: 'featured', type: 'boolean', meta: { interface: 'boolean' }, schema: { default_value: false } } - ]; - - for (const f of fields) { - try { - await fetch(`${DIRECTUS_URL}/fields/snippets`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, - body: JSON.stringify(f) - }); - } catch (_e) {} - } - - // 3. Add Example Data - const exampleSnippets = [ - { - title: 'Traefik SSL Config', - category: 'Docker', - language: 'yaml', - featured: true, - description: "Meine Standard-Konfiguration fΓΌr automatisches SSL via Let's Encrypt in Docker Swarm.", - code: "labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.myapp.rule=Host(`example.com`)\"\n - \"traefik.http.routers.myapp.entrypoints=websecure\"\n - \"traefik.http.routers.myapp.tls.certresolver=myresolver\"" - }, - { - title: 'Docker Cleanup Alias', - category: 'ZSH', - language: 'bash', - featured: true, - description: 'Ein einfacher Alias, um ungenutzte Docker-Container, Images und Volumes schnell zu entfernen.', - code: "alias dclean='docker system prune -af --volumes'" - } - ]; - - for (const s of exampleSnippets) { - try { - await fetch(`${DIRECTUS_URL}/items/snippets`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, - body: JSON.stringify(s) - }); - } catch (_e) {} - } - - console.log('βœ… Snippets setup complete!'); -} - -setupSnippets();