Compare commits

..

3 Commits

Author SHA1 Message Date
Bastian de Byl
3cfa429d12 feat: add METAR quiz tool for aviation weather practice
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 14:02:39 -05:00
Bastian de Byl
b8fb241e84 chore: updated guide with more custom nice rules 2026-01-04 14:47:31 -05:00
Bastian de Byl
ce6efb61f0 chore: remove old drone yaml 2026-01-04 14:23:39 -05:00
6 changed files with 714 additions and 51 deletions

View File

@@ -1,48 +0,0 @@
---
kind: pipeline
name: default
steps:
- name: lint
image: peterdavehello/markdownlint
commands:
- markdownlint content/
when:
event:
exclude:
- promote
- name: build
image: bdebyl/hugo
commands:
- git clone https://github.com/bdebyl/hugo-theme-even.git themes/even
- hugo
when:
event:
- promote
target:
- production
- name: deploy
image: bdebyl/awscli
environment:
DISTRIBUTION_ID:
from_secret: aws_distribution_id
AWS_ACCESS_KEY_ID:
from_secret: aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: aws_secret_key
AWS_DEFAULT_REGION: us-east-1
commands:
- aws s3 sync --acl "public-read" --sse "AES256" public/ s3://bdebyl.net
- aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths '/*'
when:
event:
- promote
target:
- production
---
kind: signature
hmac: ac042ea3723037743a119c740b039def0e68930e60e065e68c46dc4181116c17
...

View File

@@ -64,6 +64,9 @@ languages:
- name: Archive - name: Archive
url: archives url: archives
weight: 5 weight: 5
- name: Tools
url: tools/
weight: 7
- name: Search - name: Search
url: search/ url: search/
weight: 10 weight: 10

View File

@@ -195,7 +195,8 @@ yay -S cachyos-ananicy-rules-git # Community rules
sudo systemctl enable --now ananicy-cpp sudo systemctl enable --now ananicy-cpp
``` ```
Now add custom rules for package management and compilation: Now add custom rules. First, demote background tasks that shouldn't freeze your
desktop:
```bash ```bash
sudo tee /etc/ananicy.d/package-managers.rules << 'EOF' sudo tee /etc/ananicy.d/package-managers.rules << 'EOF'
@@ -212,12 +213,40 @@ sudo tee /etc/ananicy.d/package-managers.rules << 'EOF'
{"name": "cargo", "type": "BG_CPUIO"} {"name": "cargo", "type": "BG_CPUIO"}
{"name": "ninja", "type": "BG_CPUIO"} {"name": "ninja", "type": "BG_CPUIO"}
{"name": "make", "type": "BG_CPUIO"} {"name": "make", "type": "BG_CPUIO"}
# Coredump processing - can churn through 100MB+ dumps
{"name": "systemd-coredump", "type": "BG_CPUIO"}
{"name": "coredumpctl", "type": "BG_CPUIO"}
EOF EOF
``` ```
The `BG_CPUIO` type sets `nice=16`, `ioclass=idle`, and `sched=idle`. These The `BG_CPUIO` type sets `nice=16`, `ioclass=idle`, and `sched=idle`. These
processes will only get CPU and disk time when nothing else needs it. processes will only get CPU and disk time when nothing else needs it.
Next, boost interactive desktop applications that need to stay responsive:
```bash
sudo tee /etc/ananicy.d/99-desktop-priority.rules << 'EOF'
# Desktop apps that need high priority for responsiveness
# Terminal emulators
{"name": "warp", "type": "LowLatency_RT"}
{"name": "kitty", "type": "LowLatency_RT"}
{"name": "alacritty", "type": "LowLatency_RT"}
# Plasma desktop - boost higher than defaults
{"name": "plasmashell", "nice": -5, "ioclass": "best-effort", "ionice": 0, "latency_nice": -5}
# Browsers - boost for snappier UI
{"name": "vivaldi-bin", "nice": -3, "ioclass": "best-effort", "ionice": 2, "latency_nice": -3}
{"name": "firefox", "nice": -3, "ioclass": "best-effort", "ionice": 2, "latency_nice": -3}
EOF
```
The `LowLatency_RT` type sets `nice=-12` — these apps get CPU priority over
almost everything else. The `99-` prefix ensures this file loads last and
overrides any conflicting defaults.
## Step 5: Tune VM Dirty Page Handling ## Step 5: Tune VM Dirty Page Handling
The kernel's default dirty page settings are tuned for servers, not desktops. The kernel's default dirty page settings are tuned for servers, not desktops.
@@ -259,10 +288,24 @@ sudo sysctl --system
| 1.2GB ClamAV RAM usage | 0 bytes | | 1.2GB ClamAV RAM usage | 0 bytes |
| BFQ overhead on NVMe | Direct hardware scheduling | | BFQ overhead on NVMe | Direct hardware scheduling |
| Compilation starves desktop | Compilation yields to UI | | Compilation starves desktop | Compilation yields to UI |
| Coredump processing freezes system | Runs at idle priority |
| All processes equal priority | Desktop apps prioritized |
The final priority hierarchy looks like this:
| Process | Nice | Notes |
|:--------|-----:|:------|
| warp, kitty, kwin | -12 | `LowLatency_RT` — always responsive |
| plasmashell | -5 | Boosted from default -1 |
| vivaldi, firefox | -3 | Snappier than default |
| _normal processes_ | 0 | Default |
| pacman, dkms, gcc | +16 | `BG_CPUIO` — idle only |
| systemd-coredump | +16 | Won't freeze on crash dumps |
DKMS can now compile nvidia modules while I continue working. Package updates DKMS can now compile nvidia modules while I continue working. Package updates
run in the background without the mouse cursor freezing. The system finally run in the background without the mouse cursor freezing. Even when an app
behaves like it should on modern hardware. crashes and generates a 100MB+ coredump, the desktop stays smooth. The system
finally behaves like it should on modern hardware.
# Diagnostic Commands # Diagnostic Commands

