Compare commits
	
		
			33 Commits
		
	
	
		
			5a91907f4c
			...
			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 | |||
| 786418496b | |||
| 6bed384805 | |||
| 6d2247b210 | |||
| dd44ff4c6d | |||
| 32d76be296 | |||
| 3dc90a272d | |||
| 809806c8cd | |||
| 975484a03c | 
							
								
								
									
										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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								content/linux-flavour.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								content/linux-flavour.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					Title: Choosing a Linux Flavour
 | 
				
			||||||
 | 
					Date: 2020-10-31
 | 
				
			||||||
 | 
					Category: Writing
 | 
				
			||||||
 | 
					Summary: A recommendation on which flavour of Linux to run.
 | 
				
			||||||
 | 
					Wide: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[TOC]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					People often ask me which flavour of Linux they should install. In summary,
 | 
				
			||||||
 | 
					choose Ubuntu if it's your first time. Once you are comfortable, install Debian
 | 
				
			||||||
 | 
					the next time you need to install Linux.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I run Debian on my computers and servers.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Linux Distributions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					software repository.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"A typical Linux distribution comprises a Linux kernel, GNU tools and
 | 
				
			||||||
 | 
					libraries, additional software, documentation, a window system, a window
 | 
				
			||||||
 | 
					manager, and a desktop environment." [Wikipedia]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The major Linux distros are practically all the same. If you master one it's
 | 
				
			||||||
 | 
					easy to pick up the others. The main differences you'll run into are which
 | 
				
			||||||
 | 
					tools you use to install new software, and the desktop environment, which is
 | 
				
			||||||
 | 
					what all the windows and buttons look like.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I recommend two Linux distros, Debian and Ubuntu. Ubuntu is based off of
 | 
				
			||||||
 | 
					Debian, so they are very similar. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Pros of Debian
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Debian is one of the oldest distros and many other distros are based off it.
 | 
				
			||||||
 | 
					You can see a timeline visualization of all its derivatives here:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://upload.wikimedia.org/wikipedia/commons/1/1b/Linux_Distribution_Timeline.svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This image is what originally convinced me to use Debian. Scroll down until you
 | 
				
			||||||
 | 
					see it and zoom out so you grasp how many derivatives it has.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Debian is also non-commercial and requires that all software in its main
 | 
				
			||||||
 | 
					repository is free and open source. This is important because that grants you
 | 
				
			||||||
 | 
					the right to study, change, and distribute the software and source code to
 | 
				
			||||||
 | 
					anyone and for any purpose. They also follow a strong social contract you can
 | 
				
			||||||
 | 
					see here:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://www.debian.org/social_contract>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It's also a very stable Linux distro since they freeze all software features on
 | 
				
			||||||
 | 
					each release. This makes it great for servers because nothing will break when
 | 
				
			||||||
 | 
					it updates.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The main Raspberry Pi distro is nearly identical to Debian, so you'll also gain
 | 
				
			||||||
 | 
					familiarity with it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Cons of Debian
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Since Debian requires all its software to be free and open source, proprietary
 | 
				
			||||||
 | 
					hardware drivers aren't included in its main repo. This can make installing
 | 
				
			||||||
 | 
					Debian difficult if your hardware requires non-free drivers. You'll need to use
 | 
				
			||||||
 | 
					a non-free installation image found here:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://cdimage.debian.org/cdimage/unofficial/non-free/cd-including-firmware/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The fact that Debian freezes software features can also mean that your software
 | 
				
			||||||
 | 
					gets old until the next Debian release. If you want versions that are bleeding
 | 
				
			||||||
 | 
					edge, you'll need to use Debian Unstable as described here:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://wiki.debian.org/DebianUnstable#Installation>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Don't be fooled by the name "unstable". I use it for my personal computers and
 | 
				
			||||||
 | 
					it runs fine.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Pros of Ubuntu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Ubuntu is incredibly easy to install. You can also try it out before deciding
 | 
				
			||||||
 | 
					to install it. The distro pretty much just works on what ever hardware you
 | 
				
			||||||
 | 
					have.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It's very beginner friendly because it's so popular. Any problem you search for
 | 
				
			||||||
 | 
					will reveal dozens of threads with people solving the same problem.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Cons of Ubuntu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unfortunately Ubuntu is developed by a commercial company, Canonical. The
 | 
				
			||||||
 | 
					company's interests come first, before the users' and they have a track record
 | 
				
			||||||
 | 
					of betraying their users' trust and privacy. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Years ago Ubuntu had a feature enabled by default that would send your desktop
 | 
				
			||||||
 | 
					searches to Amazon so they could suggest products for you to buy:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://www.pcworld.com/article/2840401/ubuntus-unity-8-desktop-removes-the-amazon-search-spyware.html>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Currently whenever you remote login to your Ubuntu machine, it phones home to
 | 
				
			||||||
 | 
					Canonical and they collect info about your system:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<https://ubuntu.com/legal/motd>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					While these reasons are fairly minor, they are quite frowned upon in the Linux
 | 
				
			||||||
 | 
					community and are reason enough to switch to Debian once you are comfortable
 | 
				
			||||||
 | 
					with using Linux.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Wikipedia]: https://en.wikipedia.org/wiki/Linux_distribution
 | 
				
			||||||
