Compare commits
70 Commits
e7c7a26df0
...
main
Author | SHA1 | Date | |
---|---|---|---|
858b18d33e | |||
568eeed18e | |||
4167963ce3 | |||
7345f0ebc4 | |||
fa78ccec80 | |||
e4e432790c | |||
2eb833436e | |||
f14e379996 | |||
a02e3eb569 | |||
664b195cf3 | |||
c08ae199f9 | |||
e1ed459c59 | |||
2f7506fbad | |||
416a9be424 | |||
8f71737822 | |||
2d2e1866fa | |||
16052c6218 | |||
682a999cdb | |||
d7c75324b3 | |||
3fd639bd9a | |||
8afbd539de | |||
c186102be1 | |||
317de5e09a | |||
275426d7a3 | |||
503b7b134f | |||
2e8e9f5f93 | |||
fd71ea2590 | |||
c2df6c42aa | |||
a9564ba3b2 | |||
c4b54a375c | |||
2b404604f2 | |||
044713c8a0 | |||
03afdaa60d | |||
495f60003e | |||
9695785e5c | |||
00b49683fd | |||
19b0aad18e | |||
98aed49586 | |||
6e1c5bc28e | |||
3f2e0cfdd8 | |||
a390110d99 | |||
778deb5fc6 | |||
01cd37f30a | |||
ac51f7e6c4 | |||
0fe51c2d8b | |||
cda4e76a0a | |||
4ff291d3aa | |||
7092b10849 | |||
b5118b2835 | |||
6eefaf6b90 | |||
9f8d57aa62 | |||
548e25d814 | |||
830c120e54 | |||
9e1a2dfce4 | |||
e3d4fd1719 | |||
9d52cab006 | |||
c9a1cb2a46 | |||
650e61d07d | |||
4c592a0dfc | |||
75bfe523e8 | |||
5d919a51f6 | |||
96ccdaac05 | |||
63cec178e5 | |||
de9708d049 | |||
fa665929d7 | |||
4c96940da1 | |||
f74d74f489 | |||
2848173525 | |||
dbd7893655 | |||
ca1135f0f7 |
41
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||||
|
{
|
||||||
|
"name": "Existing Docker Compose (Extend)",
|
||||||
|
|
||||||
|
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
|
||||||
|
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
|
||||||
|
"dockerComposeFile": [
|
||||||
|
"../docker-compose.yml",
|
||||||
|
"docker-compose.yml"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The 'service' property is the name of the service for the container that VS Code should
|
||||||
|
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
|
||||||
|
"service": "server",
|
||||||
|
|
||||||
|
// The optional 'workspaceFolder' property is the path VS Code should open by default when
|
||||||
|
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.>
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Uncomment the next line if you want start specific services in your Docker Compose config.
|
||||||
|
// "runServices": [],
|
||||||
|
|
||||||
|
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
||||||
|
// "shutdownAction": "none",
|
||||||
|
|
||||||
|
// Uncomment the next line to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "cat /etc/os-release",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "devcontainer"
|
||||||
|
}
|
26
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||||
|
server:
|
||||||
|
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||||
|
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||||
|
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||||
|
# array). The sample below assumes your primary file is in the root of your project.
|
||||||
|
#
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: .devcontainer/Dockerfile
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Update this to wherever you want VS Code to mount the folder of your project
|
||||||
|
- ..:/workspaces:cached
|
||||||
|
|
||||||
|
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_PTRACE
|
||||||
|
# security_opt:
|
||||||
|
# - seccomp:unconfined
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
# command: sleep infinity
|
||||||
|
|
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: "{{ replace .Name "-" " " | title }}"
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
desription:
|
||||||
date: {{ .Date }}
|
date: {{ .Date }}
|
||||||
draft: true
|
draft: false
|
||||||
|
ShowLastmod: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
12
archetypes/posts.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
description:
|
||||||
|
date: {{ .Date }}
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
@ -4,12 +4,97 @@ $text: hsl(204, 28%, 93%);
|
|||||||
$light-grey: #292e32; // #494f5c;
|
$light-grey: #292e32; // #494f5c;
|
||||||
$dark-grey: #3B3E48;
|
$dark-grey: #3B3E48;
|
||||||
$highlight-grey: #7d828a;
|
$highlight-grey: #7d828a;
|
||||||
$midnightblue: #2c3e50;
|
|
||||||
$typewriter: hsl(172, 100%, 36%);
|
$typewriter: hsl(172, 100%, 36%);
|
||||||
|
$codebackground: #1c2023 !default;
|
||||||
|
$midnightblue: $codebackground;
|
||||||
|
|
||||||
|
// Scroll to Top Default colors
|
||||||
|
|
||||||
|
$stt-stroke:#CCC;
|
||||||
|
$stt-circle:#3b3e48;
|
||||||
|
$stt-arrow:#018574;
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
color: inherit;
|
||||||
|
background-color: $midnightblue;
|
||||||
|
}
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
$fonts: "IBM Plex Sans Light", "Trebuchet MS", Verdana, "Verdana Ref", "Segoe UI", Candara, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif;
|
$fonts: "IBM Plex Sans Light", "Segoe UI", Candara, sans-serif;
|
||||||
$code-fonts: Consolas, "Andale Mono WT", "Andale Mono", Menlo, Monaco, "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, "YaHei Consolas Hybrid", monospace, "Segoe UI Emoji", "PingFang SC", "Microsoft YaHei";
|
$code-fonts: "IBM Plex Mono Light", Consolas, "Andale Mono WT", "Andale Mono", Menlo, Monaco, monospace;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans Light';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('IBM Plex Sans Light'),
|
||||||
|
local('IBMPlexSans-Light'),
|
||||||
|
url('/fonts/IBMPlexSans-Light.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans Light';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('IBM Plex Sans Light Italic'),
|
||||||
|
local('IBMPlexSans-LightItalic'),
|
||||||
|
url('/fonts/IBMPlexSans-LightItalic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans Light';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('IBM Plex Sans Bold'),
|
||||||
|
local('IBMPlexSans-Bold'),
|
||||||
|
url('/fonts/IBMPlexSans-Bold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans Light';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('IBM Plex Sans Bold Italic'),
|
||||||
|
local('IBMPlexSans-BoldItalic'),
|
||||||
|
url('/fonts/IBMPlexSans-BoldItalic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono Light';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('IBM Plex Mono Light'),
|
||||||
|
local('IBMPlexMono-Light'),
|
||||||
|
url('/fonts/IBMPlexMono-Light.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono Light';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('IBM Plex Mono Light Italic'),
|
||||||
|
local('IBMPlexMono-LightItalic'),
|
||||||
|
url('/fonts/IBMPlexMono-LightItalic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono Light';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('IBM Plex Mono Medm'),
|
||||||
|
local('IBMPlexMono-Medm'),
|
||||||
|
url('/fonts/IBMPlexMono-Medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono Light';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('IBM Plex Mono Medm Italic'),
|
||||||
|
local('IBMPlexMono-MedmItalic'),
|
||||||
|
url('/fonts/IBMPlexMono-MediumItalic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
//Admonition
|
//Admonition
|
||||||
$admonition-background: (
|
$admonition-background: (
|
||||||
@ -51,4 +136,6 @@ $admonition-color: (
|
|||||||
box-shadow: inset 0 -1em 0 $theme;
|
box-shadow: inset 0 -1em 0 $theme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
28
config.toml
@ -13,9 +13,13 @@ pygmentsUseClasses = true
|
|||||||
|
|
||||||
rssLimit = 10
|
rssLimit = 10
|
||||||
|
|
||||||
copyright = "2024 Daniel Kraus"
|
copyright = "2024-2025 Daniel Kraus"
|
||||||
enableEmoji = true
|
enableEmoji = true
|
||||||
|
|
||||||
|
[permalinks]
|
||||||
|
[permalinks.page]
|
||||||
|
posts = '/posts/:year/:month/:slug/'
|
||||||
|
|
||||||
#[services]
|
#[services]
|
||||||
# [services.disqus]
|
# [services.disqus]
|
||||||
# shortname = ''
|
# shortname = ''
|
||||||
@ -38,10 +42,13 @@ expiryDate = ["expiryDate"]
|
|||||||
# nofollowLinks = true
|
# nofollowLinks = true
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tag = "tags"
|
tag = "tags"
|
||||||
# Categories are disabled by default.
|
# Categories are disabled by default.
|
||||||
# category = "categories"
|
# category = "categories"
|
||||||
|
|
||||||
|
[markup.goldmark.renderer]
|
||||||
|
unsafe = true
|
||||||
|
|
||||||
# Enable to get proper Mathjax support
|
# Enable to get proper Mathjax support
|
||||||
#[markup]
|
#[markup]
|
||||||
# [markup.goldmark]
|
# [markup.goldmark]
|
||||||
@ -70,7 +77,7 @@ expiryDate = ["expiryDate"]
|
|||||||
ShowLastmod = true
|
ShowLastmod = true
|
||||||
gitUrl = "https://git.bovender.de/daniel/blog/commit/"
|
gitUrl = "https://git.bovender.de/daniel/blog/commit/"
|
||||||
|
|
||||||
justifyContent = true
|
justifyContent = false
|
||||||
|
|
||||||
relatedPosts = true
|
relatedPosts = true
|
||||||
code_copy_button = true
|
code_copy_button = true
|
||||||
@ -91,13 +98,17 @@ expiryDate = ["expiryDate"]
|
|||||||
initialPublish = "Initally Posted on: "
|
initialPublish = "Initally Posted on: "
|
||||||
human = ["single","posts"]
|
human = ["single","posts"]
|
||||||
|
|
||||||
|
[[params.socialLinks]]
|
||||||
|
name = "bluesky"
|
||||||
|
url = "https://bsky.app/profile/bovender.bsky.social"
|
||||||
|
|
||||||
[[params.socialLinks]]
|
[[params.socialLinks]]
|
||||||
name = "mastodon"
|
name = "mastodon"
|
||||||
url = "https://neph.social/@daniel_kraus"
|
url = "https://neph.social/@daniel_kraus"
|
||||||
|
|
||||||
[[params.socialLinks]]
|
# [[params.socialLinks]]
|
||||||
name = "x"
|
# name = "x"
|
||||||
url = "https://twitter.com/bovender_de"
|
# url = "https://twitter.com/bovender_de"
|
||||||
|
|
||||||
[[params.socialLinks]]
|
[[params.socialLinks]]
|
||||||
name = "github"
|
name = "github"
|
||||||
@ -127,3 +138,8 @@ expiryDate = ["expiryDate"]
|
|||||||
name = "Impressum"
|
name = "Impressum"
|
||||||
url = "impressum/"
|
url = "impressum/"
|
||||||
weight = 30
|
weight = 30
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
name = "Cheat sheet"
|
||||||
|
url = "cheatsheet/"
|
||||||
|
weight = 15
|
||||||
|
@ -3,12 +3,18 @@ title: "About bovender"
|
|||||||
toc: true
|
toc: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
{{< figure src="plesse.png" alt="Image of the Plesse castle" title="The Plesse castle (© Daniel Kraus)">}}
|
||||||
|
|
||||||
I'm a physician-scientist from Germany.
|
I'm a physician-scientist from Germany.
|
||||||
|
|
||||||
[Bovenden](https://www.bovenden.de) is the name of the village/small town
|
[Bovenden](https://www.bovenden.de) is the name of the village/small town
|
||||||
in lovely Lower Saxony, Germany, where I grew up. Hence I'm a bovender --
|
in lovely Lower Saxony, Germany, where I grew up. Hence I'm a bovender --
|
||||||
not _the_ bovender, of course, just one of many, but proud to be one :-)
|
not _the_ bovender, of course, just one of many, but proud to be one :-)
|
||||||
|
|
||||||
|
Not far from our village is the [Plesse
|
||||||
|
castle](https://en.wikipedia.org/wiki/Plesse_Castle) which was built c. 1015 AD
|
||||||
|
and which is one of my favorite places to be.
|
||||||
|
|
||||||
Computers have been part of my life ever since I programmed my dad's
|
Computers have been part of my life ever since I programmed my dad's
|
||||||
calculator back in the 1980's.
|
calculator back in the 1980's.
|
||||||
|
|
||||||
@ -16,18 +22,31 @@ calculator back in the 1980's.
|
|||||||
|
|
||||||
### [ggap-ev.org](https://ggap-ev.org)
|
### [ggap-ev.org](https://ggap-ev.org)
|
||||||
|
|
||||||
|
GGAP is a registered charity in Germany. This NGO uses donated money to fund the
|
||||||
|
vocational training of young adults on the Philippines who would otherwise have
|
||||||
|
to "earn" their living in an unlawful way.
|
||||||
|
|
||||||
{{< figure src="ggap-ev.png">}}
|
{{< figure src="ggap-ev.png">}}
|
||||||
|
|
||||||
### [nephrowiki.de](https://nephrowiki.de)
|
### [nephrowiki.de](https://nephrowiki.de)
|
||||||
|
|
||||||
|
NephroWiki is a closed wiki system that has been up and running since 2011.
|
||||||
|
|
||||||
{{< figure src="nephrowiki.png">}}
|
{{< figure src="nephrowiki.png">}}
|
||||||
|
|
||||||
## Portfolio -- software
|
## Portfolio -- software
|
||||||
|
|
||||||
### [Daniel's XL Toolbox](https://xltoolbox.net)
|
### [Daniel's XL Toolbox](https://xltoolbox.net)
|
||||||
|
|
||||||
|
Daniel's XL Toolbox is an add-in for Excel that assists with data analysis
|
||||||
|
and visualization.
|
||||||
|
|
||||||
{{< figure src="xltoolbox.png">}}
|
{{< figure src="xltoolbox.png">}}
|
||||||
|
|
||||||
|
Even though I haven't been able to work on this project for quite some time,
|
||||||
|
I am very happy to see that the latest release has been downloaded more than
|
||||||
|
100,000 times. :-)
|
||||||
|
|
||||||
### [LinkTitles](https://github.com/bovender/linktitles)
|
### [LinkTitles](https://github.com/bovender/linktitles)
|
||||||
|
|
||||||
LinkTitles is a [MediaWiki](https://mediawiki.org) extension that automatically
|
LinkTitles is a [MediaWiki](https://mediawiki.org) extension that automatically
|
||||||
@ -38,6 +57,10 @@ links terms to existing pages in a Wiki.
|
|||||||
PubmedParser is a [MediaWiki](https://mediawiki.org) extension to include and
|
PubmedParser is a [MediaWiki](https://mediawiki.org) extension to include and
|
||||||
reference articles from [Pubmed](https://pubmed.gov) by PMID.
|
reference articles from [Pubmed](https://pubmed.gov) by PMID.
|
||||||
|
|
||||||
|
### Github Star History
|
||||||
|
|
||||||
|
[![Star History Chart][sh-img]][sh-link]
|
||||||
|
|
||||||
## Server administration
|
## Server administration
|
||||||
|
|
||||||
I have 10+ years experience with running and administrating my own server.
|
I have 10+ years experience with running and administrating my own server.
|
||||||
@ -45,3 +68,7 @@ This includes running a mail and an IMAP server, hosting my own
|
|||||||
[Nextcloud](https://nextcloud.com) and [Gitea](https://gitea.io) instances,
|
[Nextcloud](https://nextcloud.com) and [Gitea](https://gitea.io) instances,
|
||||||
[Mastodon for Nephrology](https://neph.social)
|
[Mastodon for Nephrology](https://neph.social)
|
||||||
and more. Almost all of my services are dockerized.
|
and more. Almost all of my services are dockerized.
|
||||||
|
|
||||||
|
|
||||||
|
[sh-img]: https://api.star-history.com/svg?repos=bovender/linktitles,bovender/pubmedparser,bovender/imapcli,bovender/xltoolbox&type=Date
|
||||||
|
[sh-link]: https://www.star-history.com/#bovender/linktitles&bovender/pubmedparser&bovender/imapcli&bovender/xltoolbox&Date
|
BIN
content/about/plesse.png
Normal file
After Width: | Height: | Size: 597 KiB |
97
content/cheatsheet.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: bovender's personal cheat sheet
|
||||||
|
description: >
|
||||||
|
On this page, I collect bits and pieces of information that I
|
||||||
|
never seem to be able to memorize. This stuff is helpful for me
|
||||||
|
and maybe it is helpful for you, too, internet wanderer.
|
||||||
|
date: 2024-11-05T08:00:00+0100
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
{{< git-info >}}
|
||||||
|
{{< param "description" >}}
|
||||||
|
|
||||||
|
## R
|
||||||
|
|
||||||
|
### Print R objects so that they can be re-generated from code
|
||||||
|
|
||||||
|
```r
|
||||||
|
# dput takes an R object
|
||||||
|
dput(x, file = "", control = c("keepNA", "keepInteger", "niceNames", "showAttributes"))
|
||||||
|
dget(file, keep.source = FALSE)
|
||||||
|
|
||||||
|
# dump takes a list of R object names
|
||||||
|
dump(list, file = "dumpdata.R", append = FALSE, control = "all", envir = parent.frame(), evaluate = TRUE)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
``` r
|
||||||
|
library(dplyr)
|
||||||
|
my_data <-
|
||||||
|
starwars |>
|
||||||
|
select(species, homeworld)
|
||||||
|
dput(my_data)
|
||||||
|
#> structure(list(species = c("Human", "Droid", "Droid", "Human",
|
||||||
|
#> "Human", "Human", "Human", "Droid", "Human", "Human", "Human",
|
||||||
|
#> "Human", "Wookiee", "Human", "Rodian", "Hutt", "Human", NA, "Yoda's species",
|
||||||
|
#> "Human", "Human", "Droid", "Trandoshan", "Human", "Human", "Mon Calamari",
|
||||||
|
#> "Human", "Human", "Ewok", "Sullustan", "Human", "Neimodian",
|
||||||
|
#> "Human", "Human", "Gungan", "Gungan", "Gungan", "Human", "Toydarian",
|
||||||
|
#> "Dug", "Human", "Human", "Zabrak", "Twi'lek", "Twi'lek", "Aleena",
|
||||||
|
#> "Vulptereen", "Xexto", "Toong", "Human", "Cerean", "Nautolan",
|
||||||
|
#> "Zabrak", "Tholothian", "Iktotchi", "Quermian", "Kel Dor", "Chagrian",
|
||||||
|
#> NA, NA, "Human", "Geonosian", "Mirialan", "Mirialan", "Human",
|
||||||
|
#> "Human", "Human", "Human", "Clawdite", "Besalisk", "Kaminoan",
|
||||||
|
#> "Kaminoan", "Human", "Droid", "Skakoan", "Muun", "Togruta", "Kaleesh",
|
||||||
|
#> "Wookiee", "Human", NA, "Pau'an", "Human", "Human", "Human",
|
||||||
|
#> "Droid", "Human"), homeworld = c("Tatooine", "Tatooine", "Naboo",
|
||||||
|
#> "Tatooine", "Alderaan", "Tatooine", "Tatooine", "Tatooine", "Tatooine",
|
||||||
|
#> "Stewjon", "Tatooine", "Eriadu", "Kashyyyk", "Corellia", "Rodia",
|
||||||
|
#> "Nal Hutta", "Corellia", "Bestine IV", NA, "Naboo", "Kamino",
|
||||||
|
#> NA, "Trandosha", "Socorro", "Bespin", "Mon Cala", "Chandrila",
|
||||||
|
#> NA, "Endor", "Sullust", NA, "Cato Neimoidia", "Coruscant", "Naboo",
|
||||||
|
#> "Naboo", "Naboo", "Naboo", "Naboo", "Toydaria", "Malastare",
|
||||||
|
#> "Naboo", "Tatooine", "Dathomir", "Ryloth", "Ryloth", "Aleen Minor",
|
||||||
|
#> "Vulpter", "Troiken", "Tund", "Haruun Kal", "Cerea", "Glee Anselm",
|
||||||
|
#> "Iridonia", "Coruscant", "Iktotch", "Quermia", "Dorin", "Champala",
|
||||||
|
#> "Naboo", "Naboo", "Tatooine", "Geonosis", "Mirial", "Mirial",
|
||||||
|
#> "Naboo", "Serenno", "Alderaan", "Concord Dawn", "Zolan", "Ojom",
|
||||||
|
#> "Kamino", "Kamino", "Coruscant", NA, "Skako", "Muunilinst", "Shili",
|
||||||
|
#> "Kalee", "Kashyyyk", "Alderaan", "Umbara", "Utapau", NA, NA,
|
||||||
|
#> NA, NA, NA)), row.names = c(NA, -87L), class = c("tbl_df", "tbl",
|
||||||
|
#> "data.frame"))
|
||||||
|
```
|
||||||
|
|
||||||
|
<sup>Created on 2024-11-05 with [reprex v2.1.1](https://reprex.tidyverse.org)</sup>
|
||||||
|
|
||||||
|
### Calculate percentages of subgroups
|
||||||
|
|
||||||
|
Principle: `Group` the dataset and `summarize` by counting (`n()`), drop the last group
|
||||||
|
while doing so, then `mutate` the dataset by adding the count divided by the sum of counts.
|
||||||
|
|
||||||
|
Hint to better understand the example: Look at the sexes in black-eyed subjects.
|
||||||
|
|
||||||
|
``` r
|
||||||
|
library(dplyr)
|
||||||
|
starwars |>
|
||||||
|
group_by(eye_color, gender) |>
|
||||||
|
summarize(n = n(), .groups = "drop_last") |>
|
||||||
|
mutate(p = n/sum(n))
|
||||||
|
#> # A tibble: 23 × 4
|
||||||
|
#> # Groups: eye_color [15]
|
||||||
|
#> eye_color gender n p
|
||||||
|
#> <chr> <chr> <int> <dbl>
|
||||||
|
#> 1 black feminine 2 0.2
|
||||||
|
#> 2 black masculine 8 0.8
|
||||||
|
#> 3 blue feminine 6 0.316
|
||||||
|
#> 4 blue masculine 12 0.632
|
||||||
|
#> 5 blue <NA> 1 0.0526
|
||||||
|
#> 6 blue-gray masculine 1 1
|
||||||
|
#> 7 brown feminine 4 0.190
|
||||||
|
#> 8 brown masculine 15 0.714
|
||||||
|
#> 9 brown <NA> 2 0.0952
|
||||||
|
#> 10 dark masculine 1 1
|
||||||
|
#> # ℹ 13 more rows
|
||||||
|
```
|
||||||
|
|
||||||
|
<sup>Created on 2024-11-05 with [reprex v2.1.1](https://reprex.tidyverse.org)</sup>
|
@ -3,15 +3,16 @@ title: "Impressum und Datenschutz"
|
|||||||
date: 2024-07-31T12:42:53Z
|
date: 2024-07-31T12:42:53Z
|
||||||
draft: false
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
Dr. Daniel Kraus
|
Dr. Daniel Kraus
|
||||||
H‌ö‌hen‌str‌aße 1‌5
|
|
||||||
65451 K‌e‌l‌s‌t‌e‌r‌b‌a‌c‌h
|
|
||||||
Tel. (0 61 07‌) 7 5‌6 88 4‌0
|
Tel. (0 61 07‌) 7 5‌6 88 4‌0
|
||||||
<bovender@bovender.de>
|
Mail: <bovender@bovender.de>
|
||||||
|
|
||||||
Ich bitte, von Anrufen abzusehen. Über Mail oder die verlinkten
|
Ladungsfähige Anschrift bei meinem Arbeitgeber: Unimedizin Mainz, Langenbeckstr.
|
||||||
sozialen Netzwerke (siehe Icons) bin ich i.d.R. kurzfristig
|
1, 55131 Mainz. Dies ist eine rein private Homepage und in keiner Weise von
|
||||||
|
meinem Arbeitgeber beworben oder unterstützt. Meine Privatanschrift ist zu
|
||||||
|
meinem persönlichen Schutz und dem Schutz meiner Familie hier nicht
|
||||||
|
veröffentlicht. **Ich bitte, von Anrufen abzusehen.** Über Mail oder die
|
||||||
|
verlinkten sozialen Netzwerke (siehe Icons) bin ich i.d.R. kurzfristig
|
||||||
erreichbar.
|
erreichbar.
|
||||||
|
|
||||||
## Mailserver bovender.de
|
## Mailserver bovender.de
|
||||||
|
215
content/posts/2024/dane-tlsa-record-for-letsencrypt/index.md
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
---
|
||||||
|
title: "How to create or renew a TLSA record for DANE with certificates from Let's Encrypt"
|
||||||
|
slug: create-or-renew-tlsa-records
|
||||||
|
description: >
|
||||||
|
I had to renew my TLSA records for DNS-based Authentication of Named Entities (DANE)
|
||||||
|
on my server that uses SSL certificates from Let's Encrypt.
|
||||||
|
date: 2024-10-03T09:26:58Z
|
||||||
|
draft: false
|
||||||
|
ShowLastmod: true
|
||||||
|
toc: true
|
||||||
|
scrolltotop: true
|
||||||
|
tags:
|
||||||
|
- email
|
||||||
|
- server
|
||||||
|
- security
|
||||||
|
---
|
||||||
|
|
||||||
|
I have been operating my own mail server for some 10 years. Recently, some
|
||||||
|
e-mails that others attempted to send to me any my family would not longer be
|
||||||
|
delivered. This is a very unfortunate situation, because most senders will not
|
||||||
|
make a second attempt (e.g., with a different recipient address), leave alone
|
||||||
|
provide you with an error message. However, luckily, this morning (Reunification
|
||||||
|
Day in Germany!), I received this screenshot:
|
||||||
|
|
||||||
|
{{< figure src="outlook-error.jpg"
|
||||||
|
alt="(German) screenshot of an e-mail delivery error due to an invalid TLSA record"
|
||||||
|
caption="(German) screenshot of an e-mail delivery error due to an invalid TLSA record">}}
|
||||||
|
|
||||||
|
And then it was all clear to me. The resource records (RR) in my domain-name
|
||||||
|
system (DNS) configuration that configure [DNS-based Authentication of Named
|
||||||
|
Entities (DANE)][dane] were no longer valid. I use certificates issued by Let's
|
||||||
|
Encrypt, and evidently their chain of trust had changed so that the trust
|
||||||
|
anchors that I had written into my DNS records were no longer valid.
|
||||||
|
|
||||||
|
Correctly setting up TLSA records for DANE is everything but trivial. After some
|
||||||
|
trial and error, I found the following resource most useful:
|
||||||
|
|
||||||
|
<https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html>
|
||||||
|
|
||||||
|
Key points from this and other pages to remember:
|
||||||
|
|
||||||
|
- When using TLSA RR in the form `2 1 1 ...`, i.e., declaring that the payload
|
||||||
|
(`...`) of the record is the digest of a "trust anchor", be aware that this
|
||||||
|
must be the `SHA256` digest of the _public key_ of your trust anchor.
|
||||||
|
- Let's Encrypt's `certbot` tool places three files on your server: `cert.pem`,
|
||||||
|
`chain.pem` and `fullchain.pem`, where `cert.pem` is the mail server's
|
||||||
|
certificate, `chain.pem` is the trust chain _excluding the root CA_ and
|
||||||
|
`fullchain.pem` is `cert.pem` + `chain.pem`. The important bit here is that
|
||||||
|
**none of these certificate files contain the root CA**!
|
||||||
|
- As a consequence, do not use a digest of Let's Encrypt's root CAs in your TLSA
|
||||||
|
record, _unless_ you want to add that root CA to the certificate files on your
|
||||||
|
server (don't).
|
||||||
|
- Configure TLSA records for _all_ intermediate CAs, e.g., R10-R14.
|
||||||
|
|
||||||
|
Interstingly and embarassingly, the Information Sciences Institute (ISI) on
|
||||||
|
their page linked to above has a list of mail servers that still use a TLSA
|
||||||
|
record with a _retired_ CA from Let's Encrypt -- and that list contains
|
||||||
|
`bovender.de' :-/ I guess it's about time to fix that!
|
||||||
|
|
||||||
|
## Let's Encrypt's chains of trust
|
||||||
|
|
||||||
|
Let's Encrypt's chains of trust are described here:
|
||||||
|
|
||||||
|
<https://letsencrypt.org/certificates/>
|
||||||
|
|
||||||
|
For the reasons mentioned above, we are not interested in the root CAs (ISRG
|
||||||
|
Root X1 and ISRG Root X2). Instead, we want to use the digests of the public
|
||||||
|
keys of the subordinate (intermediate) CAs. At the time of writing (2024-10),
|
||||||
|
Let's Encrypt lists four active intermediate CAs:
|
||||||
|
|
||||||
|
- E5 and E6 for certificates with ECDSA public keys
|
||||||
|
- R10 and R11 for certificates with RSA public keys
|
||||||
|
|
||||||
|
To find out which of the two algorithms (ECDSA and RSA) was used to generate
|
||||||
|
your mail server's certificate, ssh into your server, navigate to
|
||||||
|
`/etc/letsencrypt/live/<mailserver-certificate-name>` as root and issue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ openssl x509 -in cert.pem -noout -text
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
03:d3:73:ef:60:97:31:88:bc:e5:31:99:f3:00:d5:b0:c1:92
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: C = US, O = Let's Encrypt, CN = R11
|
||||||
|
Validity
|
||||||
|
Not Before: Sep 24 22:37:51 2024 GMT
|
||||||
|
Not After : Dec 23 22:37:50 2024 GMT
|
||||||
|
Subject: CN = bovender.de
|
||||||
|
Subject Public Key Info:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Thus, my mail server's current (!) certificate was issued by **R11**.
|
||||||
|
|
||||||
|
This may be different in the future when the certificate is renewed.
|
||||||
|
|
||||||
|
Currently my web server's certificate uses the ECDSA algorithm:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ cd ../bovender/
|
||||||
|
$ openssl x509 -in cert.pem -noout -text
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
04:22:00:17:8a:ed:51:09:56:15:62:2e:b5:85:49:70:05:ac
|
||||||
|
Signature Algorithm: ecdsa-with-SHA384
|
||||||
|
Issuer: C = US, O = Let's Encrypt, CN = E6
|
||||||
|
Validity
|
||||||
|
Not Before: Aug 19 11:34:11 2024 GMT
|
||||||
|
Not After : Nov 17 11:34:10 2024 GMT
|
||||||
|
Subject: CN = bovender.de
|
||||||
|
```
|
||||||
|
|
||||||
|
This certificate was issued by the **E6** intermediate CA.
|
||||||
|
|
||||||
|
An alternative way to find out more about a web server's SSL certificate is to
|
||||||
|
use `curl`:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
$ curl -I --write-out '%{certs}' https://bovender.de
|
||||||
|
HTTP/2 301
|
||||||
|
server: nginx/1.27.1
|
||||||
|
date: Thu, 03 Oct 2024 09:25:05 GMT
|
||||||
|
content-type: text/html
|
||||||
|
content-length: 169
|
||||||
|
location: https://www.bovender.de/
|
||||||
|
strict-transport-security: max-age=63072000; includeSubDomains; preload
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Subject:C = US, O = Let's Encrypt, CN = E6
|
||||||
|
Issuer:C = US, O = Internet Security Research Group, CN = ISRG Root X1
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
E6 again (surprise!).
|
||||||
|
|
||||||
|
The key type that `certbot` uses to renew certificates is contained in
|
||||||
|
`/etc/letsencrypt/renewal/<mailserver-certificate-name>.conf`:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
$ cat mail.conf
|
||||||
|
# renew_before_expiry = 30 days
|
||||||
|
version = 2.11.0
|
||||||
|
archive_dir = /etc/letsencrypt/archive/mail
|
||||||
|
cert = /etc/letsencrypt/live/mail/cert.pem
|
||||||
|
privkey = /etc/letsencrypt/live/mail/privkey.pem
|
||||||
|
chain = /etc/letsencrypt/live/mail/chain.pem
|
||||||
|
fullchain = /etc/letsencrypt/live/mail/fullchain.pem
|
||||||
|
|
||||||
|
# Options used in the renewal process
|
||||||
|
[renewalparams]
|
||||||
|
account = [REDACTED]
|
||||||
|
authenticator = webroot
|
||||||
|
server = https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
key_type = rsa
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The line `key_type` declares that RSA be used for my mail server's certificate.
|
||||||
|
|
||||||
|
## Setting up TLSA RRs in your DNS zone
|
||||||
|
|
||||||
|
As advised by [ISI.edu][isi], it is best practive to have TLSA records for all
|
||||||
|
possible intermediate CAs as you will never know which intermediate CA will be
|
||||||
|
the next to issue a renewed certificate. This is even more important if you have
|
||||||
|
a cron job set up to automatically renew certificates that are about to expire.
|
||||||
|
|
||||||
|
For the record (pun intended), here are my `_25._tcp` entries. Everything after
|
||||||
|
the pound sign (`#`) MUST NOT be included in the record, of course.
|
||||||
|
|
||||||
|
```plain
|
||||||
|
2 1 1 2bbad93ab5c79279ec121507f272cbe0c6647a3aae52e22f388afab426b4adba # R10
|
||||||
|
2 1 1 6ddac18698f7f1f7e1c69b9bce420d974ac6f94ca8b2c761701623f99c767dc7 # R11
|
||||||
|
```
|
||||||
|
|
||||||
|
Following the advice by _viktor_ on [ISI.edu][isi], I have left only those two
|
||||||
|
records in my DNS zone that represent currently used intermediate CAs (R10 and
|
||||||
|
R11). I know that `certbot` will not issue ECDSA certificates for my mail server
|
||||||
|
because the configuration file (see above) contains the line `key_type=RSA`. My
|
||||||
|
web server has an ECDSA certificate, but I currently see no use for DANE for my
|
||||||
|
web server as [Chrome and Firefox do not support it][dane].
|
||||||
|
|
||||||
|
## Verify the validity of the TLSA record
|
||||||
|
|
||||||
|
To test that DANE works as expected, try one of these sites:
|
||||||
|
|
||||||
|
- <https://www.mailhardener.com/tools/dane-validator?domain=bovender.de>
|
||||||
|
- <https://dane.sys4.de/smtp/bovender.de>
|
||||||
|
|
||||||
|
Query TLSA records:
|
||||||
|
|
||||||
|
- <https://www.nslookup.io/domains/_25._tcp.bovender.de/dns-records/tlsa/>
|
||||||
|
|
||||||
|
## "Die Moral von der Geschicht"
|
||||||
|
|
||||||
|
"The moral of the story":
|
||||||
|
|
||||||
|
- Server administration was, is, and will always be complicated.
|
||||||
|
- Always mistrust yourself.
|
||||||
|
- Regularly verify that everything works as expected, especially
|
||||||
|
security-related stuff.
|
||||||
|
- If something does not work, spend more time researching on the world wide web.
|
||||||
|
I came across the [ISI.edu][isi] page only after several hours and is was far
|
||||||
|
more useful for me than all the websites and blog and forum posts that I had
|
||||||
|
previously read.
|
||||||
|
|
||||||
|
[dane]: https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities
|
||||||
|
[isi]: https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html
|
After Width: | Height: | Size: 306 KiB |
74
content/posts/2024/duckplyr-performance/benchmarking1.svg
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='576.00pt' height='360.00pt' viewBox='0 0 576.00 360.00'>
|
||||||
|
<defs>
|
||||||
|
<style type='text/css'><![CDATA[
|
||||||
|
.svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000000;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-miterlimit: 10.00;
|
||||||
|
}
|
||||||
|
.svglite text {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
]]></style>
|
||||||
|
</defs>
|
||||||
|
<rect width='100%' height='100%' style='stroke: none; fill: #292E32;'/>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA='>
|
||||||
|
<rect x='0.00' y='0.00' width='576.00' height='360.00' />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA=)'>
|
||||||
|
<rect x='0.000000000000064' y='0.00' width='576.00' height='360.00' style='stroke-width: 1.07; stroke: #FFFFFF; fill: #292E32;' />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='cpNTkuMTB8NDIxLjE5fDQ2LjQxfDMwMi45OQ=='>
|
||||||
|
<rect x='59.10' y='46.41' width='362.09' height='256.58' />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#cpNTkuMTB8NDIxLjE5fDQ2LjQxfDMwMi45OQ==)'>
|
||||||
|
<polyline points='59.10,302.99 421.19,302.99 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,238.06 421.19,238.06 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,173.14 421.19,173.14 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,108.22 421.19,108.22 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<circle cx='151.61' cy='98.97' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='177.19' cy='91.03' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='174.46' cy='99.26' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='296.36' cy='101.41' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='320.05' cy='105.05' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='353.79' cy='91.38' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='325.12,89.89 331.35,100.67 318.89,100.67 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='327.97,92.20 334.20,102.98 321.75,102.98 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='327.74,96.20 333.97,106.98 321.52,106.98 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='167.19,87.42 173.41,98.20 160.96,98.20 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='190.42,81.99 196.65,92.77 184.20,92.77 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='165.99,82.14 172.22,92.92 159.77,92.92 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
</g>
|
||||||
|
<g clip-path='url(#cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA=)'>
|
||||||
|
<polyline points='59.10,302.99 59.10,46.41 ' style='stroke-width: 2.13; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<text x='54.17' y='306.91' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='6.29px' lengthAdjust='spacingAndGlyphs'>0</text>
|
||||||
|
<text x='54.17' y='241.99' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>10</text>
|
||||||
|
<text x='54.17' y='177.07' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>20</text>
|
||||||
|
<text x='54.17' y='112.14' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>30</text>
|
||||||
|
<polyline points='56.36,302.99 59.10,302.99 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,238.06 59.10,238.06 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,173.14 59.10,173.14 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,108.22 59.10,108.22 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,302.99 421.19,302.99 ' style='stroke-width: 2.13; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='157.85,305.73 157.85,302.99 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='322.44,305.73 322.44,302.99 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<text x='157.85' y='315.77' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='26.52px' lengthAdjust='spacingAndGlyphs'>dplyr</text>
|
||||||
|
<text x='322.44' y='315.77' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='44.47px' lengthAdjust='spacingAndGlyphs'>duckplyr</text>
|
||||||
|
<text x='240.15' y='329.01' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='36.24px' lengthAdjust='spacingAndGlyphs'>Library</text>
|
||||||
|
<text transform='translate(36.20,174.70) rotate(-90)' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='40.16px' lengthAdjust='spacingAndGlyphs'>Time (s)</text>
|
||||||
|
<rect x='432.15' y='143.95' width='115.50' height='61.50' style='stroke-width: 1.07; stroke: none; fill: #3B3E48;' />
|
||||||
|
<text x='437.63' y='158.61' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='104.54px' lengthAdjust='spacingAndGlyphs'>Laptop power mode</text>
|
||||||
|
<circle cx='446.27' cy='174.05' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='446.27,184.14 452.50,194.92 440.04,194.92 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<text x='460.39' y='177.19' style='font-size: 8.80px;fill: #FFFFFF; font-family: "Arial";' textLength='37.56px' lengthAdjust='spacingAndGlyphs'>balanced</text>
|
||||||
|
<text x='460.39' y='194.47' style='font-size: 8.80px;fill: #FFFFFF; font-family: "Arial";' textLength='53.76px' lengthAdjust='spacingAndGlyphs'>performance</text>
|
||||||
|
<text x='59.10' y='37.76' style='font-size: 13.20px;fill: #FFFFFF; font-family: "Arial";' textLength='224.18px' lengthAdjust='spacingAndGlyphs'>Time elapsed with dplyr vs. duckplyr</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
87
content/posts/2024/duckplyr-performance/benchmarking2.svg
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='576.00pt' height='360.00pt' viewBox='0 0 576.00 360.00'>
|
||||||
|
<defs>
|
||||||
|
<style type='text/css'><![CDATA[
|
||||||
|
.svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000000;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-miterlimit: 10.00;
|
||||||
|
}
|
||||||
|
.svglite text {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
]]></style>
|
||||||
|
</defs>
|
||||||
|
<rect width='100%' height='100%' style='stroke: none; fill: #292E32;'/>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA='>
|
||||||
|
<rect x='0.00' y='0.00' width='576.00' height='360.00' />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA=)'>
|
||||||
|
<rect x='0.000000000000064' y='0.00' width='576.00' height='360.00' style='stroke-width: 1.07; stroke: #FFFFFF; fill: #292E32;' />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id='cpNTkuMTB8NDIxLjE5fDQ2LjQxfDI3OS4yMw=='>
|
||||||
|
<rect x='59.10' y='46.41' width='362.09' height='232.82' />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g clip-path='url(#cpNTkuMTB8NDIxLjE5fDQ2LjQxfDI3OS4yMw==)'>
|
||||||
|
<polyline points='59.10,279.23 421.19,279.23 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,225.86 421.19,225.86 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,172.48 421.19,172.48 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,119.11 421.19,119.11 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,65.74 421.19,65.74 ' style='stroke-width: 1.07; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<circle cx='127.08' cy='111.51' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='142.09' cy='105.00' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='132.50' cy='111.77' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='246.17' cy='113.52' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='223.06' cy='116.55' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='260.64' cy='105.27' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='249.90,102.73 256.13,113.51 243.67,113.51 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='243.36,104.64 249.58,115.42 237.13,115.42 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='218.81,108.00 225.03,118.79 212.58,118.79 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='119.28,100.75 125.50,111.54 113.05,111.54 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='114.27,96.29 120.49,107.07 108.04,107.07 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='131.88,96.40 138.10,107.19 125.65,107.19 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='335.41' cy='90.10' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='351.34' cy='85.38' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<circle cx='336.95' cy='85.21' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='344.58,92.55 350.81,103.33 338.36,103.33 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='349.73,94.20 355.96,104.98 343.51,104.98 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='344.11,90.98 350.33,101.77 337.88,101.77 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
</g>
|
||||||
|
<g clip-path='url(#cpMC4wMHw1NzYuMDB8MC4wMHwzNjAuMDA=)'>
|
||||||
|
<polyline points='59.10,279.23 59.10,46.41 ' style='stroke-width: 2.13; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<text x='54.17' y='283.15' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='6.29px' lengthAdjust='spacingAndGlyphs'>0</text>
|
||||||
|
<text x='54.17' y='229.78' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>10</text>
|
||||||
|
<text x='54.17' y='176.41' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>20</text>
|
||||||
|
<text x='54.17' y='123.04' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>30</text>
|
||||||
|
<text x='54.17' y='69.67' text-anchor='end' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='12.59px' lengthAdjust='spacingAndGlyphs'>40</text>
|
||||||
|
<polyline points='56.36,279.23 59.10,279.23 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,225.86 59.10,225.86 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,172.48 59.10,172.48 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,119.11 59.10,119.11 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='56.36,65.74 59.10,65.74 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='59.10,279.23 421.19,279.23 ' style='stroke-width: 2.13; stroke: #3B3E48; stroke-linecap: butt;' />
|
||||||
|
<polyline points='126.99,281.97 126.99,279.23 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='240.15,281.97 240.15,279.23 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<polyline points='353.30,281.97 353.30,279.23 ' style='stroke-width: 1.07; stroke: #333333; stroke-linecap: butt;' />
|
||||||
|
<text x='126.99' y='292.01' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='26.52px' lengthAdjust='spacingAndGlyphs'>dplyr</text>
|
||||||
|
<text x='240.15' y='292.01' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='44.47px' lengthAdjust='spacingAndGlyphs'>duckplyr</text>
|
||||||
|
<text x='353.30' y='292.01' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='44.47px' lengthAdjust='spacingAndGlyphs'>duckplyr</text>
|
||||||
|
<text x='353.30' y='303.89' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='22.26px' lengthAdjust='spacingAndGlyphs'>with</text>
|
||||||
|
<text x='353.30' y='315.77' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='101.25px' lengthAdjust='spacingAndGlyphs'>`as_duckplyr_tibble`</text>
|
||||||
|
<text x='240.15' y='329.01' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='36.24px' lengthAdjust='spacingAndGlyphs'>Library</text>
|
||||||
|
<text transform='translate(36.20,162.82) rotate(-90)' text-anchor='middle' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='40.16px' lengthAdjust='spacingAndGlyphs'>Time (s)</text>
|
||||||
|
<rect x='432.15' y='132.07' width='115.50' height='61.50' style='stroke-width: 1.07; stroke: none; fill: #3B3E48;' />
|
||||||
|
<text x='437.63' y='146.73' style='font-size: 11.00px;fill: #FFFFFF; font-family: "Arial";' textLength='104.54px' lengthAdjust='spacingAndGlyphs'>Laptop power mode</text>
|
||||||
|
<circle cx='446.27' cy='162.17' r='4.62' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<polygon points='446.27,172.26 452.50,183.04 440.04,183.04 ' style='stroke-width: 0.71; stroke: none; fill: #FFFFFF; fill-opacity: 0.80;' />
|
||||||
|
<text x='460.39' y='165.31' style='font-size: 8.80px;fill: #FFFFFF; font-family: "Arial";' textLength='37.56px' lengthAdjust='spacingAndGlyphs'>balanced</text>
|
||||||
|
<text x='460.39' y='182.59' style='font-size: 8.80px;fill: #FFFFFF; font-family: "Arial";' textLength='53.76px' lengthAdjust='spacingAndGlyphs'>performance</text>
|
||||||
|
<text x='59.10' y='37.76' style='font-size: 13.20px;fill: #FFFFFF; font-family: "Arial";' textLength='224.18px' lengthAdjust='spacingAndGlyphs'>Time elapsed with dplyr vs. duckplyr</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.4 KiB |
127
content/posts/2024/duckplyr-performance/index.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: Performance experiments with duckplyr in R
|
||||||
|
description: I recently became aware of the 'duckplyr' library for R. Here are the results of my experimenting with it and benchmarking it against `dplyr`.
|
||||||
|
date: 2024-08-26T19:00:00+0200
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
tags:
|
||||||
|
- R
|
||||||
|
- statistics
|
||||||
|
---
|
||||||
|
I recently became aware of the [duckplyr][] library for R, which takes the place
|
||||||
|
of tidyverse's [dplyr][] library, but uses the [DuckDB] database under the hood.
|
||||||
|
Without really knowing anything about how dplyr works and if the use of DuckDB
|
||||||
|
would improve my workflow at all, I decided to perform an experiment. I am
|
||||||
|
currently analyzing two datasets, one with ~80k records and ~70 variables and
|
||||||
|
one with ~60k records and ~100 variables. Both datasets are wrangled with
|
||||||
|
[Tidyverse][]-foo in multiple ways and finally combined. The wrangling of the
|
||||||
|
data involves things like `rowwise()` and `c_across()`, which I know from
|
||||||
|
experience is quite an 'expensive' operation.
|
||||||
|
|
||||||
|
In order to get the execution times of my code, I did this repeatedly:
|
||||||
|
|
||||||
|
1. Restart R (by pressing <kbd>CTRL</kbd> <kbd>SHIFT</kbd> <kbd>F10</kbd>).
|
||||||
|
2. Run
|
||||||
|
|
||||||
|
```r
|
||||||
|
system.time(rmarkdown::render("my_file.Rmd"))
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Record the user time and the system time elapsed.
|
||||||
|
4. Repeat twice.
|
||||||
|
|
||||||
|
I did this with both the "balanced power mode" and the "performance mode" on my
|
||||||
|
[laptop][]. During execution of the code, I left the laptop alone in order not
|
||||||
|
to interfere with the timing.
|
||||||
|
|
||||||
|
This is the result of my benchmarking:
|
||||||
|
|
||||||
|
{{< figure src="benchmarking1.svg" >}}
|
||||||
|
|
||||||
|
The times are user times. I left out the system times, which are in the range of
|
||||||
|
2-3 seconds.
|
||||||
|
|
||||||
|
Not really mind-boggling, right? It occurred to me that I rather double-check
|
||||||
|
that `duckplyr` was really being used. Indeed, this was _not_ the case:
|
||||||
|
|
||||||
|
```r
|
||||||
|
> class(clinical_data)
|
||||||
|
[1] "tbl_df" "tbl" "data.frame"
|
||||||
|
```
|
||||||
|
|
||||||
|
`clinical_data` was missing the `duckplyr_df' class. How come?
|
||||||
|
|
||||||
|
I import the raw data from Excel files (don't ask...) into tibbles, and
|
||||||
|
evidently, this prevents `duckplyr` from seeing the data frames. So I piped the
|
||||||
|
data frames through `as_duckplyr_tibble()` explicitly, and this got me the right
|
||||||
|
classes:
|
||||||
|
|
||||||
|
```r
|
||||||
|
> class(clinical_data)
|
||||||
|
[1] "duckplyr_df" "tbl_df" "tbl" "data.frame"
|
||||||
|
```
|
||||||
|
|
||||||
|
However, this did not really speed up the execution either.
|
||||||
|
|
||||||
|
{{< figure src="benchmarking2.svg" >}}
|
||||||
|
|
||||||
|
I looked around my RMarkdown chunks and their outputs, but I did not find any
|
||||||
|
warning that `duckplyr` had to fall back to `dplyr`'s methods. This could have
|
||||||
|
explained the absence of a noticeable difference.
|
||||||
|
|
||||||
|
Here are the average times (in seconds) for the benchmarking runs.
|
||||||
|
|
||||||
|
```r
|
||||||
|
> runs_table
|
||||||
|
# A tibble: 6 × 4
|
||||||
|
# Groups: library, power_mode [6]
|
||||||
|
library power_mode mean sd
|
||||||
|
<chr> <chr> <dbl> <dbl>
|
||||||
|
1 dplyr balanced 31.8 0.722
|
||||||
|
2 dplyr performance 32.6 0.477
|
||||||
|
3 duckplyr balanced 31.4 1.10
|
||||||
|
4 duckplyr performance 31.3 0.495
|
||||||
|
5 duckplyr with `as_duckplyr_tibble` balanced 36.0 0.517
|
||||||
|
6 duckplyr with `as_duckplyr_tibble` performance 33.6 0.303
|
||||||
|
```
|
||||||
|
|
||||||
|
So at least for my (!!!) use case, the use of `duckplyr` instead of `dplyr` did
|
||||||
|
not make any practical difference, and I can also leave my laptop's performance
|
||||||
|
mode alone. When it comes to optimizing performance, you can't just buy a
|
||||||
|
solution off the shelf, you always have to try and find the best solution for
|
||||||
|
your specific problem.
|
||||||
|
|
||||||
|
Your mileage will vary, of course. The people who develop `duckplyr` are
|
||||||
|
brilliant, and the fact that it does not work for me tells more about me and my
|
||||||
|
work than it does about `duckplyr`.
|
||||||
|
|
||||||
|
## The duckplyr demo dataset
|
||||||
|
|
||||||
|
As a case in point, the [duckplyr demo repository][duckplyr-demo] contains a
|
||||||
|
taxi data set. The ZIP file alone is a ~1.7 GB download. Deflated, the files
|
||||||
|
take up 2.4 GB. With about 21 million records (24 variables), this dataset
|
||||||
|
is _considerably_ larger than mine.
|
||||||
|
|
||||||
|
Here are the results from running `dplyr/run_all_queries.R` and
|
||||||
|
`duckplyr/run_all_queries.R` on my Thinkpad P14s (performance mode in F40 KDE):
|
||||||
|
|
||||||
|
| Library | q01 | q02 | q03 | q04 |
|
||||||
|
|----------|------:|------:|------:|-------:|
|
||||||
|
| dplyr | 3.4 s | 3.9 s | 9.1 s | 14.3 s |
|
||||||
|
| duckplyr | 4.3 s | 4.4 s | 9.4 s | 14.8 s |
|
||||||
|
|
||||||
|
I should add that execution times vary with each run, but the big picture stays
|
||||||
|
the same.
|
||||||
|
|
||||||
|
Maybe I'm missing the point and it's not about execution times, after all.
|
||||||
|
|
||||||
|
`¯\_(ツ)_/`
|
||||||
|
|
||||||
|
[dplyr]: https:/dplyr.tidyverse.org
|
||||||
|
[duckdb]: https://duckdb.org
|
||||||
|
[duckplyr]: https://duckplyr.tidyverse.org
|
||||||
|
[duckplyr-demo]: https://github.com/Tmonster/duckplyr_demo
|
||||||
|
[laptop]: {{< relref "P14s" >}}
|
||||||
|
[tidyverse]: https://tidyverse.org
|
BIN
content/posts/2024/halloween/IMGP8429_G.JPG
Normal file
After Width: | Height: | Size: 399 KiB |
BIN
content/posts/2024/halloween/IMGP8436_G.JPG
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
content/posts/2024/halloween/IMGP8437_G.JPG
Normal file
After Width: | Height: | Size: 268 KiB |
90
content/posts/2024/halloween/index.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: My gripes with Halloween in Germany
|
||||||
|
description: >
|
||||||
|
Celebrating Halloween in Germany is a relatively new phenomenon that
|
||||||
|
I believe is a manifestation of the ego-centered and hedonistic
|
||||||
|
state of the German society.
|
||||||
|
date: 2024-10-31T23:00:00+0200
|
||||||
|
draft: false
|
||||||
|
ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images:
|
||||||
|
- IMGP8436_G.JPG
|
||||||
|
tags:
|
||||||
|
- opinion
|
||||||
|
---
|
||||||
|
|
||||||
|
{{< figure src="IMGP8429_G.JPG" alt="Halloween in Jamaica Plain, 2009"
|
||||||
|
title="Halloween in '09, Jamaica Plain, Boston, MA © Daniel Kraus">}}
|
||||||
|
|
||||||
|
In the three years that I lived in Boston, Massachusetts, while persuing my
|
||||||
|
postdoctoral fellowship at [Beth Israel Deaconess Medical Center][bidmc],
|
||||||
|
Halloween was a wonderful event. We had a little appartment literally under the
|
||||||
|
roof of a very nice and welcoming family in Boston's Jameica Plain
|
||||||
|
neighbourhood. The family was not just our landlords -- they quickly became
|
||||||
|
friends. We celebrated all kinds of events together: the [Red Sox' Win of the
|
||||||
|
World Series in 2007][sox], birthdays, Thanksgiving, ... and [Halloween][].
|
||||||
|
|
||||||
|
{{< figure src="IMGP8436_G.JPG" alt="Halloween in Jamaica Plain, 2009"
|
||||||
|
title="Halloween in '09, Jamaica Plain, Boston, MA © Daniel Kraus">}}
|
||||||
|
|
||||||
|
Halloween has a long tradition in the United States and elsewhere. Where we used
|
||||||
|
to live in Boston, the entire street celebrated together. The street was divided
|
||||||
|
into two halves that took turns in organizing the event each year. Everybody met
|
||||||
|
outside. There was live music, and of course the kids did the trick-or-treat thing.
|
||||||
|
I loved it.
|
||||||
|
|
||||||
|
{{< figure src="IMGP8437_G.JPG" alt="Halloween in Jamaica Plain, 2009"
|
||||||
|
title="Halloween in '09, Jamaica Plain, Boston, MA © Daniel Kraus">}}
|
||||||
|
|
||||||
|
Growing up in Germany in the 1980s, I was accustomed to [St. Martin's
|
||||||
|
Day][martin] on or around 11 November in commemoration of St. Martin of Tours
|
||||||
|
who is said to have divided his coat to share it with a poor person.
|
||||||
|
|
||||||
|
We would walk through our neighbourhood, ring people's door bells, and when
|
||||||
|
someone opened we would sing a song about St. Martin and ask for sweets. When I
|
||||||
|
thought about this now and before it occurred to me to write a blog post about
|
||||||
|
this, I seemed to remember that we would afterwards give most of the sweets to
|
||||||
|
children in need (refugees, in fact) that had found shelter in a nearby
|
||||||
|
facility. However, I now believe that we did this only on a similar occasion in
|
||||||
|
early January when we celebrated [Epiphany with the Star singers][epiphany]
|
||||||
|
which also involved walking around, singing, and collecting sweets. So St.
|
||||||
|
Martin's day had a lot to do with asking for sweets for our own consumption and
|
||||||
|
pleasure and not so much about caring and sharing, and this is important to keep
|
||||||
|
in mind. It is also important to realize that this tradition around St. Martin
|
||||||
|
was a regional thing and nothing nationwide (as I learned from an [article on
|
||||||
|
Wikipedia][martin-in-germany]).
|
||||||
|
|
||||||
|
In recent years, Halloween has grown very strong in Germany, and I believe that
|
||||||
|
this is a result of deliberately injecting the tradition into our culture.
|
||||||
|
Interestingly, this is also [Wikipedia's view][germany] (see references in the
|
||||||
|
linked article).
|
||||||
|
|
||||||
|
And that's my gripe with celebrating Halloween in Germany. There's nothing wrong
|
||||||
|
with partying and having a good time with friends and family. But this event is
|
||||||
|
tightly linked to buying and consuming stuff, and it is driven by "the
|
||||||
|
industry". Again, there's nothing wrong in itself with "the industry" trying to
|
||||||
|
sell stuff to people. But I do think that this is an example of how people can
|
||||||
|
be made to do things. Just like [lemmings][], everybody is following suit.
|
||||||
|
|
||||||
|
Halloween in Germany, as I see it, is a festival of hedonism. It has nothing to
|
||||||
|
to with sharing, it has nothing to do with taking a break and thinking about the
|
||||||
|
[faithful departed][halloween], but it has a lot to do with buying stuff and
|
||||||
|
consuming stuff that you don't really need.
|
||||||
|
|
||||||
|
For me, Halloween in Germany is an example for the state of our society.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Having said that, I greatly enjoyed the more or less spontaneous Halloween party
|
||||||
|
that we had at our neighbors' place tonight ;-)
|
||||||
|
|
||||||
|
[bidmc]: https://bidmc.org
|
||||||
|
[epiphany]: https://en.wikipedia.org/wiki/Star_singers
|
||||||
|
[germany]: https://en.wikipedia.org/wiki/Geography_of_Halloween#Germany
|
||||||
|
[halloween]: https://en.wikipedia.org/wiki/Halloween
|
||||||
|
[lemmings]:https://en.wikipedia.org/wiki/St._Martin%27s_Day
|
||||||
|
[martin]: https://en.wikipedia.org/wiki/St._Martin%27s_Day
|
||||||
|
[martin-in-germany]: https://en.wikipedia.org/wiki/Martinisingen#Present-day_customs
|
||||||
|
[sox]: https://en.wikipedia.org/wiki/2007_World_Series
|
36
content/posts/2024/kmail-crash/index.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: How to fix KMail not starting
|
||||||
|
description: How I got my KMail working again when it crashed during the startup process
|
||||||
|
date: 2024-09-17T06:00:00+0200
|
||||||
|
draft: false
|
||||||
|
ShowLastmod: false
|
||||||
|
tags:
|
||||||
|
- Plasma
|
||||||
|
- Fix
|
||||||
|
- KMail
|
||||||
|
---
|
||||||
|
|
||||||
|
Last night, I suddenly could not start KMail anymore.
|
||||||
|
|
||||||
|
Konsole revealed this:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
$ kmail
|
||||||
|
org.kde.pim.kmail: setFcc: collection invalid ""
|
||||||
|
We have an error during reading password "Entry not found"
|
||||||
|
*** KMail got signal 11 (Exiting)
|
||||||
|
*** Dead letters dumped.
|
||||||
|
KCrash: Application 'kmail' crashing... crashRecursionCounter = 2
|
||||||
|
fish: Job 1, 'kmail' terminated by signal SIGSEGV (Address boundary error)
|
||||||
|
````
|
||||||
|
|
||||||
|
Luckily, I quickly [found a fix][1]:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
rm ~/.local/share/kmail2/autosave/*
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't know what caused this problem and why KMail chokes on
|
||||||
|
autosave files, but at least I can now start it again.
|
||||||
|
|
||||||
|
[1]: <https://forum.archlinux.de/d/34333-kmail-kontact-starten-nach-plasma-update-nicht-mehr/10>
|
24
content/posts/2024/new-blog/index.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Welcome to the new blog"
|
||||||
|
date: 2024-08-01T00:00:00+02:00
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- meta
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to my new blog.
|
||||||
|
|
||||||
|
This blog continues where my old one at
|
||||||
|
<https://www.xltoolbox.net/blog/index.html> left off.
|
||||||
|
|
||||||
|
I thought about moving the old blog posts over to this new blog, but those posts
|
||||||
|
are really ancient and not relevant any more. I leave them at the old place for
|
||||||
|
archival purposes.
|
||||||
|
|
||||||
|
I have implemented a commenting system based on [Discourse][]. Please feel free
|
||||||
|
to comment on my posts, ask questions etc. You'll either have to create a
|
||||||
|
standalone account, or you can log in with your Google or Github account.
|
||||||
|
|
||||||
|
See the discussion forum at <https://discuss.bovender.de>.
|
||||||
|
|
||||||
|
[discourse]: https://discourse.org
|
@ -1,16 +1,18 @@
|
|||||||
---
|
---
|
||||||
title: "Experience with running Fedora Linux on a Thinkpad P14s Gen 5"
|
title: "Experience with running Fedora Linux on a Thinkpad P14s Gen 5"
|
||||||
description: "Everything works out of the box, but the keyboard and the battery leave something to be desired."
|
description: "Everything works out of the box with KDE Plasma, but the keyboard and the battery leave something to be desired."
|
||||||
date: 2024-08-05T21:00:00+02:00
|
date: 2024-08-05T21:00:00+02:00
|
||||||
ShowLastmod: true
|
ShowLastmod: false
|
||||||
draft: false
|
draft: false
|
||||||
toc: true
|
toc: true
|
||||||
scrolltotop: true
|
scrolltotop: true
|
||||||
tags:
|
tags:
|
||||||
- Thinkpad
|
|
||||||
- Linux
|
|
||||||
- Fedora
|
- Fedora
|
||||||
|
- F40
|
||||||
|
- KDE
|
||||||
|
- Linux
|
||||||
- NVIDIA
|
- NVIDIA
|
||||||
|
- Thinkpad
|
||||||
---
|
---
|
||||||
|
|
||||||
I have recently acquired a Lenovo Thinkpad P14s Gen 5. As of the time of writing
|
I have recently acquired a Lenovo Thinkpad P14s Gen 5. As of the time of writing
|
||||||
@ -193,13 +195,94 @@ All in all, I am quite happy with the P14s. In the past couple of weeks, it has
|
|||||||
already proven to be a reliable work horse, and this is what I need. The
|
already proven to be a reliable work horse, and this is what I need. The
|
||||||
keyboard really should be better, but hey, life is not perfect, is it?
|
keyboard really should be better, but hey, life is not perfect, is it?
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## Update 2024-08-14
|
||||||
|
|
||||||
|
Two important things to add:
|
||||||
|
|
||||||
|
1. After a recent system update, **suspend (sleep) stopped working**. First I
|
||||||
|
suspected the NVIDIA driver to be responsible, it had been upgrade to version
|
||||||
|
560. However, when I downgraded the driver, the problem persisted. Finally I
|
||||||
|
found a [thread][] on the Fedora Discussion forum which suggested to disable
|
||||||
|
ethernet in BIOS. Tada! That was it. Hopefully, with a future kernel or
|
||||||
|
firmware upgrade, I will be able to use ethernet again, as I prefer to have
|
||||||
|
cable-bound internet at home. The current linux kernel is 6.10, upgraded from
|
||||||
|
6.9 just recently.
|
||||||
|
|
||||||
|
2. Here's another [interesting report][techtipsy] from someone running **Linux on a P14S**,
|
||||||
|
albeit Gen 4 (mine is Gen 5) and with AMD and integrated graphics (I have
|
||||||
|
Intel with dedicated NVIDIA GPU). The author (Herman) also notes the battery
|
||||||
|
drain (even with AMD and iGPU) and the build quality which is somewhat below
|
||||||
|
the T series. Herman has also posted about [controlling fan speed with a simple
|
||||||
|
script][fancontrol], and I think I am going to tinker with it in the future,
|
||||||
|
because the following works on my P14s Gen 5 with Intel CPU and NVIDIA GPU:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This is just a proof of concept, spin up the fan and spin it down again
|
||||||
|
# Run as root
|
||||||
|
echo level 7 > /proc/acpi/ibm/fan; sleep 5; echo level 1 > /proc/acpi/ibm/fan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update 2024-08-22
|
||||||
|
|
||||||
|
Fedora installs only free software by default, which means that [Kdenlive][] and
|
||||||
|
other multimedia software will have trouble using H.264 and H.265 video codecs.
|
||||||
|
A free version of [ffmpeg][] is installed by default. This is what I did to
|
||||||
|
replace `ffmpeg-free` with the non-free `ffmpeg` on F40:
|
||||||
|
|
||||||
|
1. Enable the non-free [RPM Fusion][] repositories as written on the [RPM Fusion
|
||||||
|
homepage][]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
|
||||||
|
sudo dnf config-manager --enable fedora-cisco-openh264
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Remove `ffmpeg-free` in favor of `ffmpeg`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf swap ffmpeg-free ffmpeg --allowerasing
|
||||||
|
```
|
||||||
|
|
||||||
|
After doing that and restarting Kdenlive, I was able to render my project using
|
||||||
|
the H.264 and H.265 codecs as I used to be able to do with Ubuntu-based systems.
|
||||||
|
|
||||||
|
## Update 2024-11-10
|
||||||
|
|
||||||
|
In the last couple of months, battery life has improved significantly. As
|
||||||
|
described above, in summer, battery drained almost completely (from ~90% to
|
||||||
|
~15%) within 3.5 hours even without heavy workload.
|
||||||
|
|
||||||
|
Today I spent about the same amount of time on a train, working constantly on my
|
||||||
|
laptop (browsing, downloading articles), and the battery level only dropped from
|
||||||
|
~90% to ~40% within 3 hours. Some system update, be it a kernel update or some
|
||||||
|
other software running in the background, must have reduced energy consumption
|
||||||
|
considerably :-)
|
||||||
|
|
||||||
|
{{< figure src="p14s-battery-drain-2.png">}}
|
||||||
|
|
||||||
|
(I am well aware that there are laptops whose battery run time is in an entirely
|
||||||
|
different ball park, and even some of my own Thinkpads used to do better, but
|
||||||
|
I'm overall quite happy with battery life on this machine with bright 3k display
|
||||||
|
and dedicated GPU. I _could_ have bought a machine which can run on battery like
|
||||||
|
forever, but that's not my use case and plus, I'd never buy anything else but a
|
||||||
|
Thinkpad...)
|
||||||
|
|
||||||
[Darktable]: https://darktable.org
|
[Darktable]: https://darktable.org
|
||||||
|
[fancontrol]: https://ounapuu.ee/posts/2022/09/26/minimum-viable-fan-control-script/
|
||||||
[Fedora]: https://fedoraproject.org/
|
[Fedora]: https://fedoraproject.org/
|
||||||
|
[ffmpeg]: https://ffmpeg.org
|
||||||
[KDE forum]: https://discuss.kde.org/t/unable-to-start-kde-neon-plasma-on-core-ultra-nvidia-rtx-500-ada/18578
|
[KDE forum]: https://discuss.kde.org/t/unable-to-start-kde-neon-plasma-on-core-ultra-nvidia-rtx-500-ada/18578
|
||||||
[KDE neon]: https://neon.kde.org
|
[KDE neon]: https://neon.kde.org
|
||||||
[KDE plasma]: https://kde.org/plasma-desktop/
|
[KDE plasma]: https://kde.org/plasma-desktop/
|
||||||
[KDE spin]: https://fedoraproject.org/spins/
|
[KDE spin]: https://fedoraproject.org/spins/
|
||||||
|
[Kdenlive]: https://kdenlive.org
|
||||||
[p14s-weight]: https://www.lenovo.com/us/en/p/laptops/thinkpad/thinkpadp/thinkpad-p14s-gen-5-(14-inch-intel)-mobile-workstation/21g2002cus#tech_specs
|
[p14s-weight]: https://www.lenovo.com/us/en/p/laptops/thinkpad/thinkpadp/thinkpad-p14s-gen-5-(14-inch-intel)-mobile-workstation/21g2002cus#tech_specs
|
||||||
|
[rpm fusion]: https://rpmfusion.org
|
||||||
|
[rpm fusion homepage]: https://rpmfusion.org/Configuration
|
||||||
[rstudio]: https://posit.co/products/open-source/rstudio
|
[rstudio]: https://posit.co/products/open-source/rstudio
|
||||||
[s3]: https://en.wikipedia.org/wiki/ACPI#S3
|
[s3]: https://en.wikipedia.org/wiki/ACPI#S3
|
||||||
|
[techtipsy]: https://ounapuu.ee/posts/2024/04/12/lenovo-p14s-gen4/
|
||||||
|
[thread]: https://discussion.fedoraproject.org/t/suspend-failure-and-degraded-performance-after-failed-suspend-on-kernel-6-10-3/128299
|
||||||
[xltoolbox.net]: https://www.xltoolbox.net/blog/2018/08/exit-thinkpad-t430s-enter-thinkpad-t480s.html
|
[xltoolbox.net]: https://www.xltoolbox.net/blog/2018/08/exit-thinkpad-t430s-enter-thinkpad-t480s.html
|
BIN
content/posts/2024/p14s/p14s-battery-drain-2.png
Normal file
After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
Before Width: | Height: | Size: 954 KiB After Width: | Height: | Size: 954 KiB |
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.8 MiB |
40
content/posts/2024/plasma-5-tiling-manager/index.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: "Windows tiling in KDE Plasma 5"
|
||||||
|
date: 2024-08-08T18:04:20+0200
|
||||||
|
draft: false
|
||||||
|
toc: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
images:
|
||||||
|
tags:
|
||||||
|
- Linux
|
||||||
|
- KDE
|
||||||
|
- Plasma
|
||||||
|
- productivity
|
||||||
|
---
|
||||||
|
KDE Plasma 5 introduced a (more or less) advanced window tiling manager. Most of
|
||||||
|
the time I use `META` plus one or two of the arrow keys to move windows quickly
|
||||||
|
to one half or one quarter of the screen. Plasma's advanced tiling manager
|
||||||
|
enables us to use layouts that are not defined by half or quarter of the screen
|
||||||
|
real estate.
|
||||||
|
|
||||||
|
I keep forgetting how to use this, so this is my note to self:
|
||||||
|
|
||||||
|
- Define layout: `META+T`
|
||||||
|
- Move window to predefined location: hold `SHIFT` while dragging the title bar
|
||||||
|
of a window with the mouse, or press `META`, klick anywhere into the window
|
||||||
|
and start dragging, _then_ while still holding `META`, additionally press
|
||||||
|
`SHIFT` to make the window snap to one of the predefined layout.
|
||||||
|
|
||||||
|
Link: <https://kde.org/announcements/plasma/5/5.27.0/>
|
||||||
|
|
||||||
|
I wish there were keyboard shortcuts to move windows into one of the custom tile
|
||||||
|
layouts as well: <https://discuss.kde.org/t/keyboard-shortcuts-for-new-tiling-feature/1843>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Added 2025-03-02
|
||||||
|
|
||||||
|
With Plasma 5.3, you can now define keyboard shortcuts to move a window to
|
||||||
|
a custom tile. On my system, there were now shortcuts defined by default, but
|
||||||
|
it is very easy to do so in `System Settings > Shortcuts > KWin`. I have now
|
||||||
|
mapped `META`+`SHIFT` and the arrow keys to move windows to custom tiles.
|
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
title: "Reconcile Docker With Deutsche Bahn"
|
||||||
|
description: >
|
||||||
|
If WiFi does not work on Deutsche Bahn's ICE trains and you have Docker installed,
|
||||||
|
there may be network interference which can be fixed easily.
|
||||||
|
date: 2024-09-26T09:07:30Z
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
#featuredImg: reconcile-docker-with-deutsche-bahn.png
|
||||||
|
tags:
|
||||||
|
- network
|
||||||
|
- Deutsche Bahn
|
||||||
|
- Docker
|
||||||
|
- travel
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you happen to be a (Linux) power user travelling on a Deutsche Bahn [ICE
|
||||||
|
long-distance train][ice] and you wonder why you don't get a working network
|
||||||
|
connection with the complemenatary "WIFIonICE" wireless network, there may be a
|
||||||
|
network interference on your laptop.
|
||||||
|
|
||||||
|
There are multiple solutions for this on the web. My favorite solution is to
|
||||||
|
change Docker's network configuration by editing (or creating) the file
|
||||||
|
`/etc/docker/daemons.json`. However, you may need to prune existing Docker
|
||||||
|
networks first. Otherwise the Docker service will fail to start with the
|
||||||
|
following message:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
failed to start daemon: Error initializing network controller: error creating default "bridge" network: all predefined address pools have been fully subnetted
|
||||||
|
```
|
||||||
|
|
||||||
|
Therefore, I recommend the following steps (this is on a Fedora Linux system with *systemd*):
|
||||||
|
|
||||||
|
1. Stop any docker containers that you may have running, including services managed by docker compose.
|
||||||
|
2. Prune the networks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network prune
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create or edit `/etc/docker/daemon.json` so that it contains the following:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
{
|
||||||
|
"default-address-pools":
|
||||||
|
[
|
||||||
|
{"base":"172.19.0.0/16","size":24}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Connect to `WIFIonICE` and enjoy your network connection :-)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- StackExchange answer that provides the solution as presented here -- see also
|
||||||
|
the answer about pruning the networks below.
|
||||||
|
|
||||||
|
<https://unix.stackexchange.com/a/539258/110635>
|
||||||
|
|
||||||
|
- If you don't want to or cannot configure your Docker networks, you can also
|
||||||
|
turn Docker off and on again when you travel:
|
||||||
|
|
||||||
|
<https://kuttler.eu/code/ice-wlan-linux-docker/>
|
||||||
|
|
||||||
|
This is what I used to do myself, but nowadays I prefer to just configure the
|
||||||
|
Docker network differently.
|
||||||
|
|
||||||
|
- There are many more internet sources (and I have just added another one...), I
|
||||||
|
won't copy all those links into my post, see DuckDuckGo search results
|
||||||
|
for yourself:
|
||||||
|
|
||||||
|
<https://duckduckgo.com/?q=docker+wifi+on+ice>
|
||||||
|
|
||||||
|
[ice]: https://int.bahn.de/en/trains/long-distance-trains
|
After Width: | Height: | Size: 750 KiB |
BIN
content/posts/2025/install-pynitrokey/IMGP0069.jpg
Normal file
After Width: | Height: | Size: 213 KiB |
58
content/posts/2025/install-pynitrokey/index.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: Install pynitrokey on Fedora 41
|
||||||
|
description: Here's how I installed pynitrokey on my Fedora 41 KDE laptop, resolving missing dependencies.
|
||||||
|
date: 2025-03-30T10:53:35+0100
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- security
|
||||||
|
- Linux
|
||||||
|
- Fedora
|
||||||
|
---
|
||||||
|
|
||||||
|
{{< figure src="IMGP0069.jpg"
|
||||||
|
alt="My Nitrokey 3A mini"
|
||||||
|
title="My Nitrokey 3A mini on the right side of the picture" >}}
|
||||||
|
|
||||||
|
I use [Yubikey][]s and [Nitrokey][]s as a second factor during 2FA
|
||||||
|
authentication.
|
||||||
|
|
||||||
|
My experiences with Nitrokey devices have been mixed. Currently my 3A mini
|
||||||
|
stopped working. I wanted to investigate what's going on, only to find out that
|
||||||
|
the commandline tool `nitropy` no longer worked on my machine either! :-(
|
||||||
|
|
||||||
|
Here's how I got it to work again. I'm a Python noob, so maybe there's a better
|
||||||
|
way to install the dependencies. The operating system is Fedora 41.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install systemd-devel python3-devel
|
||||||
|
pipx install pynitrokey
|
||||||
|
```
|
||||||
|
|
||||||
|
Afterwards I was greeted with this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ nitropy nk3 test
|
||||||
|
Command line tool to interact with Nitrokey devices 0.8.1
|
||||||
|
Critical error:
|
||||||
|
No connected NK3 devices found
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Critical error occurred, exiting now
|
||||||
|
Unexpected? Is this a bug? Would you like to get support/help?
|
||||||
|
- You can report issues at: https://support.nitrokey.com/
|
||||||
|
- Writing an e-mail to support@nitrokey.com is also possible
|
||||||
|
- Please attach the log: '/tmp/nitropy.log._p6mpa3e' with any support/help request!
|
||||||
|
- Please check if you have udev rules installed: https://docs.nitrokey.com/nitrokeys/nitrokey3/firmware-update#troubleshooting-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
I made sure that I had the abovementioned `udev` rules in place. The key is not
|
||||||
|
detected by any system, be it my personal Linux laptop or any Windows machine
|
||||||
|
that I could access. [D'oh][]!
|
||||||
|
|
||||||
|
[Nitrokey]: https://www.nitrokey.com
|
||||||
|
[Yubikey]: https://www.yubico.com
|
||||||
|
[d'oh]: https://en.wikipedia.org/wiki/D'oh!
|
BIN
content/posts/2025/leaving-x/account-deactivated.png
Normal file
After Width: | Height: | Size: 48 KiB |
40
content/posts/2025/leaving-x/index.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Leaving X
|
||||||
|
description: German academic institutions are leaving Twitter/X. I'm not an academic institution, but still I'm following suit.
|
||||||
|
date: 2025-01-14T21:30:00+0100
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- academia
|
||||||
|
- social media
|
||||||
|
---
|
||||||
|
Several academic institutions in Germany have decided to terminate their
|
||||||
|
presence on X, formerly known as Twitter.
|
||||||
|
|
||||||
|
Press release of the "Informationsdienst Wissenschaft" (scientific news service,
|
||||||
|
in German): <https://idw-online.de/de/news845520>
|
||||||
|
|
||||||
|
Ironically, the press release page has several buttons to share this information
|
||||||
|
on social media, right now still including a sharing button for X :smile:
|
||||||
|
Surely just a matter of time.
|
||||||
|
|
||||||
|
{{< figure src="press-release-with-x-sharing-button.png"
|
||||||
|
title="Press release (in German) informing about German academic institutions leaving X, with X sharing button." >}}
|
||||||
|
|
||||||
|
I got aware of this by reading my former university's newsletter:
|
||||||
|
<https://www.uni-wuerzburg.de/aktuelles/einblick/single/news/universitaet-wuerzburg-kehrt-plattform-x-den-ruecken-1>
|
||||||
|
|
||||||
|
I'm following suit. I've just now deleted my account on X.
|
||||||
|
|
||||||
|
{{< figure src="account-deactivated.png"
|
||||||
|
title="I did it! My account on X has been deactivated. (Screenshot in German.)" >}}
|
||||||
|
|
||||||
|
If you would like to connect with me on social media, please head over to my profiles on [Mastodon][] or [Bluesky][].
|
||||||
|
|
||||||
|
See you!
|
||||||
|
|
||||||
|
[Mastodon]: https://neph.social/@daniel_kraus
|
||||||
|
[Bluesky]: https://bsky.app/profile/bovender.bsky.social
|
After Width: | Height: | Size: 105 KiB |
80
content/posts/2025/media-mounts-at-dm/index.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: "[In German] USB-Sticks in den Fotostationen von dm"
|
||||||
|
description:
|
||||||
|
date: 2025-03-22T22:20:07+0100
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- German
|
||||||
|
---
|
||||||
|
|
||||||
|
(This post is in German.)
|
||||||
|
|
||||||
|
In den letzten Wochen habe ich ein paar Mal Fotoabzüge benötigt und bin mit
|
||||||
|
einem USB-Stick zur örtlichen Drogerie. Zu meiner Überraschung konnten keinerlei
|
||||||
|
Bilder auf dem Stick gefunden werden. Inzwischen habe ich das mit mehreren
|
||||||
|
Sticks und an verschiedenen Foto-Stationen (aber immer in demselben Laden)
|
||||||
|
probiert, Fehlanzeige. Interessanterweise hat es mit einer SD-Karte problemlos
|
||||||
|
geklappt.
|
||||||
|
|
||||||
|
Offenbar bin ich weder der erste noch der einzige, der dieses Problem schon
|
||||||
|
einmal hatte, es gibt zu einem ähnlichen Problem mit einer CEWE-Fotostation
|
||||||
|
einen [Forumsthread][].
|
||||||
|
|
||||||
|
In diesem Blog-Beitrag sammele ich einfach mal meine Beobachtungen, vielleicht
|
||||||
|
hilft es ja einmal jemandem. Von Mal zu Mal werde ich dies updaten.
|
||||||
|
|
||||||
|
## Beispiele für "geht" und "geht nicht"
|
||||||
|
|
||||||
|
Ich hatte zuletzt zwei Speichermedien dabei, einen USB-Stick und eine SD-Karte:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
$ lsblk -f
|
||||||
|
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
|
||||||
|
sda
|
||||||
|
└─sda1 exfat 1.0 [REDACTED] E7F3-FDF9 114,5G 0% /run/media/daniel/[REDACTED]
|
||||||
|
sdb
|
||||||
|
└─sdb1 exfat 1.0 7FDE-A701 119G 0% /run/media/daniel/7FDE-A701
|
||||||
|
```
|
||||||
|
|
||||||
|
`sda` ist ein USB-Stick, den ich häufig in anderen Zusammenhängen im Einsatz
|
||||||
|
habe. `sdb` ist eine ältere SD-Karte. Beide haben nominell 128 GB Kapazität, und
|
||||||
|
beide haben eine Partitionstabelle im herkömmlichen MSDOS-Format und eine
|
||||||
|
Primärpartition, die mit dem Dateisystem EXFAT formatiert ist. Das Dateisystem
|
||||||
|
auf dem USB-Stick hat ein Label, welches aus wenigen Buchstaben ohne
|
||||||
|
Sonderzeichen besteht.
|
||||||
|
|
||||||
|
Auf beiden Speichermedien hatte ich denselben Foto-Ordner kopiert, mit etwa
|
||||||
|
einer Handvoll Fotos. Die Bilder lagen also auf beiden Medien in einem
|
||||||
|
Unterorder.
|
||||||
|
|
||||||
|
**Interessanterweise wurden die Bilder auf der SD-Karte gefunden, die Bilder auf
|
||||||
|
dem USB-Stick jedoch nicht.**
|
||||||
|
|
||||||
|
Hier meine Beobachtungen im einzelnen, denn entsprechende Überlegungen findet
|
||||||
|
man auch im Netz:
|
||||||
|
|
||||||
|
### Dateisystem
|
||||||
|
|
||||||
|
Bevor ich es sowohl mit dem Stick als auch mit der SD-Karte probierte, habe ich
|
||||||
|
folgende Dateisysteme auf dem USB-Stick ausprobiert:
|
||||||
|
|
||||||
|
- EXFAT
|
||||||
|
- FAT32
|
||||||
|
|
||||||
|
Mit beiden Dateisystemen wurden keine Bilder gefunden.
|
||||||
|
|
||||||
|
### Root-Verzeichnis vs. Unterverzeichnis
|
||||||
|
|
||||||
|
Es hat keinen Unterschied gemacht, ob ich die Bilder (JPG-Dateien) im
|
||||||
|
Wurzelverzeichnis oder in einem Unterordner auf dem Stick hatte.
|
||||||
|
|
||||||
|
### Verschiedene Terminals
|
||||||
|
|
||||||
|
Ich habe meinen USB-Stick an verschiedenen Foto-Terminals ausprobiert, an keinem
|
||||||
|
von ihnen wurden die Bilder gefunden.
|
||||||
|
|
||||||
|
[Forumsthread]: https://www.cewe-community.com/forum/fb/viewtopic.php?t=13036&p=135162
|
After Width: | Height: | Size: 130 KiB |
127
content/posts/2025/nvidia-drivers-on-fedora/index.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: "Nvidia Drivers on Fedora"
|
||||||
|
description: >
|
||||||
|
My own up-to-date instructions for installing Nvidia drivers on a Fedora Linux system.
|
||||||
|
date: 2025-03-02T08:28:26Z
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- nvidia
|
||||||
|
- fedora
|
||||||
|
- linux
|
||||||
|
---
|
||||||
|
|
||||||
|
My [Thinkpad P14s]({{< relref "p14s" >}}) has a dedicated Nvidia GPU, which
|
||||||
|
requires proprietary drivers. There is a gazillion of instructions on the web
|
||||||
|
for installing theses drivers on a Fedora system. Every now and then (with
|
||||||
|
kernel updates, I guess), my drivers stop working and I have to research _again_
|
||||||
|
how to (re-)install these drivers. Usually I notice this when developing raw
|
||||||
|
images with [Darktable][]. It gets dead slow and then I find out that OpenCL is
|
||||||
|
not working.
|
||||||
|
|
||||||
|
This post mainly serves as a reminder for myself how to re-install the NVidia
|
||||||
|
drivers on a Fedora laptop. It should be fairly up to date, because as I wrote,
|
||||||
|
I keep running into this problem over and over again.
|
||||||
|
|
||||||
|
The prerequisite is to have the [RPM Fusion][] repositories enabled.
|
||||||
|
|
||||||
|
{{< figure src="discover-settings-rpm-fusion.png" >}}
|
||||||
|
|
||||||
|
## Installing the NVidia drivers
|
||||||
|
|
||||||
|
I found out that I basically only need two packages:
|
||||||
|
|
||||||
|
- `nvidia-settings` and
|
||||||
|
- `xorg-x11-drv-nvidia-cuda`
|
||||||
|
|
||||||
|
The first one suffices to pull in the actual drivers and other packages as
|
||||||
|
dependencies. The latter is required in order for Darktable to make use of OpenCL.
|
||||||
|
|
||||||
|
```fish
|
||||||
|
sudo dnf install nvidia-settings xorg-x11-drv-nvidia-cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
```plain
|
||||||
|
Updating and loading repositories:
|
||||||
|
Repositories loaded.
|
||||||
|
Package Arch Version Repository Size
|
||||||
|
Installing:
|
||||||
|
nvidia-settings x86_64 3:570.86.16-1.fc41 rpmfusion-nonfree-updates 4.4 MiB
|
||||||
|
Installing dependencies:
|
||||||
|
akmod-nvidia x86_64 3:570.86.16-3.fc41 rpmfusion-nonfree-updates 92.4 KiB
|
||||||
|
egl-gbm x86_64 2:1.1.2^20240919gitb24587d-3.fc41 fedora 29.3 KiB
|
||||||
|
egl-wayland x86_64 1.1.18~20250114git26ba0e3-2.fc41 updates 80.9 KiB
|
||||||
|
egl-x11 x86_64 1.0.1~20241213git61e70b0-1.fc41 updates 161.1 KiB
|
||||||
|
nvidia-modprobe x86_64 3:570.86.16-1.fc41 rpmfusion-nonfree-updates 51.0 KiB
|
||||||
|
xorg-x11-drv-nvidia x86_64 3:570.86.16-5.fc41 rpmfusion-nonfree-updates 190.2 MiB
|
||||||
|
xorg-x11-drv-nvidia-kmodsrc x86_64 3:570.86.16-5.fc41 rpmfusion-nonfree-updates 75.4 MiB
|
||||||
|
xorg-x11-drv-nvidia-libs x86_64 3:570.86.16-5.fc41 rpmfusion-nonfree-updates 361.9 MiB
|
||||||
|
Installing weak dependencies:
|
||||||
|
xorg-x11-drv-nvidia-cuda-libs x86_64 3:570.86.16-5.fc41 rpmfusion-nonfree-updates 273.2 MiB
|
||||||
|
xorg-x11-drv-nvidia-power x86_64 3:570.86.16-5.fc41 rpmfusion-nonfree-updates 233.7 KiB
|
||||||
|
|
||||||
|
Transaction Summary:
|
||||||
|
Installing: 11 packages
|
||||||
|
|
||||||
|
Total size of inbound packages is 351 MiB. Need to download 114 KiB.
|
||||||
|
After this operation, 906 MiB extra will be used (install 906 MiB, remove 0 B).
|
||||||
|
Is this ok [y/N]:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the installation
|
||||||
|
|
||||||
|
```fish
|
||||||
|
inxi -G
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output the following if everything is configured correctly.
|
||||||
|
|
||||||
|
```plain
|
||||||
|
Graphics:
|
||||||
|
Device-1: Intel Meteor Lake-P [Intel Arc Graphics] driver: i915 v: kernel
|
||||||
|
Device-2: NVIDIA AD107GLM [RTX 500 Ada Generation Laptop GPU]
|
||||||
|
driver: nvidia v: 570.86.16
|
||||||
|
Device-3: Syntek Integrated Camera driver: uvcvideo type: USB
|
||||||
|
Display: wayland server: Xwayland v: 24.1.6 compositor: kwin_wayland
|
||||||
|
driver: gpu: i915 resolution: 1: 3840x2160~60Hz 2: 3072x1920
|
||||||
|
API: EGL v: 1.5 drivers: iris,nvidia
|
||||||
|
platforms: gbm,wayland,x11,surfaceless,device
|
||||||
|
API: OpenGL v: 4.6.0 compat-v: 4.6 vendor: intel mesa v: 25.0.0
|
||||||
|
renderer: Mesa Intel Arc Graphics (MTL)
|
||||||
|
API: Vulkan v: 1.4.304 drivers: N/A surfaces: xcb,xlib,wayland
|
||||||
|
Info: Tools: api: clinfo, eglinfo, glxinfo, vulkaninfo
|
||||||
|
de: kscreen-console,kscreen-doctor gpu: nvidia-settings,nvidia-smi
|
||||||
|
wl: wayland-info x11: xdriinfo, xdpyinfo, xprop, xrandr```
|
||||||
|
```
|
||||||
|
|
||||||
|
If the drivers aren't installed and loaded properly, the graphics card's name
|
||||||
|
will be some generic term, not the exact name of the model.
|
||||||
|
|
||||||
|
To test if Darktable can use the CUDA driver, start if from the commandline
|
||||||
|
like so:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
darktable -d opencl
|
||||||
|
```
|
||||||
|
|
||||||
|
This should output a lot of technical information about the graphics card.
|
||||||
|
|
||||||
|
If it doesn't, but instead complains along the lines of
|
||||||
|
|
||||||
|
> FINALLY: opencl is NOT AVAILABLE on this system
|
||||||
|
|
||||||
|
then there is something wrong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Addendum 2025-04-17
|
||||||
|
|
||||||
|
Yesterday I upgraded from Fedora 41 to Fedora 42 (KDE), and of course the NVIDIA
|
||||||
|
drivers were removed. Using the instructions above effortlessly installed the
|
||||||
|
driver (currently version 570). :-)
|
||||||
|
|
||||||
|
[Darktable]: https://www.darktable.org
|
||||||
|
[RPM fusion]: https://rpmfusion.org
|
After Width: | Height: | Size: 3.4 MiB |
After Width: | Height: | Size: 4.8 MiB |
36
content/posts/2025/schwanheimer-wiese/index.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: "Schwanheimer Wiese"
|
||||||
|
description: Two images from Schwanheimer Wiese, Frankfurt, Germany
|
||||||
|
date: 2025-03-04T06:21:18Z
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- photos
|
||||||
|
- landscape
|
||||||
|
- pentax
|
||||||
|
---
|
||||||
|
|
||||||
|
I have decided to publish some of my photos on my blog.
|
||||||
|
|
||||||
|
Here are the first two of them, both taken on
|
||||||
|
["Schwanheimer Wiese"][wiese] a meadow in the forests
|
||||||
|
of [Frankfurt-Schwanheim][Schwanheim].
|
||||||
|
|
||||||
|
{{< figure src="20250223_152944 IMGP9786.jpg" >}}
|
||||||
|
|
||||||
|
The "Struwwelpeter" is a tree named after a figure from
|
||||||
|
a famous [children's book][struwwelpeter]:
|
||||||
|
|
||||||
|
{{< figure src="20250223_153306 IMGP9790.jpg" >}}
|
||||||
|
|
||||||
|
Camera: Pentax K-3 APS-C, 18-55 mm kit lens (crop factor 1.5),
|
||||||
|
image developed in [Darktable][]. Raw image file available
|
||||||
|
upon reasonable request. All rights reserved.
|
||||||
|
|
||||||
|
[darktable]: https://darktable.org
|
||||||
|
[Schwanheim]: https://en.wikipedia.org/wiki/Schwanheim_(Frankfurt_am_Main)
|
||||||
|
[struwwelpeter]: https://en.wikipedia.org/wiki/Struwwelpeter
|
||||||
|
[wiese]: https://de.wikipedia.org/wiki/Frankfurt-Schwanheim#Schwanheimer_Wiese
|
BIN
content/posts/2025/soleier/20250414_203010.jpg
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
content/posts/2025/soleier/20250414_203911.jpg
Normal file
After Width: | Height: | Size: 289 KiB |
116
content/posts/2025/soleier/index.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
title: "Soleier: Pickled eggs for Easter"
|
||||||
|
description: >
|
||||||
|
Pickled eggs – "Soleier" – are a favorite Easter tradition in my
|
||||||
|
family. Here's my personal recipe.
|
||||||
|
date: 2025-04-17T17:55:07+02:00
|
||||||
|
draft: false
|
||||||
|
# ShowLastmod: true
|
||||||
|
toc: false
|
||||||
|
scrolltotop: true
|
||||||
|
images: []
|
||||||
|
tags:
|
||||||
|
- recipes
|
||||||
|
---
|
||||||
|
|
||||||
|
My family has a long-running tradition of producing pickled eggs for Easter
|
||||||
|
breakfast. We call these "Soleier" which is a composite of "Sole" (brine) and
|
||||||
|
"Eier" (eggs). Varous theories exist how pickled eggs came into existence, and
|
||||||
|
there are various ways to relish these eggs. As for the theories, the reader is
|
||||||
|
kindly referred to the [Wikipedia article][pickled eggs]
|
||||||
|
(also [in German][Soleier]).
|
||||||
|
|
||||||
|
As for the recipe and the way we eat pickled eggs in my family: read on.
|
||||||
|
|
||||||
|
{{< figure src="20250414_203010.jpg"
|
||||||
|
alt="My 2025 Soleier preparation."
|
||||||
|
caption="My 2025 Soleier preparation. Smartphone raw image file edited with darktable."
|
||||||
|
>}}
|
||||||
|
|
||||||
|
## How many eggs?
|
||||||
|
|
||||||
|
How many eggs you want to prepare depends on the number of people who want to
|
||||||
|
eat the pickled eggs, the number of days you want to have them for breakfast (or
|
||||||
|
any other meal to your liking) and last, but not least, what size of jar you
|
||||||
|
have to prepare them in.
|
||||||
|
|
||||||
|
My 1.5 L [Weck jar][] holds 13 medium-sized eggs.
|
||||||
|
|
||||||
|
Typically an adult will have 2 of those eggs for breakfast. With 13 eggs, this
|
||||||
|
leaves one spare egg (and toast), but there's usually a kid or two who doesn't
|
||||||
|
eat these eggs in pairs (if at all).
|
||||||
|
|
||||||
|
{{< figure src="20250414_203911.jpg"
|
||||||
|
alt="Pickled eggs with cracks."
|
||||||
|
caption="I love my pickled eggs to have cracks so that they let some of the brine in."
|
||||||
|
>}}
|
||||||
|
|
||||||
|
## Recipe
|
||||||
|
|
||||||
|
### Ingredients and materials
|
||||||
|
|
||||||
|
This recipe is for 13 eggs and a 1.5 L jar.
|
||||||
|
|
||||||
|
- 13 eggs
|
||||||
|
- 0.75 L water
|
||||||
|
- 50 g salt
|
||||||
|
- 1-2 tablespoons of caraway seeds (if you like)
|
||||||
|
- 1.5 L [Weck jar][] (doesn't need to be original Weck brand, of course, but must be sealed tight)
|
||||||
|
- 13 slices of toast
|
||||||
|
- butter
|
||||||
|
- oil, e.g. rapeseed oil, to your liking
|
||||||
|
- vinegar, e.g. any herb-flavored variant, to your liking
|
||||||
|
|
||||||
|
### Directions
|
||||||
|
|
||||||
|
You should prepare the pickled eggs a few days before you want to eat them. In
|
||||||
|
my family we typically do this 7 days before Easter, i.e. on Palm Sunday.
|
||||||
|
|
||||||
|
1. Boil the eggs so they get hard. I usually place the eggs in a pot, cover them
|
||||||
|
with water, and then slowly heat the water until it boils. Let it boil until
|
||||||
|
10 minutes have passed since the eggs were placed on the stove. Your mileage
|
||||||
|
may vary depending on your stove.
|
||||||
|
2. Pour out the hot water and shock the eggs with cold tap water.
|
||||||
|
3. Place the eggs into the jar using a clean spoon (not your hands!). I usually
|
||||||
|
let them drop into the jar so they get nice little cracks, this makes for
|
||||||
|
beautiful texture inside the eggs (see image).
|
||||||
|
4. Add the salt and caraway seeds to the jar.
|
||||||
|
5. Bring the water to boiling and pour it over the eggs in the jar.
|
||||||
|
6. Close the jar.
|
||||||
|
7. Invert the jar a couple of times (you may want to wear gloves while doing
|
||||||
|
this, the jar is hot!).
|
||||||
|
8. Store the jar in a cool place -- in our house, this is usually the basement.
|
||||||
|
|
||||||
|
## How to eat Soleier
|
||||||
|
|
||||||
|
There seem to be two fundamentally different ways to eat a _Solei_.
|
||||||
|
|
||||||
|
### Our way of eating Soleier
|
||||||
|
|
||||||
|
My family's tradition is to peel two eggs, place them on a dish, cut them into
|
||||||
|
pieces, then pour some oil and vinegar over the pieces and mash the eggs with a
|
||||||
|
fork. This leaves an ugly mess on the plate and I will update this post with a
|
||||||
|
picture of this year's appearance.
|
||||||
|
|
||||||
|
Brown two slices of toast, spread butter on them, and eat the _Solei_ mash/mess
|
||||||
|
using a fork while intermittently taking a bite from the bread.
|
||||||
|
|
||||||
|
### The more sophisticated way to eat Soleier
|
||||||
|
|
||||||
|
There is an alternative way to have your pickled eggs that supposedly is less
|
||||||
|
messy. The [German Wikipedia article][Soleier] claims it's the 'proper' or let's
|
||||||
|
say usual way to eat these eggs. I tried it once and I was totally unhappy with
|
||||||
|
the experience, so I stick with our traditional way.
|
||||||
|
|
||||||
|
Peel two eggs, cut the eggs into halves, remove the egg yolk and mash the egg
|
||||||
|
yolk with oil and vinegar. Transfer the mashed egg yolk back into the eggs. I
|
||||||
|
will look slightly less ugly, but I promise you will have a hard time getting
|
||||||
|
all the egg yolk--oil--vinegar mixture back into the eggs, the plate will look
|
||||||
|
like someone's been having _Soleier_ the traditional way, and when trying to put
|
||||||
|
the egg halves into your mouth, be prepared for everyone else who sits at the
|
||||||
|
table with you bursting into spontaneous laughter. At least that's what happened
|
||||||
|
to me one time... ;-)
|
||||||
|
|
||||||
|
[pickled eggs]: https://en.wikipedia.org/wiki/Pickled_egg
|
||||||
|
[Soleier]: https://de.wikipedia.org/wiki/Solei
|
||||||
|
[Weck jar]: https://en.wikipedia.org/wiki/Weck_jar
|
@ -1,15 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
build:
|
build:
|
||||||
image: hugomods/hugo:latest
|
image: hugomods/hugo:ci
|
||||||
command: hugo
|
command: hugo
|
||||||
volumes:
|
volumes:
|
||||||
- ".:/src"
|
- ".:/src"
|
||||||
user: 1000:1000
|
user: 1000:1000
|
||||||
server:
|
server:
|
||||||
image: hugomods/hugo:latest
|
image: hugomods/hugo:ci
|
||||||
command: server --buildDrafts --buildFuture
|
command: server --buildDrafts --buildFuture
|
||||||
volumes:
|
volumes:
|
||||||
- ".:/src"
|
- ".:/src"
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:1313:1313"
|
- "127.0.0.1:1313:1313"
|
||||||
user: 1000:1000
|
user: 1000:1000
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
17
layouts/partials/comments.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
<div id='discourse-comments'></div>
|
||||||
|
<meta name='discourse-username' content='DISCOURSE_USERNAME'>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
DiscourseEmbed = {
|
||||||
|
discourseUrl: 'https://discuss.bovender.de/',
|
||||||
|
discourseEmbedUrl: '{{ .Permalink }}',
|
||||||
|
// className: 'CLASS_NAME',
|
||||||
|
};
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
|
||||||
|
d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
|
||||||
|
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
|
||||||
|
})();
|
||||||
|
</script>
|
1
layouts/partials/extra-head.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<link rel="me" href="https://neph.social/@daniel_kraus">
|
@ -9,10 +9,10 @@
|
|||||||
{{- with .Params.categories }}
|
{{- with .Params.categories }}
|
||||||
<p>{{- partial "svg.html" (dict "context" . "name" "posts_single_categories") -}}{{- range . -}}<span class="category"><a href="{{ "categories/" | absLangURL }}{{ . | urlize }}">{{.}}</a></span>{{- end }}</p>
|
<p>{{- partial "svg.html" (dict "context" . "name" "posts_single_categories") -}}{{- range . -}}<span class="category"><a href="{{ "categories/" | absLangURL }}{{ . | urlize }}">{{.}}</a></span>{{- end }}</p>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
<p>{{- partial "svg.html" (dict "context" . "name" "posts_single_date") }}{{ dateFormat .Site.Params.dateformNumTime .Date.Local -}}
|
<p>{{- partial "svg.html" (dict "context" . "name" "posts_single_date") }}{{ .Date.Format .Site.Params.dateformNumTime }}
|
||||||
{{- if and (not (eq .Page.Params.ShowLastmod nil)) (.Page.Params.ShowLastmod) -}}
|
{{- if and (not (eq .Page.Params.ShowLastmod nil)) (.Page.Params.ShowLastmod) -}}
|
||||||
{{- if and .GitInfo .Site.Params.gitUrl -}}
|
{{ if and .GitInfo .Site.Params.gitUrl }}
|
||||||
[{{- partial "svg.html" (dict "context" . "name" "posts_single_git_commit") -}}<a href="{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}" target="_blank" rel="noopener">{{ .GitInfo.AbbreviatedHash -}}</a> @ {{ dateFormat .Site.Params.dateformNum .GitInfo.AuthorDate.Local -}}]
|
({{- partial "svg.html" (dict "context" . "name" "posts_single_git_commit") -}}<a href="{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}" target="_blank" rel="noopener">{{ .GitInfo.AbbreviatedHash -}}</a> @ {{ .GitInfo.AuthorDate.Local.Format .Site.Params.dateformNum -}})
|
||||||
{{- else if not (eq .Lastmod .Date ) -}}
|
{{- else if not (eq .Lastmod .Date ) -}}
|
||||||
[{{.Site.Params.initialPublish | default "Initial Published on : "}} {{ dateFormat .Site.Params.dateformNumTime .Lastmod.Local -}}]
|
[{{.Site.Params.initialPublish | default "Initial Published on : "}} {{ dateFormat .Site.Params.dateformNumTime .Lastmod.Local -}}]
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
|
12
layouts/shortcodes/git-info.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<p>
|
||||||
|
{{ partial "svg.html" (dict "context" . "name" "posts_single_date") }} {{ dateFormat .Site.Params.dateformNumTime .Page.Date.Local }}
|
||||||
|
{{ if and (not (eq .Page.Params.ShowLastmod nil)) (.Page.Params.ShowLastmod) }}
|
||||||
|
{{ if and .GitInfo .Site.Params.gitUrl }}
|
||||||
|
[{{ partial "svg.html" (dict "context" . "name" "posts_single_git_commit") }}<a href="{{ .Site.Params.gitUrl -}}{{ .Page.GitInfo.Hash }}" target="_blank" rel="noopener">{{ .Page.GitInfo.AbbreviatedHash -}}</a> @ {{ dateFormat .Site.Params.dateformNum .Page.GitInfo.AuthorDate.Local }}]
|
||||||
|
{{ else if not (eq .Page.Lastmod .Page.Date ) }}
|
||||||
|
[{{.Site.Params.initialPublish | default "Initial Published on : "}} {{ dateFormat .Site.Params.dateformNumTime .Page.Lastmod.Local }}]
|
||||||
|
{{ else }}
|
||||||
|
{{ errorf "Lastmod is not found in Page Frontmatter or Lastmod is same as Date" }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
48
static/bovender.svg
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
static/fonts/IBMPlexMono-Bold.woff2
Normal file
BIN
static/fonts/IBMPlexMono-BoldItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-ExtraLight.woff2
Normal file
BIN
static/fonts/IBMPlexMono-ExtraLightItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Italic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Light.woff2
Normal file
BIN
static/fonts/IBMPlexMono-LightItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Medium.woff2
Normal file
BIN
static/fonts/IBMPlexMono-MediumItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Regular.woff2
Normal file
BIN
static/fonts/IBMPlexMono-SemiBold.woff2
Normal file
BIN
static/fonts/IBMPlexMono-SemiBoldItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Text.woff2
Normal file
BIN
static/fonts/IBMPlexMono-TextItalic.woff2
Normal file
BIN
static/fonts/IBMPlexMono-Thin.woff2
Normal file
BIN
static/fonts/IBMPlexMono-ThinItalic.woff2
Normal file
BIN
static/fonts/IBMPlexSans-Bold.woff2
Normal file
BIN
static/fonts/IBMPlexSans-BoldItalic.woff2
Normal file
BIN
static/fonts/IBMPlexSans-Italic.woff2
Normal file
BIN
static/fonts/IBMPlexSans-Light.woff2
Normal file
BIN
static/fonts/IBMPlexSans-LightItalic.woff2
Normal file
BIN
static/fonts/IBMPlexSans-Medium.woff2
Normal file
BIN
static/fonts/IBMPlexSans-MediumItalic.woff2
Normal file
BIN
static/fonts/IBMPlexSans-Regular.woff2
Normal file
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
pushd /home/daniel/local/Code/blog
|
pushd /home/daniel/local/Code/blog
|
||||||
docker compose up build && rsync -av --delete public/* bovender.de:/var/www/bovender/
|
docker compose up build && rsync -av --delete public/* bovender.de:/var/docker-data/nginx/www/bovender/
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|