Compare commits
No commits in common. "badc481883b5fb73ead2048fe670b9e283cdae54" and "838f86e11f820abdbf74cf688d8b777dce76bfe7" have entirely different histories.
badc481883
...
838f86e11f
10 changed files with 867 additions and 447 deletions
|
|
@ -1,51 +0,0 @@
|
|||
name: Deploy newsbot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: deploy-host
|
||||
|
||||
steps:
|
||||
- name: Deploy on server
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
test -d /opt/noctura/newsbot || {
|
||||
echo "Deploy-Verzeichnis fehlt: /opt/noctura/newsbot"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd /opt/noctura/newsbot
|
||||
|
||||
test -d .git || {
|
||||
echo "Kein Git-Repository in /opt/noctura/newsbot"
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v git >/dev/null || {
|
||||
echo "git ist auf dem Runner nicht installiert"
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v docker >/dev/null || {
|
||||
echo "docker ist auf dem Runner nicht installiert"
|
||||
exit 1
|
||||
}
|
||||
|
||||
docker compose version
|
||||
git config --global --add safe.directory /opt/noctura/newsbot
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
ssh-keyscan -p 222 git.noctura.dev >> "$HOME/.ssh/known_hosts"
|
||||
chmod 600 "$HOME/.ssh/known_hosts"
|
||||
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
|
||||
docker compose up -d --build
|
||||
|
||||
docker image prune -f
|
||||
11
Dockerfile
11
Dockerfile
|
|
@ -1,7 +1,4 @@
|
|||
FROM node:20-slim
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
RUN npm install --production
|
||||
COPY src/ ./src/
|
||||
VOLUME ["/data"]
|
||||
CMD ["node", "src/index.js"]
|
||||
FROM nginx:alpine
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
EXPOSE 80
|
||||
|
||||
|
|
|
|||
71
README.md
71
README.md
|
|
@ -1,71 +0,0 @@
|
|||
# noctura newsbot — Setup
|
||||
|
||||
## 1. Bot-User anlegen (falls noch nicht getan)
|
||||
|
||||
```bash
|
||||
docker exec -it synapse register_new_matrix_user -u newsbot -p sicherespasswort --no-admin -c /data/homeserver.yaml http://localhost:8008
|
||||
```
|
||||
|
||||
## 2. Access Token holen
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8008/_matrix/client/v3/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"m.login.password","user":"newsbot","password":"sicherespasswort"}' \
|
||||
| grep -o '"access_token":"[^"]*"'
|
||||
```
|
||||
|
||||
Den Token aus der Ausgabe kopieren.
|
||||
|
||||
## 3. Room ID herausfinden
|
||||
|
||||
Im Matrix Client (z.B. Element):
|
||||
- In den gewünschten Room gehen
|
||||
- Raumeinstellungen → Erweitert → Room ID kopieren (beginnt mit !)
|
||||
|
||||
Den Bot in den Room einladen:
|
||||
```
|
||||
/invite @newsbot:noctura.dev
|
||||
```
|
||||
|
||||
## 4. docker-compose.yml anpassen
|
||||
|
||||
```yaml
|
||||
MATRIX_ACCESS_TOKEN: "slt_DEIN_TOKEN"
|
||||
MATRIX_ROOM_ID: "!xxxxx:noctura.dev"
|
||||
```
|
||||
|
||||
## 5. Bot starten
|
||||
|
||||
```bash
|
||||
cd /opt/noctura/newsbot
|
||||
mkdir data
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 6. Test-Digest sofort senden
|
||||
|
||||
```bash
|
||||
# TEST_RUN kurz auf true setzen
|
||||
docker compose run --rm -e TEST_RUN=true newsbot
|
||||
```
|
||||
|
||||
## Feeds anpassen
|
||||
|
||||
Einfach in `src/feeds.js` neue Feeds hinzufügen:
|
||||
|
||||
```js
|
||||
{
|
||||
category: "🎮 Gaming",
|
||||
url: "https://www.rockpapershotgun.com/feed",
|
||||
name: "Rock Paper Shotgun"
|
||||
}
|
||||
```
|
||||
|
||||
## Cron-Zeiten
|
||||
|
||||
| Wert | Bedeutung |
|
||||
|------|-----------|
|
||||
| `0 8 * * *` | Täglich 08:00 Uhr |
|
||||
| `0 8,18 * * *`| Täglich 08:00 und 18:00 Uhr |
|
||||
| `0 8 * * 1-5` | Nur Montag–Freitag 08:00 Uhr |
|
||||
|
|
@ -1,26 +1,7 @@
|
|||
services:
|
||||
newsbot:
|
||||
portfolio:
|
||||
build: .
|
||||
container_name: newsbot
|
||||
container_name: portfolio
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
# Matrix Homeserver (intern via Docker Host)
|
||||
MATRIX_HOMESERVER: "http://host.docker.internal:8008"
|
||||
|
||||
# Access Token des newsbot Users (siehe README)
|
||||
MATRIX_ACCESS_TOKEN: "syt_bmV3c2JvdA_GCpAdpNcDVQtrkNToFCa_0f4Wlv"
|
||||
|
||||
# Room ID des Ziel-Rooms (beginnt mit !)
|
||||
MATRIX_ROOM_ID: "!zCrCwVpKnpXXDtBbix:noctura.dev"
|
||||
|
||||
# Wann soll der Digest gesendet werden? (Cron-Format)
|
||||
# Standard: täglich 08:00 Uhr
|
||||
CRON_SCHEDULE: "0 8 * * *"
|
||||
|
||||
# Auf 'true' setzen um beim Start sofort einen Test zu senden
|
||||
TEST_RUN: "true"
|
||||
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "3000:80"
|
||||
|
|
|
|||
859
index.html
Normal file
859
index.html
Normal file
|
|
@ -0,0 +1,859 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>noctura.dev</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;1,300&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #07080f;
|
||||
--bg2: #0d0f1c;
|
||||
--bg3: #111428;
|
||||
--bg4: #171a31;
|
||||
--surface: rgba(255,255,255,0.04);
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--border-hover: rgba(255,255,255,0.14);
|
||||
--text: #e8eaf2;
|
||||
--muted: #8d93ad;
|
||||
--accent: #7c6dfa;
|
||||
--accent2: #4fd1c5;
|
||||
--accent3: #f7886a;
|
||||
--glow: rgba(124,109,250,0.12);
|
||||
--font: 'Syne', sans-serif;
|
||||
--mono: 'DM Mono', monospace;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- STAR FIELD --- */
|
||||
#stars {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* --- NEBULA GLOW --- */
|
||||
.nebula {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.nebula-1 { width: 600px; height: 600px; background: radial-gradient(circle, rgba(124,109,250,0.08) 0%, transparent 70%); top: -100px; left: -100px; }
|
||||
.nebula-2 { width: 500px; height: 500px; background: radial-gradient(circle, rgba(79,209,197,0.06) 0%, transparent 70%); bottom: 200px; right: -50px; }
|
||||
.nebula-3 { width: 400px; height: 400px; background: radial-gradient(circle, rgba(247,136,106,0.05) 0%, transparent 70%); top: 50%; left: 40%; }
|
||||
|
||||
/* --- LAYOUT --- */
|
||||
.wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* --- NAV --- */
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 100;
|
||||
padding: 1.25rem 0;
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(7,8,15,0.6);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav-logo {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-logo span { color: var(--accent); }
|
||||
.nav-links { display: flex; gap: 2rem; list-style: none; }
|
||||
.nav-links a {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); }
|
||||
|
||||
/* --- HERO --- */
|
||||
.hero {
|
||||
padding: 176px 0 110px;
|
||||
position: relative;
|
||||
}
|
||||
.hero-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent2);
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 1.1rem;
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards 0.2s;
|
||||
}
|
||||
.hero-tag::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent2);
|
||||
box-shadow: 0 0 8px var(--accent2);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.6;transform:scale(0.8)} }
|
||||
|
||||
.hero h1 {
|
||||
max-width: 780px;
|
||||
font-size: clamp(3.1rem, 8vw, 4.4rem);
|
||||
font-weight: 800;
|
||||
line-height: 0.96;
|
||||
letter-spacing: -0.045em;
|
||||
margin-bottom: 1.25rem;
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards 0.35s;
|
||||
}
|
||||
.hero h1 .line2 {
|
||||
display: block;
|
||||
color: #ffffff;
|
||||
}
|
||||
.hero h1 .line3 {
|
||||
display: block;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
max-width: 640px;
|
||||
font-size: 1rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.75;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards 0.5s;
|
||||
}
|
||||
.hero-desc strong {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2.5rem;
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards 0.65s;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0.7rem 1.4rem;
|
||||
border-radius: 8px;
|
||||
font-family: var(--font);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: #9284fc; transform: translateY(-1px); }
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border-hover);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
|
||||
|
||||
.hero-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
max-width: 860px;
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards 0.8s;
|
||||
}
|
||||
.hero-metric {
|
||||
padding: 1rem 1.1rem;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.hero-metric-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent2);
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
.hero-metric-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.hero-metric-copy {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.intro-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(260px, 0.9fr);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.intro-copy,
|
||||
.intro-notes {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
.intro-copy p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.intro-copy p:last-child { margin-bottom: 0; }
|
||||
.intro-notes {
|
||||
background: linear-gradient(180deg, rgba(23,26,49,0.92), rgba(10,11,20,0.9));
|
||||
}
|
||||
.note-list {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
list-style: none;
|
||||
}
|
||||
.note-list li {
|
||||
padding-left: 1rem;
|
||||
position: relative;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.note-list li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.6rem;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent3);
|
||||
box-shadow: 0 0 10px rgba(247,136,106,0.5);
|
||||
}
|
||||
|
||||
/* --- SECTION --- */
|
||||
section { padding: 80px 0; }
|
||||
.section-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent2);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.section-title {
|
||||
font-size: clamp(1.6rem, 3vw, 2.2rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.section-subtitle {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--border) 20%, var(--border) 80%, transparent);
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
/* --- SKILLS --- */
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--border);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.skill-card {
|
||||
background: var(--bg);
|
||||
padding: 2rem;
|
||||
transition: background 0.25s;
|
||||
position: relative;
|
||||
}
|
||||
.skill-card:hover { background: var(--bg3); }
|
||||
.skill-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.skill-card:hover::before { opacity: 1; }
|
||||
.skill-card.purple::before { background: linear-gradient(90deg, var(--accent), transparent); }
|
||||
.skill-card.teal::before { background: linear-gradient(90deg, var(--accent2), transparent); }
|
||||
.skill-card.orange::before { background: linear-gradient(90deg, var(--accent3), transparent); }
|
||||
|
||||
.skill-icon {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
.skill-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.skill-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.skill-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.tag {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.67rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.tag.purple { border-color: rgba(124,109,250,0.3); color: rgba(124,109,250,0.8); background: rgba(124,109,250,0.06); }
|
||||
.tag.teal { border-color: rgba(79,209,197,0.3); color: rgba(79,209,197,0.8); background: rgba(79,209,197,0.06); }
|
||||
.tag.orange { border-color: rgba(247,136,106,0.3); color: rgba(247,136,106,0.8); background: rgba(247,136,106,0.06); }
|
||||
|
||||
/* --- PROJECTS --- */
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.project-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
transition: border-color 0.25s, transform 0.2s;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.project-card:hover {
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.project-icon {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.project-arrow {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.project-card:hover .project-arrow {
|
||||
color: var(--accent);
|
||||
transform: translate(2px,-2px);
|
||||
}
|
||||
.project-eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.66rem;
|
||||
color: var(--accent2);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.project-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.project-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
flex: 1;
|
||||
}
|
||||
.project-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.status-dot {
|
||||
width: 5px; height: 5px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-live { background: #4ade80; box-shadow: 0 0 6px #4ade80; }
|
||||
.status-wip { background: #facc15; box-shadow: 0 0 6px #facc15; }
|
||||
.status-planned{ background: var(--muted); }
|
||||
|
||||
/* --- SNAPSHOT --- */
|
||||
.snapshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.snapshot-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1.1rem 1.25rem;
|
||||
min-height: 148px;
|
||||
transition: border-color 0.2s, background 0.2s, transform 0.2s;
|
||||
}
|
||||
.snapshot-card:hover {
|
||||
border-color: var(--border-hover);
|
||||
background: rgba(255,255,255,0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.snapshot-kicker {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.66rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent2);
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
.snapshot-value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.snapshot-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* --- CONTACT --- */
|
||||
.contact-box {
|
||||
background: linear-gradient(180deg, rgba(17,20,40,0.95), rgba(10,12,24,0.92));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.contact-box h2 { font-size: 2rem; font-weight: 800; margin-bottom: 0.5rem; }
|
||||
.contact-box p { color: var(--muted); margin-bottom: 2rem; }
|
||||
|
||||
/* --- FOOTER --- */
|
||||
footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 2rem 0;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer-inner {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* --- ANIMATIONS --- */
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
}
|
||||
.reveal.visible {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.nav-links { display: none; }
|
||||
.hero { padding: 120px 0 60px; }
|
||||
.contact-box { padding: 2rem 1.5rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.hero-strip,
|
||||
.intro-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Stars -->
|
||||
<canvas id="stars"></canvas>
|
||||
|
||||
<!-- Nebula glows -->
|
||||
<div class="nebula nebula-1"></div>
|
||||
<div class="nebula nebula-2"></div>
|
||||
<div class="nebula nebula-3"></div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a href="#" class="nav-logo">noctura<span>.dev</span></a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#about">Über mich</a></li>
|
||||
<li><a href="#skills">Skills</a></li>
|
||||
<li><a href="#projects">Projekte</a></li>
|
||||
<li><a href="#snapshot">Profil</a></li>
|
||||
<li><a href="#contact">Kontakt</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="wrapper">
|
||||
<section class="hero">
|
||||
<div class="hero-tag">Persönliches Portfolio / Web & Infrastruktur</div>
|
||||
<h1>
|
||||
Web entwickeln.
|
||||
<span class="line2">Systeme verstehen.</span>
|
||||
<span class="line3">Sauber bauen.</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
Ich arbeite an Projekten, die nicht nur gut aussehen, sondern auch technisch nachvollziehbar aufgebaut sind.
|
||||
Frontend, Backend, Linux und Hosting gehören für mich zusammen.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="#projects" class="btn btn-primary">Projekte ansehen</a>
|
||||
<a href="#contact" class="btn btn-outline">Kontakt</a>
|
||||
</div>
|
||||
<div class="hero-strip">
|
||||
<div class="hero-metric">
|
||||
<div class="hero-metric-label">Fokus</div>
|
||||
<div class="hero-metric-value">Frontend bis Deployment</div>
|
||||
<p class="hero-metric-copy">Ich denke nicht nur in Screens, sondern in vollständigen Umsetzungen.</p>
|
||||
</div>
|
||||
<div class="hero-metric">
|
||||
<div class="hero-metric-label">Arbeitsweise</div>
|
||||
<div class="hero-metric-value">Direkt und praktisch</div>
|
||||
<p class="hero-metric-copy">Lieber echte Projekte und klare Entscheidungen als Buzzwords und leere Claims.</p>
|
||||
</div>
|
||||
<div class="hero-metric">
|
||||
<div class="hero-metric-label">Stack</div>
|
||||
<div class="hero-metric-value">HTML, CSS, JS, Linux, Docker</div>
|
||||
<p class="hero-metric-copy">Genug Breite, um Interfaces, Services und Betrieb zusammen zu denken.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<section id="about">
|
||||
<div class="reveal">
|
||||
<div class="section-label">// Über mich</div>
|
||||
<h2 class="section-title">Weniger Firmen-Sprache, mehr echte Arbeit</h2>
|
||||
<p class="section-subtitle">Genau so sollte sich ein Portfolio für Bewerbungen lesen.</p>
|
||||
</div>
|
||||
<div class="intro-panel reveal">
|
||||
<div class="intro-copy">
|
||||
<p>
|
||||
Ich entwickle nicht nur Oberflächen, sondern kümmere mich auch darum, wie Anwendungen betrieben,
|
||||
abgesichert und langfristig verständlich gehalten werden.
|
||||
</p>
|
||||
<p>
|
||||
Gerade deshalb passt zu mir eher ein Portfolio mit Persönlichkeit: Projekte, Entscheidungen,
|
||||
Infrastruktur und Lernweg sichtbar machen, statt wie eine kleine IT-Firma aufzutreten.
|
||||
</p>
|
||||
</div>
|
||||
<div class="intro-notes">
|
||||
<div class="hero-card-label">Was Recruiter hier schnell sehen sollen</div>
|
||||
<ul class="note-list">
|
||||
<li>Ich kann gestalten, bauen und deployen.</li>
|
||||
<li>Ich arbeite eigenständig und technisch neugierig.</li>
|
||||
<li>Meine Projekte sind nicht nur Demos, sondern echte Systeme.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- Skills -->
|
||||
<section id="skills">
|
||||
<div class="reveal">
|
||||
<div class="section-label">// Fähigkeiten</div>
|
||||
<h2 class="section-title">Was ich kann</h2>
|
||||
<p class="section-subtitle">Breit genug für komplette Projekte, fokussiert genug für gute Details.</p>
|
||||
</div>
|
||||
<div class="skills-grid reveal">
|
||||
<div class="skill-card purple">
|
||||
<span class="skill-icon">⟨/⟩</span>
|
||||
<div class="skill-name">Frontend & Interfaces</div>
|
||||
<p class="skill-desc">Responsive Oberflächen, klares UI und saubere Struktur. Ich mag Seiten, die Charakter haben und trotzdem verständlich bleiben.</p>
|
||||
<div class="skill-tags">
|
||||
<span class="tag purple">HTML / CSS</span>
|
||||
<span class="tag purple">JavaScript</span>
|
||||
<span class="tag purple">UI Design</span>
|
||||
<span class="tag purple">Responsive Layouts</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card teal">
|
||||
<span class="skill-icon">⬡</span>
|
||||
<div class="skill-name">Backend & Infrastruktur</div>
|
||||
<p class="skill-desc">Services selbst aufsetzen, verbinden und betreiben. Nicht nur deployen, sondern verstehen, was dahinter läuft.</p>
|
||||
<div class="skill-tags">
|
||||
<span class="tag teal">Docker</span>
|
||||
<span class="tag teal">Caddy</span>
|
||||
<span class="tag teal">Node.js</span>
|
||||
<span class="tag teal">Forgejo</span>
|
||||
<span class="tag teal">REST APIs</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-card orange">
|
||||
<span class="skill-icon">$_</span>
|
||||
<div class="skill-name">Linux & Betrieb</div>
|
||||
<p class="skill-desc">Server einrichten, absichern und dauerhaft sinnvoll betreiben. Besonders spannend finde ich alles rund um Self-hosting und Wartbarkeit.</p>
|
||||
<div class="skill-tags">
|
||||
<span class="tag orange">Debian Linux</span>
|
||||
<span class="tag orange">SSH</span>
|
||||
<span class="tag orange">systemd</span>
|
||||
<span class="tag orange">ufw / fail2ban</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- Projects -->
|
||||
<section id="projects">
|
||||
<div class="reveal">
|
||||
<div class="section-label">// Portfolio</div>
|
||||
<h2 class="section-title">Ausgewählte Arbeiten</h2>
|
||||
<p class="section-subtitle">Nicht als Produktkatalog, sondern als Ausschnitt meiner Denkweise.</p>
|
||||
</div>
|
||||
<div class="projects-grid reveal">
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<div class="project-eyebrow">Tooling</div>
|
||||
<div class="project-name">Developer Utilities</div>
|
||||
</div>
|
||||
<span class="project-arrow">↗</span>
|
||||
</div>
|
||||
<p class="project-desc">Sammlung kleiner Web-Tools, bei denen UX und Geschwindigkeit wichtiger sind als Marketing. Gut geeignet, um Pragmatismus und Produktgefühl zu zeigen.</p>
|
||||
<div class="project-status">
|
||||
<span class="status-dot status-wip"></span>
|
||||
<span style="color: var(--muted);">In Entwicklung</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<div class="project-eyebrow">Backend</div>
|
||||
<div class="project-name">Custom Backend API</div>
|
||||
</div>
|
||||
<span class="project-arrow">↗</span>
|
||||
</div>
|
||||
<p class="project-desc">Eigene API-Struktur für persönliche Projekte. Der Reiz liegt für mich darin, Datenmodelle, Routing und Hosting selbst sauber zu kontrollieren.</p>
|
||||
<div class="project-status">
|
||||
<span class="status-dot status-wip"></span>
|
||||
<span style="color: var(--muted);">In Entwicklung</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<div class="project-eyebrow">Experiment</div>
|
||||
<div class="project-name">Lab / Playground</div>
|
||||
</div>
|
||||
<span class="project-arrow">↗</span>
|
||||
</div>
|
||||
<p class="project-desc">Platz für Prototypen, Designideen und technische Versuche. Gerade dieser Bereich macht sichtbar, wie ich lerne und neue Themen erschließe.</p>
|
||||
<div class="project-status">
|
||||
<span class="status-dot status-planned"></span>
|
||||
<span style="color: var(--muted);">Geplant</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<div class="project-eyebrow">Infrastruktur</div>
|
||||
<div class="project-name">noctura.dev Infrastruktur</div>
|
||||
</div>
|
||||
<span class="project-arrow">↗</span>
|
||||
</div>
|
||||
<p class="project-desc">Mein stärkstes Portfolio-Stück im Hintergrund: ein selbst verwalteter VPS mit echten Services, nicht nur eine statische Visitenkarte.</p>
|
||||
<div class="project-status">
|
||||
<span class="status-dot status-live"></span>
|
||||
<span style="color: var(--muted);">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<section id="snapshot">
|
||||
<div class="reveal">
|
||||
<div class="section-label">// Profil</div>
|
||||
<h2 class="section-title">Was hängen bleibt</h2>
|
||||
<p class="section-subtitle">Die kurze Zusammenfassung für Bewerbungen und schnelle Erstgespräche.</p>
|
||||
</div>
|
||||
<div class="snapshot-grid reveal">
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-kicker">Arbeitsweise</div>
|
||||
<div class="snapshot-value">Praktisch</div>
|
||||
<p class="snapshot-desc">Ich lerne am liebsten über echte Umsetzungen, nicht nur über Theorie oder Tutorial-Nachbau.</p>
|
||||
</div>
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-kicker">Stärke</div>
|
||||
<div class="snapshot-value">Full stack denken</div>
|
||||
<p class="snapshot-desc">Vom Interface bis zum Server kann ich Entscheidungen zusammenhängend betrachten und umsetzen.</p>
|
||||
</div>
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-kicker">Motivation</div>
|
||||
<div class="snapshot-value">Eigene Systeme verstehen</div>
|
||||
<p class="snapshot-desc">Mich reizt Technik dann besonders, wenn ich sie selbst aufbauen, betreiben und verbessern kann.</p>
|
||||
</div>
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-kicker">Eindruck</div>
|
||||
<div class="snapshot-value">Kein Buzzword-Profil</div>
|
||||
<p class="snapshot-desc">Die Seite soll zeigen, wie ich denke und arbeite, nicht nur welche Begriffe ich aufzählen kann.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- Contact -->
|
||||
<section id="contact">
|
||||
<div class="contact-box reveal">
|
||||
<h2>Kontakt für Bewerbungen oder Projekte</h2>
|
||||
<p>Wenn du einen Entwickler suchst, der nicht nur UI klickt, sondern Systeme wirklich verstehen will, schreib mir.</p>
|
||||
<a href="mailto:kontakt@noctura.dev" class="btn btn-primary">kontakt@noctura.dev</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<span>noctura.dev - persönliches Portfolio mit eigener Infrastruktur</span>
|
||||
<span>HTML · CSS · JavaScript · Debian 12</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Star field
|
||||
const canvas = document.getElementById('stars');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let stars = [];
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
stars = Array.from({length: 180}, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
r: Math.random() * 1.2 + 0.2,
|
||||
o: Math.random() * 0.6 + 0.1,
|
||||
s: Math.random() * 0.4 + 0.1,
|
||||
t: Math.random() * Math.PI * 2
|
||||
}));
|
||||
}
|
||||
function drawStars(ts) {
|
||||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||||
stars.forEach(s => {
|
||||
s.t += 0.008 * s.s;
|
||||
const o = s.o * (0.6 + 0.4 * Math.sin(s.t));
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
|
||||
ctx.fillStyle = `rgba(200,210,255,${o})`;
|
||||
ctx.fill();
|
||||
});
|
||||
requestAnimationFrame(drawStars);
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
requestAnimationFrame(drawStars);
|
||||
|
||||
// Scroll reveal
|
||||
const reveals = document.querySelectorAll('.reveal');
|
||||
const io = new IntersectionObserver(entries => {
|
||||
entries.forEach((e,i) => {
|
||||
if (e.isIntersecting) {
|
||||
setTimeout(() => e.target.classList.add('visible'), i * 80);
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.12 });
|
||||
reveals.forEach(el => io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
package.json
14
package.json
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "noctura-newsbot",
|
||||
"version": "1.0.0",
|
||||
"description": "Matrix News Bot für noctura.dev",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"rss-parser": "^3.13.0",
|
||||
"node-cron": "^3.0.3"
|
||||
}
|
||||
}
|
||||
94
src/feeds.js
94
src/feeds.js
|
|
@ -1,94 +0,0 @@
|
|||
// ============================================================
|
||||
// feeds.js — RSS-Feed Konfiguration
|
||||
// Einfach neue Feeds hinzufügen oder Kategorien anpassen
|
||||
// ============================================================
|
||||
|
||||
const FEEDS = [
|
||||
// --- KI & Machine Learning ---
|
||||
{
|
||||
category: "🤖 KI & Machine Learning",
|
||||
url: "https://blog.google/technology/ai/rss/",
|
||||
name: "Google AI Blog"
|
||||
},
|
||||
{
|
||||
category: "🤖 KI & Machine Learning",
|
||||
url: "https://openai.com/news/rss.xml",
|
||||
name: "OpenAI Blog"
|
||||
},
|
||||
{
|
||||
category: "🤖 KI & Machine Learning",
|
||||
url: "https://blog.google/technology/google-deepmind/rss/",
|
||||
name: "Google DeepMind"
|
||||
},
|
||||
{
|
||||
category: "🤖 KI & Machine Learning",
|
||||
url: "https://huggingface.co/blog/feed.xml",
|
||||
name: "Hugging Face"
|
||||
},
|
||||
|
||||
// --- IT Security ---
|
||||
{
|
||||
category: "🔐 IT Security",
|
||||
url: "https://feeds.feedburner.com/TheHackersNews",
|
||||
name: "The Hacker News"
|
||||
},
|
||||
{
|
||||
category: "🔐 IT Security",
|
||||
url: "https://www.bleepingcomputer.com/feed/",
|
||||
name: "BleepingComputer"
|
||||
},
|
||||
{
|
||||
category: "🔐 IT Security",
|
||||
url: "https://krebsonsecurity.com/feed/",
|
||||
name: "Krebs on Security"
|
||||
},
|
||||
|
||||
// --- Smart Home & IoT ---
|
||||
{
|
||||
category: "🏠 Smart Home & IoT",
|
||||
url: "https://www.home-assistant.io/atom.xml",
|
||||
name: "Home Assistant"
|
||||
},
|
||||
{
|
||||
category: "🏠 Smart Home & IoT",
|
||||
url: "https://rss.golem.de/rss.php?feed=RSS2.0&topic=smarthome",
|
||||
name: "Golem Smart Home"
|
||||
},
|
||||
|
||||
// --- Web Development ---
|
||||
{
|
||||
category: "⟨/⟩ Web Development",
|
||||
url: "https://css-tricks.com/feed/",
|
||||
name: "CSS-Tricks"
|
||||
},
|
||||
{
|
||||
category: "⟨/⟩ Web Development",
|
||||
url: "https://dev.to/feed",
|
||||
name: "DEV.to"
|
||||
},
|
||||
{
|
||||
category: "⟨/⟩ Web Development",
|
||||
url: "https://github.blog/feed/",
|
||||
name: "GitHub Blog"
|
||||
},
|
||||
|
||||
// --- Self-hosting & Linux ---
|
||||
{
|
||||
category: "🖥️ Self-hosting & Linux",
|
||||
url: "https://selfh.st/rss/",
|
||||
name: "selfh.st"
|
||||
},
|
||||
{
|
||||
category: "🖥️ Self-hosting & Linux",
|
||||
url: "https://rss.golem.de/rss.php?feed=RSS2.0&topic=opensource",
|
||||
name: "Golem Open Source"
|
||||
},
|
||||
];
|
||||
|
||||
// Wie viele Artikel pro Feed maximal gepostet werden
|
||||
const MAX_ITEMS_PER_FEED = 2;
|
||||
|
||||
// Wie viele Stunden rückwirkend Artikel berücksichtigt werden
|
||||
const HOURS_LOOKBACK = 24;
|
||||
|
||||
module.exports = { FEEDS, MAX_ITEMS_PER_FEED, HOURS_LOOKBACK };
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// ============================================================
|
||||
// formatter.js — Formatiert News-Nachrichten für Matrix
|
||||
// ============================================================
|
||||
|
||||
function formatDailyDigest(grouped) {
|
||||
const date = new Date().toLocaleDateString('de-DE', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
|
||||
// Plain text version
|
||||
let plain = `📰 Täglicher News-Digest — ${date}\n`;
|
||||
plain += '─'.repeat(50) + '\n\n';
|
||||
|
||||
// HTML version (für Matrix Clients die Formatting unterstützen)
|
||||
let html = `<h3>📰 Täglicher News-Digest</h3>`;
|
||||
html += `<p><em>${date}</em></p><hr>`;
|
||||
|
||||
for (const [category, items] of Object.entries(grouped)) {
|
||||
if (items.length === 0) continue;
|
||||
|
||||
plain += `${category}\n${'─'.repeat(30)}\n`;
|
||||
html += `<h4>${category}</h4><ul>`;
|
||||
|
||||
for (const item of items) {
|
||||
const source = item.feedName ? ` [${item.feedName}]` : '';
|
||||
plain += `• ${item.title}${source}\n ${item.link}\n\n`;
|
||||
html += `<li><a href="${item.link}"><strong>${item.title}</strong></a>`;
|
||||
if (item.feedName) html += ` <em>(${item.feedName})</em>`;
|
||||
if (item.summary) html += `<br><small>${item.summary}</small>`;
|
||||
html += `</li>`;
|
||||
}
|
||||
|
||||
plain += '\n';
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
const totalItems = Object.values(grouped).flat().length;
|
||||
plain += `\n📊 ${totalItems} Artikel heute · Powered by noctura newsbot`;
|
||||
html += `<p><small>📊 ${totalItems} Artikel heute · Powered by noctura newsbot</small></p>`;
|
||||
|
||||
return { plain, html };
|
||||
}
|
||||
|
||||
function formatNoNews() {
|
||||
return {
|
||||
plain: '📰 Heute keine neuen Artikel in den konfigurierten Feeds.',
|
||||
html: '<p>📰 Heute keine neuen Artikel in den konfigurierten Feeds.</p>'
|
||||
};
|
||||
}
|
||||
|
||||
function formatError(feedName, error) {
|
||||
return `⚠️ Feed-Fehler [${feedName}]: ${error.message}`;
|
||||
}
|
||||
|
||||
module.exports = { formatDailyDigest, formatNoNews, formatError };
|
||||
132
src/index.js
132
src/index.js
|
|
@ -1,132 +0,0 @@
|
|||
// ============================================================
|
||||
// index.js — noctura newsbot
|
||||
// Matrix Bot der täglich News-Feeds postet
|
||||
// ============================================================
|
||||
|
||||
const { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } = require('matrix-bot-sdk');
|
||||
const RSSParser = require('rss-parser');
|
||||
const cron = require('node-cron');
|
||||
const { FEEDS, MAX_ITEMS_PER_FEED, HOURS_LOOKBACK } = require('./feeds');
|
||||
const { formatDailyDigest, formatNoNews, formatError } = require('./formatter');
|
||||
|
||||
// ---- Konfiguration aus Umgebungsvariablen ----
|
||||
const HOMESERVER = process.env.MATRIX_HOMESERVER || 'http://localhost:8008';
|
||||
const ACCESS_TOKEN = process.env.MATRIX_ACCESS_TOKEN;
|
||||
const ROOM_ID = process.env.MATRIX_ROOM_ID;
|
||||
const CRON_SCHEDULE= process.env.CRON_SCHEDULE || '0 8 * * *'; // täglich 08:00 Uhr
|
||||
|
||||
if (!ACCESS_TOKEN) { console.error('❌ MATRIX_ACCESS_TOKEN fehlt!'); process.exit(1); }
|
||||
if (!ROOM_ID) { console.error('❌ MATRIX_ROOM_ID fehlt!'); process.exit(1); }
|
||||
|
||||
// ---- Matrix Client Setup ----
|
||||
const storage = new SimpleFsStorageProvider('/data/bot-storage.json');
|
||||
const client = new MatrixClient(HOMESERVER, ACCESS_TOKEN, storage);
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
|
||||
const parser = new RSSParser({
|
||||
timeout: 10000,
|
||||
headers: { 'User-Agent': 'noctura-newsbot/1.0' }
|
||||
});
|
||||
|
||||
// ---- RSS Feed abrufen ----
|
||||
async function fetchFeed(feed) {
|
||||
try {
|
||||
const parsed = await parser.parseURL(feed.url);
|
||||
const cutoff = Date.now() - HOURS_LOOKBACK * 60 * 60 * 1000;
|
||||
|
||||
return parsed.items
|
||||
.filter(item => {
|
||||
const pubDate = item.pubDate ? new Date(item.pubDate).getTime() : Date.now();
|
||||
return pubDate >= cutoff;
|
||||
})
|
||||
.slice(0, MAX_ITEMS_PER_FEED)
|
||||
.map(item => ({
|
||||
title: item.title?.trim() || 'Kein Titel',
|
||||
link: item.link || item.guid || '',
|
||||
summary: item.contentSnippet
|
||||
? item.contentSnippet.slice(0, 120).trim() + '…'
|
||||
: '',
|
||||
feedName: feed.name,
|
||||
category: feed.category,
|
||||
pubDate: item.pubDate
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`⚠️ Feed-Fehler [${feed.name}]: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Alle Feeds abrufen & gruppieren ----
|
||||
async function fetchAllNews() {
|
||||
console.log('🔍 Feeds werden abgerufen...');
|
||||
const results = await Promise.allSettled(FEEDS.map(fetchFeed));
|
||||
|
||||
const grouped = {};
|
||||
results.forEach((result, i) => {
|
||||
if (result.status !== 'fulfilled') return;
|
||||
const items = result.value;
|
||||
items.forEach(item => {
|
||||
if (!grouped[item.category]) grouped[item.category] = [];
|
||||
grouped[item.category].push(item);
|
||||
});
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// ---- Nachricht in Matrix Room senden ----
|
||||
async function sendToMatrix(plain, html) {
|
||||
await client.sendMessage(ROOM_ID, {
|
||||
msgtype: 'm.text',
|
||||
body: plain,
|
||||
format: 'org.matrix.custom.html',
|
||||
formatted_body: html
|
||||
});
|
||||
console.log('✅ Digest erfolgreich gesendet!');
|
||||
}
|
||||
|
||||
// ---- Haupt-Job ----
|
||||
async function runDigest() {
|
||||
console.log(`\n⏰ [${new Date().toISOString()}] Täglicher Digest wird erstellt...`);
|
||||
try {
|
||||
const grouped = await fetchAllNews();
|
||||
const totalItems = Object.values(grouped).flat().length;
|
||||
console.log(`📊 ${totalItems} Artikel gefunden`);
|
||||
|
||||
if (totalItems === 0) {
|
||||
const { plain, html } = formatNoNews();
|
||||
await sendToMatrix(plain, html);
|
||||
} else {
|
||||
const { plain, html } = formatDailyDigest(grouped);
|
||||
await sendToMatrix(plain, html);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Fehler beim Digest:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bot starten ----
|
||||
async function main() {
|
||||
console.log('🚀 noctura newsbot startet...');
|
||||
console.log(` Homeserver: ${HOMESERVER}`);
|
||||
console.log(` Room: ${ROOM_ID}`);
|
||||
console.log(` Cron: ${CRON_SCHEDULE}`);
|
||||
|
||||
await client.start();
|
||||
console.log('✅ Matrix Client verbunden!');
|
||||
|
||||
// Cron Job registrieren
|
||||
cron.schedule(CRON_SCHEDULE, runDigest, { timezone: 'Europe/Berlin' });
|
||||
console.log(`📅 Cron Job aktiv: ${CRON_SCHEDULE} (Europe/Berlin)`);
|
||||
|
||||
// Beim Start einmal sofort ausführen wenn TEST_RUN gesetzt
|
||||
if (process.env.TEST_RUN === 'true') {
|
||||
console.log('🧪 TEST_RUN aktiv – einmalig sofort ausführen...');
|
||||
await runDigest();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Bot-Fehler:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in a new issue