← Back to Home ← Zurück zur Startseite Not linked from anywhere. You found it. Nicht verlinkt. Du hast es gefunden.
System Architecture Systemarchitektur
Everything invisible running under this portfolio. Engineering decisions, pattern choices, and the occasional overcomplicated solution to a simple problem. Alles Unsichtbare, das hinter diesem Portfolio läuft. Architekturentscheidungen, Muster und gelegentlich überkomplizierte Lösungen für einfache Probleme.
⚡ You are currently being profiled by the system described below.
Your referrer, UA, country, org, and time-on-page are being recorded. ⚡ Du wirst gerade vom unten beschriebenen System analysiert.
Referrer, UA, Land, Organisation und Verweildauer werden erfasst.
01 — Analytics & Beacon Pipeline 01 — Analytics- & Beacon-Pipeline
👁️ Event Beacon 👁️ Event-Beacon
api/src/functions/beacon.js · ~400 lines
Custom-built analytics pipeline — no third-party tracking. Every meaningful user action fires a
sendBeacon() to an Azure Function that enriches, classifies, and routes it to Discord.
No cookies, no fingerprinting — sessionStorage._vid as a tab-scoped session handle (clears on tab close, no cross-session tracking). Eigene Analytics-Pipeline — kein Drittanbieter-Tracking. Jede relevante Benutzeraktion schickt einen sendBeacon()-Aufruf an eine Azure Function, die anreichert, klassifiziert und an Discord weiterleitet. Keine Cookies, kein Fingerprinting — sessionStorage._vid als tab-basierte Session-Kennung (wird beim Tab-Schließen gelöscht, kein Tracking über Sessions hinweg).
- Events:
pageview, blog_view, cv_download, contact_view, skills_view, easter_egg, sensitive_page, hire_view, meta_view Events: pageview, blog_view, cv_download, contact_view, skills_view, easter_egg, sensitive_page, hire_view, meta_view - Two Discord channels: analytics (humans) and crawlers (bots) Zwei Discord-Channels: analytics (Menschen) und crawlers (Bots)
- In-memory rate limiter per
IP:event:page — 20-min default window, 30-min for hire_view In-Memory-Ratelimiter pro IP:event:page — 20-Min.-Standardfenster, 30 Min. für hire_view - IP enriched via
ipwho.is → country, org, masked display. Spoiler-tagged in Discord so logs are shareable IP angereichert via ipwho.is → Land, Organisation, maskierte Anzeige. In Discord als Spoiler markiert, damit Logs teilbar bleiben
Azure Functions v4 sendBeacon API rate limiting Rate Limiting 02 — Visitor Classification 02 — Besucher-Klassifizierung
🤖 Bot/Human Classifier 🤖 Bot/Mensch-Klassifizierer
classifyHuman() + CRAWLERS list
Three-tier detection. First, a static UA pattern list catches known crawlers (Googlebot, GPTBot, ClaudeBot, etc.) and routes them to the crawlers channel.
Second, classifyHuman() catches headless browsers, automation tools, and scripts that slipped past.
Third, anything non-human that reached the human path gets rerouted to crawlers instead of analytics. Dreistufige Erkennung. Erstens fängt eine statische UA-Muster-Liste bekannte Crawler ab (Googlebot, GPTBot, ClaudeBot, etc.) und leitet sie in den Crawlers-Channel. Zweitens fängt classifyHuman() Headless-Browser, Automatisierungstools und Skripte ab, die durchgerutscht sind. Drittens wird alles Nicht-Menschliche, das den Menschen-Pfad erreicht hat, auf Crawlers umgeleitet statt in Analytics.
-
window.__beacon is exposed before the bot filter — so sensitive pages still fire even from headless browsers window.__beacon wird vor dem Bot-Filter bereitgestellt — sensible Seiten feuern also auch aus Headless-Browsern - Client-side headless signals (webdriver presence, plugin count, language array, screen dimensions) sent via
notify.js for impression-level scraper detection Client-seitige Headless-Signale (webdriver-Property, Plugin-Anzahl, Language-Array, Bildschirmabmessungen) werden über notify.js gesendet, für Scraper-Erkennung auf Impression-Ebene - Known-crawler UA list: 14 entries with per-crawler color, emoji, importance flag Bekannte-Crawler-UA-Liste: 14 Einträge mit Farbe, Emoji und Wichtigkeits-Flag pro Crawler
UA matching headless detection Headless-Erkennung client signals Client-Signale 🎯 Visitor Role Classifier 🎯 Besucherrollen-Klassifizierer
classifyVisitor(referrer, page, event, detail)
Once a visitor passes the bot filter, their intent is inferred from referrer + event.
No ML — just pattern matching on referrer hostnames and event types.
Returns null when there is no clear signal (field omitted rather than adding noise). Sobald ein Besucher den Bot-Filter passiert, wird die Intention aus Referrer + Event abgeleitet. Kein ML — nur Pattern-Matching auf Referrer-Hostnames und Event-Typen. Gibt null zurück, wenn kein klares Signal vorliegt (Feld wird weggelassen statt Rauschen zu erzeugen).
- Recruiter: LinkedIn, Glassdoor, Indeed, Stepstone, Xing, or any ATS domain (Greenhouse, Lever, Workday, Ashby, etc.) Recruiter: LinkedIn, Glassdoor, Indeed, Stepstone, Xing oder beliebige ATS-Domain (Greenhouse, Lever, Workday, Ashby, etc.)
- Developer: GitHub, Hacker News, Stack Overflow, Reddit, dev.to, Hashnode Developer: GitHub, Hacker News, Stack Overflow, Reddit, dev.to, Hashnode
- Social: Instagram, Twitter/X, YouTube, Facebook, TikTok — labeled by platform Social: Instagram, Twitter/X, YouTube, Facebook, TikTok — mit Plattform-Label
- Explorer:
easter_egg event with the specific egg key Explorer: easter_egg-Event mit dem jeweiligen Egg-Schlüssel -
cv_download and hire_view always classify as Recruiter regardless of referrer cv_download und hire_view werden immer als Recruiter klassifiziert, unabhängig vom Referrer
referrer analysis Referrer-Analyse intent inference Intent-Inferenz ⏱️ Time-on-Page Speed Signal ⏱️ Time-on-Page Geschwindigkeits-Signal
t0 captured in <head> script, ms sent with every beacon t0 wird im <head>-Script erfasst, ms wird mit jedem Beacon gesendet
t0 = Date.now() is captured when the BaseLayout head script initializes — before any content renders.
Every beacon payload includes ms: Date.now() - t0. The server flags events that fire
impossibly fast for human interaction. t0 = Date.now() wird gesetzt, wenn das BaseLayout-Head-Script initialisiert — bevor Inhalt rendert. Jede Beacon-Payload enthält ms: Date.now() - t0. Der Server markiert Events, die unmöglich schnell für menschliche Interaktion feuern.
-
contact_view / skills_view under 1.5s: ⚡ instant (bot?) — IntersectionObserver triggered without scrolling contact_view / skills_view unter 1,5 s: ⚡ instant (Bot?) — IntersectionObserver wurde ohne Scrollen getriggert -
cv_download / easter_egg under 1.5s: same flag cv_download / easter_egg unter 1,5 s: gleiches Flag -
pageview / blog_view excluded — these always fire immediately by design pageview / blog_view ausgenommen — feuern bauartbedingt sofort - Headless browsers typically score under 300ms on scroll-based events Headless-Browser liegen bei Scroll-Events typischerweise unter 300 ms
performance.now() IntersectionObserver bot heuristic Bot-Heuristik 03 — Scraper Trap System 03 — Scraper-Fallen-System
🕳️ Impressum Honeypot 🕳️ Impressum-Honeypot
src/pages/impressum.astro — invisible div, position: absolute left -9999px src/pages/impressum.astro — unsichtbares div, position: absolute left -9999px
The real address is shown as plain visible text — legally required by ECG §5.
An invisible off-screen div contains a fake address (unicode-corrupted, zero-width characters, right-to-left overrides),
a fake JSON candidate profile with "fit_score": 100, and honeypot links to /api/trap?p=0..n.
Real browsers render the visible real address; bots that scrape raw HTML get the honeypot. Die echte Adresse steht als sichtbarer Klartext — gesetzlich vorgeschrieben durch ECG §5. Ein unsichtbares Off-Screen-Div enthält eine Fake-Adresse (Unicode-manipuliert, Zero-Width-Zeichen, Right-to-Left-Overrides), ein Fake-JSON-Kandidatenprofil mit "fit_score": 100 und Honeypot-Links zu /api/trap?p=0..n. Echte Browser rendern die sichtbare Adresse; Bots, die rohes HTML scrapen, bekommen den Honeypot.
- Two-layer compliance: human visitors see the ECG-compliant address; non-rendering scrapers hit the trap Zwei-Ebenen-Compliance: Menschen sehen die ECG-konforme Adresse; nicht-rendernde Scraper landen in der Falle
- Fake address:
Robots-txt-Ignorierungsstraße 404, 00000 NullPointerException Fake-Adresse: Robots-txt-Ignorierungsstraße 404, 00000 NullPointerException -
/api/trap returns infinite paginated fake candidate profiles /api/trap liefert unendlich paginierte Fake-Kandidatenprofile - Headless browser fingerprinting: 6 client-side signals scored on visit, sent to
/api/notify Headless-Browser-Fingerprinting: 6 client-seitige Signale werden pro Besuch bewertet und an /api/notify gesendet
honeypot Honeypot tar pit Tarpit unicode corruption Unicode-Manipulation 🔔 Scraper Alarm 🔔 Scraper-Alarm
api/src/functions/notify.js · DISCORD_CRAWLERS_WEBHOOK_URL
Fires whenever a bot visits a robots.txt-disallowed page.
Pure humans return 200 silently — only bots get an alert.
Routes to the crawlers Discord channel with full profile: scraper tier, headless signals, country, org, spoiler-tagged IP. Feuert jedes Mal, wenn ein Bot eine robots.txt-disallowed Seite besucht. Reine Menschen erhalten stumm 200 zurück — nur Bots lösen einen Alarm aus. Geht in den Crawlers-Discord-Channel mit vollem Profil: Scraper-Kategorie, Headless-Signale, Land, Organisation, IP als Spoiler.
- Classification: Automation (Puppeteer/Playwright) → Headless Browser (scored 0–6) → Script (curl/wget/requests) Klassifizierung: Automation (Puppeteer/Playwright) → Headless-Browser (0–6 Punkte) → Skript (curl/wget/requests)
- Org lookup via
ipwho.is — shows "🏢 Microsoft" or "☁️ OVH" in the alert Organisations-Lookup über ipwho.is — zeigt im Alert z. B. „🏢 Microsoft“ oder „☁️ OVH“ - Headless signal breakdown listed:
webdriver, noPlugins, noLanguages, headlessUA, noChrome, suspiciousScreen Headless-Signale im Detail: webdriver, noPlugins, noLanguages, headlessUA, noChrome, suspiciousScreen
bot detection Bot-Erkennung discord webhook 04 — High-Value Tracking 04 — High-Value-Tracking
💼 /hire High-Priority Alert 💼 /hire High-Priority-Alarm
src/pages/hire.astro · @here Discord ping
/hire is not linked from anywhere on the site — finding it requires reading the source,
solving the 404 puzzle, or being thorough enough to explore manually.
Every visit triggers a Discord @here ping regardless of UA (raw fetch, bypasses the bot filter). /hire ist nirgends auf der Seite verlinkt — man findet sie nur, wenn man den Source liest, das 404-Rätsel löst oder gründlich genug manuell explorert. Jeder Besuch löst einen Discord-@here-Ping aus, unabhängig vom UA (Raw Fetch, umgeht den Bot-Filter).
- Beacon fires before the page renders — even if the visitor closes the tab immediately Beacon feuert, bevor die Seite rendert — auch wenn der Besucher den Tab sofort schließt
- Rate-limited to 30 min per IP to prevent refresh spam Rate-limitiert auf 30 Min. pro IP, um Refresh-Spam zu verhindern
-
allowed_mentions: { parse: ['everyone'] } required — Discord silently drops @here without it allowed_mentions: { parse: ['everyone'] } erforderlich — Discord verwirft @here sonst stumm
@here ping sendBeacon hidden page versteckte Seite 05 — LLM-Powered Analytics Digest 05 — LLM-gestützter Analytics-Digest
🤖 Gemini Report (12h) 🤖 Gemini-Report (12 h)
api/src/functions/analytics-report.js · Azure Timer Trigger
Every 12 hours, an Azure Timer Function reads the last 12h of Discord analytics messages
via the Discord Bot API, formats the embeds into compact single-line text,
and sends them to Gemini 2.0 Flash (free tier, 1M tokens/day) for analysis.
The report is posted to a separate #reports Discord channel. Alle 12 Stunden liest eine Azure Timer Function die letzten 12 h Discord-Analytics-Messages über die Discord-Bot-API, formatiert die Embeds zu kompaktem einzeiligem Text und schickt sie an Gemini 2.0 Flash (Free Tier, 1 Mio. Tokens/Tag) zur Analyse. Der Report landet in einem separaten #reports-Discord-Channel.
- Discord pagination:
after=snowflake queries, 100 messages/page, continues until batch < 100 Discord-Paginierung: after=snowflake-Abfragen, 100 Messages/Seite, läuft bis Batch < 100 - Gemini prompt instructs analysis of: traffic summary, recruiter visits, bot patterns, anomalies, quick take Gemini-Prompt fordert Analyse von: Traffic-Überblick, Recruiter-Besuche, Bot-Muster, Anomalien, Quick Take
- Output capped at 800 tokens, split into 1980-char chunks for Discord's 2000-char limit Output auf 800 Tokens begrenzt, in 1980-Zeichen-Blöcke geteilt wegen Discords 2000-Zeichen-Limit
- Graceful degradation: if Gemini fails, posts a raw summary with the error Graceful Degradation: wenn Gemini ausfällt, wird eine Raw-Zusammenfassung mit Fehlermeldung gepostet
Gemini 2.0 Flash Timer Trigger Discord Bot API 06 — Easter Egg Infrastructure 06 — Easter-Egg-Infrastruktur
🥚 Achievement System 🥚 Achievement-System
src/lib/achievements.ts · src/lib/secrets.ts
Nine achievements stored in localStorage.achievements.
A separate localStorage._beaconed key tracks what has been reported to Discord —
intentionally decoupled so pre-existing achievements still fire on next trigger after the beacon system was added. Neun Achievements liegen in localStorage.achievements. Ein separater localStorage._beaconed-Key verfolgt, was an Discord gemeldet wurde — absichtlich entkoppelt, damit bereits freigeschaltete Achievements nach Einführung des Beacon-Systems beim nächsten Trigger nochmal feuern.
-
beaconFirstUnlock(key) must be called before dispatchEvent('achievement') — otherwise the localStorage write happens first and the dedup check incorrectly skips it beaconFirstUnlock(key) muss vor dispatchEvent('achievement') aufgerufen werden — sonst schreibt localStorage zuerst und der Dedup-Check skippt fälschlicherweise - Keys:
matrix, doom, named, barrel_roll, explorer, persistent, saber, puzzle, disco Keys: matrix, doom, named, barrel_roll, explorer, persistent, saber, puzzle, disco - Available keys for blog unlock =
discoveries - opened_locks — two separate counters Verfügbare Schlüssel zum Blog-Unlock = discoveries - opened_locks — zwei getrennte Zähler
localStorage custom events Custom Events dedup ⛓️ Chain Physics (Blog Lock) ⛓️ Ketten-Physik (Blog-Lock)
src/components/blog/ChainOverlay.tsx · Canvas 2D
The easter-eggs blog post is locked behind nine chains rendered on a Canvas overlay.
The physics engine uses Verlet integration with distance constraints,
pre-settled for 800 frames on mount so chains appear taut on load. Der Easter-Eggs-Blogpost ist hinter neun Ketten verschlossen, gerendert auf einem Canvas-Overlay. Die Physik-Engine nutzt Verlet-Integration mit Abstandsbeschränkungen, 800 Frames auf Mount vorsimuliert, damit die Ketten beim Laden straff erscheinen.
- 9 chains: 5 vertical, 2 diagonal, 2 horizontal — Verlet nodes with slack=1.0005, gravity=0.001, damping=0.88, 24 constraint iterations/frame 9 Ketten: 5 vertikal, 2 diagonal, 2 horizontal — Verlet-Nodes mit slack=1.0005, gravity=0.001, damping=0.88, 24 Constraint-Iterationen/Frame
- Chain link count computed dynamically from container aspect ratio to keep link size uniform across chains Kettenglied-Anzahl wird dynamisch aus dem Container-Seitenverhältnis berechnet, damit die Glieder über alle Ketten hinweg gleich groß bleiben
- Break animation: lerp retraction to both endpoints (300ms) + fade (200ms) — three earlier approaches using physics-based falling all looked wrong Break-Animation: Lerp-Einzug zu beiden Enden (300 ms) + Fade (200 ms) — drei frühere physikbasierte Fall-Ansätze sahen alle falsch aus
- Mouse repulsion radius 50px, force 0.4 — nodes deflect on hover Maus-Abstoßungsradius 50 px, Kraft 0,4 — Nodes weichen beim Hover aus
Verlet integration Verlet-Integration Canvas 2D constraint solver Constraint-Solver 🎮 Beat Saber Mania (Canvas Game) 🎮 Beat Saber Mania (Canvas-Game)
src/components/beatsaber/ManiaGame.tsx · ~460 lines
A rhythm game built into the portfolio. Full beatmap hand-crafted for "Beat Saber" by Jaroslav Beck,
covering 0–111s at BPM 128. Runs at 60fps. Ein Rhythmusspiel, direkt ins Portfolio eingebaut. Komplette Beatmap handgemappt für „Beat Saber“ von Jaroslav Beck, 0–111 s bei BPM 128. Läuft mit 60 fps.
- FPS fix: removed
ctx.shadowBlur from the per-note draw loop (the main killer) — replaced with a transparent rounded-rect glow behind each note FPS-Fix: ctx.shadowBlur aus dem Draw-Loop pro Note entfernt (der Haupt-Killer) — ersetzt durch ein transparentes Rounded-Rect-Glow hinter jeder Note - Layout thrash fix: cached canvas size in a
sizeRef updated only on resize — eliminated getBoundingClientRect() from every animation frame Layout-Thrash-Fix: Canvas-Größe in einer sizeRef gecacht, wird nur bei Resize aktualisiert — getBoundingClientRect() aus jedem Animation-Frame entfernt - Audio latency fix:
new Audio() + preload='auto' on mount, not when countdown ends — play is instant Audio-Latenz-Fix: new Audio() + preload='auto' beim Mount, nicht erst am Countdown-Ende — Play startet sofort - Dead React state removed:
score and combo were set but never read in JSX (canvas reads gameStateRef directly) Toter React-State entfernt: score und combo wurden gesetzt, aber nie in JSX gelesen (Canvas liest gameStateRef direkt)
Canvas 2D requestAnimationFrame Web Audio 07 — GDPR & Legal Compliance 07 — DSGVO & Rechts-Compliance
⚖️ What Was Fixed & Why ⚖️ Was wurde behoben & warum
DSGVO · ECG §5 · MedienG §25 · ePrivacy-Richtlinie
A legal review flagged three genuine issues. All three were fixed. RIS case law was queried
directly via the data.bka.gv.at RIS API v2.6 to verify the exact legal standard before and after each fix. Ein Rechts-Review hat drei echte Probleme aufgezeigt. Alle drei wurden behoben. Die RIS-Rechtsprechung wurde direkt über die data.bka.gv.at RIS API v2.6 abgefragt, um den exakten Rechtsstandard vor und nach jedem Fix zu verifizieren.
- Impressum address (fixed) — Was hidden behind a JS click-to-reveal button.
OGH 4Ob186/08v (2008-11-18) established the controlling standard: ECG §5 requires information
"ständig leicht und unmittelbar zugänglich" — constantly, easily, and immediately accessible without
additional user interaction. OGH 8Ob129/23p (2024-08-26) reaffirmed: "Der Nutzer soll im
Konfliktfall einen Anknüpfungspunkt für eine etwaige Rechtsverfolgung erhalten."
Fix: address is now plain visible text. Honeypot div retained — affects only non-rendering scrapers, not human visitors. Impressum-Adresse (behoben) — War hinter einem JS-Click-to-Reveal-Button versteckt. OGH 4Ob186/08v (2008-11-18) hat den maßgeblichen Standard gesetzt: ECG §5 fordert Informationen „ständig leicht und unmittelbar zugänglich“ — durchgehend, leicht und unmittelbar verfügbar ohne zusätzliche Nutzerinteraktion. OGH 8Ob129/23p (2024-08-26) hat bestätigt: „Der Nutzer soll im Konfliktfall einen Anknüpfungspunkt für eine etwaige Rechtsverfolgung erhalten.“ Fix: Adresse steht jetzt als sichtbarer Klartext. Das Honeypot-Div bleibt — betrifft nur nicht-rendernde Scraper, nicht menschliche Besucher.
-
sessionStorage._vid (fixed) — Was localStorage: a persistent cross-session
analytics identifier with no consent banner. Under the ePrivacy Directive (Austria: TKG §165 ff.), non-strictly-necessary
persistent storage requires prior consent. Session-scoped identifiers clear on tab close and are treated equivalently
to session cookies — exempt from consent requirement. Fix: moved to sessionStorage.
Cross-session identification eliminated entirely. sessionStorage._vid (behoben) — War localStorage: eine persistente Cross-Session-Analytics-Kennung ohne Consent-Banner. Unter der ePrivacy-Richtlinie (Österreich: TKG §165 ff.) erfordert nicht-strikt-notwendige persistente Speicherung vorherige Einwilligung. Session-basierte Kennungen werden beim Tab-Schließen gelöscht und werden rechtlich wie Session-Cookies behandelt — von der Consent-Pflicht ausgenommen. Fix: zu sessionStorage verschoben. Cross-Session-Identifikation vollständig eliminiert. - Datenschutzerklärung transparency (fixed) — Previously disclosed only theme/language/achievements.
DSGVO Art. 13 requires disclosure at time of collection for all processing activities.
Now fully documents: analytics beacons, ipwho.is as third-party sub-processor, Discord as data recipient
(EU-US DPF certified), bot detection system,
sessionStorage._vid, and localStorage._beaconed. Transparenz der Datenschutzerklärung (behoben) — Früher wurden nur Theme/Sprache/Achievements offengelegt. DSGVO Art. 13 erfordert Offenlegung aller Verarbeitungstätigkeiten zum Zeitpunkt der Erhebung. Jetzt vollständig dokumentiert: Analytics-Beacons, ipwho.is als Drittanbieter-Unterauftragnehmer, Discord als Datenempfänger (EU-US-DPF-zertifiziert), Bot-Erkennungssystem, sessionStorage._vid und localStorage._beaconed.
📋 Legal Basis per Processing Activity 📋 Rechtsgrundlage pro Verarbeitungsaktivität
Art. 6 DSGVO — verified via RIS MCP · ris-saas/mcp_server.py Art. 6 DSGVO — verifiziert über RIS MCP · ris-saas/mcp_server.py
Legal verification was performed by querying the Austrian
Rechtsinformationssystem des Bundes (RIS) directly via a custom MCP server
(ris-saas/mcp_server.py, registered in Claude Code's global ~/.claude/settings.json).
The MCP server exposes search_case_law and get_document tools over the
data.bka.gv.at/ris/api/v2.6/Judikatur endpoint, covering all 15 Austrian court applications
(Vwgh, Vfgh, Justiz, BVwG, DSB, etc.). Courts searched: DSB, DSK, Vwgh, BVwG, Justiz.
Finding: the DSB does not publish decisions in the RIS Judikatur index.
No Austrian court decision was found challenging pseudonymized analytics under legitimate interest
for a non-commercial personal website — the Art. 6 Abs. 1 lit. f claim is legally untested
and therefore uncontested in Austrian case law. Die rechtliche Verifikation erfolgte durch direkte Abfragen des österreichischen Rechtsinformationssystems des Bundes (RIS) über einen eigenen MCP-Server (ris-saas/mcp_server.py, global registriert in Claude Codes ~/.claude/settings.json). Der MCP-Server stellt die Tools search_case_law und get_document über den data.bka.gv.at/ris/api/v2.6/Judikatur-Endpoint bereit und deckt alle 15 österreichischen Gerichts-Anwendungen ab (Vwgh, Vfgh, Justiz, BVwG, DSB, etc.). Abgefragte Gerichte: DSB, DSK, Vwgh, BVwG, Justiz. Befund: Die DSB veröffentlicht keine Entscheidungen im RIS-Judikatur-Index. Es wurde keine österreichische Gerichtsentscheidung gefunden, die pseudonymisierte Analytics unter berechtigtem Interesse für eine nicht-kommerzielle private Website infrage stellt — der Anspruch auf Art. 6 Abs. 1 lit. f ist rechtlich ungetestet und daher in der österreichischen Rechtsprechung unwidersprochen.
- Analytics beacons — Art. 6 Abs. 1 lit. f DSGVO (legitimate interest). Non-commercial portfolio; pseudonymized IPs (last octet masked before logging); no advertising profiling; no cross-site tracking; fully disclosed in Datenschutzerklärung. Proportionate to purpose. Analytics-Beacons — Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). Nicht-kommerzielles Portfolio; pseudonymisierte IPs (letztes Oktett vor dem Logging maskiert); kein Werbe-Profiling; kein Cross-Site-Tracking; vollständig in der Datenschutzerklärung offengelegt. Zweckangemessen.
- Bot & scraper detection — Art. 6 Abs. 1 lit. f (legitimate interest: infrastructure security). Equivalent to standard server access logging, which Austrian and EU jurisprudence accepts as legitimate without consent. Client-side fingerprint signals apply only to detected non-human visitors. Bot- & Scraper-Erkennung — Art. 6 Abs. 1 lit. f (berechtigtes Interesse: Infrastruktur-Sicherheit). Gleichzusetzen mit klassischem Server-Access-Logging, das die österreichische und europäische Rechtsprechung ohne Einwilligung als legitim akzeptiert. Client-seitige Fingerprint-Signale greifen nur bei als nicht-menschlich erkannten Besuchern.
- ipwho.is geo-lookup — Art. 6 Abs. 1 lit. f. Full IP transmitted server-side for country detection only; not retained after lookup; disclosed as third-party sub-processor in Datenschutzerklärung §3. ipwho.is-Geo-Lookup — Art. 6 Abs. 1 lit. f. Vollständige IP wird serverseitig ausschließlich zur Ländererkennung übertragen; keine Speicherung nach dem Lookup; als Drittanbieter-Unterauftragnehmer in Datenschutzerklärung §3 offengelegt.
- Discord storage — Art. 6 Abs. 1 lit. f. Discord Inc. certified under EU-US Data Privacy Framework (adequacy basis for third-country transfer). Pseudonymized data only; 30-day retention. Discord-Speicherung — Art. 6 Abs. 1 lit. f. Discord Inc. ist unter dem EU-US Data Privacy Framework zertifiziert (Angemessenheitsbasis für Drittlandübermittlung). Nur pseudonymisierte Daten; 30 Tage Aufbewahrung.
- Tarpit
/api/trap — No DSGVO applicability. No personal data collected. Defensive cybersecurity measure: feeding fake data to robots.txt-violating bots is legally recognized as lawful self-defense of infrastructure (no malware transmitted). Tarpit /api/trap — DSGVO nicht anwendbar. Keine personenbezogenen Daten werden erhoben. Defensive Cybersecurity-Maßnahme: Bots, die robots.txt verletzen, mit Fake-Daten zu füttern, ist rechtlich als zulässige Notwehr der Infrastruktur anerkannt (keine Malware übermittelt). - User preferences (theme, lang) — Art. 6 Abs. 1 lit. b / lit. f. Functional; never transmitted to any server; no retention beyond user-initiated browser clear. Benutzer-Einstellungen (Theme, Sprache) — Art. 6 Abs. 1 lit. b / lit. f. Funktional; wird nie an einen Server übertragen; keine Aufbewahrung über vom Nutzer ausgelöstes Browser-Clear hinaus.
🔍 RIS MCP Server
ris-saas/mcp_server.py · registered in ~/.claude/settings.json ris-saas/mcp_server.py · registriert in ~/.claude/settings.json
A custom MCP server wrapping the Austrian federal legal database (RIS — Rechtsinformationssystem des Bundes).
Built to make Austrian legal research first-class in Claude Code — query 15 court applications,
paginate results, and fetch full decision texts without leaving the editor.
Used here to verify that the compliance decisions documented above are grounded in actual case law,
not just legal opinion. Ein eigener MCP-Server, der das österreichische Rechtsinformationssystem des Bundes (RIS) umwickelt. Gebaut, um österreichische Rechtsrecherche in Claude Code erstklassig zu machen — 15 Gerichts-Anwendungen abfragen, Ergebnisse paginieren und vollständige Entscheidungstexte abrufen, ohne den Editor zu verlassen. Hier eingesetzt, um zu verifizieren, dass die oben dokumentierten Compliance-Entscheidungen auf tatsächlicher Rechtsprechung basieren und nicht nur auf Rechtsmeinung.
- Endpoint:
data.bka.gv.at/ris/api/v2.6/Judikatur — Austrian open government data, no API key required Endpoint: data.bka.gv.at/ris/api/v2.6/Judikatur — österreichische Open Government Data, kein API-Key nötig - Tools:
search_case_law(keywords, court, page, per_page, sort_by) · get_document(document_url) · list_courts() - 15 court applications: Vwgh, Vfgh, Justiz, BVwG, LVwG, DSB, DSK, GBK, and more 15 Gerichts-Anwendungen: Vwgh, Vfgh, Justiz, BVwG, LVwG, DSB, DSK, GBK und mehr
- Results include Geschäftszahl, Entscheidungsdatum, Normen, Schlagworte, and direct document URLs Ergebnisse enthalten Geschäftszahl, Entscheidungsdatum, Normen, Schlagworte und direkte Dokument-URLs
- Full decision texts accessible via
.html document URLs in the Dokumentliste field Vollständige Entscheidungstexte sind über .html-Dokument-URLs im Feld Dokumentliste abrufbar
MCP server Austrian law Österreichisches Recht open government data Open Government Data 🥊 Adversarial Legal Audit 🥊 Adversariales Rechts-Audit
RIS queries: Justiz, Vwgh, BVwG, DSB, DSK · 4 rounds · prosecution vs. defense RIS-Abfragen: Justiz, Vwgh, BVwG, DSB, DSK · 4 Runden · Anklage vs. Verteidigung
Claude was instructed to run adversarial cycles against itself — alternately trying to find
grounds to sue this site, then defending against each argument — using only evidence pulled live
from the Austrian RIS Judikatur API. All case citations are real and retrievable. Claude wurde angewiesen, adversariale Zyklen gegen sich selbst zu fahren — abwechselnd Gründe zu suchen, diese Seite zu verklagen, und dann gegen jedes Argument zu verteidigen — ausschließlich mit Evidenz, die live über die österreichische RIS-Judikatur-API gezogen wurde. Alle Gerichts-Zitate sind echt und abrufbar.
── ROUND 1: ECG §5 IMPRESSUM ── ── RUNDE 1: ECG §5 IMPRESSUM ──
⚔️ Prosecution: OGH 8Ob129/23p (2024-08-26) confirms ECG §5 applies to all website
operators providing "Dienste der Informationsgesellschaft." A portfolio actively soliciting internship
offers is commercial communication — it triggers ECG §5. The address was hidden behind a JavaScript
click-to-reveal button. OGH 4Ob186/08v (2008-11-18) established the controlling standard:
"ständig leicht und unmittelbar zugänglich" — immediately accessible without additional user interaction.
Click-to-reveal fails this test. Actionable under UWG as an unfair commercial practice. ⚔️ Anklage: OGH 8Ob129/23p (2024-08-26) bestätigt, dass ECG §5 für alle Website-Betreiber gilt, die „Dienste der Informationsgesellschaft“ erbringen. Ein Portfolio, das aktiv Praktikumsangebote einholt, ist kommerzielle Kommunikation — ECG §5 greift. Die Adresse war hinter einem JavaScript-Click-to-Reveal-Button versteckt. OGH 4Ob186/08v (2008-11-18) hat den maßgeblichen Standard gesetzt: „ständig leicht und unmittelbar zugänglich“ — unmittelbar zugänglich ohne zusätzliche Nutzerinteraktion. Click-to-Reveal bestehe diesen Test nicht. Gerichtlich durchsetzbar nach UWG als unlautere Geschäftspraktik.
🛡️ Defense: Already fixed — address is now plain visible text, satisfying both
the OGH 2008 and OGH 2024 standards completely. The honeypot div is off-screen and invisible to
human visitors; only bots reading raw HTML are affected. Moot point.
Secondary argument: ECG §5 applies to "Diensteanbieter" who provide services "in der Regel gegen Entgelt"
(usually for remuneration). A personal portfolio provides no service for remuneration — it is a presentation,
not a transaction. No Austrian court has extended ECG §5 to purely informational personal websites. 🛡️ Verteidigung: Bereits behoben — die Adresse steht jetzt als sichtbarer Klartext und erfüllt sowohl den OGH-2008- als auch den OGH-2024-Standard vollständig. Das Honeypot-Div ist off-screen und für menschliche Besucher unsichtbar; betroffen sind nur Bots, die rohes HTML lesen. Gegenstandslos. Sekundäres Argument: ECG §5 gilt für „Diensteanbieter“, die Dienste „in der Regel gegen Entgelt“ erbringen. Ein privates Portfolio erbringt keinen entgeltlichen Dienst — es ist eine Darstellung, keine Transaktion. Kein österreichisches Gericht hat ECG §5 auf rein informative private Websites ausgedehnt.
Verdict: ✅ Prosecution argument was valid before the fix. Now moot. Secondary ECG §5 applicability argument unresolved by Austrian case law but irrelevant given compliance. Urteil: ✅ Das Argument der Anklage war vor dem Fix berechtigt. Jetzt gegenstandslos. Das sekundäre Argument zur ECG-§5-Anwendbarkeit ist von der österreichischen Rechtsprechung ungeklärt, aber angesichts der Compliance irrelevant.
── ROUND 2: IP ADDRESS → DISCORD (DRITTLANDÜBERMITTLUNG) ── ── RUNDE 2: IP-ADRESSE → DISCORD (DRITTLANDÜBERMITTLUNG) ──
⚔️ Prosecution: OLG Wien 10R58/25k (2025-10-15) — a visitor's IP address was transmitted
to a US company ("C* LLC") when their browser loaded resources from the defendant's website.
Court examined whether Art. 6 DSGVO provided a basis. Plaintiff sued for €100 in damages for loss of
control over her IP address. This site sends visitor IPs to Discord Inc. (USA) via webhook on every page
load. Discord is a US company. Under post-Schrems II logic, even DPF-certified transfers are contested
— OGH 6Ob56/21k (2021) shows Austrian courts take US data transfers seriously.
No consent was obtained. Legitimate interest claim must survive a balancing test (OGH 7Ob121/22b, 2022). ⚔️ Anklage: OLG Wien 10R58/25k (2025-10-15) — die IP-Adresse einer Besucherin wurde an ein US-Unternehmen („C* LLC“) übermittelt, als ihr Browser Ressourcen von der Website der Beklagten geladen hat. Das Gericht hat geprüft, ob Art. 6 DSGVO eine Grundlage liefert. Die Klägerin klagte auf 100 € Schadenersatz wegen Kontrollverlust über ihre IP-Adresse. Diese Seite sendet Besucher-IPs bei jedem Seitenaufruf via Webhook an Discord Inc. (USA). Discord ist ein US-Unternehmen. Nach Post-Schrems-II-Logik sind selbst DPF-zertifizierte Übermittlungen umstritten — OGH 6Ob56/21k (2021) zeigt, dass österreichische Gerichte US-Datenübermittlungen ernst nehmen. Keine Einwilligung wurde eingeholt. Ein Anspruch auf berechtigtes Interesse muss eine Abwägung überstehen (OGH 7Ob121/22b, 2022).
🛡️ Defense: The OLG Wien 10R58/25k case is factually distinguishable on a critical point:
the violation there was client-side — the visitor's browser was directly instructed to load
resources from a US server, exposing their IP to that server without any intermediary.
On this site, IP processing is entirely server-side: the visitor's browser contacts only
Azure (Microsoft's EU datacenter available). Our Azure Function then calls ipwho.is and Discord
— the visitor's device never has a direct connection to either US service.
The IP that reaches Discord is the server's outbound IP plus the forwarded visitor IP, already
masked (last octet removed) before logging. Furthermore: (1) Discord Inc. is certified under the EU-US
Data Privacy Framework — a valid adequacy basis until a court strikes it down; (2) the processing is
disclosed in the Datenschutzerklärung; (3) this is a non-commercial personal website with no advertising
and no profiling. The OLG Wien case was also procedurally dismissed
(rechtsmissbräuchliche Geltendmachung — same plaintiff had already lost an identical claim).
The underlying substantive question remains open, but the factual distinction is strong. 🛡️ Verteidigung: Der Fall OLG Wien 10R58/25k ist an einem entscheidenden Punkt sachlich abgrenzbar: Dort war die Verletzung client-seitig — der Browser der Besucherin wurde direkt angewiesen, Ressourcen von einem US-Server zu laden, wodurch ihre IP diesem Server ohne Zwischenschritt offengelegt wurde. Auf dieser Seite ist die IP-Verarbeitung vollständig serverseitig: Der Browser des Besuchers kontaktiert ausschließlich Azure (Microsofts EU-Rechenzentrum verfügbar). Unsere Azure Function ruft dann ipwho.is und Discord auf — das Gerät des Besuchers hat nie eine direkte Verbindung zu einem der US-Dienste. Die IP, die Discord erreicht, ist die Outbound-IP des Servers plus die weitergeleitete Besucher-IP, die vor dem Logging bereits maskiert ist (letztes Oktett entfernt). Darüber hinaus: (1) Discord Inc. ist unter dem EU-US Data Privacy Framework zertifiziert — eine gültige Angemessenheits-Grundlage, bis ein Gericht sie aufhebt; (2) die Verarbeitung ist in der Datenschutzerklärung offengelegt; (3) es handelt sich um eine nicht-kommerzielle private Website ohne Werbung und ohne Profiling. Der OLG-Wien-Fall wurde zudem prozessual abgewiesen (rechtsmissbräuchliche Geltendmachung — dieselbe Klägerin hatte bereits einen identischen Anspruch verloren). Die zugrunde liegende materielle Frage bleibt offen, aber die sachliche Abgrenzung ist stark.
Verdict: ⚠️ Strongest remaining open question. Server-side processing + IP masking + DPF certification + non-commercial context = defensible, but not risk-zero until a court rules definitively on server-side analytics to DPF-certified US recipients. Urteil: ⚠️ Die stärkste verbleibende offene Frage. Serverseitige Verarbeitung + IP-Maskierung + DPF-Zertifizierung + nicht-kommerzieller Kontext = verteidigbar, aber nicht risikolos, solange kein Gericht definitiv über serverseitige Analytics an DPF-zertifizierte US-Empfänger urteilt.
── ROUND 3: ANALYTICS WITHOUT CONSENT (ePRIVACY / DSGVO) ── ── RUNDE 3: ANALYTICS OHNE EINWILLIGUNG (ePRIVACY / DSGVO) ──
⚔️ Prosecution: The ePrivacy Directive (Austria: TKG §165 ff.) requires prior informed
consent for any storage on a user's device that is not strictly technically necessary.
sessionStorage._vid is a unique session identifier created without user consent. The fact
that it clears on tab close does not automatically make it "strictly necessary" — it is created
for analytics purposes, which courts and regulators consistently hold requires consent.
No Austrian or EU court has explicitly exempted session-scoped analytics identifiers from the consent requirement.
The site has no cookie banner, no consent mechanism, and no opt-out. ⚔️ Anklage: Die ePrivacy-Richtlinie (Österreich: TKG §165 ff.) verlangt vorherige informierte Einwilligung für jede Speicherung auf dem Gerät des Nutzers, die nicht strikt technisch notwendig ist. sessionStorage._vid ist eine eindeutige Session-Kennung, die ohne Einwilligung erstellt wird. Dass sie beim Tab-Schließen gelöscht wird, macht sie nicht automatisch „strikt notwendig“ — sie wird für Analytics-Zwecke erstellt, und Gerichte sowie Regulatoren halten solche Zwecke durchgängig für einwilligungspflichtig. Kein österreichisches oder EU-Gericht hat session-basierte Analytics-Kennungen ausdrücklich von der Einwilligungspflicht ausgenommen. Die Seite hat kein Cookie-Banner, keinen Einwilligungsmechanismus und kein Opt-Out.
🛡️ Defense: RIS research found zero Austrian court decisions ruling that session-scoped
analytics identifiers require consent on a non-commercial website. The argument is legally untested
in Austria. The EDPB's own guidance distinguishes persistent trackers (clearly require consent) from
session-scoped identifiers used for basic visit continuity (lower-risk, arguable under legitimate interest
or "technically necessary for session integrity"). More importantly: the balancing test under
Art. 6 Abs. 1 lit. f DSGVO (OGH 7Ob121/22b, 2022) clearly favors this site — non-commercial purpose,
pseudonymized data, no profiling, no advertising, fully disclosed, minimal retention.
Even if the consent argument had merit, the proportionality test would likely defeat it at this scale. 🛡️ Verteidigung: Die RIS-Recherche fand null österreichische Gerichtsentscheidungen, die session-basierte Analytics-Kennungen auf nicht-kommerziellen Websites für einwilligungspflichtig erklären. Das Argument ist in Österreich rechtlich ungetestet. Selbst die EDPB-Leitlinien unterscheiden persistente Tracker (eindeutig einwilligungspflichtig) von session-basierten Kennungen für grundlegende Besuchs-Kontinuität (geringeres Risiko, argumentierbar unter berechtigtem Interesse oder „technisch notwendig für Session-Integrität“). Wichtiger: Die Abwägung unter Art. 6 Abs. 1 lit. f DSGVO (OGH 7Ob121/22b, 2022) spricht klar für diese Seite — nicht-kommerzieller Zweck, pseudonymisierte Daten, kein Profiling, keine Werbung, vollständig offengelegt, minimale Aufbewahrung. Selbst wenn das Einwilligungsargument einen Ansatz hätte, würde die Verhältnismäßigkeitsprüfung es in diesem Maßstab wohl aushebeln.
Verdict: ⚠️ Gray area. No Austrian case law mandates a consent banner for session analytics on non-commercial sites. The balancing test argument is strong. Risk exists mainly if a DSB enforcement action were specifically targeted at this configuration — which has no precedent. Urteil: ⚠️ Grauzone. Keine österreichische Rechtsprechung verlangt ein Consent-Banner für Session-Analytics auf nicht-kommerziellen Seiten. Das Abwägungsargument ist stark. Ein Risiko besteht hauptsächlich, falls eine DSB-Durchsetzungsmaßnahme gezielt auf diese Konfiguration abzielen würde — wofür es keinen Präzedenzfall gibt.
── ROUND 4: DOES ECG §5 EVEN APPLY TO A PERSONAL PORTFOLIO? ── ── RUNDE 4: GILT ECG §5 ÜBERHAUPT FÜR EIN PRIVATES PORTFOLIO? ──
⚔️ Prosecution: ECG §5 covers any "Diensteanbieter" operating a website that provides
information or commercial communication. OGH 8Ob129/23p (2024) states flatly: "Unternehmen, welche eine
Website betreiben, zählen nach der Legaldefinition des ECG in der Regel als Diensteanbieter."
This site contains a CV, a /hire page, project showcases, and a blog — it is clearly designed to
generate economic opportunity (internship offers). That makes it commercial communication under ECG §1 Z3.
MedienG §25 may also apply: a regularly updated blog with articles "geeignet, die öffentliche
Meinungsbildung zu beeinflussen" could trigger the Medieninhaber obligation. ⚔️ Anklage: ECG §5 erfasst jeden „Diensteanbieter“, der eine Website mit Informationen oder kommerzieller Kommunikation betreibt. OGH 8Ob129/23p (2024) stellt klar: „Unternehmen, welche eine Website betreiben, zählen nach der Legaldefinition des ECG in der Regel als Diensteanbieter.“ Diese Seite enthält einen Lebenslauf, eine /hire-Seite, Projekt-Showcases und einen Blog — sie ist klar darauf ausgelegt, wirtschaftliche Gelegenheiten zu generieren (Praktikumsangebote). Das macht sie zu kommerzieller Kommunikation nach ECG §1 Z3. Auch MedienG §25 könnte greifen: Ein regelmäßig aktualisierter Blog mit Artikeln, die „geeignet sind, die öffentliche Meinungsbildung zu beeinflussen“, könnte die Medieninhaber-Pflicht auslösen.
🛡️ Defense: The OGH 8Ob129/23p quote applies to "Unternehmen" (companies) — not
natural persons. ECG §1 Z2 defines "Dienst der Informationsgesellschaft" as a service provided
"in der Regel gegen Entgelt" (usually for remuneration). A portfolio that is free to access and generates
no direct revenue is not a remunerated service. Seeking employment is not itself a commercial service.
No Austrian court has held ECG §5 applicable to a private person's personal portfolio website.
Regarding MedienG §25: "öffentliche Meinungsbildung beeinflussen" has been interpreted narrowly
by Austrian courts to cover news media and political commentary — not a developer's technical blog
with a readership that can be counted on two hands. In any case, the impressum already discloses
name, address, contact, and "Grundlegende Richtung" (portfolio to present skills and work history),
satisfying §25 MedienG if it applies. 🛡️ Verteidigung: Das OGH-Zitat aus 8Ob129/23p bezieht sich auf „Unternehmen“ — nicht auf natürliche Personen. ECG §1 Z2 definiert „Dienst der Informationsgesellschaft“ als Dienst, der „in der Regel gegen Entgelt“ erbracht wird. Ein Portfolio, das kostenlos zugänglich ist und keinen direkten Umsatz erzeugt, ist kein entgeltlicher Dienst. Die Suche nach einer Anstellung ist für sich genommen kein kommerzieller Dienst. Kein österreichisches Gericht hat ECG §5 auf die private Portfolio-Website einer Privatperson angewendet. Zu MedienG §25: „Öffentliche Meinungsbildung beeinflussen“ wurde von österreichischen Gerichten eng ausgelegt und erfasst Nachrichtenmedien und politische Kommentierung — nicht den technischen Blog eines Entwicklers mit einer Leserschaft, die man an zwei Händen abzählt. In jedem Fall weist das Impressum bereits Name, Adresse, Kontakt und „Grundlegende Richtung“ (Portfolio zur Darstellung von Skills und Werdegang) aus und erfüllt §25 MedienG, sofern er anwendbar ist.
Verdict: ✅ Compliance is correct regardless. The "does ECG §5 apply?" question is unresolved, but the site now satisfies ECG §5 in full — so the answer doesn't matter practically. Urteil: ✅ Die Compliance ist unabhängig davon korrekt. Die Frage „Gilt ECG §5?“ ist ungeklärt, aber die Seite erfüllt ECG §5 nun vollständig — die Antwort spielt praktisch keine Rolle.
── OVERALL VERDICT ── ── GESAMTURTEIL ──
The site was genuinely non-compliant in three ways (hidden impressum, undisclosed analytics,
persistent tracker). All three are fixed. Two open questions remain: (1) server-side IP forwarding
to Discord under Art. 6 Abs. 1 lit. f — defensible but untested at this fact pattern in Austrian courts;
(2) session analytics without consent — gray area, no Austrian precedent against it for non-commercial sites.
Neither open question has active case law going against this configuration.
The risk level for a personal portfolio in Leogang that twelve people visit per month: negligible. Die Seite war tatsächlich in drei Punkten nicht konform (verstecktes Impressum, nicht offengelegte Analytics, persistenter Tracker). Alle drei sind behoben. Zwei offene Fragen bleiben: (1) serverseitige IP-Weiterleitung an Discord unter Art. 6 Abs. 1 lit. f — verteidigbar, aber für dieses Sachverhaltsmuster in österreichischen Gerichten ungetestet; (2) Session-Analytics ohne Einwilligung — Grauzone, kein österreichischer Präzedenzfall dagegen für nicht-kommerzielle Seiten. Keine der offenen Fragen hat aktive Rechtsprechung gegen diese Konfiguration. Das Risiko-Level für ein persönliches Portfolio in Leogang, das zwölf Leute pro Monat besuchen: vernachlässigbar.
Last updated April 2026 · Not indexed · Not linked · Found by curiosity Zuletzt aktualisiert April 2026 · Nicht indexiert · Nicht verlinkt · Gefunden durch Neugier
Built because the invisible layer is often more interesting than the visible one. Gebaut, weil die unsichtbare Schicht oft interessanter ist als die sichtbare.