12
content/tools/_index.md Normal file
View File

@@ -0,0 +1,12 @@
---
title: "Tools"
description: "Aviation and other useful tools"
summary: "Aviation and other useful tools"
_build:
list: never
render: always
---
## Aviation
- [METAR Quiz](/tools/metar-quiz/) - Test your knowledge of METAR and TAF weather abbreviations

View File

@@ -0,0 +1,26 @@
---
title: "METAR Quiz - Aviation Weather Abbreviation Practice"
layout: "metar-quiz"
description: "Free interactive quiz to learn and practice METAR and TAF weather abbreviations. Test your knowledge of aviation weather codes including precipitation, sky conditions, visibility, and more."
summary: "Practice METAR and TAF weather abbreviations"
keywords:
- METAR quiz
- TAF quiz
- aviation weather
- weather abbreviations
- pilot training
- aviation weather codes
- METAR practice
- METAR test
- learn METAR
- aviation study
tags:
- aviation
- weather
- tools
date: 2025-01-06
lastmod: 2025-01-06
_build:
list: never
render: always
---

View File

@@ -0,0 +1,627 @@
{{- define "main" }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "METAR Quiz",
"description": "{{ .Description }}",
"url": "{{ .Permalink }}",
"applicationCategory": "EducationalApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Person",
"name": "{{ .Site.Params.author }}"
}
}
</script>
<style>
.quiz-container {
max-width: 700px;
margin: 0 auto;
padding: 1rem;
}
.quiz-header {
text-align: center;
margin-bottom: 2rem;
}
.quiz-controls {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.quiz-controls select {
padding: 0.5rem 1rem;
font-size: 1rem;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--code-bg);
color: var(--primary);
cursor: pointer;
}
.quiz-controls select:focus {
outline: none;
border-color: var(--primary);
}
.start-btn, .submit-btn, .next-btn, .retake-btn, .stop-btn {
padding: 0.75rem 2rem;
font-size: 1.1rem;
border: none;
border-radius: 4px;
cursor: pointer;
background: var(--primary);
color: var(--theme);
font-weight: 600;
transition: opacity 0.2s;
}
.start-btn:hover, .submit-btn:hover, .next-btn:hover, .retake-btn:hover, .stop-btn:hover {
opacity: 0.85;
}
.stop-btn {
background: #666;
}
.quiz-area {
display: none;
text-align: center;
}
.quiz-area.active {
display: block;
}
.question-prompt {
font-size: 1.1rem;
color: var(--secondary);
margin-bottom: 0.5rem;
}
.question-text {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--primary);
padding: 1rem;
background: var(--code-bg);
border-radius: 8px;
min-height: 3rem;
}
.answer-input {
padding: 0.75rem 1rem;
font-size: 1.2rem;
border: 2px solid var(--border);
border-radius: 4px;
background: var(--code-bg);
color: var(--primary);
width: 200px;
text-align: center;
text-transform: uppercase;
}
.answer-input:focus {
outline: none;
border-color: var(--primary);
}
.input-row {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
margin-bottom: 1.5rem;
}
.progress-text {
font-size: 1rem;
color: var(--secondary);
margin-top: 1rem;
}
.feedback {
font-size: 1.2rem;
font-weight: 600;
margin: 1rem 0;
padding: 1rem;
border-radius: 4px;
display: none;
}
.feedback.correct {
display: block;
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.feedback.incorrect {
display: block;
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.results-area {
display: none;
text-align: center;
}
.results-area.active {
display: block;
}
.score-display {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--primary);
}
.score-subtitle {
font-size: 1.2rem;
color: var(--secondary);
margin-bottom: 2rem;
}
.results-breakdown {
text-align: left;
max-height: 400px;
overflow-y: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
border-radius: 8px;
}
.result-item {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.result-item:last-child {
border-bottom: none;
}
.result-item.correct {
background: rgba(34, 197, 94, 0.08);
}
.result-item.incorrect {
background: rgba(239, 68, 68, 0.08);
}
.result-question {
flex: 1;
}
.result-answers {
font-family: monospace;
font-size: 0.95rem;
}
.result-your-answer {
color: var(--secondary);
}
.result-correct-answer {
color: #22c55e;
font-weight: 600;
}
.result-icon {
font-size: 1.2rem;
}
.shuffle-stats {
font-size: 1.1rem;
color: var(--secondary);
margin-bottom: 1rem;
}
.shuffle-stats .correct-count {
color: #22c55e;
font-weight: 600;
}
.shuffle-stats .incorrect-count {
color: #ef4444;
font-weight: 600;
}
.setup-area {
text-align: center;
}
.button-row {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
</style>
<article class="quiz-container">
<header class="quiz-header">
<h1>METAR Quiz</h1>
<p class="post-description">Practice aviation weather abbreviations for METAR and TAF reports</p>
</header>
<div id="setup" class="setup-area">
<div class="quiz-controls">
<select id="modeSelect">
<option value="quiz">Quiz Mode</option>
<option value="shuffle">Shuffle Mode</option>
</select>
<select id="difficultySelect">
<option value="normal">Weather Only</option>
<option value="taf">Weather + TAF</option>
<option value="hard">All (Hard)</option>
</select>
</div>
<button class="start-btn" onclick="startQuiz()">Start</button>
</div>
<div id="quizArea" class="quiz-area">
<p class="question-prompt">What is the abbreviation for:</p>
<div id="questionText" class="question-text"></div>
<div class="input-row">
<input type="text" id="answerInput" class="answer-input" placeholder="Enter code" autocomplete="off" maxlength="10">
<button class="submit-btn" onclick="submitAnswer()">Submit</button>
</div>
<div id="feedback" class="feedback"></div>
<p id="progressText" class="progress-text"></p>
<div id="shuffleStats" class="shuffle-stats" style="display: none;">
Correct: <span class="correct-count" id="correctCount">0</span> |
Wrong: <span class="incorrect-count" id="wrongCount">0</span>
</div>
<div class="button-row" id="stopRow" style="display: none;">
<button class="stop-btn" onclick="stopShuffle()">Stop</button>
</div>
</div>
<div id="resultsArea" class="results-area">
<div id="scoreDisplay" class="score-display"></div>
<p id="scoreSubtitle" class="score-subtitle"></p>
<div id="resultsBreakdown" class="results-breakdown"></div>
<button class="retake-btn" onclick="retakeQuiz()">Retake Quiz</button>
</div>
</article>
<script>
(function() {
// METAR Data
const PRECIPITATION = [
{ abbr: "RA", desc: "Rain" },
{ abbr: "SN", desc: "Snow" },
{ abbr: "DZ", desc: "Drizzle" },
{ abbr: "GR", desc: "Hail" },
{ abbr: "GS", desc: "Small Hail/Snow Pellets" },
{ abbr: "PL", desc: "Ice Pellets" },
{ abbr: "SG", desc: "Snow Grains" },
{ abbr: "IC", desc: "Ice Crystals" },
{ abbr: "UP", desc: "Unknown Precipitation" }
];
const DESCRIPTORS = [
{ abbr: "TS", desc: "Thunderstorm" },
{ abbr: "SH", desc: "Showers" },
{ abbr: "FZ", desc: "Freezing" },
{ abbr: "BL", desc: "Blowing" },
{ abbr: "DR", desc: "Drifting" },
{ abbr: "MI", desc: "Shallow" },
{ abbr: "BC", desc: "Patches" },
{ abbr: "PR", desc: "Partial" }
];
const OBSCURATIONS = [
{ abbr: "FG", desc: "Fog" },
{ abbr: "BR", desc: "Mist" },
{ abbr: "HZ", desc: "Haze" },
{ abbr: "FU", desc: "Smoke" },
{ abbr: "DU", desc: "Widespread Dust" },
{ abbr: "SA", desc: "Sand" },
{ abbr: "VA", desc: "Volcanic Ash" },
{ abbr: "PY", desc: "Spray" }
];
const SKY_CONDITIONS = [
{ abbr: "SKC", desc: "Sky Clear" },
{ abbr: "CLR", desc: "Clear (automated)" },
{ abbr: "FEW", desc: "Few (1-2 oktas)" },
{ abbr: "SCT", desc: "Scattered (3-4 oktas)" },
{ abbr: "BKN", desc: "Broken (5-7 oktas)" },
{ abbr: "OVC", desc: "Overcast (8 oktas)" },
{ abbr: "VV", desc: "Vertical Visibility" },
{ abbr: "CB", desc: "Cumulonimbus" },
{ abbr: "TCU", desc: "Towering Cumulus" }
];
const OTHER_WEATHER = [
{ abbr: "VC", desc: "Vicinity" },
{ abbr: "FC", desc: "Funnel Cloud" },
{ abbr: "+FC", desc: "Tornado/Waterspout" },
{ abbr: "PO", desc: "Dust/Sand Whirls" },
{ abbr: "SQ", desc: "Squall" },
{ abbr: "SS", desc: "Sandstorm" },
{ abbr: "DS", desc: "Dust Storm" }
];
const TAF_CODES = [
{ abbr: "BECMG", desc: "Becoming" },
{ abbr: "TEMPO", desc: "Temporary" },
{ abbr: "PROB30", desc: "30% Probability" },
{ abbr: "PROB40", desc: "40% Probability" },
{ abbr: "FM", desc: "From (time)" },
{ abbr: "NSW", desc: "No Significant Weather" },
{ abbr: "CAVOK", desc: "Ceiling and Visibility OK" }
];
const NON_WEATHER = [
{ abbr: "FMH-1", desc: "Federal Meteorological Handbook No. 1" },
{ abbr: "FMH2", desc: "Federal Meteorological Handbook No. 2" },
{ abbr: "FIBI", desc: "Filed But Impracticable to Transmit" },
{ abbr: "METAR", desc: "Meteorological Aerodrome Report" },
{ abbr: "SPECI", desc: "Special (unscheduled observation)" },
{ abbr: "AUTO", desc: "Automated observation" },
{ abbr: "COR", desc: "Correction" },
{ abbr: "RMK", desc: "Remarks" },
{ abbr: "AO1", desc: "Automated without precipitation sensor" },
{ abbr: "AO2", desc: "Automated with precipitation sensor" },
{ abbr: "SLP", desc: "Sea Level Pressure" },
{ abbr: "NOSIG", desc: "No Significant Change" }
];
// State
let mode = 'quiz';
let difficulty = 'normal';
let questions = [];
let currentIndex = 0;
let answers = [];
let correctCount = 0;
let incorrectCount = 0;
let waitingForNext = false;
// DOM Elements
const setup = document.getElementById('setup');
const quizArea = document.getElementById('quizArea');
const resultsArea = document.getElementById('resultsArea');
const questionText = document.getElementById('questionText');
const answerInput = document.getElementById('answerInput');
const feedback = document.getElementById('feedback');
const progressText = document.getElementById('progressText');
const shuffleStats = document.getElementById('shuffleStats');
const stopRow = document.getElementById('stopRow');
const correctCountEl = document.getElementById('correctCount');
const wrongCountEl = document.getElementById('wrongCount');
const scoreDisplay = document.getElementById('scoreDisplay');
const scoreSubtitle = document.getElementById('scoreSubtitle');
const resultsBreakdown = document.getElementById('resultsBreakdown');
const modeSelect = document.getElementById('modeSelect');
const difficultySelect = document.getElementById('difficultySelect');
// Fisher-Yates shuffle
function shuffle(array) {
const arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// Build questions with intensity variants for precipitation
function buildQuestions() {
let items = [];
// Add precipitation with intensity variants
PRECIPITATION.forEach(p => {
// Random intensity: 0 = light, 1 = moderate, 2 = heavy
const intensity = Math.floor(Math.random() * 3);
if (intensity === 0) {
items.push({ abbr: "-" + p.abbr, desc: "Light " + p.desc });
} else if (intensity === 2) {
items.push({ abbr: "+" + p.abbr, desc: "Heavy " + p.desc });
} else {
items.push(p);
}
});
// Add other weather categories (actual weather phenomena)
items = items.concat(DESCRIPTORS, OBSCURATIONS, SKY_CONDITIONS, OTHER_WEATHER);
// Add TAF format codes for taf and hard modes
if (difficulty === 'taf' || difficulty === 'hard') {
items = items.concat(TAF_CODES);
}
// Add non-weather codes for hard mode only
if (difficulty === 'hard') {
items = items.concat(NON_WEATHER);
}
return shuffle(items);
}
// Start quiz
window.startQuiz = function() {
mode = modeSelect.value;
difficulty = difficultySelect.value;
questions = buildQuestions();
currentIndex = 0;
answers = [];
correctCount = 0;
incorrectCount = 0;
waitingForNext = false;
setup.style.display = 'none';
resultsArea.classList.remove('active');
quizArea.classList.add('active');
if (mode === 'shuffle') {
shuffleStats.style.display = 'block';
stopRow.style.display = 'flex';
progressText.style.display = 'none';
} else {
shuffleStats.style.display = 'none';
stopRow.style.display = 'none';
progressText.style.display = 'block';
}
showQuestion();
};
// Show current question
function showQuestion() {
if (mode === 'quiz' && currentIndex >= questions.length) {
showResults();
return;
}
// For shuffle mode, loop back
if (mode === 'shuffle' && currentIndex >= questions.length) {
questions = buildQuestions();
currentIndex = 0;
}
const q = questions[currentIndex];
questionText.textContent = q.desc;
answerInput.value = '';
answerInput.focus();
feedback.className = 'feedback';
feedback.style.display = 'none';
waitingForNext = false;
if (mode === 'quiz') {
progressText.textContent = `Question ${currentIndex + 1} of ${questions.length}`;
}
correctCountEl.textContent = correctCount;
wrongCountEl.textContent = incorrectCount;
}
// Submit answer
window.submitAnswer = function() {
if (waitingForNext) return;
const userAnswer = answerInput.value.trim().toUpperCase();
if (!userAnswer) return;
// Prevent double-submit in shuffle mode
if (mode === 'shuffle') {
waitingForNext = true;
}
const q = questions[currentIndex];
const correctAnswer = q.abbr.toUpperCase();
const isCorrect = userAnswer === correctAnswer;
if (mode === 'quiz') {
answers.push({
question: q.desc,
userAnswer: userAnswer,
correctAnswer: q.abbr,
isCorrect: isCorrect
});
currentIndex++;
if (currentIndex >= questions.length) {
showResults();
} else {
showQuestion();
}
} else {
// Shuffle mode - show feedback and auto-advance
if (isCorrect) {
correctCount++;
feedback.textContent = 'Correct!';
feedback.className = 'feedback correct';
} else {
incorrectCount++;
feedback.textContent = `Incorrect. The answer is: ${q.abbr}`;
feedback.className = 'feedback incorrect';
}
feedback.style.display = 'block';
correctCountEl.textContent = correctCount;
wrongCountEl.textContent = incorrectCount;
// Auto-advance after showing feedback
currentIndex++;
setTimeout(() => {
showQuestion();
}, isCorrect ? 800 : 1500);
}
};
// Stop shuffle
window.stopShuffle = function() {
quizArea.classList.remove('active');
setup.style.display = 'block';
};
// Show results (quiz mode)
function showResults() {
quizArea.classList.remove('active');
resultsArea.classList.add('active');
const correct = answers.filter(a => a.isCorrect).length;
const total = answers.length;
const percentage = Math.round((correct / total) * 100);
scoreDisplay.textContent = `${percentage}%`;
scoreSubtitle.textContent = `${correct} out of ${total} correct`;
let html = '';
answers.forEach(a => {
const statusClass = a.isCorrect ? 'correct' : 'incorrect';
const icon = a.isCorrect ? '&#10003;' : '&#10007;';
html += `
<div class="result-item ${statusClass}">
<span class="result-question">${a.question}</span>
<span class="result-answers">
<span class="result-your-answer">${a.userAnswer}</span>
${!a.isCorrect ? ` → <span class="result-correct-answer">${a.correctAnswer}</span>` : ''}
</span>
<span class="result-icon">${icon}</span>
</div>
`;
});
resultsBreakdown.innerHTML = html;
}
// Retake quiz
window.retakeQuiz = function() {
resultsArea.classList.remove('active');
setup.style.display = 'block';
};
// Enter key to submit
answerInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitAnswer();
}
});
})();
</script>
{{- end }}{{/* end main */}}