diff --git a/.sops.yaml b/.sops.yaml
index 7314a89..ee31af7 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -21,6 +21,27 @@ keys:
- &toto age16vzhcvz8tyxj8e0f47fy0z4p3dsg0ak4vl52ut3l07a0tz465cxslmhevl
- &twothreetunnel age1g7atkxdlt4ymeh7v7aa2yzr2hq2qkvzrc4r49ugttm3n582ymv9qrmpk8d
- &winters age1s0vssf9fey2l456hucppzx2x58xep279nsdcglvkqm30sr9ht37s8rvpza
+ - &summers-ankisync age1kyue7mfvzuxprjz2g6ulz2mxlr57rgzg6lfpnrqedkelehley5ls3enwsd
+ - &summers-atuin age1qpgj3ell93rzkpjq0ezs6t669ds3nyxx67pj50smx597pspz6fqs4jc6pt
+ - &summers-audio age1f63r2klnpfxmntswz5xydpa75ckgjqcs2yzkm0msqwqgz9aqgu0qwzr659
+ - &summers-firefly age17328xwk0z3znalpmma5rvp0lt5ghn5p8xfvnrtdxwsw80dqysacqj9j37q
+ - &summers-forgejo age1qdzkn6v3xhrfjwe8jxz3945dhyyhevwal0narjtr8whf9y7nh3wsn524u5
+ - &summers-freshrss age1etgfym5m8hn3hxs6cgg757zcv5zg5n22wq38fuq59n7qk7nef5uqyg6vvs
+ - &summers-homebox age17mugmkdw0y768a3huuf37r45eff9apyknxvwk3agg6xzsjmqp96q57tcty
+ - &summers-immich age16gf76uustmyyksm3t56zcq9g6j8avy0wrngh8laknfq733s5welqedeg4x
+ - &summers-jellyfin age1fnvlmhzju0yq908xtgags0sy85q3tacl2sc3w3vdd3yfp27xv5aq06v948
+ - &summers-kanidm age1s5gcxtatd9frwctzwg54fqycsx2sa73ll36k7qrpm9wwyknkldtst90gn4
+ - &summers-kavita age1d89878cvt7wsa07ydwtexspku5gppwstrpnpph4ufx5pcd4fadyqgf6lvl
+ - &summers-koillection age1ayupuxlrkepyvjk7xwgrd0pvcj3tfcha688mcuc8ees2hg3g2ersd0q3nc
+ - &summers-matrix age1cq7wxnugpfvjk6dgqpfmc8vemzhkg75drkgeaqjd9fuylz5qh40slazr4u
+ - &summers-monitoring age1vn6ya0japzpgc256jg57fldsqe4udmq50sj5hmkywn7rxfnskevsx2q96u
+ - &summers-nextcloud age1t7zagjfddns4yltupk7nx8xps4gh7mupyz85uuys0wd22cxj5qsq2hw0p7
+ - &summers-paperless age1rn0pxluh7m8dyeshek06d7scejqlrcewlk8xmyrwt5e5nev2dc2s3s78vq
+ - &summers-postgresql age12jh5836w3cmazec8ql652p9h3a3xn6quztztzqxg4n0kz7r96dnqqlhxxw
+ - &summers-radicale age1gxg2peektn8x36kk3nsgmeawl73e54kaadqd649ygwrv43kkvejq2cw64z
+ - &summers-storage age1kn34ny229gm0rg7wlcvxmcyjtz4gka6f2vd958fde6vmuzrxcvcsufra90
+ - &summers-transmission age1y69f2elvmq39lc3t3ucq9y7wt675520n7rvug88qg368qsmmk47qvwrtny
+
creation_rules:
- path_regex: secrets/repo/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -42,6 +63,26 @@ creation_rules:
- *dgx
- *hintbooth-adguardhome
- *hintbooth-nginx
+ - *summers-ankisync
+ - *summers-atuin
+ - *summers-audio
+ - *summers-firefly
+ - *summers-forgejo
+ - *summers-freshrss
+ - *summers-homebox
+ - *summers-immich
+ - *summers-jellyfin
+ - *summers-kanidm
+ - *summers-kavita
+ - *summers-koillection
+ - *summers-matrix
+ - *summers-monitoring
+ - *summers-nextcloud
+ - *summers-paperless
+ - *summers-postgresql
+ - *summers-radicale
+ - *summers-storage
+ - *summers-transmission
- path_regex: secrets/work/[^/]+\.(yaml|json|env|ini)$
key_groups:
@@ -159,6 +200,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-ankisync
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/atuin/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -166,6 +208,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-atuin
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/audio/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -173,6 +216,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-audio
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/firefly/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -180,6 +224,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-firefly
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/forgejo/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -187,6 +232,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-forgejo
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/freshrss/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -194,6 +240,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-freshrss
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/homebox/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -201,6 +248,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-homebox
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/immich/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -208,6 +256,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-immich
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/jellyfin/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -215,6 +264,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-jellyfin
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/kanidm/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -222,6 +272,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-kanidm
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/kavita/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -229,6 +280,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-kavita
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/koillection/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -236,6 +288,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-koillection
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/matrix/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -243,6 +296,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-matrix
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/monitoring/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -250,6 +304,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-monitoring
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/nextcloud/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -257,6 +312,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-nextcloud
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/paperless/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -264,6 +320,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-paperless
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/postgresql/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -271,6 +328,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-postgresql
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/radicale/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -278,6 +336,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-radicale
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/storage/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -285,6 +344,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-storage
- path_regex: hosts/nixos/x86_64-linux/summers/secrets/transmission/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
@@ -292,6 +352,7 @@ creation_rules:
- *swarsel
age:
- *summers
+ - *summers-transmission
- path_regex: hosts/darwin/x86_64-darwin/nbm-imba-166/secrets/[^/]+\.(yaml|json|env|ini|enc)$
key_groups:
diff --git a/SwarselSystems.org b/SwarselSystems.org
index c7be594..23924af 100644
--- a/SwarselSystems.org
+++ b/SwarselSystems.org
@@ -61,7 +61,12 @@ That is the reason why I keep this configuration as a literate one: so that I am
For a beginner, I recommend to read this file like a book, from start to finish. I will try to explain concepts whenever they first come up, and will regularly link to [[#h:8ea35dcc-ef94-4c10-9112-8be8efd6f424][Appendix C: Explanations to nix functions and operators]] when more context is needed. For the first few times that I am using a new function, I will place such a link again. However, to keep the writing of this file manageable, I will generally only do this no more than three times.
-This page offers some utilities to you: you can pin specific headings to the right "pinned" bar by hovering over the heading and clicking =[pin]=. If a section seems uninteresting to you, you can press the =↓= button to skip to the next one. And if you want to send a section to somebody else, you can click the =#= in order to copy its link to the clipboard. Your pinned headings will be saved locally, so you can continue reading in case you take a break.
+This page offers some utilities to you:
+- you can pin specific headings to the right "pinned" bar by hovering over the heading and clicking =[pin]=
+- If a section seems uninteresting to you, you can press the =↓= button to skip to the next one.
+- If a sections upcoming subsections seem uninteresting to you, you can press the =⇣= button to skip to the next heading of at least same or higher level.
+- If you are currently in a level of subsections that all seem uninteresting to you, you can press the =⇑= button to skip to the next heading that is at least one level higher.
+- And if you want to send a section to somebody else, you can click the =#= in order to copy its link to the clipboard. Your pinned headings will be saved locally, so you can continue reading in case you take a break.
** Structure of this file
:PROPERTIES:
@@ -183,7 +188,7 @@ I add a javascript bit to the file in order to have a darkmode toggle when expor
I also add this javascript to add header pinning functionality to the site, using the same trick as above (this is defined in [[#h:e5f900a0-9d68-4269-a663-53c52434c342][HTML Export: Docs QoL]]):
-#+begin_src elisp :noweb yes :exports both :results html
+#+begin_src elisp :tangle no :noweb yes :exports both :results html
"
#+end_src
-** HTML Export: Docs QoL
+** HTML export javascript: Docs QoL
:PROPERTIES:
:CUSTOM_ID: h:e5f900a0-9d68-4269-a663-53c52434c342
:END:
@@ -33447,502 +33748,562 @@ This adds the following functionalities to the [[#h:12880c64-229c-4063-9eea-387a
- Section pinning with persistent pins
- Copy section links to clipboard button
- Searching in table of contents
-- Skip to next section button
+- Skip to next section buttons (one for simply next section, one for same level, one for at least higher level)
#+begin_src javascript :noweb-ref js-docs-qol :exports code
(function() {
- function ready(fn) {
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', fn);
- } else {
- fn();
- }
- }
-
- ready(function initPinned() {
- const STORAGE_KEY = 'org-pinned-items-v2';
-
- let pinnedPanel = document.getElementById('pinned-panel');
- if (!pinnedPanel) {
- pinnedPanel = document.createElement('aside');
- pinnedPanel.id = 'pinned-panel';
- pinnedPanel.innerHTML = `
-
- Clear All
-
- `;
- document.body.appendChild(pinnedPanel);
+ function ready(fn) {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', fn);
+ } else {
+ fn();
+ }
}
- let showBtn = document.getElementById('show-pinned-btn');
- if (!showBtn) {
- showBtn = document.createElement('button');
- showBtn.id = 'show-pinned-btn';
- showBtn.type = 'button';
- showBtn.textContent = 'Pinned';
- document.body.appendChild(showBtn);
- }
+ ready(function initPinned() {
+ const STORAGE_KEY = 'org-pinned-items-v2';
- const content = document.getElementById('content');
- const pinnedList = document.getElementById('pinned-list');
- const toggleBtn = document.getElementById('toggle-pinned-btn');
- const clearAllBtn = document.getElementById('clear-all-pins-btn');
- const toc = document.getElementById('table-of-contents');
- const body = document.body;
+ let pinnedPanel = document.getElementById('pinned-panel');
+ if (!pinnedPanel) {
+ pinnedPanel = document.createElement('aside');
+ pinnedPanel.id = 'pinned-panel';
+ pinnedPanel.innerHTML = `
+
+ Clear All
+
+ `;
+ document.body.appendChild(pinnedPanel);
+ }
- if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return;
+ let showBtn = document.getElementById('show-pinned-btn');
+ if (!showBtn) {
+ showBtn = document.createElement('button');
+ showBtn.id = 'show-pinned-btn';
+ showBtn.type = 'button';
+ showBtn.textContent = 'Pinned';
+ document.body.appendChild(showBtn);
+ }
- function injectSearch() {
- // Check if already injected
- if (document.getElementById('toc-search-input')) return;
+ const content = document.getElementById('content');
+ const pinnedList = document.getElementById('pinned-list');
+ const toggleBtn = document.getElementById('toggle-pinned-btn');
+ const clearAllBtn = document.getElementById('clear-all-pins-btn');
+ const toc = document.getElementById('table-of-contents');
+ const body = document.body;
- const searchContainer = document.createElement('div');
- searchContainer.id = 'toc-search-container';
+ if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return;
- const searchInput = document.createElement('input');
- searchInput.id = 'toc-search-input';
- searchInput.type = 'text';
- searchInput.placeholder = 'Search TOC...';
- searchInput.autocomplete = 'off';
+ function injectSearch() {
+ if (document.getElementById('toc-search-input')) return;
- const clearBtn = document.createElement('button');
- clearBtn.id = 'toc-search-clear';
- clearBtn.type = 'button';
- clearBtn.textContent = 'Clear';
+ const searchContainer = document.createElement('div');
+ searchContainer.id = 'toc-search-container';
- searchContainer.appendChild(searchInput);
- searchContainer.appendChild(clearBtn);
+ const searchInput = document.createElement('input');
+ searchInput.id = 'toc-search-input';
+ searchInput.type = 'text';
+ searchInput.placeholder = 'Search TOC...';
+ searchInput.autocomplete = 'off';
- toc.insertBefore(searchContainer, toc.firstChild);
+ const clearBtn = document.createElement('button');
+ clearBtn.id = 'toc-search-clear';
+ clearBtn.type = 'button';
+ clearBtn.textContent = 'Clear';
- function filterTOC(term) {
- const allLinks = toc.querySelectorAll('a');
+ searchContainer.appendChild(searchInput);
+ searchContainer.appendChild(clearBtn);
- allLinks.forEach(link => {
- const li = link.closest('li');
- if (!li) return;
+ toc.insertBefore(searchContainer, toc.firstChild);
- const text = link.textContent.toLowerCase();
- const matches = text.includes(term);
+ function filterTOC(term) {
+ const allLinks = toc.querySelectorAll('a');
- if (matches) {
- li.classList.remove('hidden-by-search');
- let parent = li.parentElement;
- while (parent && parent !== toc) {
- if (parent.tagName === 'UL') {
- parent.style.display = '';
- }
- if (parent.tagName === 'LI') {
- parent.classList.remove('hidden-by-search');
- }
- parent = parent.parentElement;
+ allLinks.forEach(link => {
+ const li = link.closest('li');
+ if (!li) return;
+
+ const text = link.textContent.toLowerCase();
+ const matches = text.includes(term);
+
+ if (matches) {
+ li.classList.remove('hidden-by-search');
+ let parent = li.parentElement;
+ while (parent && parent !== toc) {
+ if (parent.tagName === 'UL') {
+ parent.style.display = '';
+ }
+ if (parent.tagName === 'LI') {
+ parent.classList.remove('hidden-by-search');
+ }
+ parent = parent.parentElement;
+ }
+ } else {
+ li.classList.add('hidden-by-search');
+ }
+ });
+
+ if (term === '') {
+ const allLis = toc.querySelectorAll('li');
+ allLis.forEach(li => li.classList.remove('hidden-by-search'));
+ }
}
- } else {
- li.classList.add('hidden-by-search');
- }
- });
- if (term === '') {
- const allLis = toc.querySelectorAll('li');
- allLis.forEach(li => li.classList.remove('hidden-by-search'));
+ searchInput.addEventListener('input', function(e) {
+ const term = e.target.value.toLowerCase();
+ filterTOC(term);
+ });
+
+ clearBtn.addEventListener('click', function() {
+ searchInput.value = '';
+ filterTOC('');
+ searchInput.focus();
+ });
}
- }
+ injectSearch();
- searchInput.addEventListener('input', function(e) {
- const term = e.target.value.toLowerCase();
- filterTOC(term);
- });
+ function addHeadingLinks() {
+ const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9');
+ headers.forEach(header => {
+ const id = header.getAttribute('id');
+ if (!id) return;
- clearBtn.addEventListener('click', function() {
- searchInput.value = '';
- filterTOC('');
- searchInput.focus();
- });
- }
- injectSearch();
+ if (header.querySelector('.heading-link')) return;
- function addHeadingLinks() {
- const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9');
- headers.forEach(header => {
- const id = header.getAttribute('id');
- if (!id) return;
+ const link = document.createElement('a');
+ link.className = 'heading-link';
+ link.href = '#' + id;
+ link.textContent = '#';
+ link.title = 'Copy link to this heading';
- if (header.querySelector('.heading-link')) return;
+ const pinBtn = header.querySelector('.toc-pin-btn');
+ if (pinBtn) {
+ header.insertBefore(link, pinBtn);
+ } else {
+ header.appendChild(link);
+ }
- const link = document.createElement('a');
- link.className = 'heading-link';
- link.href = '#' + id;
- link.textContent = '#';
- link.title = 'Copy link to this heading';
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ const url = window.location.origin + window.location.pathname + '#' + id;
- const pinBtn = header.querySelector('.toc-pin-btn');
- if (pinBtn) {
- header.insertBefore(link, pinBtn);
- } else {
- header.appendChild(link);
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(url)
+ .then(() => {
+ const originalText = link.textContent;
+ link.textContent = '✓';
+ setTimeout(() => {
+ link.textContent = originalText;
+ }, 1000);
+ })
+ .catch(err => {
+ console.warn('Failed to copy to clipboard', err);
+ window.location.hash = id;
+ });
+ } else {
+ window.location.hash = id;
+ }
+ });
+ });
}
+ addHeadingLinks();
- link.addEventListener('click', function(e) {
- e.preventDefault();
- const url = window.location.origin + window.location.pathname + '#' + id;
+ function addNextHeadingButtons() {
+ const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'));
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(url)
- .then(() => {
- const originalText = link.textContent;
- link.textContent = '✓';
- setTimeout(() => {
- link.textContent = originalText;
- }, 1000);
- })
- .catch(err => {
- console.warn('Failed to copy to clipboard', err);
- window.location.hash = id;
- });
- } else {
- window.location.hash = id;
- }
- });
- });
- }
- addHeadingLinks();
-
- function addNextHeadingButtons() {
- const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'));
-
- headers.forEach((header, index) => {
- // Skip if button already exists
- if (header.querySelector('.heading-next')) return;
-
- // Find next heading
- const nextHeader = headers[index + 1];
- if (!nextHeader) return; // No next heading
-
- const nextId = nextHeader.getAttribute('id');
- if (!nextId) return;
-
- const nextBtn = document.createElement('button');
- nextBtn.className = 'heading-next';
- nextBtn.type = 'button';
- nextBtn.textContent = '↓';
- nextBtn.title = 'Jump to next heading';
-
- // Insert after the heading link, before the pin button
- const headingLink = header.querySelector('.heading-link');
- const pinBtn = header.querySelector('.toc-pin-btn');
-
- if (pinBtn) {
- header.insertBefore(nextBtn, pinBtn);
- } else if (headingLink) {
- headingLink.after(nextBtn);
- } else {
- header.appendChild(nextBtn);
- }
-
- nextBtn.addEventListener('click', function(e) {
- e.preventDefault();
- nextHeader.scrollIntoView({
- behavior: 'smooth',
- block: 'start'
- });
- // Update URL hash
- history.pushState(null, null, '#' + nextId);
- });
- });
- }
- addNextHeadingButtons();
-
- let mobileTocBtn = document.getElementById('mobile-toc-toggle');
- if (!mobileTocBtn) {
- mobileTocBtn = document.createElement('button');
- mobileTocBtn.id = 'mobile-toc-toggle';
- mobileTocBtn.type = 'button';
- mobileTocBtn.textContent = 'TOC';
- document.body.appendChild(mobileTocBtn);
- }
-
- let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle');
- if (!mobilePinnedBtn) {
- mobilePinnedBtn = document.createElement('button');
- mobilePinnedBtn.id = 'mobile-pinned-toggle';
- mobilePinnedBtn.type = 'button';
- mobilePinnedBtn.textContent = 'Pinned';
- document.body.appendChild(mobilePinnedBtn);
- }
-
- function anyMobilePanelOpen() {
- return toc.classList.contains('mobile-visible') ||
- pinnedPanel.classList.contains('mobile-visible');
- }
-
- function updateBodyMobilePanelState() {
- if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open');
- else body.classList.remove('mobile-panel-open');
- }
-
- document.addEventListener('click', function(e) {
- if (window.innerWidth > 1000) return;
-
- if (!anyMobilePanelOpen()) return;
-
- const clickedInsideToc = toc.contains(e.target);
- const clickedInsidePinned = pinnedPanel.contains(e.target);
- const clickedTocBtn = mobileTocBtn.contains(e.target);
- const clickedPinnedBtn = mobilePinnedBtn.contains(e.target);
-
- const clickedInteractive =
- e.target.tagName === 'A' ||
- e.target.tagName === 'BUTTON' ||
- e.target.tagName === 'INPUT' ||
- e.target.tagName === 'TEXTAREA' ||
- e.target.closest('a') ||
- e.target.closest('button');
-
- if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) {
- toc.classList.remove('mobile-visible');
- pinnedPanel.classList.remove('mobile-visible');
- updateBodyMobilePanelState();
- }
- });
-
- mobileTocBtn.addEventListener('click', function() {
- const isOpen = toc.classList.toggle('mobile-visible');
- if (isOpen) {
- pinnedPanel.classList.remove('mobile-visible');
- }
- updateBodyMobilePanelState();
- });
-
- mobilePinnedBtn.addEventListener('click', function() {
- const isOpen = pinnedPanel.classList.toggle('mobile-visible');
- if (isOpen) {
- toc.classList.remove('mobile-visible');
- }
- updateBodyMobilePanelState();
- });
-
- const pinnedItems = new Map();
- let initiallyPinnedHrefs = new Set();
-
- function loadFromStorage() {
- try {
- const raw = window.localStorage && localStorage.getItem(STORAGE_KEY);
- if (!raw) return;
- const arr = JSON.parse(raw);
- if (!Array.isArray(arr)) return;
- initiallyPinnedHrefs = new Set(arr);
- } catch (e) {
- console.warn('Pinned: failed to load from localStorage', e);
- }
- }
-
- function saveToStorage() {
- try {
- if (!window.localStorage) return;
- const arr = [];
- pinnedItems.forEach((entry, href) => {
- if (entry.li) arr.push(href);
- });
- localStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
- } catch (e) {
- console.warn('Pinned: failed to save to localStorage', e);
- }
- }
-
- function sortPinnedList() {
- const items = Array.from(pinnedList.children)
- .map(li => {
- const link = li.querySelector('a');
- return {
- li: li,
- text: link ? link.textContent.trim()
- .toLowerCase() : ''
- };
- });
- items.sort((a, b) => a.text.localeCompare(b.text));
- items.forEach(item => pinnedList.appendChild(item.li));
- }
-
- function hidePinnedPanel() {
- pinnedPanel.classList.add('hidden');
- content.classList.add('pinned-hidden');
- showBtn.classList.add('visible');
- }
-
- function showPinnedPanel() {
- pinnedPanel.classList.remove('hidden');
- content.classList.remove('pinned-hidden');
- showBtn.classList.remove('visible');
- }
-
- toggleBtn.addEventListener('click', hidePinnedPanel);
- showBtn.addEventListener('click', showPinnedPanel);
-
- clearAllBtn.addEventListener('click', function() {
- if (pinnedItems.size === 0) return;
-
- const confirmed = confirm('Are you sure you want to clear all pinned items?');
- if (!confirmed) return;
-
- pinnedItems.forEach((entry, href) => {
- if (entry.li && entry.li.parentElement) {
- entry.li.parentElement.removeChild(entry.li);
- }
- entry.li = null;
- entry.btns.forEach(b => b.textContent = '[pin]');
- });
-
- saveToStorage();
- });
-
- function attachPinBehavior(pinBtn, href, text) {
- if (!href) return;
-
- if (!pinnedItems.has(href)) {
- pinnedItems.set(href, {
- li: null,
- btns: new Set(),
- text: text
- });
- }
- const entry = pinnedItems.get(href);
- entry.btns.add(pinBtn);
-
- pinBtn.textContent = entry.li ? '[unpin]' : '[pin]';
-
- pinBtn.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- const current = pinnedItems.get(href);
- if (!current) return;
-
- if (current.li) {
- if (current.li.parentElement) {
- current.li.parentElement.removeChild(current.li);
- }
- current.li = null;
- current.btns.forEach(b => b.textContent = '[pin]');
- saveToStorage();
- } else {
- const li = document.createElement('li');
-
- const a = document.createElement('a');
- a.href = href;
- a.textContent = current.text;
-
- const removeBtn = document.createElement('button');
- removeBtn.className = 'pin-remove';
- removeBtn.type = 'button';
- removeBtn.textContent = '✕';
- removeBtn.addEventListener('click', () => {
- const cur = pinnedItems.get(href);
- if (!cur) return;
- if (cur.li && cur.li.parentElement) {
- cur.li.parentElement.removeChild(cur.li);
+ function getLevel(header) {
+ return parseInt(header.tagName.substring(1), 10);
}
- cur.li = null;
- cur.btns.forEach(b => b.textContent = '[pin]');
+
+ headers.forEach((header, index) => {
+ const currentLevel = getLevel(header);
+
+ const headingLink = header.querySelector('.heading-link');
+ const pinBtn = header.querySelector('.toc-pin-btn');
+
+ function insertActionBtn(btn) {
+ if (pinBtn) {
+ header.insertBefore(btn, pinBtn);
+ return;
+ }
+ if (headingLink && headingLink.parentNode === header) {
+ headingLink.after(btn);
+ return;
+ }
+ header.appendChild(btn);
+ }
+
+ const nextHeader = headers[index + 1];
+ if (nextHeader && !header.querySelector('.heading-next')) {
+ const nextId = nextHeader.getAttribute('id');
+ if (nextId) {
+ const nextBtn = document.createElement('button');
+ nextBtn.className = 'heading-next';
+ nextBtn.type = 'button';
+ nextBtn.textContent = '↓';
+ nextBtn.title = 'Jump to next heading';
+
+ insertActionBtn(nextBtn);
+
+ nextBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ nextHeader.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ history.pushState(null, null, '#' + nextId);
+ });
+ }
+ }
+
+ if (!header.querySelector('.heading-skip')) {
+ let skipHeader = null;
+ for (let i = index + 1; i < headers.length; i++) {
+ if (getLevel(headers[i]) <= currentLevel) {
+ skipHeader = headers[i];
+ break;
+ }
+ }
+
+ if (skipHeader) {
+ const skipId = skipHeader.getAttribute('id');
+ if (skipId) {
+ const skipBtn = document.createElement('button');
+ skipBtn.className = 'heading-skip';
+ skipBtn.type = 'button';
+ skipBtn.textContent = '⇣';
+ skipBtn.title = 'Skip to next section (same level)';
+
+ insertActionBtn(skipBtn);
+
+ skipBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ skipHeader.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ history.pushState(null, null, '#' + skipId);
+ });
+ }
+ }
+ }
+
+ if (!header.querySelector('.heading-skip-up')) {
+ let upHeader = null;
+ for (let i = index + 1; i < headers.length; i++) {
+ if (getLevel(headers[i]) < currentLevel) {
+ upHeader = headers[i];
+ break;
+ }
+ }
+
+ if (upHeader) {
+ const upId = upHeader.getAttribute('id');
+ if (upId) {
+ const upBtn = document.createElement('button');
+ upBtn.className = 'heading-skip-up';
+ upBtn.type = 'button';
+ upBtn.textContent = '⇑';
+ upBtn.title = 'Jump to next higher-level section (escape subsections)';
+
+ insertActionBtn(upBtn);
+
+ upBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ upHeader.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ history.pushState(null, null, '#' + upId);
+ });
+ }
+ }
+ }
+ });
+ }
+ addNextHeadingButtons();
+
+ let mobileTocBtn = document.getElementById('mobile-toc-toggle');
+ if (!mobileTocBtn) {
+ mobileTocBtn = document.createElement('button');
+ mobileTocBtn.id = 'mobile-toc-toggle';
+ mobileTocBtn.type = 'button';
+ mobileTocBtn.textContent = 'TOC';
+ document.body.appendChild(mobileTocBtn);
+ }
+
+ let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle');
+ if (!mobilePinnedBtn) {
+ mobilePinnedBtn = document.createElement('button');
+ mobilePinnedBtn.id = 'mobile-pinned-toggle';
+ mobilePinnedBtn.type = 'button';
+ mobilePinnedBtn.textContent = 'Pinned';
+ document.body.appendChild(mobilePinnedBtn);
+ }
+
+ function anyMobilePanelOpen() {
+ return toc.classList.contains('mobile-visible') ||
+ pinnedPanel.classList.contains('mobile-visible');
+ }
+
+ function updateBodyMobilePanelState() {
+ if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open');
+ else body.classList.remove('mobile-panel-open');
+ }
+
+ document.addEventListener('click', function(e) {
+ if (window.innerWidth > 1000) return;
+
+ if (!anyMobilePanelOpen()) return;
+
+ const clickedInsideToc = toc.contains(e.target);
+ const clickedInsidePinned = pinnedPanel.contains(e.target);
+ const clickedTocBtn = mobileTocBtn.contains(e.target);
+ const clickedPinnedBtn = mobilePinnedBtn.contains(e.target);
+
+ const clickedInteractive =
+ e.target.tagName === 'A' ||
+ e.target.tagName === 'BUTTON' ||
+ e.target.tagName === 'INPUT' ||
+ e.target.tagName === 'TEXTAREA' ||
+ e.target.closest('a') ||
+ e.target.closest('button');
+
+ if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) {
+ toc.classList.remove('mobile-visible');
+ pinnedPanel.classList.remove('mobile-visible');
+ updateBodyMobilePanelState();
+ }
+ });
+
+ mobileTocBtn.addEventListener('click', function() {
+ const isOpen = toc.classList.toggle('mobile-visible');
+ if (isOpen) {
+ pinnedPanel.classList.remove('mobile-visible');
+ }
+ updateBodyMobilePanelState();
+ });
+
+ mobilePinnedBtn.addEventListener('click', function() {
+ const isOpen = pinnedPanel.classList.toggle('mobile-visible');
+ if (isOpen) {
+ toc.classList.remove('mobile-visible');
+ }
+ updateBodyMobilePanelState();
+ });
+
+ const pinnedItems = new Map();
+ let initiallyPinnedHrefs = new Set();
+
+ function loadFromStorage() {
+ try {
+ const raw = window.localStorage && localStorage.getItem(STORAGE_KEY);
+ if (!raw) return;
+ const arr = JSON.parse(raw);
+ if (!Array.isArray(arr)) return;
+ initiallyPinnedHrefs = new Set(arr);
+ } catch (e) {
+ console.warn('Pinned: failed to load from localStorage', e);
+ }
+ }
+
+ function saveToStorage() {
+ try {
+ if (!window.localStorage) return;
+ const arr = [];
+ pinnedItems.forEach((entry, href) => {
+ if (entry.li) arr.push(href);
+ });
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
+ } catch (e) {
+ console.warn('Pinned: failed to save to localStorage', e);
+ }
+ }
+
+ function sortPinnedList() {
+ const items = Array.from(pinnedList.children)
+ .map(li => {
+ const link = li.querySelector('a');
+ return {
+ li: li,
+ text: link ? link.textContent.trim()
+ .toLowerCase() : ''
+ };
+ });
+ items.sort((a, b) => a.text.localeCompare(b.text));
+ items.forEach(item => pinnedList.appendChild(item.li));
+ }
+
+ function hidePinnedPanel() {
+ pinnedPanel.classList.add('hidden');
+ content.classList.add('pinned-hidden');
+ showBtn.classList.add('visible');
+ }
+
+ function showPinnedPanel() {
+ pinnedPanel.classList.remove('hidden');
+ content.classList.remove('pinned-hidden');
+ showBtn.classList.remove('visible');
+ }
+
+ toggleBtn.addEventListener('click', hidePinnedPanel);
+ showBtn.addEventListener('click', showPinnedPanel);
+
+ clearAllBtn.addEventListener('click', function() {
+ if (pinnedItems.size === 0) return;
+
+ const confirmed = confirm('Are you sure you want to clear all pinned items?');
+ if (!confirmed) return;
+
+ pinnedItems.forEach((entry, href) => {
+ if (entry.li && entry.li.parentElement) {
+ entry.li.parentElement.removeChild(entry.li);
+ }
+ entry.li = null;
+ entry.btns.forEach(b => b.textContent = '[pin]');
+ });
+
saveToStorage();
- });
+ });
- li.appendChild(a);
- li.appendChild(removeBtn);
- pinnedList.appendChild(li);
+ function attachPinBehavior(pinBtn, href, text) {
+ if (!href) return;
- current.li = li;
- current.btns.forEach(b => b.textContent = '[unpin]');
+ if (!pinnedItems.has(href)) {
+ pinnedItems.set(href, {
+ li: null,
+ btns: new Set(),
+ text: text
+ });
+ }
+ const entry = pinnedItems.get(href);
+ entry.btns.add(pinBtn);
- sortPinnedList();
- saveToStorage();
+ pinBtn.textContent = entry.li ? '[unpin]' : '[pin]';
+
+ pinBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const current = pinnedItems.get(href);
+ if (!current) return;
+
+ if (current.li) {
+ if (current.li.parentElement) {
+ current.li.parentElement.removeChild(current.li);
+ }
+ current.li = null;
+ current.btns.forEach(b => b.textContent = '[pin]');
+ saveToStorage();
+ } else {
+ const li = document.createElement('li');
+
+ const a = document.createElement('a');
+ a.href = href;
+ a.textContent = current.text;
+
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'pin-remove';
+ removeBtn.type = 'button';
+ removeBtn.textContent = '✕';
+ removeBtn.addEventListener('click', () => {
+ const cur = pinnedItems.get(href);
+ if (!cur) return;
+ if (cur.li && cur.li.parentElement) {
+ cur.li.parentElement.removeChild(cur.li);
+ }
+ cur.li = null;
+ cur.btns.forEach(b => b.textContent = '[pin]');
+ saveToStorage();
+ });
+
+ li.appendChild(a);
+ li.appendChild(removeBtn);
+ pinnedList.appendChild(li);
+
+ current.li = li;
+ current.btns.forEach(b => b.textContent = '[unpin]');
+
+ sortPinnedList();
+ saveToStorage();
+ }
+ });
}
- });
- }
- loadFromStorage();
+ loadFromStorage();
- const tocLinks = document.querySelectorAll('#text-table-of-contents a');
- tocLinks.forEach(link => {
- if (link.parentElement && link.parentElement.classList.contains('toc-entry')) {
- return;
- }
- const li = link.closest('li');
- if (!li) return;
+ const tocLinks = document.querySelectorAll('#text-table-of-contents a');
+ tocLinks.forEach(link => {
+ if (link.parentElement && link.parentElement.classList.contains('toc-entry')) {
+ return;
+ }
+ const li = link.closest('li');
+ if (!li) return;
- const wrapper = document.createElement('span');
- wrapper.className = 'toc-entry';
- li.insertBefore(wrapper, link);
- wrapper.appendChild(link);
+ const wrapper = document.createElement('span');
+ wrapper.className = 'toc-entry';
+ li.insertBefore(wrapper, link);
+ wrapper.appendChild(link);
- const pinBtn = document.createElement('button');
- pinBtn.className = 'toc-pin-btn';
- pinBtn.type = 'button';
- pinBtn.textContent = '[pin]';
- wrapper.appendChild(pinBtn);
+ const pinBtn = document.createElement('button');
+ pinBtn.className = 'toc-pin-btn';
+ pinBtn.type = 'button';
+ pinBtn.textContent = '[pin]';
+ wrapper.appendChild(pinBtn);
- const href = link.getAttribute('href');
- const text = link.textContent.trim();
- attachPinBehavior(pinBtn, href, text);
- });
+ const href = link.getAttribute('href');
+ const text = link.textContent.trim();
+ attachPinBehavior(pinBtn, href, text);
+ });
- const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9');
- headers.forEach(header => {
- const id = header.getAttribute('id');
- if (!id) return;
- if (header.querySelector('.toc-pin-btn')) return;
+ const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9');
+ headers.forEach(header => {
+ const id = header.getAttribute('id');
+ if (!id) return;
+ if (header.querySelector('.toc-pin-btn')) return;
- const href = '#' + id;
- const text = header.textContent.trim();
+ const href = '#' + id;
+ const text = header.textContent.trim();
- const pinBtn = document.createElement('button');
- pinBtn.className = 'toc-pin-btn';
- pinBtn.type = 'button';
- pinBtn.textContent = '[pin]';
- pinBtn.style.marginLeft = '0.8rem';
- pinBtn.style.fontSize = '0.75em';
+ const pinBtn = document.createElement('button');
+ pinBtn.className = 'toc-pin-btn';
+ pinBtn.type = 'button';
+ pinBtn.textContent = '[pin]';
+ pinBtn.style.marginLeft = '0.8rem';
+ pinBtn.style.fontSize = '0.75em';
- header.appendChild(pinBtn);
- attachPinBehavior(pinBtn, href, text);
- });
+ header.appendChild(pinBtn);
+ attachPinBehavior(pinBtn, href, text);
+ });
- initiallyPinnedHrefs.forEach(href => {
- const entry = pinnedItems.get(href);
- if (!entry) return;
+ initiallyPinnedHrefs.forEach(href => {
+ const entry = pinnedItems.get(href);
+ if (!entry) return;
- const li = document.createElement('li');
+ const li = document.createElement('li');
- const a = document.createElement('a');
- a.href = href;
- a.textContent = entry.text;
+ const a = document.createElement('a');
+ a.href = href;
+ a.textContent = entry.text;
- const removeBtn = document.createElement('button');
- removeBtn.className = 'pin-remove';
- removeBtn.type = 'button';
- removeBtn.textContent = '✕';
- removeBtn.addEventListener('click', () => {
- const cur = pinnedItems.get(href);
- if (!cur) return;
- if (cur.li && cur.li.parentElement) {
- cur.li.parentElement.removeChild(cur.li);
- }
- cur.li = null;
- cur.btns.forEach(b => b.textContent = '[pin]');
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'pin-remove';
+ removeBtn.type = 'button';
+ removeBtn.textContent = '✕';
+ removeBtn.addEventListener('click', () => {
+ const cur = pinnedItems.get(href);
+ if (!cur) return;
+ if (cur.li && cur.li.parentElement) {
+ cur.li.parentElement.removeChild(cur.li);
+ }
+ cur.li = null;
+ cur.btns.forEach(b => b.textContent = '[pin]');
+ saveToStorage();
+ });
+
+ li.appendChild(a);
+ li.appendChild(removeBtn);
+ pinnedList.appendChild(li);
+
+ entry.li = li;
+ entry.btns.forEach(b => b.textContent = '[unpin]');
+ });
+
+ sortPinnedList();
saveToStorage();
- });
-
- li.appendChild(a);
- li.appendChild(removeBtn);
- pinnedList.appendChild(li);
-
- entry.li = li;
- entry.btns.forEach(b => b.textContent = '[unpin]');
});
-
- sortPinnedList();
- saveToStorage();
- });
})();
#+end_src
@@ -35049,6 +35410,72 @@ This is the stylesheet used by the [[#h:12880c64-229c-4063-9eea-387a97490676][HT
visibility: visible;
}
+ .heading-skip {
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s;
+ margin-left: 0.5rem;
+ color: #718ca1;
+ text-decoration: none;
+ font-size: 0.8em;
+ vertical-align: middle;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+ }
+
+ .heading-skip:hover {
+ color: #5ec4ff;
+ text-decoration: none;
+ }
+
+ h1:hover .heading-skip,
+ h2:hover .heading-skip,
+ h3:hover .heading-skip,
+ h4:hover .heading-skip,
+ h5:hover .heading-skip,
+ h6:hover .heading-skip,
+ h7:hover .heading-skip,
+ h8:hover .heading-skip,
+ h9:hover .heading-skip {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ .heading-skip-up {
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s;
+ margin-left: 0.5rem;
+ color: #718ca1;
+ text-decoration: none;
+ font-size: 0.8em;
+ vertical-align: middle;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+ }
+
+ .heading-skip-up:hover {
+ color: #5ec4ff;
+ text-decoration: none;
+ }
+
+ h1:hover .heading-skip-up,
+ h2:hover .heading-skip-up,
+ h3:hover .heading-skip-up,
+ h4:hover .heading-skip-up,
+ h5:hover .heading-skip-up,
+ h6:hover .heading-skip-up,
+ h7:hover .heading-skip-up,
+ h8:hover .heading-skip-up,
+ h9:hover .heading-skip-up {
+ opacity: 1;
+ visibility: visible;
+ }
+
@media (max-width: 1600px) {
#content {
max-width: 100%;
diff --git a/flake.lock b/flake.lock
index f579e9a..b264af1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1460,11 +1460,11 @@
"pre-commit-hooks": "pre-commit-hooks_2"
},
"locked": {
- "lastModified": 1757854196,
- "narHash": "sha256-RDr3/JTpRyXSR1OOg+wzdOUmDL1Ke05OLV/xctbuQOw=",
+ "lastModified": 1767971910,
+ "narHash": "sha256-j8vLAUaH8oAU5TSprSGa81wx+roo89iG98mUAutsjb8=",
"owner": "oddlama",
"repo": "nixos-extra-modules",
- "rev": "a584a970a05d0410dcb00e0ade684a0c0ce00c4b",
+ "rev": "672abff4255796950924b284b1a2b3dd37113bd2",
"type": "github"
},
"original": {
@@ -1598,6 +1598,22 @@
"type": "github"
}
},
+ "nixpkgs-bisect": {
+ "locked": {
+ "lastModified": 1768161245,
+ "narHash": "sha256-fSidazKIcZElti/a1SOmIwSXw6hXR2GLO/2XmkXgtX4=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "d2a6c7729bbe95f5770ccd4d15a38e5037984b04",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "master",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"nixpkgs-dev": {
"locked": {
"lastModified": 1767131767,
@@ -1893,11 +1909,11 @@
},
"nixpkgs_14": {
"locked": {
- "lastModified": 1763966396,
- "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=",
+ "lastModified": 1737885589,
+ "narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a",
+ "rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8",
"type": "github"
},
"original": {
@@ -2659,6 +2675,7 @@
"nixos-images": "nixos-images",
"nixos-nftables-firewall": "nixos-nftables-firewall",
"nixpkgs": "nixpkgs_18",
+ "nixpkgs-bisect": "nixpkgs-bisect",
"nixpkgs-dev": "nixpkgs-dev",
"nixpkgs-kernel": "nixpkgs-kernel",
"nixpkgs-stable": "nixpkgs-stable_3",
diff --git a/flake.nix b/flake.nix
index f879860..29a5883 100644
--- a/flake.nix
+++ b/flake.nix
@@ -27,6 +27,7 @@
smallpkgs.url = "github:nixos/nixpkgs/08fcb0dcb59df0344652b38ea6326a2d8271baff?narHash=sha256-HXIQzULIG/MEUW2Q/Ss47oE3QrjxvpUX7gUl4Xp6lnc%3D&shallow=1";
nixpkgs-dev.url = "github:Swarsel/nixpkgs/main";
+ nixpkgs-bisect.url = "github:nixos/nixpkgs/master";
nixpkgs-kernel.url = "github:NixOS/nixpkgs/063f43f2dbdef86376cc29ad646c45c46e93234c?narHash=sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o%3D"; #specifically pinned for kernel version
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-stable24_05.url = "github:NixOS/nixpkgs/nixos-24.05";
diff --git a/hosts/nixos/x86_64-linux/hintbooth/default.nix b/hosts/nixos/x86_64-linux/hintbooth/default.nix
index 77fabd2..937ec67 100644
--- a/hosts/nixos/x86_64-linux/hintbooth/default.nix
+++ b/hosts/nixos/x86_64-linux/hintbooth/default.nix
@@ -68,8 +68,8 @@
guests = lib.mkIf (!minimal && config.swarselsystems.withMicroVMs) (
{ }
- // confLib.mkMicrovm "adguardhome"
- // confLib.mkMicrovm "nginx"
+ // confLib.mkMicrovm "adguardhome" { }
+ // confLib.mkMicrovm "nginx" { }
);
}
diff --git a/hosts/nixos/x86_64-linux/hintbooth/guests/adguardhome/default.nix b/hosts/nixos/x86_64-linux/hintbooth/guests/adguardhome/default.nix
index b10f730..eaf90f4 100644
--- a/hosts/nixos/x86_64-linux/hintbooth/guests/adguardhome/default.nix
+++ b/hosts/nixos/x86_64-linux/hintbooth/guests/adguardhome/default.nix
@@ -3,6 +3,7 @@
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
+ "${self}/modules/nixos/optional/microvm-guest-shares.nix"
];
swarselsystems = {
diff --git a/hosts/nixos/x86_64-linux/hintbooth/guests/nginx/default.nix b/hosts/nixos/x86_64-linux/hintbooth/guests/nginx/default.nix
index 39656a1..aef38bd 100644
--- a/hosts/nixos/x86_64-linux/hintbooth/guests/nginx/default.nix
+++ b/hosts/nixos/x86_64-linux/hintbooth/guests/nginx/default.nix
@@ -6,6 +6,7 @@ in
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
+ "${self}/modules/nixos/optional/microvm-guest-shares.nix"
];
swarselsystems = {
diff --git a/hosts/nixos/x86_64-linux/summers/default.nix b/hosts/nixos/x86_64-linux/summers/default.nix
index ddc929d..ad093b0 100644
--- a/hosts/nixos/x86_64-linux/summers/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/default.nix
@@ -1,4 +1,4 @@
-{ self, inputs, lib, minimal, ... }:
+{ self, config, inputs, lib, minimal, confLib, ... }:
{
imports = [
@@ -39,7 +39,7 @@
writeGlobalNetworks = false;
networkKernelModules = [ "igb" ];
rootDisk = "/dev/disk/by-id/ata-TS120GMTS420S_J024880123";
- withMicroVMs = false;
+ withMicroVMs = true;
localVLANs = [ "services" "home" ]; # devices is only provided on interface for bmc
initrdVLAN = "home";
server = {
@@ -83,7 +83,7 @@
acme = false; # cert handled by proxy
nfs = true;
- kavita = true;
+ # kavita = true;
restic = true;
jellyfin = true;
navidrome = true;
@@ -109,29 +109,29 @@
opkssh = true;
};
- # guests = lib.mkIf (!minimal && config.swarselsystems.withMicroVMs) (
- # { }
- # // confLib.mkMicrovm "kavita"
- # // confLib.mkMicrovm "jellyfin"
- # // confLib.mkMicrovm "audio"
- # // confLib.mkMicrovm "postgresql"
- # // confLib.mkMicrovm "matrix"
- # // confLib.mkMicrovm "nextcloud"
- # // confLib.mkMicrovm "immich"
- # // confLib.mkMicrovm "paperless"
- # // confLib.mkMicrovm "transmission"
- # // confLib.mkMicrovm "storage"
- # // confLib.mkMicrovm "monitoring"
- # // confLib.mkMicrovm "freshrss"
- # // confLib.mkMicrovm "kanidm"
- # // confLib.mkMicrovm "firefly"
- # // confLib.mkMicrovm "koillection"
- # // confLib.mkMicrovm "radicale"
- # // confLib.mkMicrovm "atuin"
- # // confLib.mkMicrovm "forgejo"
- # // confLib.mkMicrovm "ankisync"
- # // confLib.mkMicrovm "homebox"
- # );
+ guests = lib.mkIf (!minimal && config.swarselsystems.withMicroVMs) (
+ { }
+ // confLib.mkMicrovm "kavita" { withZfs = true; }
+ // confLib.mkMicrovm "jellyfin" { withZfs = true; }
+ // confLib.mkMicrovm "audio" { withZfs = true; }
+ // confLib.mkMicrovm "postgresql" { withZfs = true; }
+ // confLib.mkMicrovm "matrix" { withZfs = true; }
+ // confLib.mkMicrovm "nextcloud" { withZfs = true; }
+ // confLib.mkMicrovm "immich" { withZfs = true; }
+ // confLib.mkMicrovm "paperless" { withZfs = true; }
+ // confLib.mkMicrovm "transmission" { withZfs = true; }
+ // confLib.mkMicrovm "storage" { withZfs = true; }
+ // confLib.mkMicrovm "monitoring" { withZfs = true; }
+ // confLib.mkMicrovm "freshrss" { withZfs = true; }
+ // confLib.mkMicrovm "kanidm" { withZfs = true; }
+ // confLib.mkMicrovm "firefly" { withZfs = true; }
+ // confLib.mkMicrovm "koillection" { withZfs = true; }
+ // confLib.mkMicrovm "radicale" { withZfs = true; }
+ // confLib.mkMicrovm "atuin" { withZfs = true; }
+ // confLib.mkMicrovm "forgejo" { withZfs = true; }
+ // confLib.mkMicrovm "ankisync" { withZfs = true; }
+ // confLib.mkMicrovm "homebox" { withZfs = true; }
+ );
networking.nftables.firewall.zones.untrusted.interfaces = [ "lan" "bmc" ];
diff --git a/hosts/nixos/x86_64-linux/summers/guests/ankisync/default.nix b/hosts/nixos/x86_64-linux/summers/guests/ankisync/default.nix
index e46d3ee..84c16b1 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/ankisync/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/ankisync/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- ankisync = true;
+ # ankisync = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/atuin/default.nix b/hosts/nixos/x86_64-linux/summers/guests/atuin/default.nix
index 7d4eeea..6a0e601 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/atuin/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/atuin/default.nix
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- atuin = true;
+ # atuin = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/audio/default.nix b/hosts/nixos/x86_64-linux/summers/guests/audio/default.nix
index 5f2ddd6..114e332 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/audio/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/audio/default.nix
@@ -36,9 +36,9 @@
};
swarselmodules.server = {
- navidrome = true;
- spotifyd = true;
- mpd = true;
+ # navidrome = true;
+ # spotifyd = true;
+ # mpd = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/firefly/default.nix b/hosts/nixos/x86_64-linux/summers/guests/firefly/default.nix
index 592424a..461ebff 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/firefly/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/firefly/default.nix
@@ -36,8 +36,9 @@
};
swarselmodules.server = {
- firefly-iii = true;
- nginx = true;
+ # firefly-iii = true;
+ # nginx = true;
+ # acme = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/forgejo/default.nix b/hosts/nixos/x86_64-linux/summers/guests/forgejo/default.nix
index 8efd8f2..f19dc48 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/forgejo/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/forgejo/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- forgejo = true;
+ # forgejo = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/freshrss/default.nix b/hosts/nixos/x86_64-linux/summers/guests/freshrss/default.nix
index 0d43b72..bde8bbb 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/freshrss/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/freshrss/default.nix
@@ -36,8 +36,9 @@
};
swarselmodules.server = {
- freshrss = true;
- nginx = true;
+ # freshrss = true;
+ # nginx = true;
+ # acme = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/homebox/default.nix b/hosts/nixos/x86_64-linux/summers/guests/homebox/default.nix
index da4d446..062c0df 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/homebox/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/homebox/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- homebox = true;
+ # homebox = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/immich/default.nix b/hosts/nixos/x86_64-linux/summers/guests/immich/default.nix
index b2362e4..86a4814 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/immich/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/immich/default.nix
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- immich = true;
+ # immich = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/jellyfin/default.nix b/hosts/nixos/x86_64-linux/summers/guests/jellyfin/default.nix
index 7b7701b..3f80bef 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/jellyfin/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/jellyfin/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 3;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- jellyfin = true;
+ # jellyfin = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/kanidm/default.nix b/hosts/nixos/x86_64-linux/summers/guests/kanidm/default.nix
index 7b0f882..c70f9df 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/kanidm/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/kanidm/default.nix
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- kanidm = true;
+ # kanidm = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/kavita/default.nix b/hosts/nixos/x86_64-linux/summers/guests/kavita/default.nix
index 4c06623..4a3027b 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/kavita/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/kavita/default.nix
@@ -29,6 +29,7 @@
microvm = {
mem = 1024 * 1;
vcpu = 1;
+
};
swarselprofiles = {
@@ -36,7 +37,7 @@
};
swarselmodules.server = {
- kavita = true;
+ # kavita = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/koillection/default.nix b/hosts/nixos/x86_64-linux/summers/guests/koillection/default.nix
index 31a62fc..cbd5f2a 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/koillection/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/koillection/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- koillection = true;
+ # koillection = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/matrix/default.nix b/hosts/nixos/x86_64-linux/summers/guests/matrix/default.nix
index f406585..1575ad4 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/matrix/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/matrix/default.nix
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- matrix = true;
+ # matrix = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/monitoring/default.nix b/hosts/nixos/x86_64-linux/summers/guests/monitoring/default.nix
index 71dea57..d015cb2 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/monitoring/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/monitoring/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 3;
vcpu = 2;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- grafana = true;
+ # grafana = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/nextcloud/default.nix b/hosts/nixos/x86_64-linux/summers/guests/nextcloud/default.nix
index 09bc775..d8580b0 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/nextcloud/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/nextcloud/default.nix
@@ -36,8 +36,9 @@
};
swarselmodules.server = {
- nextcloud = true;
- nginx = true;
+ # nextcloud = true;
+ # nginx = true;
+ # acme = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/paperless/default.nix b/hosts/nixos/x86_64-linux/summers/guests/paperless/default.nix
index fc66f5c..74fe3a6 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/paperless/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/paperless/default.nix
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- paperless = true;
+ # paperless = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/postgresql/default.nix b/hosts/nixos/x86_64-linux/summers/guests/postgresql/default.nix
index 663af81..f94f5f1 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/postgresql/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/postgresql/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- postgresql = true;
+ # postgresql = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/radicale/default.nix b/hosts/nixos/x86_64-linux/summers/guests/radicale/default.nix
index 3bb36f9..aa70516 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/radicale/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/radicale/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 1;
vcpu = 1;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- radicale = true;
+ # radicale = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/storage/default.nix b/hosts/nixos/x86_64-linux/summers/guests/storage/default.nix
index cfd5fd5..b6da313 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/storage/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/storage/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 4;
vcpu = 2;
};
@@ -36,8 +36,8 @@
};
swarselmodules.server = {
- nfs = true;
- syncthing = true;
+ # nfs = true;
+ # syncthing = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/summers/guests/transmission/default.nix b/hosts/nixos/x86_64-linux/summers/guests/transmission/default.nix
index e5629b8..e4e7bd3 100644
--- a/hosts/nixos/x86_64-linux/summers/guests/transmission/default.nix
+++ b/hosts/nixos/x86_64-linux/summers/guests/transmission/default.nix
@@ -27,7 +27,7 @@
} // lib.optionalAttrs (!minimal) {
microvm = {
- mem = 1024 * 2;
+ mem = 1024 * 4;
vcpu = 2;
};
@@ -36,7 +36,7 @@
};
swarselmodules.server = {
- transmission = true;
+ # transmission = true;
};
}
diff --git a/hosts/nixos/x86_64-linux/winters/hardware-configuration.nix b/hosts/nixos/x86_64-linux/winters/hardware-configuration.nix
index 13a956d..3222d17 100644
--- a/hosts/nixos/x86_64-linux/winters/hardware-configuration.nix
+++ b/hosts/nixos/x86_64-linux/winters/hardware-configuration.nix
@@ -1,4 +1,4 @@
-{ lib, modulesPath, ... }:
+{ lib, config, modulesPath, ... }:
{
imports =
diff --git a/modules/nixos/common/pii.nix b/modules/nixos/common/pii.nix
index 17d8130..d10ed18 100644
--- a/modules/nixos/common/pii.nix
+++ b/modules/nixos/common/pii.nix
@@ -1,5 +1,5 @@
# largely based on https://github.com/oddlama/nix-config/blob/main/modules/secrets.nix
-{ config, inputs, lib, nodes, ... }:
+{ config, inputs, lib, nodes, globals, ... }:
let
# If the given expression is a bare set, it will be wrapped in a function,
# so that the imported file can always be applied to the inputs, similar to
@@ -53,7 +53,7 @@ in
secrets = lib.mkOption {
readOnly = true;
- default = lib.mapAttrs (_: x: importEncrypted x { inherit lib nodes inputs; inherit (inputs.topologyPrivate) topologyPrivate; }) config.repo.secretFiles;
+ default = lib.mapAttrs (_: x: importEncrypted x { inherit lib nodes globals inputs; inherit (inputs.topologyPrivate) topologyPrivate; }) config.repo.secretFiles;
type = lib.types.unspecified;
description = "Exposes the loaded repo secrets. This option is read-only.";
};
diff --git a/modules/nixos/common/users.nix b/modules/nixos/common/users.nix
index 52e389a..e6c11a4 100644
--- a/modules/nixos/common/users.nix
+++ b/modules/nixos/common/users.nix
@@ -13,6 +13,8 @@
};
"${config.swarselsystems.mainUser}" = {
isNormalUser = true;
+ uid = 1000;
+ autoSubUidGidRange = false;
description = "Leon S";
password = lib.mkIf (minimal || config.swarselsystems.isPublic) "setup";
hashedPasswordFile = lib.mkIf (!minimal && !config.swarselsystems.isPublic) config.sops.secrets.main-user-hashed-pw.path;
diff --git a/modules/nixos/optional/microvm-guest-shares.nix b/modules/nixos/optional/microvm-guest-shares.nix
new file mode 100644
index 0000000..f6ad083
--- /dev/null
+++ b/modules/nixos/optional/microvm-guest-shares.nix
@@ -0,0 +1,15 @@
+{ self, lib, config, inputs, microVMParent, nodes, ... }:
+{
+ config = {
+ microvm = {
+ shares = [
+ {
+ tag = "persist";
+ source = "${lib.optionalString nodes.${microVMParent}.config.swarselsystems.isImpermanence "/persist"}/microvms/${config.networking.hostName}";
+ mountPoint = "/persist";
+ proto = "virtiofs";
+ }
+ ];
+ };
+ };
+}
diff --git a/modules/nixos/optional/microvm-guest.nix b/modules/nixos/optional/microvm-guest.nix
index 21c29ce..28c8c91 100644
--- a/modules/nixos/optional/microvm-guest.nix
+++ b/modules/nixos/optional/microvm-guest.nix
@@ -1,4 +1,4 @@
-{ self, lib, config, inputs, microVMParent, nodes, ... }:
+{ self, lib, config, inputs, microVMParent, nodes, globals, confLib, ... }:
{
imports = [
inputs.disko.nixosModules.disko
@@ -49,24 +49,16 @@
};
};
- microvm = {
- shares = [
- {
- tag = "persist";
- source = "${lib.optionalString nodes.${microVMParent}.config.swarselsystems.isImpermanence "/persist"}/microvms/${config.networking.hostName}";
- mountPoint = "/persist";
- proto = "virtiofs";
- }
- ];
- # mount the writeable overlay so that we can use nix shells inside the microvm
- volumes = [
- {
- image = "/tmp/nix-store-overlay-${config.networking.hostName}.img";
- autoCreate = true;
- mountPoint = config.microvm.writableStoreOverlay;
- size = 1024;
- }
- ];
- };
+ # microvm = {
+ # mount the writeable overlay so that we can use nix shells inside the microvm
+ # volumes = [
+ # {
+ # image = "/tmp/nix-store-overlay-${config.networking.hostName}.img";
+ # autoCreate = true;
+ # mountPoint = config.microvm.writableStoreOverlay;
+ # size = 1024;
+ # }
+ # ];
+ # };
};
}
diff --git a/modules/nixos/optional/microvm-host.nix b/modules/nixos/optional/microvm-host.nix
index 073353c..5d5c503 100644
--- a/modules/nixos/optional/microvm-host.nix
+++ b/modules/nixos/optional/microvm-host.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, confLib, ... }:
{
config = lib.mkIf (config.guests != { }) {
@@ -17,5 +17,7 @@
(builtins.attrNames config.guests)
);
+ users.persistentIds.microvm = confLib.mkIds 999;
+
};
}
diff --git a/modules/nixos/server/acme.nix b/modules/nixos/server/acme.nix
index 1bbeaec..e36bdf2 100644
--- a/modules/nixos/server/acme.nix
+++ b/modules/nixos/server/acme.nix
@@ -1,4 +1,4 @@
-{ self, pkgs, lib, config, globals, ... }:
+{ self, pkgs, lib, config, globals, confLib, ... }:
let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
@@ -21,7 +21,10 @@ in
'';
};
- users.groups.acme.members = lib.mkIf config.swarselmodules.server.nginx [ "nginx" ];
+ users = {
+ persistentIds.acme = confLib.mkIds 967;
+ groups.acme.members = lib.mkIf config.swarselmodules.server.nginx [ "nginx" ];
+ };
security.acme = {
acceptTerms = true;
diff --git a/modules/nixos/server/disk-encrypt.nix b/modules/nixos/server/disk-encrypt.nix
index 8a8ea3e..d5dde08 100644
--- a/modules/nixos/server/disk-encrypt.nix
+++ b/modules/nixos/server/disk-encrypt.nix
@@ -45,9 +45,9 @@ in
};
boot = lib.mkIf (!config.swarselsystems.isClient) {
- kernelParams = lib.mkIf (!config.swarselsystems.isCloud && ((config.swarselsystems.localVLANs == [ ]) || isRouter)) [
- "ip=${localIp}::${gatewayIp}:${subnetMask}:${config.networking.hostName}::none"
- ];
+ # kernelParams = lib.mkIf (!config.swarselsystems.isCloud && ((config.swarselsystems.localVLANs == []) || isRouter)) [
+ # "ip=${localIp}::${gatewayIp}:${subnetMask}:${config.networking.hostName}::none"
+ # ];
initrd = {
secrets."/tmp${hostKeyPathBase}" = if minimal then (lib.mkForce generatedHostKey) else (lib.mkForce hostKeyPath); # need to mkForce this or it behaves stateful
availableKernelModules = config.swarselsystems.networkKernelModules;
diff --git a/modules/nixos/server/firefly-iii.nix b/modules/nixos/server/firefly-iii.nix
index f1ed188..cf77b73 100644
--- a/modules/nixos/server/firefly-iii.nix
+++ b/modules/nixos/server/firefly-iii.nix
@@ -13,6 +13,9 @@ in
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users = {
+ persistentIds = {
+ firefly-iii = confLib.mkIds 983;
+ };
groups.${serviceGroup} = { };
users.${serviceUser} = {
group = lib.mkForce serviceGroup;
diff --git a/modules/nixos/server/forgejo.nix b/modules/nixos/server/forgejo.nix
index ea246b1..5ae8125 100644
--- a/modules/nixos/server/forgejo.nix
+++ b/modules/nixos/server/forgejo.nix
@@ -12,9 +12,14 @@ in
# networking.firewall.allowedTCPPorts = [ servicePort ];
- users.users.${serviceUser} = {
- group = serviceGroup;
- isSystemUser = true;
+ users = {
+ persistentIds = {
+ forgejo = confLib.mkIds 985;
+ };
+ users.${serviceUser} = {
+ group = serviceGroup;
+ isSystemUser = true;
+ };
};
users.groups.${serviceGroup} = { };
diff --git a/modules/nixos/server/freshrss.nix b/modules/nixos/server/freshrss.nix
index dd764b4..df0a809 100644
--- a/modules/nixos/server/freshrss.nix
+++ b/modules/nixos/server/freshrss.nix
@@ -9,10 +9,15 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
- users.users.${serviceUser} = {
- extraGroups = [ "users" ];
- group = serviceGroup;
- isSystemUser = true;
+ users = {
+ persistentIds = {
+ freshrss = confLib.mkIds 986;
+ };
+ users.${serviceUser} = {
+ extraGroups = [ "users" ];
+ group = serviceGroup;
+ isSystemUser = true;
+ };
};
users.groups.${serviceGroup} = { };
diff --git a/modules/nixos/server/homebox.nix b/modules/nixos/server/homebox.nix
index 468ce7d..5325cce 100644
--- a/modules/nixos/server/homebox.nix
+++ b/modules/nixos/server/homebox.nix
@@ -13,6 +13,10 @@ in
icon = "${self}/files/topology-images/${serviceName}.png";
};
+ users.persistentIds = {
+ homebox = confLib.mkIds 981;
+ };
+
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
@@ -29,14 +33,25 @@ in
};
};
+ systemd.services.homebox = {
+ environment = {
+ TMPDIR = "/var/lib/homebox/.tmp";
+ };
+ serviceConfig = {
+ # ReadWritePaths = "/var/lib/homebox";
+ RuntimeDirectory = "homebox";
+ BindPaths = "/run/homebox:/var/lib/homebox/.tmp";
+ };
+ };
+
services.${serviceName} = {
enable = true;
- package = pkgs.dev.homebox;
+ package = pkgs.bisect.homebox;
database.createLocally = true;
settings = {
HBOX_WEB_PORT = builtins.toString servicePort;
HBOX_OPTIONS_ALLOW_REGISTRATION = "false";
- HBOX_STORAGE_CONN_STRING = "file:///Vault/data/homebox";
+ HBOX_STORAGE_CONN_STRING = "file:///var/lib/homebox";
HBOX_STORAGE_PREFIX_PATH = ".data";
};
};
diff --git a/modules/nixos/server/id.nix b/modules/nixos/server/id.nix
new file mode 100644
index 0000000..f6db715
--- /dev/null
+++ b/modules/nixos/server/id.nix
@@ -0,0 +1,103 @@
+{ lib, config, confLib, ... }:
+let
+ inherit (lib)
+ concatLists
+ flip
+ mapAttrsToList
+ mkDefault
+ mkIf
+ mkOption
+ types
+ ;
+
+ cfg = config.users.persistentIds;
+in
+{
+ options = {
+ swarselmodules.server.ids = lib.mkEnableOption "enable persistent ids on server";
+ users.persistentIds = mkOption {
+ default = { };
+ description = ''
+ Maps a user or group name to its expected uid/gid values. If a user/group is
+ used on the system without specifying a uid/gid, this module will assign the
+ corresponding ids defined here, or show an error if the definition is missing.
+ '';
+ type = types.attrsOf (
+ types.submodule {
+ options = {
+ uid = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = "The uid to assign if it is missing in `users.users.`.";
+ };
+ gid = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = "The gid to assign if it is missing in `users.groups.`.";
+ };
+ };
+ }
+ );
+ };
+
+ users.users = mkOption {
+ type = types.attrsOf (
+ types.submodule (
+ { name, ... }:
+ {
+ config.uid =
+ let
+ persistentUid = cfg.${name}.uid or null;
+ in
+ mkIf (persistentUid != null) (mkDefault persistentUid);
+ }
+ )
+ );
+ };
+
+ users.groups = mkOption {
+ type = types.attrsOf (
+ types.submodule (
+ { name, ... }:
+ {
+ config.gid =
+ let
+ persistentGid = cfg.${name}.gid or null;
+ in
+ mkIf (persistentGid != null) (mkDefault persistentGid);
+ }
+ )
+ );
+ };
+ };
+ config = lib.mkIf config.swarselmodules.server.ids {
+ assertions =
+ concatLists
+ (
+ flip mapAttrsToList config.users.users (
+ name: user: [
+ {
+ assertion = user.uid != null;
+ message = "non-persistent uid detected for '${name}', please assign one via `users.persistentIds`";
+ }
+ {
+ assertion = !user.autoSubUidGidRange;
+ message = "non-persistent subUids/subGids detected for: ${name}";
+ }
+ ]
+ )
+ )
+ ++ flip mapAttrsToList config.users.groups (
+ name: group: {
+ assertion = group.gid != null;
+ message = "non-persistent gid detected for '${name}', please assign one via `users.persistentIds`";
+ }
+ );
+ users.persistentIds = {
+ systemd-coredump = confLib.mkIds 998;
+ systemd-oom = confLib.mkIds 997;
+ polkituser = confLib.mkIds 973;
+ nscd = confLib.mkIds 972;
+ };
+ };
+}
diff --git a/modules/nixos/server/immich.nix b/modules/nixos/server/immich.nix
index bdbc739..568b516 100644
--- a/modules/nixos/server/immich.nix
+++ b/modules/nixos/server/immich.nix
@@ -7,8 +7,14 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
- users.users.${serviceUser} = {
- extraGroups = [ "video" "render" "users" ];
+ users = {
+ persistentIds = {
+ immich = confLib.mkIds 989;
+ redis-immich = confLib.mkIds 977;
+ };
+ users.${serviceUser} = {
+ extraGroups = [ "video" "render" "users" ];
+ };
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
diff --git a/modules/nixos/server/jellyfin.nix b/modules/nixos/server/jellyfin.nix
index 4c2a972..fcd9910 100644
--- a/modules/nixos/server/jellyfin.nix
+++ b/modules/nixos/server/jellyfin.nix
@@ -7,10 +7,12 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
- users.users.${serviceUser} = {
- extraGroups = [ "video" "render" "users" ];
+ users = {
+ persistentIds.jellyfin = confLib.mkIds 994;
+ users.${serviceUser} = {
+ extraGroups = [ "video" "render" "users" ];
+ };
};
-
# nixpkgs.config.packageOverrides = pkgs: {
# intel-vaapi-driver = pkgs.intel-vaapi-driver.override { enableHybridCodec = true; };
# };
diff --git a/modules/nixos/server/kanidm.nix b/modules/nixos/server/kanidm.nix
index bdf247b..17dd259 100644
--- a/modules/nixos/server/kanidm.nix
+++ b/modules/nixos/server/kanidm.nix
@@ -34,6 +34,9 @@ in
users = {
+ persistentIds = {
+ kanidm = confLib.mkIds 984;
+ };
users.${serviceUser} = {
group = serviceGroup;
isSystemUser = true;
diff --git a/modules/nixos/server/kavita.nix b/modules/nixos/server/kavita.nix
index f952f49..ce6bf10 100644
--- a/modules/nixos/server/kavita.nix
+++ b/modules/nixos/server/kavita.nix
@@ -12,11 +12,14 @@ in
calibre
];
-
- users.users.${serviceUser} = {
- extraGroups = [ "users" ];
+ users = {
+ persistentIds.kavita = confLib.mkIds 995;
+ users.${serviceUser} = {
+ extraGroups = [ "users" ];
+ };
};
+
sops.secrets.kavita-token = { inherit sopsFile; owner = serviceUser; };
# networking.firewall.allowedTCPPorts = [ servicePort ];
diff --git a/modules/nixos/server/kea.nix b/modules/nixos/server/kea.nix
index 93eee37..21c4228 100644
--- a/modules/nixos/server/kea.nix
+++ b/modules/nixos/server/kea.nix
@@ -82,6 +82,8 @@ in
{ directory = serviceDir; mode = "0700"; }
];
+ users.persistentIds.kea = confLib.mkIds 968;
+
topology = {
extractors.kea.enable = false;
self.services.${serviceName} = {
diff --git a/modules/nixos/server/matrix.nix b/modules/nixos/server/matrix.nix
index a2a7aba..c77caa7 100644
--- a/modules/nixos/server/matrix.nix
+++ b/modules/nixos/server/matrix.nix
@@ -314,6 +314,11 @@ in
# restart the bridges daily. this is done for the signal bridge mainly which stops carrying
# messages out after a while.
+ users.persistentIds = {
+ mautrix-signal = confLib.mkIds 993;
+ mautrix-whatsapp = confLib.mkIds 992;
+ mautrix-telegram = confLib.mkIds 991;
+ };
nodes =
let
diff --git a/modules/nixos/server/monitoring.nix b/modules/nixos/server/monitoring.nix
index 3e4e3b3..a74489e 100644
--- a/modules/nixos/server/monitoring.nix
+++ b/modules/nixos/server/monitoring.nix
@@ -42,6 +42,11 @@ in
};
users = {
+ persistentIds = {
+ nextcloud-exporter = confLib.mkIds 988;
+ node-exporter = confLib.mkIds 987;
+ grafana = confLib.mkIds 974;
+ };
users = {
nextcloud-exporter = {
extraGroups = [ "nextcloud" ];
diff --git a/modules/nixos/server/nextcloud.nix b/modules/nixos/server/nextcloud.nix
index 5b9bdda..c91e79a 100644
--- a/modules/nixos/server/nextcloud.nix
+++ b/modules/nixos/server/nextcloud.nix
@@ -16,6 +16,11 @@ in
kanidm-nextcloud-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
+ users.persistentIds = {
+ nextcloud = confLib.mkIds 990;
+ redis-nextcloud = confLib.mkIds 976;
+ };
+
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
diff --git a/modules/nixos/server/nfs.nix b/modules/nixos/server/nfs.nix
index 746a583..fb6a776 100644
--- a/modules/nixos/server/nfs.nix
+++ b/modules/nixos/server/nfs.nix
@@ -1,10 +1,15 @@
-{ lib, config, pkgs, globals, ... }:
+{ lib, config, pkgs, globals, confLib, ... }:
let
nfsUser = globals.user.name;
in
{
options.swarselmodules.server.nfs = lib.mkEnableOption "enable nfs on server";
config = lib.mkIf config.swarselmodules.server.nfs {
+
+ users.persistentIds = {
+ avahi = confLib.mkIds 978;
+ };
+
services = {
# add a user with sudo smbpasswd -a
samba = {
diff --git a/modules/nixos/server/opkssh.nix b/modules/nixos/server/opkssh.nix
index 178e3d7..597240d 100644
--- a/modules/nixos/server/opkssh.nix
+++ b/modules/nixos/server/opkssh.nix
@@ -11,6 +11,9 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
+ users.persistentIds = {
+ opksshuser = confLib.mkIds 980;
+ };
services.${serviceName} = {
enable = true;
diff --git a/modules/nixos/server/paperless.nix b/modules/nixos/server/paperless.nix
index 0bd12f6..0d119a5 100644
--- a/modules/nixos/server/paperless.nix
+++ b/modules/nixos/server/paperless.nix
@@ -12,8 +12,13 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
- users.users.${serviceUser} = {
- extraGroups = [ "users" ];
+ users = {
+ persistentIds = {
+ redis-paperless = confLib.mkIds 975;
+ };
+ users.${serviceUser} = {
+ extraGroups = [ "users" ];
+ };
};
sops.secrets = {
diff --git a/modules/nixos/server/pipewire.nix b/modules/nixos/server/pipewire.nix
index b6b315a..41a602d 100644
--- a/modules/nixos/server/pipewire.nix
+++ b/modules/nixos/server/pipewire.nix
@@ -1,9 +1,11 @@
-{ lib, config, ... }:
+{ lib, config, confLib, ... }:
{
- config = lib.mkIf (config?swarselmodules.server.mpd || config?swarselmodules.server.navidrome) {
+ config = lib.mkIf (config.swarselmodules.server.mpd || config.swarselmodules.server.navidrome) {
security.rtkit.enable = true; # this is required for pipewire real-time access
+ users.persistentIds.rtkit = confLib.mkIds 996;
+
services.pipewire = {
enable = true;
pulse.enable = true;
diff --git a/modules/nixos/server/podman.nix b/modules/nixos/server/podman.nix
index 192e0c3..0a27be5 100644
--- a/modules/nixos/server/podman.nix
+++ b/modules/nixos/server/podman.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, confLib, ... }:
let
serviceName = "podman";
in
@@ -6,6 +6,10 @@ in
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
+ users.persistentIds = {
+ podman = confLib.mkIds 969;
+ };
+
virtualisation = {
podman.enable = true;
oci-containers.backend = "podman";
diff --git a/modules/nixos/server/radicale.nix b/modules/nixos/server/radicale.nix
index ed09675..5953bdc 100644
--- a/modules/nixos/server/radicale.nix
+++ b/modules/nixos/server/radicale.nix
@@ -29,6 +29,10 @@ in
};
};
+ users.persistentIds = {
+ radicale = confLib.mkIds 982;
+ };
+
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
diff --git a/modules/nixos/server/restic.nix b/modules/nixos/server/restic.nix
index cb5c046..2b0bd01 100644
--- a/modules/nixos/server/restic.nix
+++ b/modules/nixos/server/restic.nix
@@ -11,6 +11,10 @@ in
paths = lib.mkOption {
type = lib.types.listOf lib.types.str;
};
+ withPostgres = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ };
};
config = lib.mkIf config.swarselmodules.server.restic {
diff --git a/modules/nixos/server/ssh.nix b/modules/nixos/server/ssh.nix
index 877552a..22a2204 100644
--- a/modules/nixos/server/ssh.nix
+++ b/modules/nixos/server/ssh.nix
@@ -1,4 +1,4 @@
-{ self, lib, config, withHomeManager, ... }:
+{ self, lib, config, withHomeManager, confLib, ... }:
{
options.swarselmodules.server.ssh = lib.mkEnableOption "enable ssh on server";
config = lib.mkIf config.swarselmodules.server.ssh {
@@ -21,17 +21,22 @@
}
];
};
- users.users = {
- "${config.swarselsystems.mainUser}".openssh.authorizedKeys.keyFiles = lib.mkIf withHomeManager [
- (self + /secrets/public/ssh/yubikey.pub)
- (self + /secrets/public/ssh/magicant.pub)
- # (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
- ];
- root.openssh.authorizedKeys.keyFiles = [
- (self + /secrets/public/ssh/yubikey.pub)
- (self + /secrets/public/ssh/magicant.pub)
- # (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
- ];
+ users = {
+ persistentIds = {
+ sshd = lib.mkIf config.swarselmodules.server.ids (confLib.mkIds 979);
+ };
+ users = {
+ "${config.swarselsystems.mainUser}".openssh.authorizedKeys.keyFiles = lib.mkIf withHomeManager [
+ (self + /secrets/public/ssh/yubikey.pub)
+ (self + /secrets/public/ssh/magicant.pub)
+ # (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
+ ];
+ root.openssh.authorizedKeys.keyFiles = [
+ (self + /secrets/public/ssh/yubikey.pub)
+ (self + /secrets/public/ssh/magicant.pub)
+ # (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
+ ];
+ };
};
security.sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
diff --git a/modules/nixos/server/transmission.nix b/modules/nixos/server/transmission.nix
index 8d53c9a..c3f8e4c 100644
--- a/modules/nixos/server/transmission.nix
+++ b/modules/nixos/server/transmission.nix
@@ -25,6 +25,10 @@ in
# this user/group section is probably unneeded
users = {
+ persistentIds = {
+ prowlarr = confLib.mkIds 971;
+ readarr = confLib.mkIds 970;
+ };
groups = {
dockeruser = {
gid = 1155;
diff --git a/modules/shared/config-lib.nix b/modules/shared/config-lib.nix
index 9f91ae2..4439036 100644
--- a/modules/shared/config-lib.nix
+++ b/modules/shared/config-lib.nix
@@ -53,44 +53,104 @@ in
homeServiceAddress = lib.optionalString (config.swarselsystems.server.wireguard.interfaces ? wgHome) globals.networks."${config.swarselsystems.server.wireguard.interfaces.wgHome.serverNetConfigPrefix}-wgHome".hosts.${config.node.name}.ipv4;
};
+ mkIds = id: {
+ uid = id;
+ gid = id;
+ };
+
+ mkDeviceMac = id:
+ let
+ mod = n: d: n - (n / d) * d;
+ toHexByte = n:
+ let
+ hex = "0123456789abcdef";
+ hi = n / 16;
+ lo = mod n 16;
+ in
+ builtins.substring hi 1 hex
+ + builtins.substring lo 1 hex;
+
+ max = 16777215; # 256^3 - 1
+
+ b1 = id / (256 * 256);
+ r1 = mod id (256 * 256);
+ b2 = r1 / 256;
+ b3 = mod r1 256;
+ in
+ if
+ (id <= max)
+ then
+ (builtins.concatStringsSep ":"
+ (map toHexByte [ b1 b2 b3 ]))
+ else
+ (throw "Device MAC ID too large (max is 16777215)");
+
mkMicrovm =
if config.swarselsystems.withMicroVMs then
- (guestName: {
- ${guestName} = {
- backend = "microvm";
- autostart = true;
- modules = [
- (config.node.configDir + /guests/${guestName}/default.nix)
- {
- node.secretsDir = config.node.configDir + /secrets/${guestName};
- node.configDir = config.node.configDir + /guests/${guestName};
- networking.nftables.firewall = {
- zones.untrusted.interfaces = lib.mkIf
- (
- lib.length config.guests.${guestName}.networking.links == 1
- )
- config.guests.${guestName}.networking.links;
+ (guestName:
+ { enableStorage ? false
+ , withZfs ? false
+ , ...
+ }:
+ {
+ ${guestName} = {
+ backend = "microvm";
+ autostart = true;
+ zfs = lib.mkIf withZfs {
+ # stateful config that should be backed up
+ "/state" = {
+ pool = "Vault";
+ dataset = "guests/${guestName}/state";
};
- }
- "${self}/modules/nixos/optional/microvm-guest.nix"
- "${self}/modules/nixos/optional/systemd-networkd-base.nix"
- ];
- microvm = {
- system = config.node.arch;
- baseMac = config.repo.secrets.local.networking.networks.lan.mac;
- interfaces.vlan-services = { };
+ # data that should be backed up
+ "/storage" = lib.mkIf enableStorage {
+ pool = "Vault";
+ dataset = "guests/${guestName}/storage";
+ };
+ # other stuff that should only reside on disk, not backed up
+ "/persist" = {
+ pool = "Vault";
+ dataset = "guests/${guestName}/persist";
+ };
+ };
+ modules = [
+ (config.node.configDir + /guests/${guestName}/default.nix)
+ {
+ node.secretsDir = config.node.configDir + /secrets/${guestName};
+ node.configDir = config.node.configDir + /guests/${guestName};
+ networking.nftables.firewall = {
+ zones.untrusted.interfaces = lib.mkIf
+ (
+ lib.length config.guests.${guestName}.networking.links == 1
+ )
+ config.guests.${guestName}.networking.links;
+ };
+ }
+ "${self}/modules/nixos/optional/microvm-guest.nix"
+ "${self}/modules/nixos/optional/systemd-networkd-base.nix"
+ ];
+ microvm = {
+ system = config.node.arch;
+ baseMac = config.repo.secrets.local.networking.networks.lan.mac;
+ interfaces.vlan-services = {
+ mac = lib.mkForce "02:${lib.substring 3 5 config.guests.${guestName}.microvm.baseMac}:${mkDeviceMac globals.networks.home-lan.vlans.services.hosts."${config.node.name}-${guestName}".id}";
+
+ };
+ };
+ extraSpecialArgs = {
+ inherit (inputs.self) nodes;
+ inherit (inputs.self.pkgs.${config.node.arch}) lib;
+ inherit inputs outputs minimal;
+ inherit (inputs) self;
+ withHomeManager = false;
+ microVMParent = config.node.name;
+ globals = inputs.self.globals.${config.node.arch};
+ };
};
- extraSpecialArgs = {
- inherit (inputs.self) nodes;
- inherit (inputs.self.pkgs.${config.node.arch}) lib;
- inherit inputs outputs minimal;
- inherit (inputs) self;
- withHomeManager = false;
- microVMParent = config.node.name;
- globals = inputs.self.globals.${config.node.arch};
- };
- };
- }) else (_: { _ = { }; });
+ }) else
+ (_: {
+ _ = { };
+ });
genNginx =
{ serviceAddress
diff --git a/profiles/nixos/localserver/default.nix b/profiles/nixos/localserver/default.nix
index 7de318e..8ff0411 100644
--- a/profiles/nixos/localserver/default.nix
+++ b/profiles/nixos/localserver/default.nix
@@ -17,6 +17,7 @@
nftables = lib.mkDefault true;
server = {
general = lib.mkDefault true;
+ ids = lib.mkDefault true;
network = lib.mkDefault true;
diskEncryption = lib.mkDefault true;
packages = lib.mkDefault true;
diff --git a/profiles/nixos/microvm/default.nix b/profiles/nixos/microvm/default.nix
index 689bbbf..226edc7 100644
--- a/profiles/nixos/microvm/default.nix
+++ b/profiles/nixos/microvm/default.nix
@@ -18,6 +18,7 @@
nftables = lib.mkDefault true;
server = {
general = lib.mkDefault true;
+ ids = lib.mkDefault true;
packages = lib.mkDefault true;
ssh = lib.mkDefault true;
wireguard = lib.mkDefault true;
diff --git a/secrets/repo/pii.nix.enc b/secrets/repo/pii.nix.enc
index 7dbf7b6..f2d6444 100644
--- a/secrets/repo/pii.nix.enc
+++ b/secrets/repo/pii.nix.enc
@@ -1,5 +1,5 @@
{
- "data": "ENC[AES256_GCM,data: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,iv:6cj17MfBuVgr3MEEOGcU7IhGnxJA3jhkduRzgK2IHjU=,tag:bOsZVs+51ojqIZA9V2Dk3w==,type:str]",
+ "data": "ENC[AES256_GCM,data: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,iv:uO2n6uM8pG16CzQoRGpqqRp9CpSALkzFGnVdgGSeMS0=,tag:yPHLjZ+spz2djJuzg11U9A==,type:str]",
"sops": {
"age": [
{
@@ -63,8 +63,8 @@
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1RGpibkIreUVYNlA2TXUw\nNmZzdnFtNHNOQ21QTE5ycTZHaFU0U0d3RzFnClFXRGFlZnB1WjRBZW5WN1BSR3dK\nY0Q4R3FGdEQycDEranUzWFJyWWVwOWMKLS0tIHp1Rm0wdi94Tk9kM3crWUtjb2Vo\nSHU4ay9WenpGTTg5T2Y1TzVicHBPZWMKEpeP/SVorT881MiLvR+6O9lcc9Fn0VV9\n5PqcowYMKFFXBQ27qjoFBDSwmD0UxpUgJeMtMAL+QLMILZ+G/0EcGw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
- "lastmodified": "2026-01-09T13:03:32Z",
- "mac": "ENC[AES256_GCM,data:1IPWZKNuDEObu7tuBZ5WiwWhx3srmudOiZFVFh27zfr7N5sGG2VAQGOCYgw8e8SuscUyyHRcw1uPuXGy0gB+ITX/kcClwJNqdT/ZvDvmP9zDAQMSAkwA+3xUBlobkabtj4+txtAc4xYleXIZN/22hpyeHJ1bOHaHNw+Zm2elk5Q=,iv:NL8trDkA+lMLnQqgrzsb31Tglvc6PtFky+vvSA6yIKY=,tag:S4BcowZWKscT2y1/OtZ86g==,type:str]",
+ "lastmodified": "2026-01-12T20:26:18Z",
+ "mac": "ENC[AES256_GCM,data:jIB7rrK4yYjLlqUdl10JfYg3qaAYbuePPjZoGCBCCaohFdTWJPDoJMg5EFMFrZIDVOeWDjYVh6bB+PvXiIvU39NrEymS/Gu50ehCG7Lls7a90K+NSZQhuHS1y91+93t8vOq3kPisN0fh0J9tJwphB/NbmgDrE5e/NQsA4FjOsWU=,iv:VITEc80bKA5ZXuy9iUz7NjobYvVDKXDWhnXFyFwy84c=,tag:POCbrUFiKcc0418v9njPNw==,type:str]",
"pgp": [
{
"created_at": "2026-01-10T00:52:57Z",