@@ -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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
# -*- coding: utf-8 -*- #
 | 
					# -*- coding: utf-8 -*- #
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
AUTHOR = 'Tanner'
 | 
					AUTHOR = 'Tanner Collin'
 | 
				
			||||||
SITENAME = 'Tanner Collin'
 | 
					SITENAME = 'Tanner Collin'
 | 
				
			||||||
SITEURL = ''
 | 
					SITEURL = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,7 +34,24 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
THEME = 'themes/theme'
 | 
					THEME = 'themes/theme'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# turn off useless outputs
 | 
				
			||||||
 | 
					TAG_SAVE_AS = ''
 | 
				
			||||||
 | 
					CATEGORY_SAVE_AS = ''
 | 
				
			||||||
 | 
					AUTHOR_SAVE_AS = ''
 | 
				
			||||||
 | 
					ARCHIVES_SAVE_AS = ''
 | 
				
			||||||
 | 
					AUTHORS_SAVE_AS = ''
 | 
				
			||||||
 | 
					CATEGORIES_SAVE_AS = ''
 | 
				
			||||||
 | 
					TAGS_SAVE_AS = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PROD = False
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,24 +2,62 @@
 | 
				
			|||||||
# -*- coding: utf-8 -*- #
 | 
					# -*- coding: utf-8 -*- #
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This file is only used if you use `make publish` or
 | 
					AUTHOR = 'Tanner Collin'
 | 
				
			||||||
# explicitly specify it as your config file.
 | 
					SITENAME = 'Tanner Collin'
 | 
				
			||||||
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
sys.path.append(os.curdir)
 | 
					 | 
				
			||||||
from pelicanconf import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# If your site is available via HTTPS, make sure SITEURL begins with https://
 | 
					 | 
				
			||||||
SITEURL = ''
 | 
					SITEURL = ''
 | 
				
			||||||
RELATIVE_URLS = False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
FEED_ALL_ATOM = 'feeds/all.atom.xml'
 | 
					PATH = 'content'
 | 
				
			||||||
CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
DELETE_OUTPUT_DIRECTORY = True
 | 
					TIMEZONE = 'Canada/Mountain'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Following items are often useful when publishing
 | 
					DEFAULT_LANG = 'en'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#DISQUS_SITENAME = ""
 | 
					# Feed generation is usually not desired when developing
 | 
				
			||||||
