Compare commits
14 Commits
e0ce9334d0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f6db88acb | |||
| e5e386639e | |||
| bfe4ec756d | |||
| 70ed5bc629 | |||
| 07b2f7b031 | |||
| dbfe277eb0 | |||
| 971c6c9f97 | |||
| 399a8cb73d | |||
| e9b9656ce0 | |||
| 2afac7f49a | |||
| 04e9e4f6b8 | |||
| c4cababe98 | |||
| 3931473799 | |||
| cb4e80cf23 |
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Same check as CI: abort the commit if Hugo can't build cleanly.
|
||||
make build || exit 1
|
||||
make clean
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Build Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: "0.152.2"
|
||||
extended: true
|
||||
|
||||
# Fails the run on any Hugo error (broken templates, shortcodes, refs).
|
||||
- run: hugo --gc --minify --panicOnWarning
|
||||
working-directory: blog
|
||||
@@ -11,16 +11,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Deploy to VPS
|
||||
uses: appleboy/ssh-action@v0.1.9
|
||||
uses: appleboy/ssh-action@v1.2.5
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: agres
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
port: 22
|
||||
script: |
|
||||
set -e
|
||||
|
||||
# Pull latest changes
|
||||
cd /home/agres/website
|
||||
git pull origin main
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Private Google Datei
|
||||
googleeb4005177cc2703c.html
|
||||
|
||||
# public folder
|
||||
public/
|
||||
|
||||
#
|
||||
# hugo build cache
|
||||
resources/
|
||||
|
||||
# work-in-progress drafts
|
||||
tbd*.md
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "Blog"
|
||||
---
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: "Bruteforce-Analyse"
|
||||
draft: false
|
||||
date: "2025-11-02"
|
||||
description: "Einen Parser für meine fail2ban-Logs vom VPS schreiben und diese analysieren"
|
||||
---
|
||||
|
||||
## Einleitung
|
||||
|
||||
Vor Kurzem habe ich diese Website als kleines Nebenprojekt aufgesetzt, um ein wenig HTML, CSS und den Umgang mit Static-Site-Generatoren zu lernen. Zum Hosten der Website nutze ich meinen VPS bei [IONOS](https://www.ionos.de/). Da ich viel Zeit in der Cybersecurity-Welt verbracht habe, habe ich mein Bestes gegeben, den VPS so gut wie möglich abzusichern. Der Login per SSH ist nur über Private-/Public-Key-Authentifizierung möglich und [Fail2Ban](https://github.com/fail2ban/fail2ban) ist so eingerichtet, dass jeder gebannt wird, der mehr als fünf Mal versucht, sich anzumelden. Nachdem das eingerichtet war, habe ich eine Möglichkeit geschaffen, die Logs von nginx und fail2ban auf meinen privaten Proxmox-Server zu „ziehen“ und diese in Grafana zu verarbeiten. Dabei ist mir aufgefallen, dass die fail2ban-Logdatei über 31.000 Zeilen hat. Nichts Verdächtiges für eine öffentliche IP-Adresse, aber dennoch eine interessante Menge an Daten.
|
||||
|
||||
Nachdem ich diese Dateien in die Hände bekommen hatte, beschloss ich, diese Bruteforce-Angriffe zu parsen und zu analysieren. Außerdem schreibe ich das Ganze in Go, weil ich meine Fähigkeiten darin verbessern möchte und es uns erlaubt, extrem effizienten, schnellen und leichtgewichtigen Code zu schreiben.
|
||||
|
||||
<!--more-->
|
||||
|
||||
## Daten
|
||||
|
||||
Der Datensatz (die Datei fail2ban.log) besteht aus relativ einfachen und verständlichen Logs. Hier ein Ausschnitt eines durchschnittlichen Log-Abschnitts:
|
||||
|
||||
```txt
|
||||
// I will not be exposing the IP addresses for obvious reasons
|
||||
2025-10-25 17:04:35,850 fail2ban.filter [77278]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-10-25 17:04:35
|
||||
2025-10-25 17:04:36,414 fail2ban.actions [77278]: WARNING [sshd] xxx.xxx.xxx.xxx already banned
|
||||
2025-10-25 17:04:37,099 fail2ban.filter [77278]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-10-25 17:04:36
|
||||
2025-10-25 17:04:40,100 fail2ban.filter [77278]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-10-25 17:04:39
|
||||
2025-10-25 17:04:40,420 fail2ban.actions [77278]: NOTICE [sshd] Ban xxx.xxx.xxx.xxx
|
||||
```
|
||||
|
||||
Es gibt außerdem einige Fehlereinträge in der Logdatei, die nicht eindeutig gekennzeichnet sind. Sie sind zwar für die Datenanalyse nicht so wichtig, müssen aber beim Parsen berücksichtigt werden:
|
||||
|
||||
```txt
|
||||
2025-10-25 17:04:40,426 fail2ban.utils [77278]: ERROR 7f57b0136b10 -- exec: { iptables -w -C f2b-sshd -j RETURN >/dev/null 2>&1; } || { iptables -w -N f2b-sshd || true; iptables -w -A f2b-sshd -j RETURN; }
|
||||
for proto in $(echo 'tcp' | sed 's/,/ /g'); do
|
||||
{ iptables -w -C INPUT -p $proto -m multiport --dports 22 # or your custom SSH port -j f2b-sshd >/dev/null 2>&1; } || { iptables -w -I INPUT -p $proto -m multiport --dports 22 # or your custom SSH port -j f2b-sshd; }
|
||||
done
|
||||
```
|
||||
|
||||
## Implementierung
|
||||
|
||||
Das Folgende wird in drei Abschnitte unterteilt: Parsing, Datenerfassung und Visualisierung.
|
||||
|
||||
### Parsing & Regex
|
||||
|
||||
Ich habe entschieden, dass der einfachste Weg ist, die Logs in einer JSON-Datei zu speichern, mit einem Objekt für jeden Log-Eintrag. Das vereinfacht das Schreiben und Lesen derselben Datei in Zukunft. In Go machen wir das, indem wir ein Struct mit Tags dafür definieren, wie die JSON-Struktur später aussehen soll:
|
||||
|
||||
```go
|
||||
type Logs struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Handler string `json:"handler"`
|
||||
Level string `json:"level"`
|
||||
Source string `json:"source"`
|
||||
IpAddress string `json:"ipAddress"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
```
|
||||
|
||||
Das sind die Key-Value-Felder, die wir mit den Informationen aus der Logdatei füllen wollen. Die Logdatei wird mit Gos `os`- und `bufio`-Paketen gelesen. Damit öffnen wir die Logdatei und übergeben sie an eine Scanner-Instanz. Der Scanner bietet uns dann eine Vielzahl von Funktionen für das Lesen der Datei.
|
||||
Am einfachsten ist es, eine for-Schleife wie diese zu erstellen:
|
||||
|
||||
```go
|
||||
for scanner.Scan() {
|
||||
|
||||
line := scanner.Text()
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Das stellt sicher, dass in jeder Iteration der Schleife die Variable `line` die nächste Zeile erhält, bis keine mehr verfügbar ist. `line` enthält nun eine Zeile aus der Logdatei.
|
||||
Jetzt kommt das Parsing. Ich habe beschlossen, dass ich mich selbst hasse und dass ich reguläre Ausdrücke (Regex) verwende, um die üblichen Werte in den fail2ban-Logs zu finden und zu extrahieren. Dafür habe ich für jeden der oben genannten JSON-Tags einen regulären Ausdruck definiert, nämlich[^2]:
|
||||
|
||||
```go
|
||||
// For (probably much) better efficiency these expressions can be grouped into one large expression with matching groups for each field.
|
||||
// Because I do not like Regex, I will not do this.
|
||||
dateRegex, _ := regexp.Compile(`\d{4}-\d{2}-\d{2}`)
|
||||
handlerRegex, _ := regexp.Compile(`fail2ban\.\w+`)
|
||||
ipRegex, _ := regexp.Compile(`(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`)
|
||||
levelRegex, _ := regexp.Compile(`\s*(?:[A-Z]+)\s+`)
|
||||
serviceRegex, _ := regexp.Compile(`\s*(?:\[[a-z]+\])\s+`)
|
||||
actionRegex, _ := regexp.Compile(`(Found|already banned|Ban|Unban)`)
|
||||
```
|
||||
|
||||
Theoretisch sollte dieses Regelwerk, angewendet auf die Variable `line`, Werte wie diese extrahieren:
|
||||
|
||||
```text
|
||||
2025-10-25 17:04:35,850 fail2ban.filter [77278]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-10-25 17:04:35
|
||||
[ dateRegex ][ handlerRegex ] [levelRegex][serviceRegex][actionRegex][ipRegex]
|
||||
```
|
||||
|
||||
Und sie damit in ein sauberes JSON exportieren:
|
||||
|
||||
```json
|
||||
...
|
||||
{
|
||||
"timestamp": "2025-10-25 17:04:35,850",
|
||||
"handler": "fail2ban.filter",
|
||||
"level": "INFO",
|
||||
"source": "[sshd]",
|
||||
"ipAddress": "xxx.xxx.xxx.xxx",
|
||||
"message": "Found"
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Während ich meinen Code geschrieben, umgeschrieben und korrigiert habe, hatte ich auch die Performance im Blick. Ich habe meinen Parser-Funktionsaufruf zwischen zwei `time.Now().UnixMilli()` gepackt, um zu berechnen, wie lange das Parsen der 31.456 Zeilen dauern würde. Mein bestes Ergebnis lag um die `580 ms`, was bedeutet, dass mein Code ca. 55.000 Zeilen pro Sekunde parsen kann, oder – da die Datei 3,6 MB groß ist – ca. 6,2 MB pro Sekunde in einem Stream. (Deine Ergebnisse können je nach Festplatte, Single-Core-CPU-Leistung usw. variieren.)
|
||||
|
||||
Und da haben wir es, ich habe einen sehr einfachen fail2ban-Log-zu-JSON-Parser geschrieben. Ab jetzt wird es einfacher (und performanter).
|
||||
|
||||
### Analyse
|
||||
|
||||
Um die gesammelten Daten zu analysieren, lese ich unsere JSON-Datei mit allen Logs und erstelle eine neue Datei, die die verschiedenen Log-Nachrichten nach IP-Adresse aggregiert. Das bedeutet, wir erstellen ein neues Struct, das so aussieht:
|
||||
|
||||
```go
|
||||
type StatsByIp struct {
|
||||
IpAddress string `json:"ipAddress"`
|
||||
TotalLogs int `json:"totalLogs"`
|
||||
TotalFound int `json:"totalFound"`
|
||||
TotalBanned int `json:"totalBanned"`
|
||||
TotalUnbanned int `json:"totalUnbanned"`
|
||||
Country string `json:"county"`
|
||||
}
|
||||
```
|
||||
_Hinweis: Wenn du jemals Gos `encoding/json` verwendest, um das Marshalling und Unmarshalling der JSON-Dateien selbst zu übernehmen, beachte, dass du deine Variablen mit Großbuchstaben benennen **musst**. Go verwendet Groß-/Kleinschreibung, um zu unterscheiden, ob deine Variable oder Funktion öffentlich oder privat ist. Nur das Struct mit Großbuchstaben zu benennen reicht nicht aus, und wenn du das nicht tust, funktionieren die eingebauten Funktionen `json.Marshal()` und `json.Unmarshal()` nicht – ebenso wie einige andere Eigenheiten, wenn du externe Funktionen und Pointer verwendest._
|
||||
|
||||
Sobald diese Werte nach IP-Adresse aggregiert wurden, können wir sehen, wie oft sich welche IP erfolglos auf unserem SSH-Server authentifizieren wollte und wie oft sie gebannt und entbannt wurde.
|
||||
|
||||
Für zusätzliche Informationen habe ich ein einfaches Skript implementiert, das das Herkunftsland der IP-Adresse abfragt. Das geht leicht mit öffentlichen APIs wie [ipadress.com](https://www.ipaddress.com/apis/ip-to-country-api). Auch wenn das eine schöne Information ist, ist sie nicht zu 100 % korrekt, da sich IP-Adressen ändern und sie am Tag der Abfrage nicht demselben Nutzer gehören muss wie vor 10 oder mehr Tagen, als das Log sie erfasst hat.[^3]
|
||||
|
||||
### Visualisierung
|
||||
|
||||
#### Statisch
|
||||
|
||||
Ich bin ein großer Fan von Data Science und habe die meiste Zeit meiner „Software“-Entwicklung Python verwendet. Das erste Tool, das einem beim Visualisieren von Daten in den Sinn kommt, ist Pythons matplotlib. Ich habe es in allen Data-Science-nahen Projekten verwendet, bis hin zu meiner Bachelorarbeit. Daher war ich froh, herauszufinden, dass es für Go ein ähnliches Paket gibt. Dafür habe ich [gonum/plot](https://github.com/gonum/plot) verwendet. Damit habe ich eine einfache Wrapper-Funktion zum Erstellen von Balkendiagrammen geschrieben und alle relevanten Daten aus unseren Analyseschritten gesammelt. Die meisten einzelnen IPs kommen aus China mit insgesamt 180 individuellen IP-Adressen. Wenn man sich die meisten Bans ansieht, sieht das Diagramm so aus:
|
||||
|
||||

|
||||
|
||||
Interessanterweise führen die IPs aus den Niederlanden das Rennen an, wenn es darum geht, sich mit meinem VPS zu verbinden und zu authentifizieren.
|
||||
|
||||
#### Dynamisch
|
||||
|
||||
Ein paar Tage später habe ich mir das angesehen und beschlossen, dass ich es in ein Dashboard integrieren und meine Daten live anzeigen lassen möchte. Dazu habe ich die Parsing-Funktion so umgeschrieben, dass sie das zuletzt gelesene Byte in der Logdatei verfolgt. Jetzt kann ich das Synchronisieren der Logs von meinem VPS und das Ausführen meines Log-Parsers in einem LXC-Container automatisieren, in dem ich Grafana bereitgestellt habe. Über Grafana habe ich JSON-Dateien als Datenquelle eingerichtet (ich musste außerdem einen einfachen Python-HTTP-Server aufsetzen, um das geparste JSON über eine REST-API bereitzustellen) und begonnen, ein Dashboard zu erstellen.
|
||||
|
||||

|
||||
|
||||
Alles in allem ein interessantes kurzes Projekt. Eines Tages werde ich mir selbst die Herausforderung stellen, den Code so weit wie möglich zu optimieren.
|
||||
|
||||
## Post Scriptum
|
||||
|
||||
Keine Texte, Bilder, Codes, Konzepte oder Ideen wurden mit Slop-Generatoren[^1] erstellt. Auch wenn ich nicht glaube, dass Slop-Generatoren grundsätzlich schlecht sind, noch dass man sie nicht produktiv einsetzen kann (wie ich es in manchen Fällen tue), sind alle Dinge, die ich hier schreibe, Dinge, die mich wirklich interessieren und die ich selbst erstellen und bearbeiten möchte. Ich glaube, ein Mensch kann nur besser in dem werden, was er tut, wenn er nicht ständig nach sofortiger Befriedigung sucht, die einem bei der Verwendung von Slop-Generatoren oft gegeben wird.
|
||||
|
||||
[^1]: [Slop-Generatoren](https://asahilinux.org/docs/project/policies/slop/)
|
||||
[^2]: [Danke an ihateregex.io](https://ihateregex.io/expr/ip/)
|
||||
[^3]: [DSGVO: Erwägungsgrund 49 erkennt Netz- und Informationssicherheit ausdrücklich als berechtigtes Interesse an.](https://eur-lex.europa.eu/eli/reg/2016/679/oj#rct_49)
|
||||
@@ -5,7 +5,7 @@ date: "2025-11-02"
|
||||
description: "Writing a parser for and analysing my VPS fail2ban logs"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
## Introduction
|
||||
|
||||
Recently I set up this Website as a small side Project in order to learn a little bit of HTML, CSS and the use of static site generators. In order to host this website I used my VPS I own on [IONOS](https://www.ionos.de/). As someone who has spent a lot of time in the Cybersecurity world, I gave my best to secure the VPS as well as possible. Login via SSH is enabled only via Private/Public Key Authentification and [Fail2Ban](https://github.com/fail2ban/fail2ban) is setup to ban anyone who tries to login more than 5 Times. Once this was setup I created a way to "pull" the logs from nginx and fail2ban onto my private ProxMox server and Process these into Grafana. As I was doing this, I noticed that the fail2ban logfile has over 31.000 rows. Nothing suspicious for a public IP Address, yet a interesting amount of data.
|
||||
|
||||
@@ -13,7 +13,7 @@ After getting my hands on these files I decided that I will try to parse and ana
|
||||
|
||||
<!--more-->
|
||||
|
||||
# Data
|
||||
## Data
|
||||
|
||||
The dataset (fail2ban.log file) consists of relatively simple and understandable logs. Here is a snippet of the average log section:
|
||||
|
||||
@@ -35,11 +35,11 @@ for proto in $(echo 'tcp' | sed 's/,/ /g'); do
|
||||
done
|
||||
```
|
||||
|
||||
# Implementation
|
||||
## Implementation
|
||||
|
||||
The following will be separated into three sections: Parsing, Data Collection and Visualization
|
||||
|
||||
## Parsing & Regex
|
||||
### Parsing & Regex
|
||||
|
||||
I decided the most simple way to go forward is to save the logs into a JSON file, with an object for each log entry. This simplifies the writing and reading of the same file in the future. In go we do this by defining a struct with tags for how the JSON structure will look like in the future:
|
||||
|
||||
@@ -106,7 +106,7 @@ While writing, rewriting and fixing my code I also kept an eye on performance. I
|
||||
|
||||
And there we go, I have written a very simple fail2ban log to json parser. From now on it gets easier (and more performant).
|
||||
|
||||
## Analysing
|
||||
### Analysing
|
||||
|
||||
In order to analyse the data we have collected, I will read our json file containing all the logs and creating a new file which aggregates the different log messages by IP Address. This means we will create a new struct which looks like this:
|
||||
|
||||
@@ -126,9 +126,9 @@ Once these values have been aggregated by IP Address, we can see how often which
|
||||
|
||||
For extra information I have implemented a simple script to query the source country of the IP Address. This is done easily with public apis like [ipadress.com](https://www.ipaddress.com/apis/ip-to-country-api). Although this is nice information to have, it is not 100% correct, as IP addresses change and on the day we query it does not have to belong to the same user 10 or more days ago when the log found it.[^3]
|
||||
|
||||
## Visualising
|
||||
### Visualising
|
||||
|
||||
### Static
|
||||
#### Static
|
||||
|
||||
I am a big sucker for Data Science and have used Python for most of my time Developing "Software". The first tool which comes to mind when thinking of visualising data is pythons matplotlib. I have used this in all Data Science adjacent project, all through to my Bachelors Thesis. Therefore I was happy to find out go had a similar package out there. For this I have used [gonum/plot](https://github.com/gonum/plot). With it I created a simple wrapper function for creating bar plots, and collected all relevant data from our analytics steps. The most amount of individual IPs comes from China with a total count of 180 Individual IP Addresses. Taking a look at the most amount of bans, the chart looks like this:
|
||||
|
||||
@@ -136,7 +136,7 @@ I am a big sucker for Data Science and have used Python for most of my time Deve
|
||||
|
||||
Interestingly the IPs from Netherlands are leading the race in trying to connect and authentificate on my VPS.
|
||||
|
||||
### Dynamic
|
||||
#### Dynamic
|
||||
|
||||
A few days later I looked at this and decided I want to integrate it into a Dashboard and have my data shown to me live. In order to do this I rewrote the parsing function so that it tracks the last byte it read in the log file. Now I can automate the syncing of the logs from my VPS and running my log parser in a LXC Container in which I deployed Grafana. Through Grafana I setup JSON Files as a Data Source (also needed to set up a simple python http server to serve the parsed json through REST API) and started creating a Dashboard.
|
||||
|
||||
@@ -144,7 +144,7 @@ A few days later I looked at this and decided I want to integrate it into a Dash
|
||||
|
||||
All in all an interesting short project. One day I will give myself the challenge to optimize the code as much as possible.
|
||||
|
||||
# Post Scriptum
|
||||
## Post Scriptum
|
||||
|
||||
No text, images, code, concepts or ideas were created with Slop Generators[^1]. Although I do not believe that Slop Generators are inherently bad nor do I believe that they cannot be used in a productive matter (as do I in some cases), everything I write on here are things which genuinely interest me and I want to create and work on by myself. I believe a person can only become better at the thing they are doing if they do not constantly search for instant gratification, which is often given to one when using Slop Generators.
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "LaTeX für Anfänger"
|
||||
date: "2026-06-22"
|
||||
description: "Hoste deine eigene Overleaf-Instanz"
|
||||
---
|
||||
|
||||
## Einleitung
|
||||
|
||||
Vor ein paar Monaten habe ich mein Masterstudium in Medizinischer Informatik an der Universität Heidelberg und der Hochschule Heilbronn begonnen. Eine der allerersten Aufgaben ist das Schreiben eines Systematic Reviews zu einem vorgegebenen Special Issue, das wir natürlich als Gruppe schreiben.[^ai]
|
||||
|
||||
<!--more-->
|
||||
|
||||
## LaTeX & co.
|
||||
|
||||
Nahezu jede ernst zu nehmende wissenschaftliche Arbeit wird heutzutage in LaTeX geschrieben, mit ein paar Ausnahmen[^*]. Falls du noch nie damit zu tun hattest: LaTeX ist einfach eine Sprache wie jede andere. Es definiert ein Regelwerk und eine Syntax, an die du dich hältst, um eine erwartete Ausgabe zu erhalten, in diesem Fall eine saubere `pdf`.
|
||||
|
||||
Es gibt zwei Wege, es zu nutzen. So, wie es gedacht war: lokal, auf dem eigenen Rechner, indem man alles selbst kompiliert. Oder der andere Weg: über eine aufgeblähte, teils kostenpflichtige Webanwendung. Und doch ist genau diese Webanwendung, Overleaf, die Art und Weise, wie die meisten Menschen es nutzen, und das aus gutem Grund. Sie bietet Zusammenarbeit in Echtzeit, verständliche Fehlermeldungen, eine zugängliche Dokumentation und ein Dutzend weiterer Annehmlichkeiten. Man sagt gerne, dass *Linux nur dann kostenlos ist, wenn man seine Zeit nicht wertschätzt*, und derselbe Witz gilt für handgemachtes, blankes LaTeX auf der Kommandozeile im Vergleich zu Overleaf. Overleaf ist schlicht einfacher.
|
||||
|
||||
## Was jetzt?
|
||||
|
||||
Universitätsprojekte sind selten Einzelarbeit, was bedeutet, dass man mit Menschen zusammenarbeitet, die aus unterschiedlichen Hintergründen kommen und unterschiedliche Prioritäten haben. Ich komme aus dem Software Engineering, genauer gesagt aus der Softwareentwicklung, und lebe daher tendenziell in einer bestimmten Blase. Zum Kontext: Ich nutze täglich ein MacBook M1 mit Linux, ermöglicht durch ein kleines Team, das Apples Treiber zurückentwickelt, in einem Projekt, das offiziell noch in der Alpha-Phase ist[^1]. Die meisten Menschen wollen, ganz nachvollziehbar, einfach den bequemsten Weg zu einem fertigen Dokument, also haben wir uns darauf geeinigt, Overleaf für unsere Arbeit zu nutzen.
|
||||
|
||||
Es gibt allerdings einen Haken. Zusammenarbeit in Echtzeit erfordert bei Overleaf ein kostenpflichtiges Abo. Ohne dieses muss jeder das Dokument nacheinander statt gleichzeitig bearbeiten, und das war für uns von vornherein keine Option. Wie die meisten Studierendengruppen erledigen wir den Großteil der Arbeit in den letzten zwei Tagen vor der Deadline, alle auf einmal, also genau dann, wenn mehrere Personen gleichzeitig dieselbe Datei bearbeiten müssen. Ein Abo zu kaufen hätte das gelöst, aber es fühlte sich unnötig an, wenn es kostenlose Alternativen gibt. Die erste, die mir in den Sinn kam, war, Git als VCS zu verwenden und die `.tex`-Dateien auf GitHub zu hosten.
|
||||
|
||||
Das bringt allerdings seine eigenen Reibungspunkte mit sich. Nicht jeder arbeitet täglich mit Git über `add`, `commit` und `push` hinaus. (Ich habe selbst erst kürzlich herausgefunden, dass man alte Commits ziemlich bequem umbenennen kann, neben anderen Dingen.) Git entfaltet sein Potenzial außerdem erst dann richtig, wenn sich alle an dieselben Konventionen halten, und eine Handvoll Studierender, die zusammen eine Arbeit schreiben, ist nun einmal ein anderes Umfeld als ein Team von Entwicklern, die den ganzen Tag in Git leben. Keines ist besser oder schlechter, es sind einfach unterschiedliche Kontexte.
|
||||
|
||||
Also, was jetzt? Die Antwort war am Ende wunderbar einfach:
|
||||
|
||||
Overleaf.
|
||||
|
||||
Vor ein paar Tagen habe ich erfahren, dass man die Overleaf Community Edition selbst hosten kann. Sie hat denselben Funktionsumfang wie die gehostete kostenlose Version, jedoch ohne die künstlichen Beschränkungen wie die Obergrenze gleichzeitiger Bearbeiter und die Timeouts beim Kompilieren größerer Dokumente.
|
||||
|
||||
## Zurück zu FOSS
|
||||
|
||||
Das Aufsetzen von Overleaf CE hat mich auf einem VPS, den ich noch herumliegen hatte, insgesamt etwa 20 Minuten gekostet. Overleaf liefert einen vereinfachten Wrapper namens [Toolkit](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit) mit. Nachdem man fünf oder sechs Umgebungsvariablen gesetzt hat, führt man einen einzigen Befehl aus, der eigentlich nur ein Bootstrapper für `docker compose` ist, und schon läuft die Instanz. Wenn du eine Domain besitzt und einen Schritt weiter gehen möchtest, verschafft dir eine minimale Nginx-Reverse-Proxy-Konfiguration plus ein einziger Let's-Encrypt-Befehl eine ordentliche Domain mit TLS.
|
||||
|
||||
Das war's. Kein Abo, keine Begrenzung der Bearbeiter, keine Timeouts, und jeder im Team bekommt die vertraute Overleaf-Oberfläche, die er ohnehin schon kennt.
|
||||
|
||||
## Abschließende Gedanken
|
||||
|
||||
Falls du jemals in einem Team landest, das gemeinsam LaTeX schreiben muss, und du den Git-Weg lieber nicht gehen willst, hoste deine eigene Overleaf-Instanz. Es geht schnell, es ist kostenlos, und es lässt alle auf vertrautem Boden arbeiten.
|
||||
|
||||
Eine letzte Sache: Sichere, was auch immer du hostest. In einem gemeinsamen LaTeX-Projekt steckt viel gemeinsame Arbeit, und es ein paar Tage vor der Deadline zu verlieren, macht niemandem Spaß. Deshalb gilt: immer Backups machen.
|
||||
|
||||
[^ai]: Dieser Beitrag wurde von einer KI aus dem [englischen Original]({{< relref path="/blog/overleaf.md" lang="en" >}}) übersetzt.
|
||||
[^*]: Alternativen: Typst, Word
|
||||
[^1]: [Asahi Linux: Linux auf Apple Silicon, offiziell noch in der Alpha-Phase](https://asahilinux.org/)
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "LaTeX for Beginners"
|
||||
date: "2026-06-22"
|
||||
description: "Host your own Overleaf instance"
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
A few months ago I started my Master's in Medical Informatics at Heidelberg University and Hochschule Heilbronn. One of the very first assignments is writing a Systematic Review on a given special issue, which we of course write as a group.
|
||||
|
||||
<!--more-->
|
||||
|
||||
## LaTeX & co.
|
||||
|
||||
Almost every scientific paper worth taking seriously is written in LaTeX these days, with a few exceptions[^*]. If you have never touched it: LaTeX is just a language like any other. It defines a set of rules and syntax you follow to get an expected output, in this case a clean `pdf`.
|
||||
|
||||
There are two ways to use it. The way it was meant to be used is locally, on your own machine, compiling everything yourself. The other way is through a bloated, partly paywalled web application. And yet that web application, Overleaf, is how most people use it, and honestly for good reason. It gives you real-time collaboration, readable error messages, approachable documentation and a dozen other conveniences. People like to say that *Linux is only free if you do not value your time*, and the same joke applies to hand-rolled, bare-metal, CLI LaTeX versus Overleaf. Overleaf is simply easier.
|
||||
|
||||
## What now?
|
||||
|
||||
University projects are rarely a solo effort, which means working with people who come from different backgrounds and have different priorities. I come from a Software Engineering background, or really Software Development, so I tend to live in a particular bubble. For context, I daily-drive a MacBook M1 running Linux, made possible by a small team reverse-engineering Apple's drivers in a project that is officially still in alpha[^1]. Most people, very reasonably, just want the most convenient path to a finished document, so we agreed to use Overleaf for our paper.
|
||||
|
||||
There is a catch, though. Real-time collaboration on Overleaf requires a paid subscription. Without it everyone has to edit the document one after another instead of at the same time, and that was never going to work for us. Like most student groups, we do the bulk of the work in the last two days before the deadline, all at once, which is exactly when you need several people editing the same file simultaneously. Buying a subscription would have solved this, but it felt unnecessary when there are free alternatives out there. The first one that came to mind was to use Git as our VCS and host the `.tex` files on GitHub.
|
||||
|
||||
That adds friction of its own, though. Not everyone works with git day-to-day beyond `add`, `commit` and `push`. (I only recently found out myself that you can rename old commits quite comfortably, among other things.) Git also only really shines when everyone follows the same conventions, and a handful of students writing one paper together is simply a different setting from a team of devs who live in git all day. Neither is better or worse, they are just different contexts.
|
||||
|
||||
So, what now? The answer turned out to be wonderfully simple:
|
||||
|
||||
Overleaf.
|
||||
|
||||
A few days ago I learned that you can self-host the Overleaf Community Edition. It has the same core features as the hosted free version, without the artificial limitations like the cap on concurrent editors and the compile timeouts on larger documents.
|
||||
|
||||
## Back to FOSS
|
||||
|
||||
Setting up Overleaf CE took me about 20 minutes on a VPS I had lying around. Overleaf ships a simplified wrapper called the [Toolkit](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit). After setting five or six environment variables, you run a single command, which is really just a bootstrapper for `docker compose`, and your instance is up. If you own a Domain and want to go one step further, a minimal Nginx reverse-proxy config plus one Let's Encrypt command gets you a proper domain with TLS.
|
||||
|
||||
That is it. No subscription, no editor limit, no timeouts, and everyone on the team gets the familiar Overleaf interface they already know.
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
If you ever land in a collaborative team that needs to write LaTeX together and you would rather not go the git route, self-host your own Overleaf instance. It is quick, it is free, and it keeps everyone on comfortable ground.
|
||||
|
||||
One last thing: back up whatever you end up hosting. A shared LaTeX project is a lot of collective work, and losing it a few days before the deadline will be no fun for everyone. That is why: always backup.
|
||||
|
||||
[^*]: Alternatives: Typst, Word
|
||||
[^1]: [Asahi Linux: Linux on Apple Silicon, officially still in alpha](https://asahilinux.org/)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Über mich"
|
||||
---
|
||||
|
||||
Hallo, ich bin Dominik — ich begeistere mich für Technologie, Software und Cybersicherheit.
|
||||
|
||||
## Studium
|
||||
|
||||
- Bachelor in **Software Engineering** an der **Hochschule Heilbronn (HHN)**
|
||||
- Aktuell Masterstudium **Medizinische Informatik** an der **Universität Heidelberg** und der **HHN**
|
||||
- Bachelorarbeit über die Anwendung von Machine Learning zur Optimierung von Prozessen im Bereich Cybersicherheit
|
||||
|
||||
## Beruf
|
||||
|
||||
- 1.5 Jahre Erfahrung als Praktikant / Werkstudent bei **Schwarz IT** in einem **Cybersecurity**-Team
|
||||
- Aktuell Werkstudent bei **dmTech** in einem **Platform- & DevOps**-Team
|
||||
|
||||
## Außerdem
|
||||
|
||||
- Natur erkunden, Wandern und Motorradfahren
|
||||
- Bücher über Soziologie, Philosophie und Politik lesen
|
||||
|
||||
---
|
||||
|
||||
## Bücher
|
||||
|
||||
Hier einige meiner Lieblingsbücher, die ich jedem empfehlen kann:
|
||||
|
||||
- [Homo Deus - Yuval Noah Harari](https://search.worldcat.org/title/986950078)
|
||||
- [Ikigai - Ken Mogi](https://search.worldcat.org/title/1357534605)
|
||||
- [Mein Katalonien - George Orwell](https://search.worldcat.org/title/1259326703)
|
||||
- [On Anarchism - Noam Chomsky](https://search.worldcat.org/title/872702854)
|
||||
|
||||
Lese ich gerade:
|
||||
|
||||
- [Nexus - Yuval Noah Harari](https://search.worldcat.org/title/1458312575)
|
||||
- [Atomic Habits - James Clear](https://search.worldcat.org/title/1111781445)
|
||||
|
||||
---
|
||||
|
||||
## PGP-Schlüssel
|
||||
|
||||
Wenn du mir verschlüsselte Nachrichten senden möchtest, nutze den folgenden [PGP-Schlüssel](/keys/publickey.asc). Zum Überprüfen des Fingerprints:
|
||||
|
||||
```
|
||||
896B 3FF1 2E82 A80C 10F6 42CB 4634 761C A7CE E0A8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ressourcen
|
||||
|
||||
### Mach mit
|
||||
|
||||
[Folding@Home](https://foldingathome.org/) - Nutze deine GPU und CPU, um durch die Simulation der Proteinfaltung auf deinem eigenen Gerät zur wissenschaftlichen Forschung beizutragen
|
||||
|
||||
[Internet Archive](https://archive.org/donate) - Hilf dem Internet Archive, eine digitale Bibliothek von Internetseiten und kulturellen Artefakten in digitaler Form aufzubauen und bereitzustellen
|
||||
|
||||
[Help Fight Chat Control](https://fightchatcontrol.eu/) - Wenn du in der EU lebst, schreibe deinem Europaabgeordneten und sprich dich gegen „Chatkontrolle (2.0)“ aus
|
||||
|
||||
### FOSS-Stiftungen
|
||||
|
||||
[Free Software Foundation](https://www.fsf.org/)
|
||||
|
||||
[Mozilla Foundation](https://www.mozillafoundation.org/en/)
|
||||
|
||||
[The Linux Foundation](https://www.linuxfoundation.org)
|
||||
|
||||
[Signal Foundation](https://signalfoundation.org/)
|
||||
@@ -2,19 +2,27 @@
|
||||
title: "Whoami"
|
||||
---
|
||||
|
||||
Hi, I’m Dominik
|
||||
Hi, I’m Dominik — passionate about technology, software, and cybersecurity.
|
||||
|
||||
I’m passionate about technology, software, and cybersecurity.
|
||||
## Studies
|
||||
|
||||
- Studied **Software Engineering** at **Heilbronn University (HHN)**
|
||||
- Worked at **Schwarz IT** in a **Cybersecurity** team during my studies
|
||||
- Wrote my Bachelors Thesis about the application of Machine Learning to optimise distinct processes in the Cybersecurity world
|
||||
- Love exploring nature, going on hiking trips as well as driving my motorcycle
|
||||
- Love reading books about sociology, philosophy and politics
|
||||
- Bachelor’s Degree in **Software Engineering** at **Heilbronn University (HHN)**
|
||||
- Currently studying a Master’s in **Medical Informatics** at **Heidelberg University** and **HHN**
|
||||
- Bachelor’s Thesis on applying Machine Learning to optimise processes in the cybersecurity world
|
||||
|
||||
## Work
|
||||
|
||||
- 1.5 years of intern / working-student experience at **Schwarz IT** in a **Cybersecurity** team
|
||||
- Currently a working student at **dmTech** in a **Platform & DevOps** team
|
||||
|
||||
## Beyond work
|
||||
|
||||
- Exploring nature, going on hiking trips, and riding my motorcycle
|
||||
- Reading books about sociology, philosophy and politics
|
||||
|
||||
---
|
||||
|
||||
# Books
|
||||
## Books
|
||||
|
||||
Here are some of my favourite books I would recommend to anyone:
|
||||
|
||||
@@ -30,19 +38,19 @@ Reading right now:
|
||||
|
||||
---
|
||||
|
||||
# PGP Key
|
||||
## PGP Key
|
||||
|
||||
If you want to send me encrypted messages use the following [PGP-Key](/keys/publickey.asc). If you want to check the fingerprint:
|
||||
|
||||
```
|
||||
896B 3FF1 2E82 A80C 10F6 42CB 4634 761C A7CE E0A
|
||||
896B 3FF1 2E82 A80C 10F6 42CB 4634 761C A7CE E0A8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Resources
|
||||
## Resources
|
||||
|
||||
## Do this
|
||||
### Do this
|
||||
|
||||
[Folding@Home](https://foldingathome.org/) - Use your GPU and CPU to contribute to scientifical research by simulating the folding of proteins on your own device
|
||||
|
||||
@@ -50,7 +58,7 @@ If you want to send me encrypted messages use the following [PGP-Key](/keys/publ
|
||||
|
||||
[Help Fight Chat Control](https://fightchatcontrol.eu/) - If you live in the EU message your local MEP to oppose "Chat Control (2.0)"
|
||||
|
||||
## FOSS Foundations
|
||||
### FOSS Foundations
|
||||
|
||||
[Free Software Foundation](https://www.fsf.org/)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
baseURL = 'https://agres.online/'
|
||||
languageCode = 'en-us'
|
||||
title = 'agres.online'
|
||||
defaultContentLanguage = 'en'
|
||||
|
||||
[params]
|
||||
name = "Dominik Agreš"
|
||||
@@ -53,8 +54,8 @@ title = 'agres.online'
|
||||
guessSyntax = false
|
||||
lineNos = false
|
||||
lineNumbersInTable = true
|
||||
noClasses = false
|
||||
style = "dracula"
|
||||
noClasses = true
|
||||
style = "solarized-light"
|
||||
[markup.goldmark]
|
||||
[markup.goldmark.extensions]
|
||||
footnote = true
|
||||
@@ -66,12 +67,14 @@ title = 'agres.online'
|
||||
weight = 2
|
||||
[languages.de.params]
|
||||
reading_speed = 179
|
||||
greeting = "Willkommen"
|
||||
[languages.en]
|
||||
languageCode = 'en-US'
|
||||
languageName = 'English'
|
||||
weight = 1
|
||||
[languages.en.params]
|
||||
reading_speed = 228
|
||||
greeting = "Welcome"
|
||||
|
||||
[outputs]
|
||||
home = ["HTML"]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[nav_about]
|
||||
other = "Über mich"
|
||||
|
||||
[hero_tagline]
|
||||
other = "Medizinische Informatik · Platform & DevOps · Security"
|
||||
|
||||
[latest_writing]
|
||||
other = "Neueste Beiträge"
|
||||
|
||||
[all_posts]
|
||||
other = "alle Beiträge →"
|
||||
|
||||
[date_published]
|
||||
other = "Veröffentlicht"
|
||||
|
||||
[read_time]
|
||||
other = "Lesezeit"
|
||||
|
||||
[minutes]
|
||||
other = "Min"
|
||||
@@ -0,0 +1,20 @@
|
||||
[nav_about]
|
||||
other = "Whoami"
|
||||
|
||||
[hero_tagline]
|
||||
other = "Medical Informatics · Platform & DevOps · Security"
|
||||
|
||||
[latest_writing]
|
||||
other = "Latest Writing"
|
||||
|
||||
[all_posts]
|
||||
other = "all posts →"
|
||||
|
||||
[date_published]
|
||||
other = "Date Published"
|
||||
|
||||
[read_time]
|
||||
other = "Read time"
|
||||
|
||||
[minutes]
|
||||
other = "min"
|
||||
@@ -1,4 +0,0 @@
|
||||
{{- $level := .Level -}}
|
||||
<h{{ $level }} id="{{ .Anchor | safeURL }}">
|
||||
{{ .Text | safeHTML }} <em>{{ range seq $level }}#{{ end }}</em>
|
||||
</h{{ $level }}><br>
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="infobox">
|
||||
<div class="infobox-content">
|
||||
<span class="infobox-label">Date Published:</span>
|
||||
<span class="infobox-value">{{ .Date | time.Format "January 2, 2006" }}</span>
|
||||
<span class="infobox-label">{{ i18n "date_published" }}:</span>
|
||||
<span class="infobox-value">{{ .Date | time.Format ":date_long" }}</span>
|
||||
</div>
|
||||
<div class="infobox-content">
|
||||
<span class="infobox-label">Read time: </span>
|
||||
<span class="infobox-value">{{ printf " %d minute(s)" .ReadingTime }}</span>
|
||||
<span class="infobox-label">{{ i18n "read_time" }}:</span>
|
||||
<span class="infobox-value">~{{ .ReadingTime }} {{ i18n "minutes" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-content">
|
||||
<em>
|
||||
<a href="{{ "/" | relURL }}" class="site-title">
|
||||
{{ .Site.Title }}
|
||||
</a>
|
||||
</em>
|
||||
<a href="{{ "/" | relLangURL }}" class="site-title">{{ .Site.Title }}</a>
|
||||
<nav class="nav-links">
|
||||
<a href="{{ "blog/" | relURL }}">~/blog</a>
|
||||
<a href="{{ "whoami/" | relURL }}">~/whoami</a>
|
||||
<a href="{{ "blog/" | relLangURL }}">Blog</a>
|
||||
<a href="{{ "whoami/" | relLangURL }}">{{ i18n "nav_about" }}</a>
|
||||
{{ with .Translations }}
|
||||
{{ range . }}<a class="lang-switch" href="{{ .RelPermalink }}">{{ .Language.Lang | upper }}</a>{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<style>
|
||||
#typedText::after {
|
||||
content: "|";
|
||||
animation: blink 1s infinite;
|
||||
font-weight: bolder;
|
||||
}
|
||||
@keyframes blink {
|
||||
60% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const text = "{{ .Site.Params.Greeting }}";
|
||||
const element = document.getElementById("typedText");
|
||||
let index = 0;
|
||||
|
||||
function type() {
|
||||
if (index < text.length) {
|
||||
element.textContent += text.charAt(index);
|
||||
index++;
|
||||
setTimeout(type, 256);
|
||||
}
|
||||
}
|
||||
|
||||
type();
|
||||
});
|
||||
</script>
|
||||
@@ -1,12 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ .Site.Language.Lang }}">
|
||||
|
||||
<head>
|
||||
<link rel="preconnect" href="https://rsms.me/">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Site.Params.browserTitle }} :: {{ .Title }}</title>
|
||||
{{ with .Description }}<meta name="description" content="{{ . }}">{{ end }}
|
||||
<link rel="canonical" href="{{ .Permalink }}">
|
||||
{{ range .AllTranslations }}<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
|
||||
{{ end }}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/lora-400.woff2" crossorigin>
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/lora-600.woff2" crossorigin>
|
||||
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
{{ define "main" }}
|
||||
|
||||
{{ partial "typed.html" . }}
|
||||
|
||||
<div class="landing-welcome">
|
||||
<h1 id="typedText"></h1>
|
||||
<section class="landing">
|
||||
<h1 class="landing-name">{{ .Site.Params.name }}</h1>
|
||||
<p class="landing-tagline">{{ i18n "hero_tagline" }}</p>
|
||||
<div class="landing-links">
|
||||
{{ range .Site.Params.socials }}
|
||||
<a href="{{ .url }}" target="_blank">{{ .name }}</a>
|
||||
<a href="{{ .url }}" target="_blank" rel="noopener">{{ .name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ with .Site.GetPage "/blog" }}
|
||||
{{ with first 4 .Pages }}
|
||||
<div class="landing-latest">
|
||||
<h2 class="latest-heading">{{ i18n "latest_writing" }}</h2>
|
||||
<ul class="latest-list">
|
||||
{{ range . }}
|
||||
<li>
|
||||
<a class="latest-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
||||
<span class="latest-date">{{ .Date | time.Format ":date_medium" }}</span>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<a class="latest-all" href="{{ "blog/" | relLangURL }}">{{ i18n "all_posts" }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</section>
|
||||
|
||||
{{ end }}
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
<h1>{{ .Title }}</h1>
|
||||
|
||||
<ul>
|
||||
<ul class="postlist">
|
||||
{{ range .Pages }}
|
||||
<div class="blogpost">
|
||||
<div class="blogpost-content">
|
||||
<a class="blogpost-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
||||
<span class="blogpost-date">{{ .Date | time.Format "January 2, 2006" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="blogpost-content">
|
||||
<span>{{ .Description }}</span>
|
||||
</div>
|
||||
|
||||
<li>
|
||||
<div class="post-head">
|
||||
<a class="post-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
||||
<span class="post-date">{{ .Date | time.Format ":date_medium" }} · ~{{ .ReadingTime }} {{ i18n "minutes" }}</span>
|
||||
</div>
|
||||
{{ with .Description }}<p class="post-desc">{{ . }}</p>{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,343 +1,345 @@
|
||||
/* ---------- Base ---------- */
|
||||
:root {
|
||||
--color-bg: #13171D;
|
||||
--color-fg: #C3CBCB;
|
||||
--color-accent: #181E25;
|
||||
--color-secondary: #E3511E;
|
||||
--light-yellow: color(display-p3 1 1 0.7)
|
||||
/* ===========================================================
|
||||
agres.online — light, warm, editorial serif
|
||||
palette: cream paper · soft ink · ochre / olive accents
|
||||
=========================================================== */
|
||||
|
||||
--link: #fff;
|
||||
--link-hover: #E45420;
|
||||
|
||||
--font-base: 'Inter', sans-serif;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-bold: 600;
|
||||
|
||||
--max-width: 70ch;
|
||||
--padding-page: 1rem;
|
||||
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
font-size: 12pt;
|
||||
--font-size-title: 20pt;
|
||||
/* ---------- Fonts (self-hosted Lora — GDPR-friendly) ---------- */
|
||||
@font-face {
|
||||
font-family: 'Lora';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/lora-400.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lora';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/lora-400-italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lora';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/lora-600.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lora';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/lora-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: var(--padding-page);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-fg);
|
||||
font-family: var(--font-base);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: 1.6;
|
||||
max-width: var(--max-width);
|
||||
margin-inline: auto;
|
||||
/* ---------- Tokens ---------- */
|
||||
:root {
|
||||
--bg: #f4f1ea; /* cream paper */
|
||||
--surface: #ece7db; /* slightly deeper cream for boxes/code */
|
||||
--text: #2b2a27; /* soft ink */
|
||||
--dim: #6b6760; /* muted grey for meta */
|
||||
--rule: #d8d2c4; /* hairlines */
|
||||
--link: #8a6a2e; /* deeper ochre — readable on cream */
|
||||
--link-hover:#a98b3d; /* ochre */
|
||||
--olive: #6f7148; /* secondary accent */
|
||||
|
||||
--font-serif: 'Lora', Georgia, 'Times New Roman', serif;
|
||||
--font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
|
||||
|
||||
--measure: 68ch;
|
||||
--pad: 1.25rem;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* ---------- Base ---------- */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--pad);
|
||||
max-width: var(--measure);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
line-height: 1.72;
|
||||
font-feature-settings: 'liga' 1, 'calt' 1, 'onum' 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* ---------- Typography ---------- */
|
||||
main h1, main h2, main h3, main h4 {
|
||||
color: var(--color-fg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1.8;
|
||||
display: inline;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin: 2.2rem 0 0.8rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
main h1 { font-size: 1.9rem; font-weight: 700; }
|
||||
main h2 { font-size: 1.5rem; }
|
||||
main h3 { font-size: 1.25rem; }
|
||||
main h4 { font-size: 1.1rem; }
|
||||
|
||||
main h1 { font-size: 1.8rem; }
|
||||
main h2 { font-size: 1.6rem; }
|
||||
main h3 { font-size: 1.4rem; }
|
||||
main h4 { font-size: 1.2rem; }
|
||||
p { margin: 1rem 0; }
|
||||
|
||||
em { font-style: italic; }
|
||||
strong { font-weight: 700; }
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: var(--rule);
|
||||
transition: color 0.12s ease, text-decoration-color 0.12s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-hover);
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
margin: 1rem 0;
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid var(--color-secondary);
|
||||
border-top: 1px solid var(--rule);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
/* ---------- Top Bar ---------- */
|
||||
.topbar {
|
||||
background: var(--color-bg);
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 3px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.topbar-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 18pt;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-title:hover {
|
||||
color:var(--link-hover);
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--link);
|
||||
margin-left: 1rem;
|
||||
text-decoration: none;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color:var(--link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---------- Bottom Bar ---------- */
|
||||
.bottombar {
|
||||
background: var(--color-bg);
|
||||
width: 100%;
|
||||
border-top: 3px solid var(--color-secondary);
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottombar-content {
|
||||
max-width: 900px; /* same as .topbar-content */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bottombar a {
|
||||
color: var(--link);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.bottombar a:hover {
|
||||
color: var(--link-hover);
|
||||
}
|
||||
|
||||
/* ---------- Content ---------- */
|
||||
main {
|
||||
padding-left: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-bottom: 2rem;
|
||||
blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.25rem 0 0.25rem 1.2rem;
|
||||
border-left: 3px solid var(--olive);
|
||||
color: var(--dim);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
padding-left: 0.1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.01em;
|
||||
}
|
||||
|
||||
ul li::before {
|
||||
content: "• ";
|
||||
padding-left: 1.4rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
li { margin-bottom: 0.3rem; }
|
||||
li::marker { color: var(--olive); }
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1.5rem auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.footnotes ol {
|
||||
list-style: decimal;
|
||||
margin-left: 1rem;
|
||||
.footnotes {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-size: 0.9rem;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
/* ---------- Infobox Styling ---------- */
|
||||
|
||||
.infobox {
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
padding-left: 30px;
|
||||
background-color: var(--color-accent);
|
||||
border: 1.5px solid;
|
||||
border-color: var(--color-fg);
|
||||
border-radius: 10px;
|
||||
/* ---------- Layout ---------- */
|
||||
main {
|
||||
margin: 3rem 0 4rem;
|
||||
}
|
||||
article { margin-bottom: 2rem; }
|
||||
|
||||
.infobox-content {
|
||||
display: flex;
|
||||
margin: 10px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 150px 1fr;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.infobox-label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.infobox-value {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ---------- Blogpost box Styling ---------- */
|
||||
|
||||
.blogpost {
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
padding-left: 15px;
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.blogpost-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.blogpost-title {
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.blogpost-date {
|
||||
font-style: italic;
|
||||
font-size: 14pt;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* ---------- Responsive Adjustments ---------- */
|
||||
@media (max-width: 600px) {
|
||||
|
||||
/* ---------- Top bar ---------- */
|
||||
.topbar {
|
||||
width: 90%;
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 1.4rem 0 1.1rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.topbar-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 0.1rem;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-links { white-space: nowrap; }
|
||||
.site-title {
|
||||
font-size: 14pt;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-title:hover { color: var(--link-hover); }
|
||||
|
||||
.nav-links a {
|
||||
font-size: 12pt;
|
||||
margin-left: 0.5rem;
|
||||
color: var(--dim);
|
||||
margin-left: 1.4rem;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.nav-links a:hover { color: var(--link-hover); }
|
||||
.nav-links a.lang-switch {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--olive);
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
/* ---------- Bottom bar ---------- */
|
||||
.bottombar {
|
||||
padding: 0.75rem 0rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
padding: 1.4rem 0 2rem;
|
||||
text-align: center;
|
||||
color: var(--dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.bottombar-content {
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
.bottombar a {
|
||||
color: var(--dim);
|
||||
text-decoration: none;
|
||||
margin: 0 0.35rem;
|
||||
}
|
||||
.bottombar a:hover { color: var(--link-hover); }
|
||||
.copyright { margin-top: 0.5rem; }
|
||||
|
||||
body {
|
||||
padding: 0.5rem;
|
||||
/* ---------- Landing ---------- */
|
||||
.landing { margin-top: 1.5rem; }
|
||||
.landing-name {
|
||||
font-size: 2.6rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.4rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-left: 0;
|
||||
margin-top: 1rem;
|
||||
.landing-tagline {
|
||||
color: var(--dim);
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Codeblock Styling ---------- */
|
||||
pre,
|
||||
pre.chroma {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-fg);
|
||||
padding: 1em;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-fg);
|
||||
padding: 0.15em 0.35em;
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chroma .k { color: #ff9349; }
|
||||
.chroma .s { color: #b8e3b7; }
|
||||
.chroma .c { color: #737983; font-style: italic; }
|
||||
.chroma .m { color: #eacaa6; }
|
||||
.chroma .o { color: #e1e5ec; }
|
||||
.chroma .kt { color: #9bb8ff; }
|
||||
|
||||
|
||||
/* ---------- Landing Page ---------- */
|
||||
|
||||
.landing-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 55vh;
|
||||
}
|
||||
|
||||
.landing-welcome h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.landing-links {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-size: 1.1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.landing-links a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.landing-links a:hover { color: var(--link-hover); }
|
||||
|
||||
/* latest writing */
|
||||
.landing-latest { margin-top: 3.5rem; }
|
||||
.latest-heading {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text);
|
||||
margin: 0 0 0.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.latest-list { list-style: none; padding: 0; margin: 0; }
|
||||
.latest-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.latest-title { text-decoration: none; color: var(--text); }
|
||||
.latest-title:hover { color: var(--link-hover); }
|
||||
.latest-date {
|
||||
flex: none;
|
||||
color: var(--dim);
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.latest-all {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.latest-all:hover { color: var(--link-hover); }
|
||||
|
||||
/* ---------- Blog list ---------- */
|
||||
.postlist { list-style: none; padding: 0; margin: 2rem 0 0; }
|
||||
.postlist li {
|
||||
margin: 0;
|
||||
padding: 1.4rem 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.postlist li:last-child { border-bottom: 1px solid var(--rule); }
|
||||
|
||||
.post-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
}
|
||||
.post-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.post-title:hover { color: var(--link-hover); }
|
||||
.post-date {
|
||||
flex: none;
|
||||
color: var(--dim);
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.post-desc {
|
||||
margin: 0.4rem 0 0;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.landing-links a:hover {
|
||||
color: var(--color-secondary);
|
||||
/* ---------- Infobox (post meta) ---------- */
|
||||
.infobox {
|
||||
margin: 0 0 2rem;
|
||||
padding: 0.9rem 1.1rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.infobox-content {
|
||||
display: grid;
|
||||
grid-template-columns: 130px 1fr;
|
||||
row-gap: 4px;
|
||||
}
|
||||
.infobox-label { color: var(--dim); }
|
||||
.infobox-value { font-style: italic; }
|
||||
|
||||
/* ---------- Code ---------- */
|
||||
pre, pre.chroma {
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
code { font-family: var(--font-mono); }
|
||||
|
||||
:not(pre) > code {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--rule);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* ---------- LLM Prompt Injection ---------- */
|
||||
|
||||
.info {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max-width: 600px) {
|
||||
:root { font-size: 17px; }
|
||||
main { margin: 2rem 0 3rem; }
|
||||
.post-head { flex-direction: column; gap: 0.2rem; }
|
||||
.landing-name { font-size: 2.1rem; }
|
||||
.nav-links a { margin-left: 0.9rem; }
|
||||
.nav-links a.lang-switch { margin-left: 0.7rem; padding-left: 0.7rem; }
|
||||
}
|
||||
|
||||
/* ---------- LLM prompt-injection bait (kept hidden) ---------- */
|
||||
.info { display: none; visibility: hidden; }
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#f4f1ea"/>
|
||||
<text x="16" y="23.5" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="27" font-weight="700" fill="#8a6a2e">a</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1 @@
|
||||
google-site-verification: googleeb4005177cc2703c.html
|
||||
@@ -18,24 +18,34 @@ xleiRfKG7zC6j5P74jkCmcMe/fuNh8tOxTvrwUr+oqh09h+MSveWmg7rCkEbfkQ3
|
||||
f7UlTqsuMIPgTh8e4anZWjJNBF1/rIiuYwYCTKLNYnCKRxhR3qIqkXQWd5QB/1lH
|
||||
Ln8Lm5mGWA970C/tfXCP8kKvkRdkHpQuqc1rt96HCwSZls7f1Vxrsh43edLhRF6t
|
||||
RLHlIIX6GNqfmtbMJk/JwTRwYNbYqA6di1u2F2ANbhQscUnpRwarIAAG//U+yxeH
|
||||
hhvlgSo+E8S5q5nFHaaSOYWDRHERMo98PXaUamDw6RxqFeE3MbNLMtSguQGNBGjR
|
||||
vo8BDACy1cn2TWTAdDAt9TDymvihsyXSHHmVzlRLs0+KVG1PkRSJ7GThEqx563jn
|
||||
AilMCNXnayDihl/x4PxEWybp/6u8xKBUXH65B4HwjxRUl9pTHGiQUbOpt/qkMQZl
|
||||
vTDcWqCcWAzX12xN41WZetxnNpPyx1VYOJx6ZUwqdwmY5ZcPzFFqC0094+HdrGrY
|
||||
Gx22G62dzQpxrNBWGRFfDNJ8DNPsoMEK9evut8tszy+jMgQGYWj7AtmHnvA+2YAW
|
||||
509y83pLlkTArzD0h8BFxYvjIWfNNa1j3o+ddpa5TLOGo+CBuNqKCZPPXIrDwcSF
|
||||
+bpbhPyRM+C3HKupfkzFDzv+eIEb5SP7UoGmGktIEbXoHB93x/ZQbXSI9h8RLYW3
|
||||
hZRkF5STlR88ZkLTeGMM/KhEbzJshgLJv2cNpmAJnAxsH+3G/8X+sqWdzYsAN3Ab
|
||||
5cz2yAy48fk8FEtneOQKQW7OnCI946N83fm1QAfC4TogBowUiWir3NgfOD2bK/+N
|
||||
hGOUyLcAEQEAAYkBtgQYAQgAIBYhBIlrP/EugqgMEPZCy0Y0dhynzuCoBQJo0b6P
|
||||
AhsMAAoJEEY0dhynzuCoHeQL/jYU9kalwUTvQWJkB4taDqodHzILBUmIb1y8259+
|
||||
qLenrwUik5Eyrgwn472CNenc3tecxUmXpMeAscoZP93NGFDmUqBXiEFVFNklWVCk
|
||||
k4w+IwCvnjPa+Y6XWhYTY57Qjcb0J5Bs25ezf/o1PTJdyHXvMPBcE0v45tSvgnCz
|
||||
yHq9UD4w6jZgzLxmsEzrptJygIfNsRmCpemW4fAwT04bgth452ahenQWsBV/7413
|
||||
nOkoa1T8puBczVQKSEa6HHg3zV8iJrq3jSVMfyHZy8KFpuf1IIVV6RqX/YoZEmpY
|
||||
KOi5W8FbHfoJmLhbJhW8lbA4nzY2HXgpt4xtQgOFHcPQj2zAWjQBcAFm5LomDiFX
|
||||
R/yDpkn9Y3mHN6iBpTekdnPsS1dfU7rBzrIA7vv7N49pa/EvBB8bSBtbxdMtB2nZ
|
||||
RgF2upc1JqPyNO7jPMUeiJBQnr4qKWm99ZHXnk/pLCsncKiPMuU9EqgJDM/gb7VW
|
||||
HK+dEsiN2r/KxgpAych6xtyuxQ==
|
||||
=OLgg
|
||||
hhvlgSo+E8S5q5nFHaaSOYWDRHERMo98PXaUamDw6RxqFeE3MbNLMtSgtCREb21p
|
||||
bmlrIEFncmVzIDxjb250YWN0QGFncmVzLm9ubGluZT6JAdEEEwEKADsWIQSJaz/x
|
||||
LoKoDBD2QstGNHYcp87gqAUCajkYMgIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe
|
||||
BwIXgAAKCRBGNHYcp87gqOMeC/9cT0Dhm9CXAZS/aEG3fX1wlIunrGj2iirSHwXC
|
||||
rHHPXtwJnZ74gmAzxc83mhxZJ2WinfWPkWD+WYrcFrZvTDiUsLGvROIThYIqLq9p
|
||||
UJZTZLr0hsGxHy544S0H8tfuOFLCSzb2CHry5ZthzzNWhclIk1T7mcEXmTDc1acf
|
||||
O49F/2cqRYhxYAK8R9DQe1osCOQF8gKkJeGxfGdc6SYfctWHLeimhe8q++gZ1Uu9
|
||||
3YcXUxhVT5Xd0lwIqzN1buIMOrlq5uK5bGD1R1xm192GXV7gmd3nkAAvo9E+WiDE
|
||||
7x53cp8CHog+qjuH12/HQhf29VkoS08k2PtHJup6GYccX3e4sW0prmhdXVxGlG2g
|
||||
LKp1qdSFwxP3FIWuNLumSdG2Z1h7v8fvVeu50zqVpCncp99/2bNAcaHJcv5pTClZ
|
||||
p8Pk+jxUf+y5aEgtVSJMvB87EWG/6baxgVV1gpwixsiK1VZJGPzR4kPADbLJwhsG
|
||||
OwQ3M9u1boE7c7/bh2uCrVcSn4K5AY0EaNG+jwEMALLVyfZNZMB0MC31MPKa+KGz
|
||||
JdIceZXOVEuzT4pUbU+RFInsZOESrHnreOcCKUwI1edrIOKGX/Hg/ERbJun/q7zE
|
||||
oFRcfrkHgfCPFFSX2lMcaJBRs6m3+qQxBmW9MNxaoJxYDNfXbE3jVZl63Gc2k/LH
|
||||
VVg4nHplTCp3CZjllw/MUWoLTT3j4d2satgbHbYbrZ3NCnGs0FYZEV8M0nwM0+yg
|
||||
wQr16+63y2zPL6MyBAZhaPsC2Yee8D7ZgBbnT3LzekuWRMCvMPSHwEXFi+MhZ801
|
||||
rWPej512lrlMs4aj4IG42ooJk89cisPBxIX5uluE/JEz4Lccq6l+TMUPO/54gRvl
|
||||
I/tSgaYaS0gRtegcH3fH9lBtdIj2HxEthbeFlGQXlJOVHzxmQtN4Ywz8qERvMmyG
|
||||
Asm/Zw2mYAmcDGwf7cb/xf6ypZ3NiwA3cBvlzPbIDLjx+TwUS2d45ApBbs6cIj3j
|
||||
o3zd+bVAB8LhOiAGjBSJaKvc2B84PZsr/42EY5TItwARAQABiQG2BBgBCAAgFiEE
|
||||
iWs/8S6CqAwQ9kLLRjR2HKfO4KgFAmjRvo8CGwwACgkQRjR2HKfO4Kgd5Av+NhT2
|
||||
RqXBRO9BYmQHi1oOqh0fMgsFSYhvXLzbn36ot6evBSKTkTKuDCfjvYI16dze15zF
|
||||
SZekx4Cxyhk/3c0YUOZSoFeIQVUU2SVZUKSTjD4jAK+eM9r5jpdaFhNjntCNxvQn
|
||||
kGzbl7N/+jU9Ml3Ide8w8FwTS/jm1K+CcLPIer1QPjDqNmDMvGawTOum0nKAh82x
|
||||
GYKl6Zbh8DBPThuC2HjnZqF6dBawFX/vjXec6ShrVPym4FzNVApIRroceDfNXyIm
|
||||
ureNJUx/IdnLwoWm5/UghVXpGpf9ihkSalgo6LlbwVsd+gmYuFsmFbyVsDifNjYd
|
||||
eCm3jG1CA4Udw9CPbMBaNAFwAWbkuiYOIVdH/IOmSf1jeYc3qIGlN6R2c+xLV19T
|
||||
usHOsgDu+/s3j2lr8S8EHxtIG1vF0y0HadlGAXa6lzUmo/I07uM8xR6IkFCeviop
|
||||
ab31kdeeT+ksKydwqI8y5T0SqAkMz+BvtVYcr50SyI3av8rGCkDJyHrG3K7F
|
||||
=6zxY
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||