Compare commits
25 Commits
786418496b
...
columns
Author | SHA1 | Date | |
---|---|---|---|
42698372ad | |||
eddde95b57 | |||
b5b9e68df0 | |||
5bcad7eb77 | |||
2be883b19f | |||
98f61d6d2f | |||
4ebf756c88 | |||
0bea1250bc | |||
175c9e77c9 | |||
64a5cdd775 | |||
9ee0254509 | |||
fc2afda5d0 | |||
63842070cc | |||
1a98f7a163 | |||
5b0f01d804 | |||
38607ec437 | |||
8fb1b29aef | |||
21c25413fc | |||
3e0e0beb0f | |||
fac9de10aa | |||
e9c31546ab | |||
be19efb887 | |||
43b28bc982 | |||
01e83795ad | |||
bfd988a53e |
343
content/backup-strategy.md
Normal file
343
content/backup-strategy.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
Title: My Backup Strategy
|
||||||
|
Date: 2021-04-08
|
||||||
|
Category: Writing
|
||||||
|
Summary: Details about the backup system for all of my data.
|
||||||
|
Wide: true
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
Regularly backing up all the data I care about is very important to me. This
|
||||||
|
article outlines my strategy to make sure I never lose essential data.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Backups should be as automatic as possible. This ensures laziness and
|
||||||
|
forgetfulness won't interfere with the regularity.
|
||||||
|
|
||||||
|
All software used to create and store the backups should be free and open source
|
||||||
|
so I'm not depending on the survival of a company.
|
||||||
|
|
||||||
|
Backups need to be tested to ensure they are correct and happening regularly.
|
||||||
|
Multiple copies of the backups should exist, including at least one offsite to
|
||||||
|
protect against my building burning down.
|
||||||
|
|
||||||
|
Backups should also be incremental when possible (rather than mirror copies) so
|
||||||
|
an accidental deletion isn't propagated into the backups, making the file
|
||||||
|
irrecoverable.
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
|
||||||
|
I have one backup folder `/mnt/backup` on my media server at home that serves as
|
||||||
|
the destination for all my backup sources. All scheduled automatic backups write
|
||||||
|
to their own subfolder inside of it.
|
||||||
|
|
||||||
|
This backup folder is then synced to encrypted 2.5" 1 TB hard drives which I
|
||||||
|
rotate between my bag, offsite, and my parents' house.
|
||||||
|
|
||||||
|
## Backup Sources
|
||||||
|
|
||||||
|
I use the tool `rdiff-backup` extensively because it allows me to take
|
||||||
|
incremental backups locally or over SSH. It acts very similar to `rsync` and has
|
||||||
|
no configuration.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
I have every email since 2010 backed up continuously in case my email provider
|
||||||
|
disappears.
|
||||||
|
|
||||||
|
I use `offlineimap` to sync my mail to the directory `~/email` on my media
|
||||||
|
server as a Maildir. Since offlineimap is only a syncing tool, the emails need
|
||||||
|
to be copied elsewhere to be backed up. I run `rdiff-backup` from a weekly cron
|
||||||
|
job:
|
||||||
|
|
||||||
|
<span class="aside">I'll explain what backup_check.txt does below</span>
|
||||||
|
|
||||||
|
```
|
||||||
|
*/15 * * * * offlineimap > /var/log/offlineimap.log 2>&1
|
||||||
|
00 12 * * 1 date -Iseconds > /home/email/email/backup_check.txt
|
||||||
|
|
||||||
|
20 12 * * 1 rdiff-backup /home/email/email /mnt/backup/local/email/
|
||||||
|
40 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/email/
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's my `.offlineimaprc` for reference:
|
||||||
|
|
||||||
|
```
|
||||||
|
[general]
|
||||||
|
accounts = main
|
||||||
|
[Account main]
|
||||||
|
localrepository = Local
|
||||||
|
remoterepository = Remote
|
||||||
|
[Repository Local]
|
||||||
|
type = Maildir
|
||||||
|
localfolders = ~/email
|
||||||
|
[Repository Remote]
|
||||||
|
type = IMAP
|
||||||
|
readonly = True
|
||||||
|
folderfilter = lambda foldername: foldername not in ['Trash', 'Spam', 'Drafts']
|
||||||
|
remotehost = example.com
|
||||||
|
remoteuser = mail@example.com
|
||||||
|
remotepass = supersecret
|
||||||
|
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
I use Standard Notes to take notes and wrote the tool
|
||||||
|
[standardnotes-fs](https://github.com/tannercollin/standardnotes-fs) to mount my
|
||||||
|
notes as a file system to view and edit them as plain text files.
|
||||||
|
|
||||||
|
I take weekly backups of the mounted file system on my media server with cron:
|
||||||
|
|
||||||
|
```
|
||||||
|
00 12 * * 1 date -Iseconds > /home/notes/notes/backup_check.txt
|
||||||
|
15 12 * * 1 rdiff-backup /home/notes/notes /mnt/backup/local/notes/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
|
||||||
|
I self-host a Nextcloud instance to store all my personal documents (non-code
|
||||||
|
projects, tax forms, spreadsheets, etc.). Since it's only a syncing software,
|
||||||
|
the files need to be copied elsewhere to be backed up.
|
||||||
|
|
||||||
|
I take weekly backups of the Nextcloud data folder with cron:
|
||||||
|
|
||||||
|
```
|
||||||
|
00 12 * * 1 rdiff-backup /var/www/nextcloud/data/tanner/files /mnt/backup/local/nextcloud/
|
||||||
|
30 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/nextcloud/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
I self-host a Gitea instance to store all my git repositories for code-based
|
||||||
|
projects. My home folder is also a git repo so I can easily sync my config files
|
||||||
|
and password database between servers and machines.
|
||||||
|
|
||||||
|
I take weekly backups of the Gitea data folder with cron:
|
||||||
|
|
||||||
|
```
|
||||||
|
00 12 * * 1 date -Iseconds > /home/gitea/gitea/data/backup_check.txt
|
||||||
|
10 12 * * 1 rdiff-backup --exclude **data/indexers --exclude **data/sessions /home/gitea/gitea/data /mnt/backup/local/gitea/
|
||||||
|
35 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/gitea/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram
|
||||||
|
|
||||||
|
Telegram Messenger is my main app for communication. My parents, most of my
|
||||||
|
friends, and friend groups are on there so I don't want to lose those messages
|
||||||
|
in case Telegram disappears or my account gets banned.
|
||||||
|
|
||||||
|
<span class="aside">Saves the messages to a sqlite db</span>
|
||||||
|
|
||||||
|
Telegram includes a data export feature, but it can't be automated. Instead I
|
||||||
|
run the deprecated software
|
||||||
|
[telegram-export](https://github.com/expectocode/telegram-export) hourly with
|
||||||
|
cron:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 * * * * bash -c 'timeout 50m /home/tanner/opt/telegram-export/env/bin/python -m telegram_export' > /var/log/telegramexport.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
It likes to hang, so `timeout` kills it if it's still running after 50 minutes.
|
||||||
|
Hasn't corrupted the database yet.
|
||||||
|
|
||||||
|
### Phone
|
||||||
|
|
||||||
|
[Signal
|
||||||
|
Messenger](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&hl=en_CA&gl=US)
|
||||||
|
automatically exports a copy of my text messages database, and
|
||||||
|
[Aegis](https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis&hl=en_CA&gl=US)
|
||||||
|
allows me to export an encrypted JSON file of my two-factor authentication
|
||||||
|
codes.
|
||||||
|
|
||||||
|
I mount my phone's internal storage as a file system on my desktop using
|
||||||
|
[adbfs-rootless](https://github.com/spion/adbfs-rootless). I then rsync the
|
||||||
|
files over to my media server:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./adbfs ~/mntphone
|
||||||
|
$ time rsync -Wav \
|
||||||
|
--exclude '*cache' --exclude nobackup \
|
||||||
|
--exclude '*thumb*' --exclude 'Telegram *' \
|
||||||
|
--exclude 'collection.media' \
|
||||||
|
--exclude 'org.thunderdog.challegram' \
|
||||||
|
--exclude '.trashed-*' --exclude '.pending-*' \
|
||||||
|
~/mntphone/storage/emulated/0/ \
|
||||||
|
localmediaserver:/mnt/backup/files/phone/
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately this is a manual process because I need to plug my phone in each
|
||||||
|
time. Ideally it would happen automatically while I'm asleep and the phone is
|
||||||
|
charging.
|
||||||
|
|
||||||
|
### Miscellaneous Files
|
||||||
|
|
||||||
|
The directory `/backup/files` is a repository for any kind of files I want to
|
||||||
|
keep forever. My phone data, old archives, computer files, Minecraft worlds,
|
||||||
|
files from previous jobs, and so on.
|
||||||
|
|
||||||
|
All the files will be included in the 1 TB hard drive backup rotations.
|
||||||
|
|
||||||
|
### Web Services
|
||||||
|
|
||||||
|
Web services that I run like [txt.t0.vc](https://txt.t0.vc) and
|
||||||
|
[QotNews](https://news.t0.vc) are backed up daily, weekly, and monthly depending
|
||||||
|
on how frequently the data changes.
|
||||||
|
|
||||||
|
I run `rdiff-backup` on the remote server with cron:
|
||||||
|
|
||||||
|
```
|
||||||
|
00 14 * * * date -Iseconds > /home/tanner/tbot/t0txt/data/backup_check.txt
|
||||||
|
|
||||||
|
04 14 * * * rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/daily/t0txt/
|
||||||
|
14 14 * * * rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/daily/t0txt/
|
||||||
|
|
||||||
|
24 14 * * 1 rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/weekly/t0txt/
|
||||||
|
34 14 * * 1 rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/weekly/t0txt/
|
||||||
|
|
||||||
|
44 14 1 * * rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/monthly/t0txt/
|
||||||
|
55 14 1 * * rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/monthly/t0txt/
|
||||||
|
```
|
||||||
|
|
||||||
|
The `tbotbak` user has write access to the `/mnt/backup/remote/tbotbak`
|
||||||
|
directory only. It has its own passwordless SSH key that's only permitted to run
|
||||||
|
the `rdiff-backup --server` command for security.
|
||||||
|
|
||||||
|
### Protospace
|
||||||
|
|
||||||
|
I run a lot of services for [Protospace](https://protospace.ca/), my city's
|
||||||
|
makerspace.
|
||||||
|
|
||||||
|
The member portal I wrote called [Spaceport](https://my.protospace.ca/) creates
|
||||||
|
an archive I download daily:
|
||||||
|
|
||||||
|
```
|
||||||
|
40 10 * * * wget --content-disposition \
|
||||||
|
--header="Authorization: secretkeygoeshere" \
|
||||||
|
--directory-prefix /mnt/backup/remote/portalbak/ \
|
||||||
|
--no-verbose --append-output=/var/log/portalbackup.log \
|
||||||
|
https://api.my.protospace.ca/backup/
|
||||||
|
```
|
||||||
|
|
||||||
|
The website and [wiki](https://wiki.protospace.ca) that I sysadmin get
|
||||||
|
backed up weekly:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 12 * * 1 mysqldump --all-databases > /var/www/dump.sql
|
||||||
|
15 12 * * 1 date -Iseconds > /var/www/backup_check.txt
|
||||||
|
20 12 * * 1 rdiff-backup /var/www pshostbak@remotebackup::/mnt/backup/remote/pshostbak/weekly/www/
|
||||||
|
```
|
||||||
|
|
||||||
|
The Protospace [Minecraft
|
||||||
|
server](http://games.protospace.ca:8123/?worldname=world&mapname=flat&zoom=3&x=74&y=64&z=354)
|
||||||
|
I run gets backed up daily:
|
||||||
|
|
||||||
|
```
|
||||||
|
00 15 * * * date -Iseconds > /home/tanner/minecraft/backup_check.txt
|
||||||
|
00 15 * * * rdiff-backup --exclude **CoreProtect --exclude **dynmap /home/tanner/minecraft psminebak@remotebackup::/mnt/backup/remote/psminebak/
|
||||||
|
30 15 * * * rdiff-backup --remove-older-than 12B --force psminebak@remotebackup::/mnt/backup/remote/psminebak/
|
||||||
|
```
|
||||||
|
|
||||||
|
I also back up our Google Drive with rclone:
|
||||||
|
|
||||||
|
```
|
||||||
|
45 12 * * 1 rclone copy -v protospace: /mnt/backup/files/protospace/google-drive/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup Copies
|
||||||
|
|
||||||
|
My backup folder `/mnt/backup` now looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/backup/
|
||||||
|
├── files
|
||||||
|
│ ├── docs
|
||||||
|
│ ├── phone
|
||||||
|
│ ├── protospace
|
||||||
|
│ ├── telegram
|
||||||
|
│ ├── usbsticks
|
||||||
|
│ └── ... and so on
|
||||||
|
├── local
|
||||||
|
│ ├── email
|
||||||
|
│ ├── gitea
|
||||||
|
│ ├── nextcloud
|
||||||
|
│ └── notes
|
||||||
|
└── remote
|
||||||
|
├── portalbak
|
||||||
|
├── pshostbak
|
||||||
|
├── psminebak
|
||||||
|
├── tbotbak
|
||||||
|
└── telebak
|
||||||
|
```
|
||||||
|
|
||||||
|
This directory tree is the master backup and I make a copy of the entire tree
|
||||||
|
every Saturday to a hard drive.
|
||||||
|
|
||||||
|
The directory is copied over with the following script:
|
||||||
|
|
||||||
|
```text
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cryptsetup luksOpen /dev/sdf external
|
||||||
|
mount /dev/mapper/external /mnt/external
|
||||||
|
|
||||||
|
time rsync -av --delete /mnt/backup/local/ /mnt/external/backup/local/
|
||||||
|
time rsync -av --delete /mnt/backup/remote/ /mnt/external/backup/remote/
|
||||||
|
time rdiff-backup --force -v5 /mnt/backup/files/ /mnt/external/backup/files/
|
||||||
|
|
||||||
|
python3 /home/tanner/scripts/checkbackup.py
|
||||||
|
|
||||||
|
umount /mnt/external
|
||||||
|
cryptsetup luksClose external
|
||||||
|
```
|
||||||
|
|
||||||
|
I wrote a Python script `checkbackup.py` that goes through each backup and
|
||||||
|
compares the timestamp in `backup_check.txt` files to the current time. This
|
||||||
|
makes sure that the cron ran, backups were taken, and transferred over
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
## Rotating Hard Drives
|
||||||
|
|
||||||
|
I rotate through 2.5" 1 TB hard drives each Saturday when I do a backup. They
|
||||||
|
are quite cheap at [$65 CAD](https://www.memoryexpress.com/Products/MX65194)
|
||||||
|
each so I can have a bunch floating around.
|
||||||
|
|
||||||
|
|
||||||
|
I keep one connected to the server, one in my bag, one offsite, one at my
|
||||||
|
mother's house, and one at my dad's house. Every Saturday I run the script above
|
||||||
|
to take a copy and then swap the drive with the one in my bag. It then gets
|
||||||
|
<span class="aside">I go back home about twice per year</span>
|
||||||
|
swapped when I visit my offsite location. Same for when I visit my parents. This
|
||||||
|
means that all hard drives eventually get rotated through with new data and
|
||||||
|
don't sit too long unpowered.
|
||||||
|
|
||||||
|
The drives are all encrypted with full-disk LUKS encryption using a password I'm
|
||||||
|
unlikely to forget.
|
||||||
|
|
||||||
|
I run the check-summing `btrfs` file system on them in RAID-1 to protect against
|
||||||
|
bitrot. This means I can only use 0.5 TB of storage for my backups, but the data
|
||||||
|
is stored redundantly.
|
||||||
|
|
||||||
|
Here's how I set up new hard drives to do this:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo cryptsetup luksOpen /dev/sdf external
|
||||||
|
$ sudo mkfs.btrfs -f -m dup -d dup /dev/mapper/external
|
||||||
|
$ sudo mount /dev/mapper/external /mnt/external/
|
||||||
|
$ sudo mkdir /mnt/external/backup
|
||||||
|
$ sudo chown -R tanner:tanner /mnt/external/backup
|
||||||
|
$ sudo umount /mnt/external
|
||||||
|
$ sudo cryptsetup luksClose external
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
I'm working on a system to automatically back up all my home directories to my
|
||||||
|
media server. I need this to grab Bash histories and code that's
|
||||||
|
work-in-progress. I've been burned by not having this once when a server died.
|
||||||
|
|
||||||
|
I'd like to automate backing up my phone by connecting it to a Raspberry Pi when
|
||||||
|
I go to sleep.
|
||||||
|
|
||||||
|
I need to get better at fully testing my backups by restoring them on a blank
|
||||||
|
machine.
|
392
content/bypassing-ports.md
Normal file
392
content/bypassing-ports.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
Title: Bypassing ISP Blocked Ports
|
||||||
|
Date: 2021-04-10
|
||||||
|
Category: Writing
|
||||||
|
Summary: Bypass ISP blocked ports using VPN port forwarding for public access.
|
||||||
|
Wide: true
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
My residential ISP blocks inbound traffic to common ports like 22, 80, and 443.
|
||||||
|
I use an OpenVPN tunnel to forward these ports so that I can self-host a
|
||||||
|
public media server. It does __not__ require users to be on the VPN.
|
||||||
|
|
||||||
|
This article explains how I set it up and is targeted towards Linux sysadmins.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
I have a cheap $5 per month virtual server with [Digital
|
||||||
|
Ocean](https://digitalocean.com) that runs Debian GNU/Linux 10. An OpenVPN
|
||||||
|
server is running on this virtual server.
|
||||||
|
|
||||||
|
My media server at home has an OpenVPN client connected to the server and is
|
||||||
|
assigned a static IP on the VPN network.
|
||||||
|
|
||||||
|
The virtual server has routing enabled and forwards inbound traffic __from the
|
||||||
|
internet__ to my media server at home. This allows me to have external HTTP and SSH
|
||||||
|
access.
|
||||||
|
|
||||||
|
## Server Setup
|
||||||
|
|
||||||
|
Spin up a Debian 10 virtual server on your favourite hosting provider and set
|
||||||
|
your user up as you would normally. You should probably harden this server.
|
||||||
|
Assign a subdomain to it like `vpn.example.com`.
|
||||||
|
|
||||||
|
Install the following requirements:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo apt update
|
||||||
|
$ sudo apt install openvpn ufw
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenVPN Server
|
||||||
|
|
||||||
|
These steps roughly follow [this
|
||||||
|
guide](https://wiki.debian.org/OpenVPN#TLS-enabled_VPN).
|
||||||
|
|
||||||
|
Generate TLS certificates and keys:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd /etc/openvpn
|
||||||
|
$ sudo openvpn --genkey --secret static.key
|
||||||
|
$ sudo make-cadir easy-rsa/
|
||||||
|
$ sudo chown -R tanner:tanner easy-rsa/
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `tanner` with your own username, this is temporary.
|
||||||
|
|
||||||
|
<span class="aside">The `.rnd` file prevents a warning</span>
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd easy-rsa/
|
||||||
|
$ ./easyrsa init-pki
|
||||||
|
$ head /dev/urandom > pki/.rnd
|
||||||
|
$ ./easyrsa build-ca
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter a password you won't forget in case you want to add another client later.
|
||||||
|
The Common Name you choose is not important.
|
||||||
|
|
||||||
|
Generate Diffie–Hellman params:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./easyrsa gen-dh
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a server cert:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./easyrsa build-server-full server nopass
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a client cert:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./easyrsa build-client-full mediaserver nopass
|
||||||
|
```
|
||||||
|
|
||||||
|
We make a `mediaserver` client because we want to assign a static IP to it. You
|
||||||
|
need to make a different one for each client you want with a static IP.
|
||||||
|
|
||||||
|
Also, if you want generic clients that all get dynamic IPs for use on your
|
||||||
|
laptop, phone, etc. to protect you from public WiFi, create only a single extra one:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./easyrsa build-client-full client nopass # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave off `nopass` if you want to password protect the config file keys when you
|
||||||
|
set up a new client.
|
||||||
|
|
||||||
|
Create the server config file `/etc/openvpn/server.conf`:
|
||||||
|
|
||||||
|
<span class="aside">Can't use port 443 here since it'll be forwarded</span>
|
||||||
|
|
||||||
|
```
|
||||||
|
port 1194
|
||||||
|
proto udp
|
||||||
|
dev tun
|
||||||
|
topology subnet
|
||||||
|
ca /etc/openvpn/easy-rsa/pki/ca.crt
|
||||||
|
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
|
||||||
|
key /etc/openvpn/easy-rsa/pki/private/server.key
|
||||||
|
dh /etc/openvpn/easy-rsa/pki/dh.pem
|
||||||
|
tls-auth /etc/openvpn/static.key 0
|
||||||
|
client-config-dir /etc/openvpn/ccd
|
||||||
|
server 10.8.0.0 255.255.255.0
|
||||||
|
client-to-client
|
||||||
|
duplicate-cn
|
||||||
|
keepalive 10 120
|
||||||
|
cipher AES-256-GCM
|
||||||
|
auth SHA256
|
||||||
|
comp-lzo
|
||||||
|
max-clients 10
|
||||||
|
user nobody
|
||||||
|
group nogroup
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
```
|
||||||
|
|
||||||
|
Assign a static IP + chmod:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd /etc/openvpn
|
||||||
|
$ sudo chown -R root:root easy-rsa/
|
||||||
|
$ sudo mkdir ccd
|
||||||
|
$ sudo touch ccd/mediaserver
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `mediaserver` with whatever client name you used above. Edit it like so:
|
||||||
|
|
||||||
|
<span class="aside">Your home server will be `10.8.0.100`</span>
|
||||||
|
|
||||||
|
```
|
||||||
|
ifconfig-push 10.8.0.100 255.255.255.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Test your config by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo openvpn --config /etc/openvpn/server.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run `ip addr` in another terminal, you should see an entry like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
5: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> stuff
|
||||||
|
link/none
|
||||||
|
inet 10.8.0.1/24 brd 10.8.0.255 scope global tun0
|
||||||
|
valid_lft forever preferred_lft forever
|
||||||
|
inet6 fe80::d9fc:b2f9:34e6:5ed2/64 scope link stable-privacy
|
||||||
|
valid_lft forever preferred_lft forever
|
||||||
|
```
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
|
||||||
|
If it works fine, persist OpenVPN with systemd:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo systemctl enable openvpn@server
|
||||||
|
$ sudo systemctl start openvpn@server
|
||||||
|
$ sudo systemctl daemon-reload
|
||||||
|
$ sudo service openvpn restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it works by rebooting:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo reboot
|
||||||
|
$ ssh vpn.example.com
|
||||||
|
$ ip addr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Forwarding
|
||||||
|
|
||||||
|
I use `ufw` to handle the iptables rules because I use it anyway as a firewall
|
||||||
|
when I harden my servers.
|
||||||
|
|
||||||
|
Enable routing:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo sysctl net.ipv4.ip_forward=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `/etc/sysctl.conf` to set:
|
||||||
|
|
||||||
|
```
|
||||||
|
net.ipv4.ip_forward=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `/etc/default/ufw` to set:
|
||||||
|
|
||||||
|
```
|
||||||
|
DEFAULT_FORWARD_POLICY="ACCEPT"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this to the top of `/etc/ufw/before.rules`:
|
||||||
|
|
||||||
|
```
|
||||||
|
*nat
|
||||||
|
:POSTROUTING ACCEPT [0:0]
|
||||||
|
|
||||||
|
# ssh port forwarding
|
||||||
|
-A PREROUTING -d 123.123.123.123 -p tcp --dport 2222 -j DNAT --to-dest 10.8.0.100:2222
|
||||||
|
-A POSTROUTING -d 10.8.0.100 -p tcp --dport 2222 -j SNAT --to-source 10.8.0.1
|
||||||
|
|
||||||
|
# Allow traffic from OpenVPN client to eth0
|
||||||
|
-A POSTROUTING -s 10.8.0.0/8 -o eth0 -j MASQUERADE
|
||||||
|
COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `123.123.123.123` with your VPN server's external IP address and `eth0`
|
||||||
|
with the external interface.
|
||||||
|
|
||||||
|
This will forward TCP traffic on port 2222 to your home server. If you want to use
|
||||||
|
port 22, then you need to set the VPN SSH server to something else.
|
||||||
|
|
||||||
|
A full example of `/etc/ufw/before.rules` with other ports included can be found
|
||||||
|
here:
|
||||||
|
|
||||||
|
[https://txt.t0.vc/URUG](https://txt.t0.vc/URUG)
|
||||||
|
|
||||||
|
Apply the changes to `ufw`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo ufw disable && sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Setup
|
||||||
|
|
||||||
|
Switch to your home server or client machine.
|
||||||
|
|
||||||
|
Install OpenVPN:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo apt update
|
||||||
|
$ sudo apt install openvpn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Configs
|
||||||
|
|
||||||
|
For static IP clients (like your home server), create the config file `/etc/openvpn/client.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto udp
|
||||||
|
remote vpn.example.com 1194
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
remote-cert-tls server
|
||||||
|
cipher AES-256-GCM
|
||||||
|
auth SHA256
|
||||||
|
comp-lzo
|
||||||
|
key-direction 1
|
||||||
|
<ca>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/ca.crt]
|
||||||
|
</ca>
|
||||||
|
<cert>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/issued/mediaserver.crt]
|
||||||
|
</cert>
|
||||||
|
<key>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/private/mediaserver.key]
|
||||||
|
</key>
|
||||||
|
<tls-auth>
|
||||||
|
[server /etc/openvpn/static.key]
|
||||||
|
</tls-auth>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `[server ...]` lines with the contents of that file on the __VPN
|
||||||
|
server__, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo cat /etc/openvpn/easy-rsa/pki/ca.crt
|
||||||
|
---> copy & paste result
|
||||||
|
```
|
||||||
|
|
||||||
|
Also replace `vpn.example.com` with the subdomain you assigned earlier.
|
||||||
|
|
||||||
|
For device clients (like your laptop and phone), create the config file `client.ovpn`:
|
||||||
|
|
||||||
|
<span class="aside">`redirect-gateway def1` forces traffic over the VPN</span>
|
||||||
|
|
||||||
|
```
|
||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto udp
|
||||||
|
remote vpn.example.com 1194
|
||||||
|
redirect-gateway def1
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
remote-cert-tls server
|
||||||
|
cipher AES-256-GCM
|
||||||
|
auth SHA256
|
||||||
|
comp-lzo
|
||||||
|
key-direction 1
|
||||||
|
<ca>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/ca.crt]
|
||||||
|
</ca>
|
||||||
|
<cert>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/issued/client.crt]
|
||||||
|
</cert>
|
||||||
|
<key>
|
||||||
|
[server /etc/openvpn/easy-rsa/pki/private/client.key]
|
||||||
|
</key>
|
||||||
|
<tls-auth>
|
||||||
|
[server /etc/openvpn/static.key]
|
||||||
|
</tls-auth>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `client.ovpn` file is ready to be imported into your VPN clients.
|
||||||
|
|
||||||
|
Test your config by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo openvpn --config /etc/openvpn/client.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run `ip addr` in another terminal, you should see an entry like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
7: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> stuff
|
||||||
|
link/none
|
||||||
|
inet 10.8.0.100/24 brd 10.8.0.255 scope global tun0
|
||||||
|
valid_lft forever preferred_lft forever
|
||||||
|
inet6 fe80::b2:ed71:6c98:4bc9/64 scope link stable-privacy
|
||||||
|
valid_lft forever preferred_lft forever
|
||||||
|
```
|
||||||
|
|
||||||
|
Try pinging the server:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ping 10.8.0.1
|
||||||
|
PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data.
|
||||||
|
64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=71.5 ms
|
||||||
|
64 bytes from 10.8.0.1: icmp_seq=2 ttl=64 time=73.0 ms
|
||||||
|
... etc
|
||||||
|
```
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
|
||||||
|
If it works fine, persist OpenVPN with systemd:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo chown root:root /etc/openvpn/client.conf
|
||||||
|
$ sudo chmod 600 /etc/openvpn/client.conf
|
||||||
|
$ sudo systemctl enable openvpn@client
|
||||||
|
$ sudo systemctl start openvpn@client
|
||||||
|
$ sudo systemctl daemon-reload
|
||||||
|
$ sudo service openvpn restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Apps
|
||||||
|
|
||||||
|
On Android I use "OpenVPN for Android" and on Linux I use the
|
||||||
|
`network-manager-openvpn-gnome` Debian package.
|
||||||
|
|
||||||
|
To add your VPN on Gnome, open VPN settings, import file, and select
|
||||||
|
`client.ovpn`. If the private key is missing, select it from
|
||||||
|
`~/.cert/nm-openvpn/`.
|
||||||
|
|
||||||
|
## Closing Thoughts
|
||||||
|
|
||||||
|
You should now be fine to access your home server from over the internet.
|
||||||
|
|
||||||
|
To forward additional ports, just edit the `/etc/ufw/before.rules` file like
|
||||||
|
above.
|
||||||
|
|
||||||
|
You can now point a domain to your virtual server's IP and use that to connect
|
||||||
|
to your home server. Use a CNAME to make it easy to change later:
|
||||||
|
|
||||||
|
```
|
||||||
|
NAME TYPE VALUE
|
||||||
|
--------------------------------------------------
|
||||||
|
vpn.example.com. A 123.123.123.123
|
||||||
|
myserver.example.com. CNAME vpn.example.com.
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, make sure any server programs are listening / bound to `10.8.0.100` or
|
||||||
|
`0.0.0.0` so that they can get traffic from that interface.
|
1
content/extra/favicon.svg
Normal file
1
content/extra/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🏴☠️</text></svg>
|
After Width: | Height: | Size: 119 B |
@@ -8,7 +8,7 @@ their shop. I wanted to create a sculpture, so with pieces of scrap metal I
|
|||||||
welded together this hand. The beads are far from perfect. Working with small
|
welded together this hand. The beads are far from perfect. Working with small
|
||||||
pieces of rusted metal made it difficult.
|
pieces of rusted metal made it difficult.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## The Name
|
## The Name
|
||||||
|
|
||||||
@@ -25,4 +25,4 @@ grinder. It was made in a machine shop with no real planning done ahead of time.
|
|||||||
In between welds, I used my own hand as a reference. Below is a picture of me
|
In between welds, I used my own hand as a reference. Below is a picture of me
|
||||||
adding a bead to it.
|
adding a bead to it.
|
||||||
|
|
||||||

|

|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 286 KiB |
@@ -8,23 +8,21 @@ wanted to wear a dress that was lit up with LEDs acting as twinkling stars.
|
|||||||
Seven of the 28 stars are aligned to resemble the Big Dipper constellation and
|
Seven of the 28 stars are aligned to resemble the Big Dipper constellation and
|
||||||
twinkle differently than the rest, which twinkle in a random pattern.
|
twinkle differently than the rest, which twinkle in a random pattern.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Construction
|
## Construction
|
||||||
|
|
||||||
The LEDs came from that strip that was cut up and soldered together with very
|
The LEDs came from that strip that was cut up and soldered together with very
|
||||||
small wires. Each of the LEDs can be controlled individually.
|
small wires. Each of the LEDs can be controlled individually.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
21 of the stars are light magenta in color and twinkle by fading randomly. The
|
21 of the stars are light magenta in color and twinkle by fading randomly. The
|
||||||
seven LEDs that form the Big Dipper continually scroll through a gradient of
|
seven LEDs that form the Big Dipper continually scroll through a gradient of
|
||||||
three colors. Instead of calculating the values of each color in the gradient as
|
three colors. Instead of calculating the values of each color in the gradient as
|
||||||
the program runs, a lookup table is used.
|
the program runs, a lookup table is used.
|
||||||
|
|
||||||
<center>
|
<video autoplay muted loop style="display:block; margin: 0 auto;">
|
||||||
<video autoplay muted loop>
|
<source src="{static}/videos/dress/dress3.mp4" type="video/mp4">
|
||||||
<source src="{static}/videos/dress/dress3.mp4" type="video/mp4">
|
Your browser does not support the video tag.
|
||||||
Your browser does not support the video tag.
|
</video>
|
||||||
</video>
|
|
||||||
</center>
|
|
||||||
|
@@ -12,7 +12,7 @@ face-plate and turned it into a capacitive touch sensor. The slightest touch
|
|||||||
anywhere on the plate is enough to toggle the light. I had to electrically
|
anywhere on the plate is enough to toggle the light. I had to electrically
|
||||||
isolate the metal screws from it because they screw into a grounded switch box.
|
isolate the metal screws from it because they screw into a grounded switch box.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Function
|
## Function
|
||||||
|
|
||||||
@@ -26,5 +26,8 @@ by an AC-DC converter.
|
|||||||
|
|
||||||
This entire process happens quicker than half a second, so it feels instant.
|
This entire process happens quicker than half a second, so it feels instant.
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||
|
<span class="aside">Black stuff's liquid electrical tape</span>
|
||||||
|
|
||||||
|

|
||||||
|
@@ -2,6 +2,7 @@ Title: Choosing a Linux Flavour
|
|||||||
Date: 2020-10-31
|
Date: 2020-10-31
|
||||||
Category: Writing
|
Category: Writing
|
||||||
Summary: A recommendation on which flavour of Linux to run.
|
Summary: A recommendation on which flavour of Linux to run.
|
||||||
|
Wide: true
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ I run Debian on my computers and servers.
|
|||||||
## Linux Distributions
|
## Linux Distributions
|
||||||
|
|
||||||
When people refer to the "flavour of Linux" they are talking about a Linux
|
When people refer to the "flavour of Linux" they are talking about a Linux
|
||||||
|
<span class="aside">Interjection: it's technically called GNU/Linux</span>
|
||||||
distribution (distro). It mostly describes what software is distributed in its
|
distribution (distro). It mostly describes what software is distributed in its
|
||||||
software repository.
|
software repository.
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ attempted to paint it. I eventually got it framed at Michaels. Many thanks to my
|
|||||||
friend Laura for the opportunity to do this, I couldn't have done it without her
|
friend Laura for the opportunity to do this, I couldn't have done it without her
|
||||||
help.
|
help.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## The Meaning
|
## The Meaning
|
||||||
|
|
||||||
@@ -37,4 +37,4 @@ in. It was quite difficult to get the blending and shadows perfect, but I had
|
|||||||
Laura to tell me when things didn't look right. Below I am trying to figure out
|
Laura to tell me when things didn't look right. Below I am trying to figure out
|
||||||
what a hand looks like in a mirror.
|
what a hand looks like in a mirror.
|
||||||
|
|
||||||

|

|
||||||
|
@@ -8,7 +8,7 @@ It's also great for when I'm on vacation. The plant is a year old now and
|
|||||||
doesn't look as good as it used to (kinda like you). So this machine is like its
|
doesn't look as good as it used to (kinda like you). So this machine is like its
|
||||||
life support.
|
life support.
|
||||||
|
|
||||||
Update: this plant died long ago.
|
<span class="aside">Update: this plant died long ago</span>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -36,4 +36,6 @@ Another feature was the ability to run the pump backwards. This completely
|
|||||||
eliminated the siphoning problem from before. After pumping for a set duration,
|
eliminated the siphoning problem from before. After pumping for a set duration,
|
||||||
it would run backwards until the tube was cleared of water.
|
it would run backwards until the tube was cleared of water.
|
||||||
|
|
||||||

|
<span class="aside">Also dead :(</span>
|
||||||
|
|
||||||
|

|
||||||
|
@@ -9,7 +9,7 @@ interests. The car was the top Canadian team in a 3000 km race from Darwin to
|
|||||||
Adelaide, Australia in 2011. We met up at a shop on campus every Saturday
|
Adelaide, Australia in 2011. We met up at a shop on campus every Saturday
|
||||||
morning to work on the new Generation IV of the solar car.
|
morning to work on the new Generation IV of the solar car.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## The Helianthus MPPT
|
## The Helianthus MPPT
|
||||||
|
|
||||||
@@ -20,4 +20,4 @@ without them. The Generation IV car, Schulich Delta (pictured below) uses seven
|
|||||||
of them: one per section of solar cells with similar lighting conditions. Andrei
|
of them: one per section of solar cells with similar lighting conditions. Andrei
|
||||||
and I designed the MPPT above.
|
and I designed the MPPT above.
|
||||||
|
|
||||||

|

|
||||||
|
@@ -34,6 +34,12 @@ MARKDOWN = {
|
|||||||
'output_format': 'html5',
|
'output_format': 'html5',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STATIC_PATHS = ['images', 'extra']
|
||||||
|
|
||||||
|
EXTRA_PATH_METADATA = {
|
||||||
|
'extra/favicon.svg': {'path': 'favicon.svg'},
|
||||||
|
}
|
||||||
|
|
||||||
# Uncomment following line if you want document-relative URLs when developing
|
# Uncomment following line if you want document-relative URLs when developing
|
||||||
#RELATIVE_URLS = True
|
#RELATIVE_URLS = True
|
||||||
|
|
||||||
|
@@ -34,6 +34,12 @@ MARKDOWN = {
|
|||||||
'output_format': 'html5',
|
'output_format': 'html5',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STATIC_PATHS = ['images', 'extra']
|
||||||
|
|
||||||
|
EXTRA_PATH_METADATA = {
|
||||||
|
'extra/favicon.svg': {'path': 'favicon.svg'},
|
||||||
|
}
|
||||||
|
|
||||||
# Uncomment following line if you want document-relative URLs when developing
|
# Uncomment following line if you want document-relative URLs when developing
|
||||||
#RELATIVE_URLS = True
|
#RELATIVE_URLS = True
|
||||||
|
|
||||||
@@ -48,10 +54,10 @@ AUTHORS_SAVE_AS = ''
|
|||||||
CATEGORIES_SAVE_AS = ''
|
CATEGORIES_SAVE_AS = ''
|
||||||
TAGS_SAVE_AS = ''
|
TAGS_SAVE_AS = ''
|
||||||
|
|
||||||
INDEX_SAVE_AS = 'index.php'
|
INDEX_SAVE_AS = 'index.html'
|
||||||
ARTICLE_URL = '{slug}/'
|
ARTICLE_URL = '{slug}/'
|
||||||
ARTICLE_SAVE_AS = '{slug}/index.php'
|
ARTICLE_SAVE_AS = '{slug}/index.html'
|
||||||
PAGE_URL = '{slug}/'
|
PAGE_URL = '{slug}/'
|
||||||
PAGE_SAVE_AS = '{slug}/index.php'
|
PAGE_SAVE_AS = '{slug}/index.html'
|
||||||
|
|
||||||
PROD = True
|
PROD = True
|
||||||
|
@@ -24,9 +24,25 @@
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info %}
|
||||||
|
Tanner Collin
|
||||||
|
<p class="contact-icons">
|
||||||
|
<a href="mailto:site2@tannercollin.com" rel="noreferrer noopener"><img alt="email" src="/theme/mail.svg" width="20" height="20" /></a>
|
||||||
|
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram" src="/theme/telegram.svg" width="20" height="20" /></a>
|
||||||
|
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github" src="/theme/github.svg" width="20" height="20" /></a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p><a href="/">← Return to Home</a></p>
|
|
||||||
<header>
|
{% if article.wide %}
|
||||||
|
<div class="content content-wide">
|
||||||
|
{% else %}
|
||||||
|
<div class="content">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><a href="/">← Return to Home</a></p>
|
||||||
|
<header>
|
||||||
<h1>{{ article.title }}</h1>
|
<h1>{{ article.title }}</h1>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
{{ article.summary }}
|
{{ article.summary }}
|
||||||
@@ -37,9 +53,10 @@
|
|||||||
— updated {{ article.locale_modified }}
|
— updated {{ article.locale_modified }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<hr />
|
<hr />
|
||||||
<article>
|
<article>
|
||||||
{{ article.content }}
|
{{ article.content }}
|
||||||
</article>
|
</article>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -11,8 +11,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="utf-8"/>
|
|
||||||
|
|
||||||
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
|
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
|
||||||
<meta name="author" content="{{ AUTHOR }}" />
|
<meta name="author" content="{{ AUTHOR }}" />
|
||||||
@@ -28,80 +28,30 @@
|
|||||||
<link rel="preload" href="/theme/fonts/Lato-Italic.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
<link rel="preload" href="/theme/fonts/Lato-Italic.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
||||||
<link rel="preload" href="/theme/fonts/Lato-Regular.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
<link rel="preload" href="/theme/fonts/Lato-Regular.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
{% include 'style.css' %}
|
{% include 'style.css' %}
|
||||||
{% include 'fonts.css' %}
|
{% include 'fonts.css' %}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script defer src="/theme/instant-page.js"></script>
|
<script defer src="/theme/instant-page.js"></script>
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<style type="text/css">
|
|
||||||
.theme-select {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</noscript>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
{% if PROD %}
|
|
||||||
<body class="<?php echo $themeClass; ?>">
|
|
||||||
{% else %}
|
|
||||||
<body>
|
<body>
|
||||||
{% endif %}
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="sidebar">
|
<div class="bar">
|
||||||
<img src="/theme/me.jpg" class="me" alt="A picture of me smiling" />
|
{% block info %}
|
||||||
<div class="info">
|
{% endblock %}
|
||||||
<p>
|
|
||||||
Tanner Collin
|
|
||||||
</p>
|
|
||||||
<p class="contact-icons">
|
|
||||||
<a href="mailto:site@tannercollin.com" rel="noreferrer noopener"><img alt="email icon" src="/theme/mail.svg" /></a>
|
|
||||||
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram logo" src="/theme/telegram.svg" /></a>
|
|
||||||
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github logo" src="/theme/github.svg" /></a>
|
|
||||||
</p>
|
|
||||||
<p class='theme-select'>
|
|
||||||
<a onClick="setTheme('light')">Light</a> / <a onClick="setTheme('dark')">Dark</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="topbar">
|
|
||||||
<img src="/theme/me.jpg" class="me" alt="A picture of me smiling" />
|
|
||||||
<div class="info">
|
|
||||||
Tanner Collin
|
|
||||||
<p class="contact-icons">
|
|
||||||
<a href="mailto:site@tannercollin.com" rel="noreferrer noopener"><img alt="email icon" src="/theme/mail.svg" /></a>
|
|
||||||
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram logo" src="/theme/telegram.svg" /></a>
|
|
||||||
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github logo" src="/theme/github.svg" /></a>
|
|
||||||
</p>
|
|
||||||
<p class='theme-select'>
|
|
||||||
<a onClick="setTheme('light')">Light</a> / <a onClick="setTheme('dark')">Dark</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function setTheme(theme) {
|
|
||||||
console.log('Setting theme to', theme);
|
|
||||||
|
|
||||||
if (theme == 'dark') {
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
document.body.classList.remove('light');
|
|
||||||
} else if (theme == 'light') {
|
|
||||||
document.body.classList.add('light');
|
|
||||||
document.body.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.cookie = 'theme=' + theme + '; Max-Age=31536000; Path=/; SameSite=Lax';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
<p class="copyright">
|
||||||
|
© 2012–2021 Tanner Collin
|
||||||
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@@ -7,43 +7,116 @@
|
|||||||
<meta name="summary" content="The personal website of Tanner Collin." />
|
<meta name="summary" content="The personal website of Tanner Collin." />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info %}
|
||||||
|
<img src="/theme/me.jpg" width="128" class="me" alt="me smiling wide and looking into the camera lit up brightly" />
|
||||||
|
<div class="info">
|
||||||
|
<h1>Tanner Collin</h1>
|
||||||
|
<p class="contact-icons">
|
||||||
|
<a href="mailto:site2@tannercollin.com" rel="noreferrer noopener"><img alt="email" src="/theme/mail.svg" width="20" height="20" /></a>
|
||||||
|
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram" src="/theme/telegram.svg" width="20" height="20" /></a>
|
||||||
|
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github" src="/theme/github.svg" width="20" height="20" /></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<div class="content content-index">
|
||||||
|
<p>
|
||||||
Hi, I'm Tanner! I do firmware and web development in Calgary.
|
Hi, I'm Tanner! I do firmware and web development in Calgary.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Contact Info</h2>
|
<div class="leftcol">
|
||||||
|
<div class="inside">
|
||||||
|
|
||||||
<p>
|
<h2>Contact Info</h2>
|
||||||
Email: <a href="mailto:site@tannercollin.com">site@tannercollin.com</a> <br />
|
|
||||||
|
<p>
|
||||||
|
Email: <a href="mailto:site2@tannercollin.com">site2@tannercollin.com</a> <br />
|
||||||
Telegram: <a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener">@tannercollin</a>
|
Telegram: <a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener">@tannercollin</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Resume</h2>
|
<h2>Resume</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Firmware Engineer at <a href="https://cabanablockchain.com" target="_blank" rel="noreferrer noopener">Cabana Blockchain</a>, 2018–</li>
|
<li>Firmware Engineer at <a href="https://cabanablockchain.com" target="_blank" rel="noreferrer noopener">Cabana Blockchain</a>, 2018–</li>
|
||||||
<li>Lead Hardware Engineer at <a href="https://criticalcontrol.com/" target="_blank" rel="noreferrer noopener">Critical Control</a>, 2016–2018</li>
|
<li>Lead Hardware Engineer at <a href="https://criticalcontrol.com/" target="_blank" rel="noreferrer noopener">Critical Control</a>, 2016–2018</li>
|
||||||
<li>Electrical Engineer at <a href="https://www.opener.aero/" target="_blank" rel="noreferrer noopener">Opener Aero</a>, 2016–2016</li>
|
<li>Electrical Engineer at <a href="https://www.opener.aero/" target="_blank" rel="noreferrer noopener">Opener Aero</a>, 2016–2016</li>
|
||||||
<li>Electrical Engineer Intern at <a href="https://www.pason.com/" target="_blank" rel="noreferrer noopener">Pason Systems</a>, 2014–2015</li>
|
<li>Electrical Engineer Intern at <a href="https://www.pason.com/" target="_blank" rel="noreferrer noopener">Pason Systems</a>, 2014–2015</li>
|
||||||
<li>BSc. Electrical Engineering from University of Calgary</li>
|
<li>BSc. Electrical Engineering from University of Calgary</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Projects</h2>
|
<h2>Projects</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
My main hobby is working on technical projects. I typically design websites or
|
My main hobby is working on software projects. I typically design websites or
|
||||||
build tools that make my life easier. Sometimes art.
|
build tools that make my life easier.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<h3>
|
||||||
You can find my code on <a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener">GitHub</a>.
|
<a href="https://news.t0.vc/" target="_blank" rel="noreferrer noopener">QotNews</a>
|
||||||
</p>
|
— <a class="source" href="https://git.tannercollin.com/tanner/qotnews" target="_blank" rel="noreferrer noopener">source code</a>
|
||||||
|
</h3>
|
||||||
|
<div class="summary">
|
||||||
|
<p>Hacker News, Reddit, Lobsters, and Tildes articles pre-rendered in reader mode. Optimized for speed and distraction-free reading.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for article in articles_page.object_list if article.category.name == 'Projects' %}
|
<h3>
|
||||||
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
|
<a href="https://notica.us" target="_blank" rel="noreferrer noopener">Notica</a>
|
||||||
<div class="summary">
|
— <a class="source" href="https://github.com/tannercollin/Notica" target="_blank" rel="noreferrer noopener">source code</a>
|
||||||
|
</h3>
|
||||||
|
<div class="summary">
|
||||||
|
<p>Send browser notifications from your terminal. No installation. No registration.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<a href="https://my.protospace.ca" target="_blank" rel="noreferrer noopener">Spaceport</a>
|
||||||
|
— <a class="source" href="https://github.com/Protospace/spaceport" target="_blank" rel="noreferrer noopener">source code</a>
|
||||||
|
</h3>
|
||||||
|
<div class="summary">
|
||||||
|
<p>Makerspace members' portal for Calgary Protospace. It tracks membership, courses, training, access cards, and more.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<a href="https://txt.t0.vc" target="_blank" rel="noreferrer noopener">t0txt</a>
|
||||||
|
— <a class="source" href="https://github.com/tannercollin/t0txt" target="_blank" rel="noreferrer noopener">source code</a>
|
||||||
|
</h3>
|
||||||
|
<div class="summary">
|
||||||
|
<p>Minimal command line pastebin. Allows you to upload text notes from a bash pipe or web browser.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<a href="https://github.com/tannercollin/standardnotes-fs" target="_blank" rel="noreferrer noopener">standardnotes-fs</a>
|
||||||
|
</h3>
|
||||||
|
<div class="summary">
|
||||||
|
<p>A filesystem that mounts your Standard Notes account as a directory of text files that you can edit.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rightcol">
|
||||||
|
<div class="inside">
|
||||||
|
<h2>Creations</h2>
|
||||||
|
|
||||||
|
<p>Sometimes I create art or interactive tech.</p>
|
||||||
|
|
||||||
|
{% for article in articles_page.object_list if article.category.name == 'Projects' %}
|
||||||
|
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
|
||||||
|
<div class="summary">
|
||||||
{{ article.summary }}
|
{{ article.summary }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<h2>Writing</h2>
|
||||||
|
|
||||||
|
<p>Various articles, mostly about computers.</p>
|
||||||
|
|
||||||
|
{% for article in articles_page.object_list if article.category.name == 'Writing' %}
|
||||||
|
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
|
||||||
|
<div class="summary">
|
||||||
|
{{ article.summary }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -12,85 +12,33 @@ a {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info h1 {
|
||||||
|
font: 1.2rem/1.0 Lato,sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
font: 1rem/1.5 Apparatus SIL,serif;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre)>code {
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
margin: 2rem auto 0 auto;
|
margin: 2rem auto 12rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-select {
|
.copyright {
|
||||||
font-size: 1rem;
|
font: 1rem/1.5 Apparatus SIL,serif;
|
||||||
margin-bottom: 0;
|
text-align: center;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
margin-top: 2px;
|
|
||||||
float: left;
|
|
||||||
width: 8rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .me {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .info {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .contact-icons a {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .contact-icons img {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar {
|
|
||||||
display: table;
|
|
||||||
overflow: auto;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: -0.5rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .me {
|
|
||||||
float: left;
|
|
||||||
height: 6.5rem;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .info {
|
|
||||||
float: left;
|
|
||||||
margin-top: -0.25rem;
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .contact-icons {
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .contact-icons a {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .contact-icons img {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar .theme-select {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc {
|
.toc {
|
||||||
@@ -124,6 +72,18 @@ pre {
|
|||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-wide {
|
||||||
|
max-width: 46rem;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-index {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-index h3 {
|
||||||
|
font: 1.2rem/1.5 Apparatus SIL,serif;
|
||||||
|
}
|
||||||
|
|
||||||
.content p {
|
.content p {
|
||||||
font: 1.2rem/1.5 Apparatus SIL,serif;
|
font: 1.2rem/1.5 Apparatus SIL,serif;
|
||||||
}
|
}
|
||||||
@@ -141,35 +101,115 @@ pre {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toclink:not(:hover)::after {
|
.content .aside {
|
||||||
visibility: hidden;
|
display: inline;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
width: 8rem;
|
||||||
|
margin-left: -9rem;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toclink::after {
|
.toclink:not(:hover) {
|
||||||
color: #999;
|
border-bottom: none;
|
||||||
content: "\00B6";
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width:36rem) {
|
@media screen and (min-width:36rem) {
|
||||||
.content {
|
.content {
|
||||||
margin-left: 10rem;
|
margin-left: 10rem;
|
||||||
}
|
}
|
||||||
.topbar {
|
|
||||||
display: none;
|
.bar {
|
||||||
|
margin-top: 2px;
|
||||||
|
float: left;
|
||||||
|
width: 8rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .me {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .info {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .contact-icons a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .contact-icons img {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width:36rem) {
|
@media screen and (max-width:36rem) {
|
||||||
.sidebar {
|
.bar {
|
||||||
display: none;
|
display: table;
|
||||||
|
overflow: auto;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .me {
|
||||||
|
float: left;
|
||||||
|
height: 6.5rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .info {
|
||||||
|
float: left;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .contact-icons {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .contact-icons a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar .contact-icons img {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width:58rem) {
|
||||||
|
.container {
|
||||||
|
max-width: 75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftcol {
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
margin-top: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftcol > .inside {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightcol {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightcol > .inside {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #fff;
|
background-color: #eee;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,92 +219,40 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background-color: #eee;
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre)>code {
|
||||||
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc {
|
.toc {
|
||||||
background-color: #eee;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content p.metadata {
|
.content p.metadata {
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark {
|
|
||||||
background-color: #000;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark a {
|
|
||||||
color: #fff;
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark pre {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .toc {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .content p.metadata {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .content img {
|
|
||||||
filter: brightness(75%);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark .contact-icons img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body.light {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light a {
|
|
||||||
color: #000;
|
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light pre {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light .toc {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light .content p.metadata {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light .contact-icons img {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light .content img {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
color: #fff;
|
color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: #eee;
|
||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:not(pre)>code {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
.toc {
|
.toc {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user