#GOOGLE_ANALYTICS = ""
 | 
					FEED_ALL_ATOM = None
 | 
				
			||||||
 | 
					CATEGORY_FEED_ATOM = None
 | 
				
			||||||
 | 
					TRANSLATION_FEED_ATOM = None
 | 
				
			||||||
 | 
					AUTHOR_FEED_ATOM = None
 | 
				
			||||||
 | 
					AUTHOR_FEED_RSS = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEFAULT_PAGINATION = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MARKDOWN = {
 | 
				
			||||||
 | 
					    'extension_configs': {
 | 
				
			||||||
 | 
					        'markdown.extensions.codehilite': {'css_class': 'highlight'},
 | 
				
			||||||
 | 
					        'markdown.extensions.extra': {},
 | 
				
			||||||
 | 
					        'markdown.extensions.meta': {},
 | 
				
			||||||
 | 
					        'markdown.extensions.toc': {
 | 
				
			||||||
 | 
					            'toc_depth': '2-3',
 | 
				
			||||||
 | 
					            'anchorlink': True,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    '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
 | 
				
			||||||
 | 
					#RELATIVE_URLS = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THEME = 'themes/theme'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# turn off useless outputs
 | 
				
			||||||
 | 
					TAG_SAVE_AS = ''
 | 
				
			||||||
 | 
					CATEGORY_SAVE_AS = ''
 | 
				
			||||||
 | 
					AUTHOR_SAVE_AS = ''
 | 
				
			||||||
 | 
					ARCHIVES_SAVE_AS = ''
 | 
				
			||||||
 | 
					AUTHORS_SAVE_AS = ''
 | 
				
			||||||
 | 
					CATEGORIES_SAVE_AS = ''
 | 
				
			||||||
 | 
					TAGS_SAVE_AS = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					INDEX_SAVE_AS = 'index.html'
 | 
				
			||||||
 | 
					ARTICLE_URL = '{slug}/'
 | 
				
			||||||
 | 
					ARTICLE_SAVE_AS = '{slug}/index.html'
 | 
				
			||||||
 | 
					PAGE_URL = '{slug}/'
 | 
				
			||||||
 | 
					PAGE_SAVE_AS = '{slug}/index.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PROD = True
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								themes/theme/static/darkmode-js.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								themes/theme/static/darkmode-js.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								themes/theme/static/instant-page.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								themes/theme/static/instant-page.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					/*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */
 | 
				
			||||||
 | 
					let t,e;const n=new Set,o=document.createElement("link"),i=o.relList&&o.relList.supports&&o.relList.supports("prefetch")&&window.IntersectionObserver&&"isIntersecting"in IntersectionObserverEntry.prototype,s="instantAllowQueryString"in document.body.dataset,a="instantAllowExternalLinks"in document.body.dataset,r="instantWhitelist"in document.body.dataset,c="instantMousedownShortcut"in document.body.dataset,d=1111;let l=65,u=!1,f=!1,m=!1;if("instantIntensity"in document.body.dataset){const t=document.body.dataset.instantIntensity;if("mousedown"==t.substr(0,"mousedown".length))u=!0,"mousedown-only"==t&&(f=!0);else if("viewport"==t.substr(0,"viewport".length))navigator.connection&&(navigator.connection.saveData||navigator.connection.effectiveType&&navigator.connection.effectiveType.includes("2g"))||("viewport"==t?document.documentElement.clientWidth*document.documentElement.clientHeight<45e4&&(m=!0):"viewport-all"==t&&(m=!0));else{const e=parseInt(t);isNaN(e)||(l=e)}}if(i){const n={capture:!0,passive:!0};if(f||document.addEventListener("touchstart",function(t){e=performance.now();const n=t.target.closest("a");if(!h(n))return;v(n.href)},n),u?c||document.addEventListener("mousedown",function(t){const e=t.target.closest("a");if(!h(e))return;v(e.href)},n):document.addEventListener("mouseover",function(n){if(performance.now()-e<d)return;const o=n.target.closest("a");if(!h(o))return;o.addEventListener("mouseout",p,{passive:!0}),t=setTimeout(()=>{v(o.href),t=void 0},l)},n),c&&document.addEventListener("mousedown",function(t){if(performance.now()-e<d)return;const n=t.target.closest("a");if(t.which>1||t.metaKey||t.ctrlKey)return;if(!n)return;n.addEventListener("click",function(t){1337!=t.detail&&t.preventDefault()},{capture:!0,passive:!1,once:!0});const o=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1,detail:1337});n.dispatchEvent(o)},n),m){let t;(t=window.requestIdleCallback?t=>{requestIdleCallback(t,{timeout:1500})}:t=>{t()})(()=>{const t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){const n=e.target;t.unobserve(n),v(n.href)}})});document.querySelectorAll("a").forEach(e=>{h(e)&&t.observe(e)})})}}function p(e){e.relatedTarget&&e.target.closest("a")==e.relatedTarget.closest("a")||t&&(clearTimeout(t),t=void 0)}function h(t){if(t&&t.href&&(!r||"instant"in t.dataset)&&(a||t.origin==location.origin||"instant"in t.dataset)&&["http:","https:"].includes(t.protocol)&&("http:"!=t.protocol||"https:"!=location.protocol)&&(s||!t.search||"instant"in t.dataset)&&!(t.hash&&t.pathname+t.search==location.pathname+location.search||"noInstant"in t.dataset))return!0}function v(t){if(n.has(t))return;const e=document.createElement("link");e.rel="prefetch",e.href=t,document.head.appendChild(e),n.add(t)}
 | 
				
			||||||
@@ -1,21 +1,62 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block title %}{{ SITENAME }} - {{ article.title|striptags  }}{% endblock %}
 | 
				
			||||||
<p><a href="/">← Return to Home</a></p>
 | 
					
 | 
				
			||||||
<header>
 | 
					{% block head %}
 | 
				
			||||||
	<h1>{{ article.title }}</h1>
 | 
						{{ super() }}
 | 
				
			||||||
	<div class="summary">
 | 
					
 | 
				
			||||||
		{{ article.summary }}
 | 
						{% if article.date %}
 | 
				
			||||||
	</div>
 | 
							<meta name="date" content="{{article.date}}" />
 | 
				
			||||||
	<p class="metadata">
 | 
						{% endif %}
 | 
				
			||||||
		{{ article.locale_date }}
 | 
					
 | 
				
			||||||
		{% if article.modified %}
 | 
						{% if article.summary %}
 | 
				
			||||||
			— updated {{ article.locale_modified }}
 | 
							<meta name="description" content="{{article.summary|striptags}}" />
 | 
				
			||||||
		{% endif %}
 | 
							<meta name="summary" content="{{article.summary|striptags}}" />
 | 
				
			||||||
	</p>
 | 
						{% endif %}
 | 
				
			||||||
</header>
 | 
					
 | 
				
			||||||
<hr />
 | 
						{% if article.category %}
 | 
				
			||||||
<article>
 | 
							<meta name="category" content="{{article.category}}" />
 | 
				
			||||||
	{{ article.content }}
 | 
						{% endif %}
 | 
				
			||||||
</article>
 | 
					
 | 
				
			||||||
 | 
						{% for tag in article.tags %}
 | 
				
			||||||
 | 
							<meta name="tags" content="{{tag}}" />
 | 
				
			||||||
 | 
						{% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% 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 %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{% 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>
 | 
				
			||||||
 | 
								<div class="summary">
 | 
				
			||||||
 | 
									{{ article.summary }}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<p class="metadata">
 | 
				
			||||||
 | 
									{{ article.locale_date }}
 | 
				
			||||||
 | 
									{% if article.modified %}
 | 
				
			||||||
 | 
										— updated {{ article.locale_modified }}
 | 
				
			||||||
 | 
									{% endif %}
 | 
				
			||||||
 | 
								</p>
 | 
				
			||||||
 | 
							</header>
 | 
				
			||||||
 | 
							<hr />
 | 
				
			||||||
 | 
							<article>
 | 
				
			||||||
 | 
								{{ article.content }}
 | 
				
			||||||
 | 
							</article>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,61 +1,57 @@
 | 
				
			|||||||
 | 
					{% if PROD %}
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
						$themeClass = '';
 | 
				
			||||||
 | 
						if (!empty($_COOKIE['theme'])) {
 | 
				
			||||||
 | 
							$themeClass = $_COOKIE['theme'];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					?>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
	<head>
 | 
						<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>
 | 
				
			||||||
 | 
							<meta name="author" content="{{ AUTHOR }}" />
 | 
				
			||||||
 | 
							{% endblock head %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/AppSILB.ttf" as="font" type="font/ttf" crossorigin="anonymous">
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/AppSILBI.ttf" as="font" type="font/ttf" crossorigin="anonymous">
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/AppSILI.ttf" as="font" type="font/ttf" crossorigin="anonymous">
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/AppSILR.ttf" as="font" type="font/ttf" crossorigin="anonymous">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/Lato-Bold.ttf" as="font" type="font/ttf" crossorigin="anonymous">
 | 
				
			||||||
 | 
							<link rel="preload" href="/theme/fonts/Lato-BoldItalic.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="icon" href="favicon.svg">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<style>
 | 
							<style>
 | 
				
			||||||
		{% include 'style.css' %}
 | 
							{% include 'style.css' %}
 | 
				
			||||||
 | 
							{% include 'fonts.css' %}
 | 
				
			||||||
		</style>
 | 
							</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<link rel="stylesheet" type="text/css" href="/theme/fonts/fonts.css" />
 | 
							<script defer src="/theme/instant-page.js"></script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
		<title>Tanner Collin</title>
 | 
					 | 
				
			||||||
	</head>
 | 
						</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<body>
 | 
						<body>
 | 
				
			||||||
		<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">
 | 
					 | 
				
			||||||
					<p>
 | 
					 | 
				
			||||||
						Tanner Collin
 | 
					 | 
				
			||||||
					</p>
 | 
					 | 
				
			||||||
					<p class="contact-icons">
 | 
					 | 
				
			||||||
						<a href="mailto:site@tannercollin.com"><img src="/theme/mail.svg" /></a>
 | 
					 | 
				
			||||||
						<a href="https://t.me/tannercollin" target="_blank"><img src="/theme/telegram.svg" /></a>
 | 
					 | 
				
			||||||
						<a href="https://github.com/tannercollin" target="_blank"><img src="/theme/github.svg" /></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"><img src="/theme/mail.svg" /></a>
 | 
					 | 
				
			||||||
							<a href="https://t.me/tannercollin" target="_blank"><img src="/theme/telegram.svg" /></a>
 | 
					 | 
				
			||||||
							<a href="https://github.com/tannercollin" target="_blank"><img src="/theme/github.svg" /></a>
 | 
					 | 
				
			||||||
						</p>
 | 
					 | 
				
			||||||
					</div>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				{% block content %}
 | 
					 | 
				
			||||||
				{% endblock %}
 | 
									{% endblock %}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								{% block content %}
 | 
				
			||||||
 | 
								{% endblock %}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<script src="/theme/darkmode-js.min.js"></script>
 | 
							<p class="copyright">
 | 
				
			||||||
		<script>
 | 
								© 2012–2021 Tanner Collin
 | 
				
			||||||
			const options = {
 | 
							</p>
 | 
				
			||||||
				bottom: '16px',
 | 
					 | 
				
			||||||
				right: '16px',
 | 
					 | 
				
			||||||
				buttonColorDark: '#666',
 | 
					 | 
				
			||||||
				buttonColorLight: '#aaa',
 | 
					 | 
				
			||||||
				label: '🌙',
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
			new Darkmode(options).showWidget();
 | 
					 | 
				
			||||||
		</script>
 | 
					 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,20 @@
 | 
				
			|||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
	font-family: 'Apparatus SIL';
 | 
						font-family: 'Apparatus SIL';
 | 
				
			||||||
	src: url('AppSILR.ttf') format('truetype');
 | 
						src: url('/theme/fonts/AppSILR.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
	font-family: 'Apparatus SIL';
 | 
						font-family: 'Apparatus SIL';
 | 
				
			||||||
	font-style: italic;
 | 
						font-style: italic;
 | 
				
			||||||
	src: url('AppSILI.ttf') format('truetype');
 | 
						src: url('/theme/fonts/AppSILI.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
	font-family: 'Apparatus SIL';
 | 
						font-family: 'Apparatus SIL';
 | 
				
			||||||
	font-weight: bold;
 | 
						font-weight: bold;
 | 
				
			||||||
	src: url('AppSILB.ttf') format('truetype');
 | 
						src: url('/theme/fonts/AppSILB.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,7 +22,7 @@
 | 
				
			|||||||
	font-family: 'Apparatus SIL';
 | 
						font-family: 'Apparatus SIL';
 | 
				
			||||||
	font-weight: bold;
 | 
						font-weight: bold;
 | 
				
			||||||
	font-style: italic;
 | 
						font-style: italic;
 | 
				
			||||||
	src: url('AppSILBI.ttf') format('truetype');
 | 
						src: url('/theme/fonts/AppSILBI.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,7 +30,7 @@
 | 
				
			|||||||
	font-family: 'Lato';
 | 
						font-family: 'Lato';
 | 
				
			||||||
	font-style: italic;
 | 
						font-style: italic;
 | 
				
			||||||
	font-weight: 400;
 | 
						font-weight: 400;
 | 
				
			||||||
	src: local('Lato Italic'), local('Lato-Italic'), url('Lato-Italic.ttf') format('truetype');
 | 
						src: local('Lato Italic'), local('Lato-Italic'), url('/theme/fonts/Lato-Italic.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,7 +38,7 @@
 | 
				
			|||||||
	font-family: 'Lato';
 | 
						font-family: 'Lato';
 | 
				
			||||||
	font-style: italic;
 | 
						font-style: italic;
 | 
				
			||||||
	font-weight: 700;
 | 
						font-weight: 700;
 | 
				
			||||||
	src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url('Lato-BoldItalic.ttf') format('truetype');
 | 
						src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url('/theme/fonts/Lato-BoldItalic.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,7 +46,7 @@
 | 
				
			|||||||
	font-family: 'Lato';
 | 
						font-family: 'Lato';
 | 
				
			||||||
	font-style: normal;
 | 
						font-style: normal;
 | 
				
			||||||
	font-weight: 400;
 | 
						font-weight: 400;
 | 
				
			||||||
	src: local('Lato Regular'), local('Lato-Regular'), url('Lato-Regular.ttf') format('truetype');
 | 
						src: local('Lato Regular'), local('Lato-Regular'), url('/theme/fonts/Lato-Regular.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,6 +54,6 @@
 | 
				
			|||||||
	font-family: 'Lato';
 | 
						font-family: 'Lato';
 | 
				
			||||||
	font-style: normal;
 | 
						font-style: normal;
 | 
				
			||||||
	font-weight: 700;
 | 
						font-weight: 700;
 | 
				
			||||||
	src: local('Lato Bold'), local('Lato-Bold'), url('Lato-Bold.ttf') format('truetype');
 | 
						src: local('Lato Bold'), local('Lato-Bold'), url('/theme/fonts/Lato-Bold.ttf') format('truetype');
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,42 +1,122 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block head %}
 | 
				
			||||||
<p>
 | 
						{{ super() }}
 | 
				
			||||||
	Hi, I'm Tanner! I do firmware design and web development.
 | 
					 | 
				
			||||||
</p>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2>Contact Info</h2>
 | 
						<meta name="description" content="The personal website of Tanner Collin." />
 | 
				
			||||||
 | 
						<meta name="summary" content="The personal website of Tanner Collin." />
 | 
				
			||||||
<p>
 | 
					{% endblock %}
 | 
				
			||||||
	Email: <a href="mailto:site@tannercollin.com">site@tannercollin.com</a> <br />
 | 
					
 | 
				
			||||||
	Telegram: <a href="https://t.me/tannercollin" target="_blank">@tannercollin</a>
 | 
					{% block info %}
 | 
				
			||||||
</p>
 | 
						<img src="/theme/me.jpg" width="128" class="me" alt="me smiling wide and looking into the camera lit up brightly" />
 | 
				
			||||||
 | 
						<div class="info">
 | 
				
			||||||
<h2>Resume</h2>
 | 
							<h1>Tanner Collin</h1>
 | 
				
			||||||
 | 
							<p class="contact-icons">
 | 
				
			||||||
<ul>
 | 
								<a href="mailto:site2@tannercollin.com" rel="noreferrer noopener"><img alt="email" src="/theme/mail.svg" width="20" height="20" /></a>
 | 
				
			||||||
	<li>Firmware Engineer at <a href="https://cabanablockchain.com" target="_blank">Cabana Blockchain</a>, 2018–</li>
 | 
								<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram" src="/theme/telegram.svg" width="20" height="20" /></a>
 | 
				
			||||||
	<li>Lead Hardware Engineer at <a href="https://criticalcontrol.com/" target="_blank">Critical Control</a>, 2016–2018</li>
 | 
								<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github" src="/theme/github.svg" width="20" height="20" /></a>
 | 
				
			||||||
	<li>Electrical Engineer at <a href="https://www.opener.aero/" target="_blank">Opener Aero</a>, 2016–2016</li>
 | 
							</p>
 | 
				
			||||||
	<li>Electrical Engineer Intern at <a href="https://www.pason.com/" target="_blank">Pason Systems</a>, 2014–2015</li>
 | 
						</div>
 | 
				
			||||||
	<li>BSc. Electrical Engineering from University of Calgary</li>
 | 
					{% endblock %}
 | 
				
			||||||
</ul>
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
<h2>Projects</h2>
 | 
						<div class="content content-index">
 | 
				
			||||||
 | 
							<p>
 | 
				
			||||||
<p>
 | 
								Hi, I'm Tanner! I do firmware and web development in Calgary.
 | 
				
			||||||
	My main hobby is working on technical projects. I typically design websites or
 | 
							</p>
 | 
				
			||||||
	build tools that make my life easier. Sometimes art.
 | 
					
 | 
				
			||||||
</p>
 | 
							<div class="leftcol">
 | 
				
			||||||
 | 
							<div class="inside">
 | 
				
			||||||
<p>
 | 
					
 | 
				
			||||||
	You can find my code on <a href="https://github.com/tannercollin" target="_blank">GitHub</a>.
 | 
								<h2>Contact Info</h2>
 | 
				
			||||||
</p>
 | 
					
 | 
				
			||||||
 | 
								<p>
 | 
				
			||||||
{% for article in articles_page.object_list %}
 | 
									Email: <a href="mailto:site2@tannercollin.com">site2@tannercollin.com</a> <br />
 | 
				
			||||||
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
 | 
									Telegram: <a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener">@tannercollin</a>
 | 
				
			||||||
<div class="summary">
 | 
								</p>
 | 
				
			||||||
	{{ article.summary }}
 | 
					
 | 
				
			||||||
</div>
 | 
								<h2>Resume</h2>
 | 
				
			||||||
{% endfor %}
 | 
					
 | 
				
			||||||
 | 
								<ul>
 | 
				
			||||||
 | 
									<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>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>BSc. Electrical Engineering from University of Calgary</li>
 | 
				
			||||||
 | 
								</ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<h2>Projects</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<p>
 | 
				
			||||||
 | 
									My main hobby is working on software projects. I typically design websites or
 | 
				
			||||||
 | 
									build tools that make my life easier.
 | 
				
			||||||
 | 
								</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<h3>
 | 
				
			||||||
 | 
									<a href="https://news.t0.vc/" target="_blank" rel="noreferrer noopener">QotNews</a>
 | 
				
			||||||
 | 
									— <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<h3>
 | 
				
			||||||
 | 
									<a href="https://notica.us" target="_blank" rel="noreferrer noopener">Notica</a>
 | 
				
			||||||
 | 
									— <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 }}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{% 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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,95 +5,47 @@ html {
 | 
				
			|||||||
body {
 | 
					body {
 | 
				
			||||||
	text-rendering: optimizeLegibility;
 | 
						text-rendering: optimizeLegibility;
 | 
				
			||||||
	font: 1.2rem/1.0 Lato,sans-serif;
 | 
						font: 1.2rem/1.0 Lato,sans-serif;
 | 
				
			||||||
	background-color: rgb(245, 245, 245);
 | 
					 | 
				
			||||||
	background-color: white;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					a {
 | 
				
			||||||
	color: #000000;
 | 
					 | 
				
			||||||
	text-decoration: none;
 | 
						text-decoration: none;
 | 
				
			||||||
	outline: none;
 | 
						outline: none;
 | 
				
			||||||
	border-bottom: 1px solid #000;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.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;
 | 
				
			||||||
	background-color: #eee;
 | 
					 | 
				
			||||||
	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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar {
 | 
					.copyright {
 | 
				
			||||||
	margin-top: 2px;
 | 
						font: 1rem/1.5 Apparatus SIL,serif;
 | 
				
			||||||
	float: left;
 | 
						text-align: center;
 | 
				
			||||||
	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: 6rem;
 | 
					 | 
				
			||||||
	width: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.topbar .info {
 | 
					 | 
				
			||||||
	float: left;
 | 
					 | 
				
			||||||
	margin-top: -0.25rem;
 | 
					 | 
				
			||||||
	margin-left: 1.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.topbar .contact-icons {
 | 
					 | 
				
			||||||
	margin-top: 1.5rem;
 | 
					 | 
				
			||||||
	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;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toc {
 | 
					.toc {
 | 
				
			||||||
	float: right;
 | 
						float: right;
 | 
				
			||||||
	background-color: #eee;
 | 
					 | 
				
			||||||
	padding: 0.75rem;
 | 
						padding: 0.75rem;
 | 
				
			||||||
	padding-top: 0;
 | 
						padding-top: 0;
 | 
				
			||||||
 | 
						margin-left: 0.75rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toc ul {
 | 
					.toc ul {
 | 
				
			||||||
@@ -106,7 +58,6 @@ pre {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content p.metadata {
 | 
					.content p.metadata {
 | 
				
			||||||
	color: #555;
 | 
					 | 
				
			||||||
	font: 1rem/1.0 Apparatus SIL,serif;
 | 
						font: 1rem/1.0 Apparatus SIL,serif;
 | 
				
			||||||
	font-style: italic;
 | 
						font-style: italic;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -121,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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -138,43 +101,171 @@ 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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.darkmode-toggle {
 | 
					@media screen and (min-width:58rem) {
 | 
				
			||||||
	z-index: 500;
 | 
						.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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.darkmode--activated .content img {
 | 
					body {
 | 
				
			||||||
	filter: brightness(75%);
 | 
						background-color: #eee;
 | 
				
			||||||
 | 
						color: #000;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.darkmode--activated video {
 | 
					a {
 | 
				
			||||||
	mix-blend-mode: difference;
 | 
						color: #000;
 | 
				
			||||||
 | 
						border-bottom: 1px solid #000;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.darkmode--activated .contact-icons img {
 | 
					pre {
 | 
				
			||||||
	filter: invert(1);
 | 
						background-color: #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:not(pre)>code {
 | 
				
			||||||
 | 
						background-color: #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toc {
 | 
				
			||||||
 | 
						background-color: #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content p.metadata {
 | 
				
			||||||
 | 
						color: #555;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
						body {
 | 
				
			||||||
 | 
							background-color: #000;
 | 
				
			||||||
 | 
							color: #eee;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						a {
 | 
				
			||||||
 | 
							color: #eee;
 | 
				
			||||||
 | 
							border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pre {
 | 
				
			||||||
 | 
							background-color: #222;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:not(pre)>code {
 | 
				
			||||||
 | 
							background-color: #222;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.toc {
 | 
				
			||||||
 | 
							background-color: #222;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.content p.metadata {
 | 
				
			||||||
 | 
							color: #aaa;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.content img {
 | 
				
			||||||
 | 
							filter: brightness(75%);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.contact-icons img {
 | 
				
			||||||
 | 
							filter: invert(1);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user