54 Commits

Author SHA1 Message Date
f51c362086 upd 2026-04-17 19:23:32 -05:00
mjallen18
a158d401ae filtering 2026-04-16 20:12:18 -05:00
mjallen18
95842b22f0 agh 2026-04-16 19:24:59 -05:00
mjallen18
3977227889 idk 2026-04-16 19:22:57 -05:00
mjallen18
cb8ef87229 nuc 2026-04-16 13:03:55 -05:00
mjallen18
44b3459d49 lol 2026-04-16 13:01:27 -05:00
mjallen18
c59ac2ccb6 kern 2026-04-16 12:40:03 -05:00
mjallen18
1767debfd8 upd 2026-04-16 12:38:07 -05:00
mjallen18
95f08a258e hue 2026-04-16 09:58:32 -05:00
mjallen18
c5ba5d4164 bluetooth 2026-04-15 11:39:41 -05:00
mjallen18
004eb3c29c esphome 2026-04-14 17:45:29 -05:00
mjallen18
616d357a59 cyd 2026-04-14 17:42:19 -05:00
mjallen18
d4481923a8 cyd 2026-04-14 17:38:59 -05:00
mjallen18
246f65190e cyd 2026-04-14 17:37:38 -05:00
mjallen18
b6df62a875 cyd 2026-04-14 17:31:10 -05:00
mjallen18
8d81a1d6e1 cyd 2026-04-14 17:29:22 -05:00
mjallen18
7368968fd5 cyd 2026-04-14 17:25:44 -05:00
mjallen18
9a719479bc cyd 2026-04-14 17:15:13 -05:00
mjallen18
38922cd526 cyd 2026-04-14 17:01:48 -05:00
mjallen18
26e7fffbd1 cyd 2026-04-14 16:56:12 -05:00
mjallen18
9792f86548 cyd 2026-04-14 16:54:32 -05:00
mjallen18
dd9fa58c5c cyd 2026-04-14 16:46:58 -05:00
mjallen18
db620cd22a cyd 2026-04-14 16:36:35 -05:00
mjallen18
dab3a37b0a cyd 2026-04-14 16:18:33 -05:00
mjallen18
74b1825d4d cyd 2026-04-14 16:12:54 -05:00
mjallen18
c3abeb846d ip 2026-04-13 14:22:27 -05:00
mjallen18
d676b6dc1e nuc 2026-04-13 14:11:45 -05:00
mjallen18
86fffbd512 upd 2026-04-13 13:25:52 -05:00
mjallen18
1b5f695f40 todo remove 2026-04-13 09:41:40 -05:00
mjallen18
9491c0356d grafana 2026-04-13 09:41:27 -05:00
152efb84da esp 2026-04-10 09:49:19 -05:00
26d5a8c686 esp 2026-04-10 09:48:58 -05:00
mjallen18
ee55a543fa caddy int 2026-04-09 15:01:01 -05:00
mjallen18
7cc6732a7e caddy int 2026-04-09 14:57:27 -05:00
mjallen18
b73ad049e7 darwin 2026-04-09 11:20:29 -05:00
mjallen18
5d23b3db93 .face 2026-04-09 11:03:35 -05:00
mjallen18
aa609630a1 darwin 2026-04-09 10:35:50 -05:00
mjallen18
1e1eb9886c darwin 2026-04-09 10:32:06 -05:00
9c326f5768 neb 2026-04-08 17:36:21 -05:00
e8cae7fff1 vesktop 2026-04-08 17:32:32 -05:00
88b9d5309f vesktop 2026-04-08 17:23:36 -05:00
d44d03d0b1 vesktop 2026-04-08 17:14:32 -05:00
4ac7463a1f ... 2026-04-08 16:11:17 -05:00
mjallen18
b354dc202a nas 2026-04-08 16:08:00 -05:00
mjallen18
079493b55e nas 2026-04-08 16:08:00 -05:00
mjallen18
d06a43bf06 build2 2026-04-08 15:40:48 -05:00
mjallen18
6b8395ffdb nebula 2026-04-08 15:16:25 -05:00
mjallen18
7adbafb848 attic 2026-04-08 15:13:03 -05:00
mjallen18
3af0d99f98 attic 2026-04-08 15:08:00 -05:00
025ab854f0 vesktop 2026-04-08 14:57:15 -05:00
mjallen18
5ce8433aa8 lol 2026-04-08 14:56:39 -05:00
mjallen18
2e8c2ddd3a lol 2026-04-08 13:24:09 -05:00
mjallen18
4cb746afc5 hmm 2026-04-07 22:02:54 -05:00
mjallen18
3234029ae5 hmm 2026-04-07 22:02:54 -05:00
67 changed files with 4464 additions and 548 deletions

175
flake.lock generated
View File

@@ -25,7 +25,7 @@
"flake-utils": "flake-utils",
"napalm": "napalm",
"nixpkgs": [
"nixpkgs"
"nixpkgs-stable"
],
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
@@ -33,11 +33,11 @@
"uv2nix": "uv2nix"
},
"locked": {
"lastModified": 1774079362,
"narHash": "sha256-HkoEWTxU5gNigcnhIa3GXukHqC5xGmgVaLICGUKlpdo=",
"lastModified": 1776085803,
"narHash": "sha256-JvvWVbXJYSY8qOReMbAOD4lxcN2cjKV6lg/jLz8CEuY=",
"owner": "nix-community",
"repo": "authentik-nix",
"rev": "1f279763d8b4a9138c01f1021f53e09bc2c54eb9",
"rev": "4370b561c8bafb59773ce3a518506bcf1161dbdb",
"type": "github"
},
"original": {
@@ -49,16 +49,16 @@
"authentik-src": {
"flake": false,
"locked": {
"lastModified": 1772567399,
"narHash": "sha256-0Vpf1hj9C8r+rhrCgwoNazpQ+mwgjdjDhuoKCxYQFWw=",
"lastModified": 1775573258,
"narHash": "sha256-Xq7JGI/8ppIydIuWd9KRJKUrh7UpeniwvZ4NAtXbYJ4=",
"owner": "goauthentik",
"repo": "authentik",
"rev": "0dccbd4193c45c581e9fb7cd89df0c1487510f1f",
"rev": "5249546862986202b901c2afd860992ec48c6ef6",
"type": "github"
},
"original": {
"owner": "goauthentik",
"ref": "version/2026.2.1",
"ref": "version/2026.2.2",
"repo": "authentik",
"type": "github"
}
@@ -151,11 +151,11 @@
"cachyos-kernel": {
"flake": false,
"locked": {
"lastModified": 1775145950,
"narHash": "sha256-AfVja9nvYHm0BHbuTvn+K8rKfLmPl5QjoiNecp9HOJU=",
"lastModified": 1776183001,
"narHash": "sha256-lvLKB5dTqjO1S/YonS9ZyWemEjO6QXtN4D76rYEYy4s=",
"owner": "CachyOS",
"repo": "linux-cachyos",
"rev": "b91624f68ceaf5394ef1571f60290dca6ba22b45",
"rev": "4224303b6d7a50dd1cc3ffa78864050cc9536eec",
"type": "github"
},
"original": {
@@ -167,11 +167,11 @@
"cachyos-kernel-patches": {
"flake": false,
"locked": {
"lastModified": 1775157685,
"narHash": "sha256-g8HgH7gADoEnrBN30BK3pz7+M2pT/p3xtfRFEuEov5w=",
"lastModified": 1776355454,
"narHash": "sha256-b9Hc0sTxjEzDbphzS9yQqxVha/7bsPIs2cQQQvaG45E=",
"owner": "CachyOS",
"repo": "kernel-patches",
"rev": "c1ba300617a12d257b5721572b9bbe28efae182f",
"rev": "b5e029226df5cc30c103651072d49a7af2878202",
"type": "github"
},
"original": {
@@ -584,11 +584,11 @@
]
},
"locked": {
"lastModified": 1775457580,
"narHash": "sha256-ikws/ssAmG20AGrEwBuwspwPlkubJu34mB+Uz2fJBJs=",
"lastModified": 1776454077,
"narHash": "sha256-7zSUFWsU0+jlD7WB3YAxQ84Z/iJurA5hKPm8EfEyGJk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "5de7dbd151b0bd65d45785553d4a22d832733ffc",
"rev": "565e5349208fe7d0831ef959103c9bafbeac0681",
"type": "github"
},
"original": {
@@ -604,11 +604,11 @@
]
},
"locked": {
"lastModified": 1775457580,
"narHash": "sha256-ikws/ssAmG20AGrEwBuwspwPlkubJu34mB+Uz2fJBJs=",
"lastModified": 1776454077,
"narHash": "sha256-7zSUFWsU0+jlD7WB3YAxQ84Z/iJurA5hKPm8EfEyGJk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "5de7dbd151b0bd65d45785553d4a22d832733ffc",
"rev": "565e5349208fe7d0831ef959103c9bafbeac0681",
"type": "github"
},
"original": {
@@ -658,11 +658,11 @@
"homebrew-cask": {
"flake": false,
"locked": {
"lastModified": 1775484338,
"narHash": "sha256-ylzTIrXzlCDjz9R3WxFkqqZuPboaVB/W5utc00R3wR4=",
"lastModified": 1776469040,
"narHash": "sha256-IX5UflSmiXkJnRUCNjzBl4/HMw0NMLQqsfdwA4l0kyU=",
"owner": "homebrew",
"repo": "homebrew-cask",
"rev": "fdc3c0fbd192076460dfd1d188ef45d68305397c",
"rev": "906ff3d493d3e9f50ceb5041fcc14bcfe3d63ff1",
"type": "github"
},
"original": {
@@ -674,11 +674,11 @@
"homebrew-core": {
"flake": false,
"locked": {
"lastModified": 1775481683,
"narHash": "sha256-3PHpmYBLRXoe6r5Bx5H8jri4gW1410Uk8H8ssDS8BOA=",
"lastModified": 1776461416,
"narHash": "sha256-AqxPJs6cy7ZwsS2ovNuLxUJM+2kgnEi4ECXitf6nb18=",
"owner": "homebrew",
"repo": "homebrew-core",
"rev": "f43fd25bcd80d6af26670239676b830351a50144",
"rev": "2aab2c98676928d65d72ce7fc2abd5c7f3634319",
"type": "github"
},
"original": {
@@ -714,11 +714,11 @@
]
},
"locked": {
"lastModified": 1775287496,
"narHash": "sha256-tCBlt+RP85MLrMYntro/YvG7NWktbmFiyItGBo85Tf8=",
"lastModified": 1776428236,
"narHash": "sha256-+0SyQglnT2xUiyY07155G+O7aUWISELwqtTnfURufRU=",
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"rev": "0a7a3feb77606db451aa10287ad4c4c8f85922f8",
"rev": "eac78fc379ca47f7e21be8539c405e5fb489a857",
"type": "github"
},
"original": {
@@ -757,11 +757,11 @@
]
},
"locked": {
"lastModified": 1775501721,
"narHash": "sha256-IU2KcBvb8tulQ8NKz3OXujzOdeWXXv63sNDTjo8gPu4=",
"lastModified": 1776442631,
"narHash": "sha256-8AXOo0Yhbi3jpQFe4Ql+0HZDz/p708GdrbZVepNjITo=",
"owner": "ggml-org",
"repo": "llama.cpp",
"rev": "2e1f0a889e19a3922db57452268f4574c35c36e5",
"rev": "45cac7ca703fb9085eae62b9121fca01d20177f6",
"type": "github"
},
"original": {
@@ -825,11 +825,11 @@
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1775239578,
"narHash": "sha256-MKJmDHlaxwBcnfCUEA89AwKOOONjOjbjHNNWdSdg5RA=",
"lastModified": 1776386586,
"narHash": "sha256-eVAUaL/6n8mnmBiPpEVW1NDNVSKLWhYVfycG+P0SvWU=",
"owner": "xddxdd",
"repo": "nix-cachyos-kernel",
"rev": "beaf7a533ae106c2681de2624da94707f9857f1f",
"rev": "c65c3faf90ae07bae101c15ef502f0bcb06c5d74",
"type": "github"
},
"original": {
@@ -886,11 +886,11 @@
]
},
"locked": {
"lastModified": 1775365369,
"narHash": "sha256-DgH5mveLoau20CuTnaU5RXZWgFQWn56onQ4Du2CqYoI=",
"lastModified": 1775970782,
"narHash": "sha256-7jt9Vpm48Yy5yAWigYpde+HxtYEpEuyzIQJF4VYehhk=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "cef5cf82671e749ac87d69aadecbb75967e6f6c3",
"rev": "bedba5989b04614fc598af9633033b95a937933f",
"type": "github"
},
"original": {
@@ -943,11 +943,11 @@
"nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1775444751,
"narHash": "sha256-7rAvWDPdSyeul4E0uKuVezJMN69tutpNGpujOODAX10=",
"lastModified": 1776396489,
"narHash": "sha256-lF3GX4VvQzff/5gpu5WytHKd2GQXJDrWChmK+JNNRO4=",
"owner": "nix-community",
"repo": "nix-vscode-extensions",
"rev": "ed33cc3b1eabe6c04af158dd7155c4198b6679fe",
"rev": "64839596bff67e8280a2fcd829a858d88530aa6f",
"type": "github"
},
"original": {
@@ -962,11 +962,11 @@
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1774264319,
"narHash": "sha256-aAsO35YtqIdvBhCIKZ0a+OcC8wB0H1+mAoPKBY0jxeQ=",
"lastModified": 1776370524,
"narHash": "sha256-0Gt5qnjNkIZJdOBfu2u47zgyhYL3WmgUrguUhGSxUdk=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "9fe29a63b23005acfcd1324a9e78b6241226cdb1",
"rev": "f9f0650b45e31b3f6c3e2a0405fa198a286e2741",
"type": "github"
},
"original": {
@@ -977,11 +977,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1775203647,
"narHash": "sha256-6MWaMLXK9QMndI94CIxeiPafi3wuO+imCtK9tfhsZdw=",
"lastModified": 1775490113,
"narHash": "sha256-2ZBhDNZZwYkRmefK5XLOusCJHnoeKkoN95hoSGgMxWM=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "80afbd13eea0b7c4ac188de949e1711b31c2b5f0",
"rev": "c775c2772ba56e906cbeb4e0b2db19079ef11ff7",
"type": "github"
},
"original": {
@@ -1049,22 +1049,6 @@
"type": "github"
}
},
"nixpkgs-otbr": {
"locked": {
"lastModified": 1766776257,
"narHash": "sha256-MG9DnzBn6TdAztaMPVhW9sjYj2bi9Jcux0F0fJ6LeO4=",
"owner": "mrene",
"repo": "nixpkgs",
"rev": "0c4c97066d555b7d27a0a56ee400130ec51f02ee",
"type": "github"
},
"original": {
"owner": "mrene",
"ref": "openthread-border-router",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1751048012,
@@ -1083,11 +1067,11 @@
},
"nixpkgs-stable_2": {
"locked": {
"lastModified": 1775305101,
"narHash": "sha256-/74n1oQPtKG52Yw41cbToxspxHbYz6O3vi+XEw16Qe8=",
"lastModified": 1776221942,
"narHash": "sha256-FbQAeVNi7G4v3QCSThrSAAvzQTmrmyDLiHNPvTF2qFM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "36a601196c4ebf49e035270e10b2d103fe39076b",
"rev": "1766437c5509f444c1b15331e82b8b6a9b967000",
"type": "github"
},
"original": {
@@ -1099,27 +1083,27 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"lastModified": 1776447299,
"narHash": "sha256-fhkbQptSg6w3CG4TCxalK6UZkj4+Afsi+6p0PuofJ48=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "2c1b4e855f7cded41541747173c697b53c63de9b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-unstable-small",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_10": {
"locked": {
"lastModified": 1775126147,
"narHash": "sha256-J0dZU4atgcfo4QvM9D92uQ0Oe1eLTxBVXjJzdEMQpD0=",
"lastModified": 1775888245,
"narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8d8c1fa5b412c223ffa47410867813290cdedfef",
"rev": "13043924aaa7375ce482ebe2494338e058282925",
"type": "github"
},
"original": {
@@ -1163,11 +1147,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1775231746,
"narHash": "sha256-EFaDQ0rnuSjKfC/DUKHS4toV4rEBuWhSgyX2Yy0kp00=",
"lastModified": 1776311487,
"narHash": "sha256-9U8bL9X/0R9cZD3Uc/mN37AWvv5dB4WQqqjLRAxQfas=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0eac666efaa8a9afea2821f9efc7921b4ef39b4e",
"rev": "cc1e0e027707ad53dddae39d3b3e992262c7d8c7",
"type": "github"
},
"original": {
@@ -1243,16 +1227,16 @@
},
"nixpkgs_9": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"lastModified": 1776447299,
"narHash": "sha256-fhkbQptSg6w3CG4TCxalK6UZkj4+Afsi+6p0PuofJ48=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "2c1b4e855f7cded41541747173c697b53c63de9b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-unstable-small",
"repo": "nixpkgs",
"type": "github"
}
@@ -1292,11 +1276,11 @@
]
},
"locked": {
"lastModified": 1774915545,
"narHash": "sha256-COT4l/+ZddGBvrDVfPf7MEOJxV8EDKame6/aRnNIKcY=",
"lastModified": 1775856943,
"narHash": "sha256-b7Mp7P+q2Md5AGt4rjHfMcBykzMumFTen10ST++AuTU=",
"owner": "nix-community",
"repo": "plasma-manager",
"rev": "f3177b3c69fb3f03201098d7fe8ab6422cce7fc1",
"rev": "a524a6160e6df89f7673ba293cf7d78b559eb1a5",
"type": "github"
},
"original": {
@@ -1337,11 +1321,11 @@
]
},
"locked": {
"lastModified": 1775036584,
"narHash": "sha256-zW0lyy7ZNNT/x8JhzFHBsP2IPx7ATZIPai4FJj12BgU=",
"lastModified": 1775585728,
"narHash": "sha256-8Psjt+TWvE4thRKktJsXfR6PA/fWWsZ04DVaY6PUhr4=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "4e0eb042b67d863b1b34b3f64d52ceb9cd926735",
"rev": "580633fa3fe5fc0379905986543fd7495481913d",
"type": "github"
},
"original": {
@@ -1424,7 +1408,6 @@
"nixos-apple-silicon": "nixos-apple-silicon",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_9",
"nixpkgs-otbr": "nixpkgs-otbr",
"nixpkgs-stable": "nixpkgs-stable_2",
"nixpkgs-unstable": "nixpkgs-unstable",
"plasma-manager": "plasma-manager",
@@ -1505,11 +1488,11 @@
"nixpkgs": "nixpkgs_10"
},
"locked": {
"lastModified": 1775365543,
"narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=",
"lastModified": 1776119890,
"narHash": "sha256-Zm6bxLNnEOYuS/SzrAGsYuXSwk3cbkRQZY0fJnk8a5M=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "a4ee2de76efb759fe8d4868c33dec9937897916f",
"rev": "d4971dd58c6627bfee52a1ad4237637c0a2fb0cd",
"type": "github"
},
"original": {
@@ -1561,11 +1544,11 @@
"tinted-zed": "tinted-zed"
},
"locked": {
"lastModified": 1775429060,
"narHash": "sha256-wbFF5cRxQOCzL/wHOKYm21t5AHPH2Lfp0mVPCOAvEoc=",
"lastModified": 1776170745,
"narHash": "sha256-Tl1aZVP5EIlT+k0+iAKH018GLHJpLz3hhJ0LNQOWxCc=",
"owner": "nix-community",
"repo": "stylix",
"rev": "d27951a6539951d87f75cf0a7cda8a3a24016019",
"rev": "e3861617645a43c9bbefde1aa6ac54dd0a44bfa9",
"type": "github"
},
"original": {
@@ -1712,11 +1695,11 @@
]
},
"locked": {
"lastModified": 1775125835,
"narHash": "sha256-2qYcPgzFhnQWchHo0SlqLHrXpux5i6ay6UHA+v2iH4U=",
"lastModified": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "75925962939880974e3ab417879daffcba36c4a3",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"type": "github"
},
"original": {

View File

@@ -1,12 +1,8 @@
{
inputs = rec {
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable-small";
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
# Fork required: openthread-border-router is not yet in nixpkgs-unstable.
# Used by modules/nixos/homeassistant/services/thread/default.nix
nixpkgs-otbr.url = "github:mrene/nixpkgs/openthread-border-router";
home-manager-unstable = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs-unstable";
@@ -47,7 +43,7 @@
authentik-nix = {
url = "github:nix-community/authentik-nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "nixpkgs-stable";
};
disko = {

View File

@@ -88,6 +88,7 @@ in
nwg-panel = disabled;
opencode = enabled;
thunderbird = enabled;
vesktop = enabled;
waybar = {
enable = false;

View File

@@ -30,6 +30,7 @@ in
jq
]
++ (with pkgs.${namespace}; [
hueforge
moondeck-buddy
]);
};
@@ -37,7 +38,7 @@ in
${namespace} = {
sops.enable = true;
programs.opencode = enabled;
# desktop.plasma = enabled;
desktop.plasma = enabled;
};
sops.secrets = {
@@ -83,11 +84,14 @@ in
viAlias = true;
vimAlias = true;
defaultEditor = true;
withRuby = false;
withPython3 = true;
plugins = [
pkgs.vimPlugins.nvim-tree-lua
{
plugin = pkgs.vimPlugins.vim-startify;
config = "let g:startify_change_to_vcs_root = 0";
type = "lua";
}
];
};

View File

@@ -13,6 +13,9 @@ in
${namespace} = {
desktop.gnome = enabled;
sops.enable = true;
programs = {
vesktop = enabled;
};
};
sops.secrets = {
@@ -51,7 +54,7 @@ in
ryujinx.enable = true; # Switch (ryubing fork)
yuzu.enable = true; # Switch (eden fork)
dolphin-emu.enable = true; # GameCube / Wii
cemu.enable = true; # Wii U
cemu.enable = false; # Wii U
melonDS.enable = true; # DS
citra.enable = true; # 3DS (azahar fork)
mgba.enable = true; # Game Boy / GBC

View File

@@ -31,6 +31,7 @@ in
desktop.plasma = enabled;
programs = {
vesktop = enabled;
opencode = enabled;
thunderbird = enabled;
hyprland = {
@@ -187,7 +188,7 @@ in
virt-manager
vorta
waydroid-helper
# winboat
winboat
]
++ (with pkgs.${namespace}; [
discord-krisp

View File

@@ -99,6 +99,7 @@
# ---- pi5 services --------------------------------------------------
pi5 = {
adguard = 3000;
attic = 9012;
nebula = 4242;
dns = 53;
};

View File

@@ -8,13 +8,9 @@
# lib.${namespace}.nixSettings.commonSettings
# lib.${namespace}.nixSettings.commonGc
{ lib, ... }:
let
net = lib.mjallen.network;
in
{
nixSettings = {
commonSubstituters = [
"http://${net.hosts.nas.lan}:${toString net.ports.nas.attic}/nas-cache"
"https://nixos-apple-silicon.cachix.org"
"https://nixos-raspberrypi.cachix.org"
"https://nix-community.cachix.org"
@@ -22,7 +18,6 @@ in
];
commonTrustedPublicKeys = [
"nas-cache:eK0eRVAt9QNwbkLIyOo9N5Z5+zi6ukI4mSlL196C7Yg="
"nixos-apple-silicon.cachix.org-1:8psDu5SA5dAD7qA0zMy5UT292TxeEPzIz8VVEr2Js20="
"nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="

View File

@@ -38,12 +38,12 @@
};
# Make ALL external HM modules available globally
# Note: sops-nix, nix-index-database, and stylix are already injected
# globally via systems.modules.home in flake.nix; only darwin-specific
# modules that aren't in the global list should go here.
sharedModules = with inputs; [
sops-nix.homeManagerModules.sops
nix-plist-manager.homeManagerModules.default
nix-index-database.homeModules.nix-index
stylix.homeModules.stylix
# Add any other external HM modules here
# Add any other darwin-specific external HM modules here
];
users."mattjallen" = lib.mkAliasDefinitions options.${namespace}.home.extraOptions;

View File

@@ -9,10 +9,8 @@ in
{
nix = {
settings = nixSettings.commonSettings // {
inherit (nixSettings)
commonSubstituters
commonTrustedPublicKeys
;
substituters = nixSettings.commonSubstituters;
trusted-public-keys = nixSettings.commonTrustedPublicKeys;
};
gc = nixSettings.commonGc;

View File

@@ -25,24 +25,28 @@ in
programs.plasma = {
enable = true;
workspace = {
colorScheme = "BreezeDark";
cursor = {
theme = "breeze_cursors";
size = 24;
};
iconTheme = "breeze-dark";
theme = "breeze-dark";
lookAndFeel = "org.kde.breezedark.desktop";
# Explicitly set Breeze to prevent QT_STYLE_OVERRIDE=kvantum (set by
# Stylix's qt6ct target) from being picked up by KWin/plasmashell, which
# would cause a fatal "module kvantum is not installed" QML error and
# leave the desktop blank.
widgetStyle = "Breeze";
configFile.kded5rc = {
"Module-gtkconfig"."autoload" = false;
};
# input.mice and input.touchpads require device-specific vendorId/productId
# identifiers — configure those per-host in the home config instead.
# workspace = {
# colorScheme = "BreezeDark";
# cursor = {
# theme = "breeze_cursors";
# size = 24;
# };
# iconTheme = "breeze-dark";
# theme = "breeze-dark";
# lookAndFeel = "org.kde.breezedark.desktop";
# # Explicitly set Breeze to prevent QT_STYLE_OVERRIDE=kvantum (set by
# # Stylix's qt6ct target) from being picked up by KWin/plasmashell, which
# # would cause a fatal "module kvantum is not installed" QML error and
# # leave the desktop blank.
# widgetStyle = "Breeze";
# };
# # input.mice and input.touchpads require device-specific vendorId/productId
# # identifiers — configure those per-host in the home config instead.
kscreenlocker = {
autoLock = true;
@@ -67,20 +71,20 @@ in
};
};
panels = [
{
location = "bottom";
floating = true;
height = 44;
widgets = [
"org.kde.plasma.kickoff"
"org.kde.plasma.icontasks"
"org.kde.plasma.marginsseparator"
"org.kde.plasma.systemtray"
"org.kde.plasma.digitalclock"
];
}
];
# panels = [
# {
# location = "bottom";
# floating = true;
# height = 44;
# widgets = [
# "org.kde.plasma.kickoff"
# "org.kde.plasma.icontasks"
# "org.kde.plasma.marginsseparator"
# "org.kde.plasma.systemtray"
# "org.kde.plasma.digitalclock"
# ];
# }
# ];
shortcuts = {
kwin = {

View File

@@ -37,8 +37,8 @@ in
baseURL = "http://${net.hosts.nas.lan}:${toString net.ports.nas.llamaCpp}/v1";
};
models = {
Qwen3-Coder-Next-Q4_0 = {
name = "Qwen3 Coder (local)";
"gemma-4-26B-A4B-it-UD-Q8_K_XL" = {
name = "Gemma 4 26B-A4B (local)";
modalities = {
input = [
"image"
@@ -47,8 +47,8 @@ in
output = [ "text" ];
};
limit = {
context = 131072;
output = 32768;
context = 32768;
output = 8192;
};
};
};

View File

@@ -0,0 +1,51 @@
{
config,
lib,
namespace,
...
}:
let
cfg = config.${namespace}.programs.vesktop;
in
{
options.${namespace}.programs.vesktop = {
enable = lib.mkEnableOption "vesktop";
};
config = lib.mkIf cfg.enable {
programs.vesktop = {
enable = true;
settings = {
appBadge = false;
arRPC = true;
checkUpdates = false;
customTitleBar = false;
disableMinSize = true;
minimizeToTray = true;
tray = true;
splashBackground = "#000000";
splashColor = "#ffffff";
splashTheming = true;
staticTitle = true;
hardwareAcceleration = true;
discordBranch = "stable";
};
vencord = {
settings = {
autoUpdate = false;
autoUpdateNotification = false;
notifyAboutUpdates = false;
useQuickCss = true;
disableMinSize = true;
plugins = {
MessageLogger = {
enabled = false;
ignoreSelf = true;
};
FakeNitro.enabled = false;
};
};
};
};
};
}

View File

@@ -35,6 +35,10 @@ in
enable = true;
wayland.enable = cfg.wayland.enable;
};
# Required for Bluetooth D-Bus policy (allows WirePlumber/PipeWire
# to communicate with bluetoothd on the system bus).
blueman.enable = true;
};
xdg.portal.extraPortals = [ ];

View File

@@ -0,0 +1,202 @@
############################################################
# SmartDisplay Bedroom
# Tiles: Lights | Lamp | Fan | Air Purifier | Closet | BedJet
############################################################
substitutions:
DIRECT_ACTIONS: "true"
ROOM_NAME: "Bedroom"
TIME_24H: "false"
# ── TILE 1 Bedroom Lights (group) ─────────────────────
TILE1_ENTITY: "light.bedroom_lights"
TILE1_STATE_ENTITY: "light.bedroom_light_1"
TILE1_TITLE: "Lights"
TILE1_ICON: "\U000F0769"
TILE1_TYPE: "light"
TILE1_TAP_ACTION: "toggle"
TILE1_LONGPRESS: "auto"
TILE1_VALUE_MODE: "auto"
TILE1_LABEL_OFF: "Off"
TILE1_LABEL_ON: "On"
TILE1_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE1_ICON_DISABLED_COLOR: "0xFEC600"
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE1_BG_DISABLED_COLOR: "0x3a3a3a"
TILE1_TITLE_ACTIVE_COLOR: "0x000000"
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE1_TAP_SERVICE: ""
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS_OFF_VALUE: "0"
# ── TILE 2 Bedroom Lamp ────────────────────────────────
TILE2_ENTITY: "light.bedroom_lamp"
TILE2_STATE_ENTITY: "light.bedroom_lamp"
TILE2_TITLE: "Lamp"
TILE2_ICON: "\U000F06B5"
TILE2_TYPE: "light"
TILE2_TAP_ACTION: "auto"
TILE2_LONGPRESS: "auto"
TILE2_VALUE_MODE: "auto"
TILE2_LABEL_OFF: "Off"
TILE2_LABEL_ON: "On"
TILE2_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE2_ICON_DISABLED_COLOR: "0xFEC600"
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE2_BG_DISABLED_COLOR: "0x3a3a3a"
TILE2_TITLE_ACTIVE_COLOR: "0x000000"
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE2_TAP_SERVICE: ""
TILE2_TAP_PARAM_KEY: ""
TILE2_TAP_PARAM_VAL: ""
TILE2_LONGPRESS_OFF_VALUE: "0"
# ── TILE 3 Bedroom Fan ─────────────────────────────────
TILE3_ENTITY: "fan.bedroom_fan"
TILE3_STATE_ENTITY: "fan.bedroom_fan"
TILE3_TITLE: "Bedroom Fan"
TILE3_ICON: "\U000F0210"
TILE3_TYPE: "fan"
TILE3_TAP_ACTION: "fan_toggle_preset"
TILE3_LONGPRESS: "auto"
TILE3_VALUE_MODE: "auto"
TILE3_LABEL_OFF: "Off"
TILE3_LABEL_ON: "On"
TILE3_CIRCLE_ACTIVE_COLOR: "0x00C5EC"
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE3_ICON_DISABLED_COLOR: "0x00C5EC"
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE3_BG_DISABLED_COLOR: "0x3a3a3a"
TILE3_TITLE_ACTIVE_COLOR: "0x000000"
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE3_TAP_SERVICE: "fan.toggle"
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS_OFF_VALUE: "0"
# ── TILE 4 Air Purifier ────────────────────────────────
TILE4_ENTITY: "fan.bedroom_air_purifier"
TILE4_STATE_ENTITY: "fan.bedroom_air_purifier"
TILE4_TITLE: "Air Purifier"
TILE4_ICON: "\U000F0D43"
TILE4_TYPE: "fan"
TILE4_TAP_ACTION: "fan_toggle_preset"
TILE4_LONGPRESS: "auto"
TILE4_VALUE_MODE: "auto"
TILE4_LABEL_OFF: "Off"
TILE4_LABEL_ON: "On"
TILE4_CIRCLE_ACTIVE_COLOR: "0x00C5EC"
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE4_ICON_DISABLED_COLOR: "0x00C5EC"
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE4_BG_DISABLED_COLOR: "0x3a3a3a"
TILE4_TITLE_ACTIVE_COLOR: "0x000000"
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE4_TAP_SERVICE: "fan.toggle"
TILE4_TAP_PARAM_KEY: "preset_mode"
TILE4_TAP_PARAM_VAL: "auto"
TILE4_LONGPRESS_OFF_VALUE: "0"
# ── TILE 5 Front Closet Lights ────────────────────────
TILE5_ENTITY: "light.front_closet_lights"
TILE5_STATE_ENTITY: "light.front_closet_light_1"
TILE5_TITLE: "Closet"
TILE5_ICON: "\U000F1051"
TILE5_TYPE: "light"
TILE5_TAP_ACTION: "toggle"
TILE5_LONGPRESS: "none"
TILE5_VALUE_MODE: "auto"
TILE5_LABEL_OFF: "Off"
TILE5_LABEL_ON: "On"
TILE5_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE5_ICON_DISABLED_COLOR: "0xFEC600"
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE5_BG_DISABLED_COLOR: "0x3a3a3a"
TILE5_TITLE_ACTIVE_COLOR: "0x000000"
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE5_TAP_SERVICE: ""
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS_OFF_VALUE: "0"
# ── TILE 6 BedJet ─────────────────────────────────────
TILE6_ENTITY: "fan.bedjet_dbbf_c222"
TILE6_STATE_ENTITY: "fan.bedjet_dbbf_c222"
TILE6_TITLE: "BedJet"
TILE6_ICON: "\U000F07E4"
TILE6_TYPE: "fan"
TILE6_TAP_ACTION: "toggle"
TILE6_LONGPRESS: "auto"
TILE6_VALUE_MODE: "percentage"
TILE6_LABEL_OFF: "Off"
TILE6_LABEL_ON: "On"
TILE6_CIRCLE_ACTIVE_COLOR: "0xFF6B35"
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE6_ICON_DISABLED_COLOR: "0xFF6B35"
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE6_BG_DISABLED_COLOR: "0x3a3a3a"
TILE6_TITLE_ACTIVE_COLOR: "0x000000"
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE6_TAP_SERVICE: "fan.toggle"
TILE6_TAP_PARAM_KEY: ""
TILE6_TAP_PARAM_VAL: ""
TILE6_LONGPRESS_OFF_VALUE: "0"
packages:
hw: !include cyd-base-hw.yaml
ui: !include cyd-base-ui.yaml
esphome:
name: bedroom-display
friendly_name: BedroomDisplay
on_boot:
priority: 600
then:
- script.execute: ui_refresh
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "BedroomDisplay Fallback"
password: !secret ota_password
captive_portal:

View File

@@ -0,0 +1,306 @@
############################################################
# CYD Base Hardware layer
# ESP32-2432S028 (Cheap Yellow Display)
# Include via: packages: !include cyd-base-hw.yaml
############################################################
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: ui_refresh
globals:
- id: active_entity
type: std::string
restore_value: no
- id: active_control_kind
type: std::string
restore_value: no
- id: active_value_suffix
type: std::string
restore_value: no
- id: overlay_slider_bg_color
type: uint32_t
restore_value: no
initial_value: '0x939391'
- id: overlay_slider_fill_color
type: uint32_t
restore_value: no
initial_value: '0xFEC600'
font:
- file: "gfonts://Roboto@500"
id: time_label
size: 18
bpp: 4
glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ','A','M','P']
- file: "gfonts://Roboto@500"
id: headline
size: 18
bpp: 4
glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/']
- file: "gfonts://Roboto@700"
id: label
size: 11
bpp: 4
glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/']
- file: "gfonts://Roboto@400"
id: sublabel
size: 11
bpp: 4
glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','…']
- file: "gfonts://Roboto@400"
id: sublabel_big
size: 14
bpp: 4
glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','…']
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons
size: 28
glyphs:
- "${TILE1_ICON}"
- "${TILE2_ICON}"
- "${TILE3_ICON}"
- "${TILE4_ICON}"
- "${TILE5_ICON}"
- "${TILE6_ICON}"
sensor:
- platform: wifi_signal
name: "Wifi Signal"
update_interval: 600s
- platform: uptime
name: "Uptime"
id: uptime_s
update_interval: 15s
- platform: homeassistant
id: tile1_brightness
entity_id: ${TILE1_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile1_percentage
entity_id: ${TILE1_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile2_brightness
entity_id: ${TILE2_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile2_percentage
entity_id: ${TILE2_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile3_brightness
entity_id: ${TILE3_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile3_percentage
entity_id: ${TILE3_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile4_brightness
entity_id: ${TILE4_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile4_percentage
entity_id: ${TILE4_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile5_brightness
entity_id: ${TILE5_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile5_percentage
entity_id: ${TILE5_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile6_brightness
entity_id: ${TILE6_ENTITY}
attribute: brightness
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile6_percentage
entity_id: ${TILE6_ENTITY}
attribute: percentage
on_value: { then: [ script.execute: ui_refresh ] }
binary_sensor:
- platform: status
name: "Node Status"
id: system_status
- platform: homeassistant
id: ha_state_tile1
entity_id: ${TILE1_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: ha_state_tile2
entity_id: ${TILE2_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: ha_state_tile3
entity_id: ${TILE3_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: ha_state_tile4
entity_id: ${TILE4_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: ha_state_tile5
entity_id: ${TILE5_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: ha_state_tile6
entity_id: ${TILE6_STATE_ENTITY}
on_state: { then: [ script.execute: ui_refresh ] }
text_sensor:
- platform: template
id: smartdisplay_action
name: "${ROOM_NAME} Action"
icon: mdi:gesture-tap-button
- platform: homeassistant
id: tile1_preset
entity_id: ${TILE1_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile2_preset
entity_id: ${TILE2_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile3_preset
entity_id: ${TILE3_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile4_preset
entity_id: ${TILE4_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile5_preset
entity_id: ${TILE5_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
- platform: homeassistant
id: tile6_preset
entity_id: ${TILE6_ENTITY}
attribute: preset_mode
on_value: { then: [ script.execute: ui_refresh ] }
switch:
- platform: restart
name: "Restart"
spi:
- id: lcd
clk_pin: GPIO14
mosi_pin: GPIO13
miso_pin: GPIO12
- id: my_touchscreen
clk_pin: GPIO25
mosi_pin: GPIO32
miso_pin: GPIO39
output:
- platform: ledc
pin: GPIO21
id: gpio_backlight_pwm
- platform: ledc
id: output_red
pin: GPIO4
inverted: true
- platform: ledc
id: output_green
pin: GPIO16
inverted: true
- platform: ledc
id: output_blue
pin: GPIO17
inverted: true
light:
- platform: monochromatic
output: gpio_backlight_pwm
name: "Power Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
- platform: rgb
name: LED
red: output_red
id: led
green: output_green
blue: output_blue
restore_mode: ALWAYS_OFF
touchscreen:
platform: xpt2046
id: ts_touch
spi_id: my_touchscreen
cs_pin: 33
interrupt_pin: 36
update_interval: 20ms
threshold: 300
calibration:
x_min: 280
x_max: 3860
y_min: 340
y_max: 3860
transform:
swap_xy: true
mirror_x: true
mirror_y: true
display:
- platform: ili9xxx
id: my_display
spi_id: lcd
model: ILI9341
cs_pin: 15
dc_pin: 2
invert_colors: false
update_interval: never
auto_clear_enabled: false
transform:
swap_xy: false
mirror_x: true
mirror_y: false
dimensions:
width: 320
height: 240

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,15 @@ in
config = mkIf cfg.enable {
virtualisation.oci-containers.containers.esphome = {
autoStart = true;
image = "ghcr.io/esphome/esphome:2026.2.4";
ports = [ "6052:6052" ];
image = "ghcr.io/esphome/esphome";
# host networking is required for mDNS (multicast UDP) to work so that
# ESPHome can discover devices via <name>.local and perform OTA updates
# without needing to know device IPs in advance.
extraOptions = [ "--network=host" ];
devices = [
"/dev/ttyUSB0"
];
privileged = true;
volumes = [
"/esphome:/config"
# Persist the PlatformIO core dir (penv, packages, platforms) so the

View File

@@ -0,0 +1,207 @@
############################################################
# SmartDisplay iOS Home appinspired Tiles UI
# Target device: ESP32-2432S028 / Cheap Yellow Display (CYD)
#
# Edit the substitutions below, then flash.
# All shared hardware/UI logic lives in:
# cyd-base-hw.yaml sensors, fonts, SPI, display, touch
# cyd-base-ui.yaml LVGL layout, scripts
############################################################
substitutions:
DIRECT_ACTIONS: "true"
ROOM_NAME: "Bedroom"
TIME_24H: "false"
# ── TILE 1 ──────────────────────────────────────────────
TILE1_ENTITY: "light.bedroom_lights"
TILE1_STATE_ENTITY: "light.bedroom_light_1"
TILE1_TITLE: "Lights"
TILE1_ICON: "\U000F0769"
TILE1_TYPE: "light"
TILE1_TAP_ACTION: "toggle"
TILE1_LONGPRESS: "auto"
TILE1_VALUE_MODE: "auto"
TILE1_LABEL_OFF: "Off"
TILE1_LABEL_ON: "On"
TILE1_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE1_ICON_DISABLED_COLOR: "0xFEC600"
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE1_BG_DISABLED_COLOR: "0x3a3a3a"
TILE1_TITLE_ACTIVE_COLOR: "0x000000"
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE1_TAP_SERVICE: ""
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS_OFF_VALUE: "0"
# ── TILE 2 ──────────────────────────────────────────────
TILE2_ENTITY: "light.bedroom_lamp"
TILE2_STATE_ENTITY: "light.bedroom_lamp"
TILE2_TITLE: "Lamp"
TILE2_ICON: "\U000F06B5"
TILE2_TYPE: "light"
TILE2_TAP_ACTION: "auto"
TILE2_LONGPRESS: "auto"
TILE2_VALUE_MODE: "auto"
TILE2_LABEL_OFF: "Off"
TILE2_LABEL_ON: "On"
TILE2_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE2_ICON_DISABLED_COLOR: "0xFEC600"
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE2_BG_DISABLED_COLOR: "0x3a3a3a"
TILE2_TITLE_ACTIVE_COLOR: "0x000000"
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE2_TAP_SERVICE: ""
TILE2_TAP_PARAM_KEY: ""
TILE2_TAP_PARAM_VAL: ""
TILE2_LONGPRESS_OFF_VALUE: "0"
# ── TILE 3 ──────────────────────────────────────────────
TILE3_ENTITY: "light.living_room_lights"
TILE3_STATE_ENTITY: "light.living_room_lights"
TILE3_TITLE: "Living Room"
TILE3_ICON: "\U000F08DD"
TILE3_TYPE: "light"
TILE3_TAP_ACTION: "auto"
TILE3_LONGPRESS: "auto"
TILE3_VALUE_MODE: "auto"
TILE3_LABEL_OFF: "Off"
TILE3_LABEL_ON: "On"
TILE3_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE3_ICON_DISABLED_COLOR: "0xFEC600"
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE3_BG_DISABLED_COLOR: "0x3a3a3a"
TILE3_TITLE_ACTIVE_COLOR: "0x000000"
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE3_TAP_SERVICE: ""
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS_OFF_VALUE: "0"
# ── TILE 4 ──────────────────────────────────────────────
TILE4_ENTITY: "switch.closet_lights"
TILE4_STATE_ENTITY: "switch.closet_lights"
TILE4_TITLE: "Closet Lights"
TILE4_ICON: "\U000F1051"
TILE4_TYPE: "light"
TILE4_TAP_ACTION: "auto"
TILE4_LONGPRESS: "auto"
TILE4_VALUE_MODE: "auto"
TILE4_LABEL_OFF: "Off"
TILE4_LABEL_ON: "On"
TILE4_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE4_ICON_DISABLED_COLOR: "0xFEC600"
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE4_BG_DISABLED_COLOR: "0x3a3a3a"
TILE4_TITLE_ACTIVE_COLOR: "0x000000"
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE4_TAP_SERVICE: ""
TILE4_TAP_PARAM_KEY: ""
TILE4_TAP_PARAM_VAL: ""
TILE4_LONGPRESS_OFF_VALUE: "0"
# ── TILE 5 ──────────────────────────────────────────────
TILE5_ENTITY: "fan.bedroom_fan"
TILE5_STATE_ENTITY: "fan.bedroom_fan"
TILE5_TITLE: "Bedroom Fan"
TILE5_ICON: "\U000F0210"
TILE5_TYPE: "fan"
TILE5_TAP_ACTION: "auto"
TILE5_LONGPRESS: "auto"
TILE5_VALUE_MODE: "auto"
TILE5_LABEL_OFF: "Off"
TILE5_LABEL_ON: "On"
TILE5_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE5_ICON_DISABLED_COLOR: "0xFEC600"
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE5_BG_DISABLED_COLOR: "0x3a3a3a"
TILE5_TITLE_ACTIVE_COLOR: "0x000000"
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE5_TAP_SERVICE: ""
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS_OFF_VALUE: "0"
# ── TILE 6 ──────────────────────────────────────────────
TILE6_ENTITY: "fan.bedroom_air_purifier"
TILE6_STATE_ENTITY: "fan.bedroom_air_purifier"
TILE6_TITLE: "Air Purifier"
TILE6_ICON: "\U000F0D43"
TILE6_TYPE: "fan"
TILE6_TAP_ACTION: "fan_toggle_preset"
TILE6_LONGPRESS: "auto"
TILE6_VALUE_MODE: "auto"
TILE6_LABEL_OFF: "Off"
TILE6_LABEL_ON: "On"
TILE6_CIRCLE_ACTIVE_COLOR: "0x00C5EC"
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE6_ICON_DISABLED_COLOR: "0x00C5EC"
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE6_BG_DISABLED_COLOR: "0x3a3a3a"
TILE6_TITLE_ACTIVE_COLOR: "0x000000"
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE6_TAP_SERVICE: "fan.toggle"
TILE6_TAP_PARAM_KEY: "preset_mode"
TILE6_TAP_PARAM_VAL: "Auto"
TILE6_LONGPRESS_OFF_VALUE: "0"
packages:
hw: !include cyd-base-hw.yaml
ui: !include cyd-base-ui.yaml
esphome:
name: smartdisplay
friendly_name: SmartDisplay
on_boot:
priority: 600
then:
- script.execute: ui_refresh
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Smartdisplay Fallback"
password: !secret ota_password
captive_portal:

View File

@@ -0,0 +1,202 @@
############################################################
# SmartDisplay Living Room
# Tiles: Lights | Air Purifier | Vacuum | TV | Speaker | Closet
############################################################
substitutions:
DIRECT_ACTIONS: "true"
ROOM_NAME: "Living Room"
TIME_24H: "false"
# ── TILE 1 Living Room Lights ──────────────────────────
TILE1_ENTITY: "light.living_room_lights"
TILE1_STATE_ENTITY: "light.living_room_light_1"
TILE1_TITLE: "Lights"
TILE1_ICON: "\U000F08DD"
TILE1_TYPE: "light"
TILE1_TAP_ACTION: "toggle"
TILE1_LONGPRESS: "auto"
TILE1_VALUE_MODE: "auto"
TILE1_LABEL_OFF: "Off"
TILE1_LABEL_ON: "On"
TILE1_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE1_ICON_DISABLED_COLOR: "0xFEC600"
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE1_BG_DISABLED_COLOR: "0x3a3a3a"
TILE1_TITLE_ACTIVE_COLOR: "0x000000"
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE1_TAP_SERVICE: ""
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS_OFF_VALUE: "0"
# ── TILE 2 Living Room Air Purifier ───────────────────
TILE2_ENTITY: "fan.living_room_air_purifier"
TILE2_STATE_ENTITY: "fan.living_room_air_purifier"
TILE2_TITLE: "Air Purifier"
TILE2_ICON: "\U000F0D43"
TILE2_TYPE: "fan"
TILE2_TAP_ACTION: "fan_toggle_preset"
TILE2_LONGPRESS: "auto"
TILE2_VALUE_MODE: "auto"
TILE2_LABEL_OFF: "Off"
TILE2_LABEL_ON: "On"
TILE2_CIRCLE_ACTIVE_COLOR: "0x00C5EC"
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE2_ICON_DISABLED_COLOR: "0x00C5EC"
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE2_BG_DISABLED_COLOR: "0x3a3a3a"
TILE2_TITLE_ACTIVE_COLOR: "0x000000"
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE2_TAP_SERVICE: "fan.toggle"
TILE2_TAP_PARAM_KEY: "preset_mode"
TILE2_TAP_PARAM_VAL: "auto"
TILE2_LONGPRESS_OFF_VALUE: "0"
# ── TILE 3 Garbage Goober (vacuum) ────────────────────
TILE3_ENTITY: "vacuum.garbage_goober"
TILE3_STATE_ENTITY: "binary_sensor.garbage_goober_cleaning"
TILE3_TITLE: "Vacuum"
TILE3_ICON: "\U000F09A8"
TILE3_TYPE: "switch"
TILE3_TAP_ACTION: "custom"
TILE3_LONGPRESS: "none"
TILE3_VALUE_MODE: "text"
TILE3_LABEL_OFF: "Docked"
TILE3_LABEL_ON: "Cleaning"
TILE3_CIRCLE_ACTIVE_COLOR: "0x43A047"
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE3_ICON_DISABLED_COLOR: "0x43A047"
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE3_BG_DISABLED_COLOR: "0x3a3a3a"
TILE3_TITLE_ACTIVE_COLOR: "0x000000"
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE3_TAP_SERVICE: "vacuum.start_pause"
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS_OFF_VALUE: "0"
# ── TILE 4 TV (remote) ────────────────────────────────
TILE4_ENTITY: "remote.living_room"
TILE4_STATE_ENTITY: "remote.living_room"
TILE4_TITLE: "TV"
TILE4_ICON: "\U000F0502"
TILE4_TYPE: "switch"
TILE4_TAP_ACTION: "custom"
TILE4_LONGPRESS: "none"
TILE4_VALUE_MODE: "text"
TILE4_LABEL_OFF: "Off"
TILE4_LABEL_ON: "On"
TILE4_CIRCLE_ACTIVE_COLOR: "0x1565C0"
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE4_ICON_DISABLED_COLOR: "0x1565C0"
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE4_BG_DISABLED_COLOR: "0x3a3a3a"
TILE4_TITLE_ACTIVE_COLOR: "0x000000"
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE4_TAP_SERVICE: "remote.toggle"
TILE4_TAP_PARAM_KEY: ""
TILE4_TAP_PARAM_VAL: ""
TILE4_LONGPRESS_OFF_VALUE: "0"
# ── TILE 5 Living Room Speaker ────────────────────────
TILE5_ENTITY: "media_player.living_room_speaker"
TILE5_STATE_ENTITY: "media_player.living_room_speaker"
TILE5_TITLE: "Speaker"
TILE5_ICON: "\U000F04C3"
TILE5_TYPE: "switch"
TILE5_TAP_ACTION: "custom"
TILE5_LONGPRESS: "none"
TILE5_VALUE_MODE: "text"
TILE5_LABEL_OFF: "Off"
TILE5_LABEL_ON: "Playing"
TILE5_CIRCLE_ACTIVE_COLOR: "0x8E24AA"
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE5_ICON_DISABLED_COLOR: "0x8E24AA"
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE5_BG_DISABLED_COLOR: "0x3a3a3a"
TILE5_TITLE_ACTIVE_COLOR: "0x000000"
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE5_TAP_SERVICE: "media_player.toggle"
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS_OFF_VALUE: "0"
# ── TILE 6 Front Closet Lights ────────────────────────
TILE6_ENTITY: "light.front_closet_lights"
TILE6_STATE_ENTITY: "light.front_closet_light_1"
TILE6_TITLE: "Closet"
TILE6_ICON: "\U000F1051"
TILE6_TYPE: "light"
TILE6_TAP_ACTION: "toggle"
TILE6_LONGPRESS: "none"
TILE6_VALUE_MODE: "auto"
TILE6_LABEL_OFF: "Off"
TILE6_LABEL_ON: "On"
TILE6_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE6_ICON_DISABLED_COLOR: "0xFEC600"
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE6_BG_DISABLED_COLOR: "0x3a3a3a"
TILE6_TITLE_ACTIVE_COLOR: "0x000000"
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE6_TAP_SERVICE: ""
TILE6_TAP_PARAM_KEY: ""
TILE6_TAP_PARAM_VAL: ""
TILE6_LONGPRESS_OFF_VALUE: "0"
packages:
hw: !include cyd-base-hw.yaml
ui: !include cyd-base-ui.yaml
esphome:
name: livingroom-display
friendly_name: LivingroomDisplay
on_boot:
priority: 600
then:
- script.execute: ui_refresh
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "LivingroomDisplay Fallback"
password: !secret ota_password
captive_portal:

View File

@@ -0,0 +1,202 @@
############################################################
# SmartDisplay Pets
# Tiles: Joey Feed | Luci Feed | Litter Box | Fountain | Vacuum | Ftn Reset
############################################################
substitutions:
DIRECT_ACTIONS: "true"
ROOM_NAME: "Pets"
TIME_24H: "false"
# ── TILE 1 Joey Smart Feeder (manual feed) ─────────────
TILE1_ENTITY: "button.joey_smart_feeder_manual_feed"
TILE1_STATE_ENTITY: "binary_sensor.joey_smart_feeder_low_food"
TILE1_TITLE: "Joey Feed"
TILE1_ICON: "\U000F009A"
TILE1_TYPE: "script"
TILE1_TAP_ACTION: "custom"
TILE1_LONGPRESS: "none"
TILE1_VALUE_MODE: "text"
TILE1_LABEL_OFF: "Ready"
TILE1_LABEL_ON: "Fed"
TILE1_CIRCLE_ACTIVE_COLOR: "0xF4A820"
TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE1_ICON_DISABLED_COLOR: "0xF4A820"
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE1_BG_DISABLED_COLOR: "0x3a3a3a"
TILE1_TITLE_ACTIVE_COLOR: "0x000000"
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE1_TAP_SERVICE: "button.press"
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS_OFF_VALUE: "0"
# ── TILE 2 Luci Smart Feeder (manual feed) ─────────────
TILE2_ENTITY: "button.luci_smart_feeder_manual_feed"
TILE2_STATE_ENTITY: "binary_sensor.luci_smart_feeder_low_food"
TILE2_TITLE: "Luci Feed"
TILE2_ICON: "\U000F1114"
TILE2_TYPE: "script"
TILE2_TAP_ACTION: "custom"
TILE2_LONGPRESS: "none"
TILE2_VALUE_MODE: "text"
TILE2_LABEL_OFF: "Ready"
TILE2_LABEL_ON: "Fed"
TILE2_CIRCLE_ACTIVE_COLOR: "0xEC407A"
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE2_ICON_DISABLED_COLOR: "0xEC407A"
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE2_BG_DISABLED_COLOR: "0x3a3a3a"
TILE2_TITLE_ACTIVE_COLOR: "0x000000"
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE2_TAP_SERVICE: "button.press"
TILE2_TAP_PARAM_KEY: ""
TILE2_TAP_PARAM_VAL: ""
TILE2_LONGPRESS_OFF_VALUE: "0"
# ── TILE 3 Litter-Robot 4 ──────────────────────────────
TILE3_ENTITY: "vacuum.litter_robot_4_litter_box"
TILE3_STATE_ENTITY: "binary_sensor.litter_robot_4_is_cleaning"
TILE3_TITLE: "Litter Box"
TILE3_ICON: "\U000F054D"
TILE3_TYPE: "switch"
TILE3_TAP_ACTION: "custom"
TILE3_LONGPRESS: "none"
TILE3_VALUE_MODE: "text"
TILE3_LABEL_OFF: "Docked"
TILE3_LABEL_ON: "Cycling"
TILE3_CIRCLE_ACTIVE_COLOR: "0x43A047"
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE3_ICON_DISABLED_COLOR: "0x43A047"
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE3_BG_DISABLED_COLOR: "0x3a3a3a"
TILE3_TITLE_ACTIVE_COLOR: "0x000000"
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE3_TAP_SERVICE: "vacuum.start"
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS_OFF_VALUE: "0"
# ── TILE 4 Pet Fountain ────────────────────────────────
TILE4_ENTITY: "switch.dockstream_smart_rfid_fountain_wi_fi"
TILE4_STATE_ENTITY: "binary_sensor.dockstream_smart_rfid_fountain_indicator"
TILE4_TITLE: "Fountain"
TILE4_ICON: "\U000F0765"
TILE4_TYPE: "switch"
TILE4_TAP_ACTION: "toggle"
TILE4_LONGPRESS: "none"
TILE4_VALUE_MODE: "text"
TILE4_LABEL_OFF: "Off"
TILE4_LABEL_ON: "On"
TILE4_CIRCLE_ACTIVE_COLOR: "0x039BE5"
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE4_ICON_DISABLED_COLOR: "0x039BE5"
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE4_BG_DISABLED_COLOR: "0x3a3a3a"
TILE4_TITLE_ACTIVE_COLOR: "0x000000"
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE4_TAP_SERVICE: ""
TILE4_TAP_PARAM_KEY: ""
TILE4_TAP_PARAM_VAL: ""
TILE4_LONGPRESS_OFF_VALUE: "0"
# ── TILE 5 Garbage Goober (vacuum) ────────────────────
TILE5_ENTITY: "vacuum.garbage_goober"
TILE5_STATE_ENTITY: "binary_sensor.garbage_goober_cleaning"
TILE5_TITLE: "Vacuum"
TILE5_ICON: "\U000F09A8"
TILE5_TYPE: "switch"
TILE5_TAP_ACTION: "custom"
TILE5_LONGPRESS: "none"
TILE5_VALUE_MODE: "text"
TILE5_LABEL_OFF: "Docked"
TILE5_LABEL_ON: "Cleaning"
TILE5_CIRCLE_ACTIVE_COLOR: "0x6D4C41"
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE5_ICON_DISABLED_COLOR: "0x6D4C41"
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE5_BG_DISABLED_COLOR: "0x3a3a3a"
TILE5_TITLE_ACTIVE_COLOR: "0x000000"
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE5_TAP_SERVICE: "vacuum.start_pause"
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS_OFF_VALUE: "0"
# ── TILE 6 Fountain Cleaning Reset ────────────────────
TILE6_ENTITY: "button.dockstream_smart_rfid_fountain_cleaning_reset"
TILE6_STATE_ENTITY: "binary_sensor.dockstream_smart_rfid_fountain_wi_fi"
TILE6_TITLE: "Ftn. Reset"
TILE6_ICON: "\U000F0ECC"
TILE6_TYPE: "script"
TILE6_TAP_ACTION: "custom"
TILE6_LONGPRESS: "none"
TILE6_VALUE_MODE: "text"
TILE6_LABEL_OFF: "Offline"
TILE6_LABEL_ON: "Online"
TILE6_CIRCLE_ACTIVE_COLOR: "0x039BE5"
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE6_ICON_DISABLED_COLOR: "0x039BE5"
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE6_BG_DISABLED_COLOR: "0x3a3a3a"
TILE6_TITLE_ACTIVE_COLOR: "0x000000"
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE6_TAP_SERVICE: "button.press"
TILE6_TAP_PARAM_KEY: ""
TILE6_TAP_PARAM_VAL: ""
TILE6_LONGPRESS_OFF_VALUE: "0"
packages:
hw: !include cyd-base-hw.yaml
ui: !include cyd-base-ui.yaml
esphome:
name: pets-display
friendly_name: PetsDisplay
on_boot:
priority: 600
then:
- script.execute: ui_refresh
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "PetsDisplay Fallback"
password: !secret ota_password
captive_portal:

View File

@@ -0,0 +1,202 @@
############################################################
# SmartDisplay 3D Printer (Bambu P1P)
# Tiles: Status | Chamber Light | Pause | Cooling Fan | AMS | Stop
############################################################
substitutions:
DIRECT_ACTIONS: "true"
ROOM_NAME: "Printer"
TIME_24H: "false"
# ── TILE 1 P1P Online / Force Refresh ─────────────────
TILE1_ENTITY: "button.p1p_01s00c371400147_force_refresh_data"
TILE1_STATE_ENTITY: "binary_sensor.p1p_01s00c371400147_online"
TILE1_TITLE: "P1P Status"
TILE1_ICON: "\U000F08F5"
TILE1_TYPE: "script"
TILE1_TAP_ACTION: "custom"
TILE1_LONGPRESS: "none"
TILE1_VALUE_MODE: "text"
TILE1_LABEL_OFF: "Offline"
TILE1_LABEL_ON: "Online"
TILE1_CIRCLE_ACTIVE_COLOR: "0x43A047"
TILE1_CIRCLE_DISABLED_COLOR: "0xE53935"
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE1_ICON_DISABLED_COLOR: "0xFFFFFF"
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE1_BG_DISABLED_COLOR: "0x3a3a3a"
TILE1_TITLE_ACTIVE_COLOR: "0x000000"
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE1_TAP_SERVICE: "button.press"
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS_OFF_VALUE: "0"
# ── TILE 2 Chamber Light ───────────────────────────────
TILE2_ENTITY: "light.p1p_01s00c371400147_chamber_light"
TILE2_STATE_ENTITY: "light.p1p_01s00c371400147_chamber_light"
TILE2_TITLE: "Chamber"
TILE2_ICON: "\U000F0A02"
TILE2_TYPE: "light"
TILE2_TAP_ACTION: "toggle"
TILE2_LONGPRESS: "none"
TILE2_VALUE_MODE: "text"
TILE2_LABEL_OFF: "Off"
TILE2_LABEL_ON: "On"
TILE2_CIRCLE_ACTIVE_COLOR: "0xFEC600"
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE2_ICON_DISABLED_COLOR: "0xFEC600"
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE2_BG_DISABLED_COLOR: "0x3a3a3a"
TILE2_TITLE_ACTIVE_COLOR: "0x000000"
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE2_TAP_SERVICE: ""
TILE2_TAP_PARAM_KEY: ""
TILE2_TAP_PARAM_VAL: ""
TILE2_LONGPRESS_OFF_VALUE: "0"
# ── TILE 3 Pause Printing ──────────────────────────────
TILE3_ENTITY: "button.p1p_01s00c371400147_pause_printing"
TILE3_STATE_ENTITY: "binary_sensor.p1p_01s00c371400147_print_error"
TILE3_TITLE: "Pause"
TILE3_ICON: "\U000F03E4"
TILE3_TYPE: "script"
TILE3_TAP_ACTION: "custom"
TILE3_LONGPRESS: "none"
TILE3_VALUE_MODE: "text"
TILE3_LABEL_OFF: "Idle"
TILE3_LABEL_ON: "Error"
TILE3_CIRCLE_ACTIVE_COLOR: "0xE53935"
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE3_ICON_DISABLED_COLOR: "0x7B7B6F"
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE3_BG_DISABLED_COLOR: "0x3a3a3a"
TILE3_TITLE_ACTIVE_COLOR: "0x000000"
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE3_TAP_SERVICE: "button.press"
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS_OFF_VALUE: "0"
# ── TILE 4 Cooling Fan ────────────────────────────────
TILE4_ENTITY: "fan.p1p_01s00c371400147_cooling_fan"
TILE4_STATE_ENTITY: "fan.p1p_01s00c371400147_cooling_fan"
TILE4_TITLE: "Cooling"
TILE4_ICON: "\U000F0210"
TILE4_TYPE: "fan"
TILE4_TAP_ACTION: "toggle"
TILE4_LONGPRESS: "auto"
TILE4_VALUE_MODE: "percentage"
TILE4_LABEL_OFF: "Off"
TILE4_LABEL_ON: "On"
TILE4_CIRCLE_ACTIVE_COLOR: "0x00C5EC"
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE4_ICON_DISABLED_COLOR: "0x00C5EC"
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE4_BG_DISABLED_COLOR: "0x3a3a3a"
TILE4_TITLE_ACTIVE_COLOR: "0x000000"
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE4_TAP_SERVICE: "fan.toggle"
TILE4_TAP_PARAM_KEY: ""
TILE4_TAP_PARAM_VAL: ""
TILE4_LONGPRESS_OFF_VALUE: "0"
# ── TILE 5 AMS 1 Active ───────────────────────────────
TILE5_ENTITY: "button.p1p_01s00c371400147_force_refresh_data"
TILE5_STATE_ENTITY: "binary_sensor.p1p_01s00c371400147_ams_1_active"
TILE5_TITLE: "AMS"
TILE5_ICON: "\U000F01AE"
TILE5_TYPE: "script"
TILE5_TAP_ACTION: "custom"
TILE5_LONGPRESS: "none"
TILE5_VALUE_MODE: "text"
TILE5_LABEL_OFF: "Idle"
TILE5_LABEL_ON: "Active"
TILE5_CIRCLE_ACTIVE_COLOR: "0x8E24AA"
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE5_ICON_DISABLED_COLOR: "0x8E24AA"
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE5_BG_DISABLED_COLOR: "0x3a3a3a"
TILE5_TITLE_ACTIVE_COLOR: "0x000000"
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE5_TAP_SERVICE: "button.press"
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS_OFF_VALUE: "0"
# ── TILE 6 Stop Printing (red accent) ─────────────────
TILE6_ENTITY: "button.p1p_01s00c371400147_stop_printing"
TILE6_STATE_ENTITY: "binary_sensor.p1p_01s00c371400147_online"
TILE6_TITLE: "Stop"
TILE6_ICON: "\U000F04DB"
TILE6_TYPE: "script"
TILE6_TAP_ACTION: "custom"
TILE6_LONGPRESS: "none"
TILE6_VALUE_MODE: "text"
TILE6_LABEL_OFF: "Offline"
TILE6_LABEL_ON: "Ready"
TILE6_CIRCLE_ACTIVE_COLOR: "0xE53935"
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F"
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF"
TILE6_ICON_DISABLED_COLOR: "0xE53935"
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF"
TILE6_BG_DISABLED_COLOR: "0x3a3a3a"
TILE6_TITLE_ACTIVE_COLOR: "0x000000"
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF"
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C"
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9"
TILE6_TAP_SERVICE: "button.press"
TILE6_TAP_PARAM_KEY: ""
TILE6_TAP_PARAM_VAL: ""
TILE6_LONGPRESS_OFF_VALUE: "0"
packages:
hw: !include cyd-base-hw.yaml
ui: !include cyd-base-ui.yaml
esphome:
name: printer-display
friendly_name: PrinterDisplay
on_boot:
priority: 600
then:
- script.execute: ui_refresh
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "PrinterDisplay Fallback"
password: !secret ota_password
captive_portal:

View File

@@ -11,10 +11,6 @@ let
name = "otbr";
cfg = config.${namespace}.services.${name};
otbrPackage =
pkgs.callPackage "${inputs.nixpkgs-otbr}/pkgs/by-name/op/openthread-border-router/package.nix"
{ };
otbrConfig = lib.${namespace}.mkModule {
inherit config name;
description = "Openthread border router";
@@ -24,8 +20,9 @@ let
moduleConfig = {
services.openthread-border-router = {
enable = true;
backboneInterface = "enp2s0";
package = otbrPackage;
backboneInterfaces = [
"enp2s0"
];
rest = {
inherit (cfg) listenAddress;
listenPort = cfg.restPort;
@@ -45,7 +42,6 @@ let
in
{
imports = [
"${inputs.nixpkgs-otbr}/nixos/modules/services/home-automation/openthread-border-router.nix"
otbrConfig
];
}

View File

@@ -15,15 +15,39 @@ let
description = "Actual Personal Finance Planner";
options = { };
moduleConfig = {
sops = {
secrets = {
"jallen-nas/actual/client-id" = {
sopsFile = lib.snowfall.fs.get-file "secrets/nas-secrets.yaml";
owner = "actual";
restartUnits = [ "actual.service" ];
};
"jallen-nas/actual/client-secret" = {
sopsFile = lib.snowfall.fs.get-file "secrets/nas-secrets.yaml";
owner = "actual";
restartUnits = [ "actual.service" ];
};
};
};
services.actual = {
inherit (cfg) openFirewall;
enable = true;
settings = {
inherit (cfg) port;
trustedProxies = [ config.${namespace}.network.ipv4.address ];
trustedProxies = [
config.${namespace}.network.ipv4.address
"10.0.1.4"
];
serverFiles = "${cfg.configDir}/${name}/server-files";
userFiles = "${cfg.configDir}/${name}/user-files";
dataDir = "${cfg.configDir}/${name}";
openId = {
discoveryURL = "https://authentik.mjallen.dev/application/o/actual/.well-known/openid-configuration";
client_id._secret = config.sops.secrets."jallen-nas/actual/client-id".path;
client_secret._secret = config.sops.secrets."jallen-nas/actual/client-secret".path;
server_hostname = "https://actual.mjallen.dev";
authMethod = "openid";
};
};
};

View File

@@ -13,14 +13,14 @@ let
cfg = config.${namespace}.services.ai;
ntfyModelFailScript = pkgs.writeShellScript "update-qwen-model-notify-failure" ''
ntfyModelFailScript = pkgs.writeShellScript "update-gemma-model-notify-failure" ''
HOST="$(${pkgs.hostname}/bin/hostname)"
${pkgs.curl}/bin/curl -sf \
--user "$NTFY_USER:$NTFY_PASSWORD" \
-H "Title: Qwen model update FAILED on $HOST" \
-H "Title: Gemma model update FAILED on $HOST" \
-H "Priority: high" \
-H "Tags: rotating_light,robot_face" \
-d "The daily update-qwen-model job failed. Check: journalctl -u update-qwen-model.service" \
-d "The daily update-gemma-model job failed. Check: journalctl -u update-gemma-model.service" \
"https://ntfy.mjallen.dev/builds" || true
'';
@@ -30,10 +30,7 @@ let
description = "AI Services";
options = {
llama-cpp = {
model =
mkOpt types.str
"models--unsloth--gemma-4-26B-A4B-it-GGUF/snapshots/b8654b48d979f2853b7a81d6541ca64eea7dc3c5/gemma-4-26B-A4B-it-UD-Q8_K_XL"
"";
model = mkOpt types.str "gemma-4-26B-A4B-it-UD-Q8_K_XL" "";
};
};
moduleConfig = {
@@ -59,21 +56,17 @@ let
model = "${cfg.configDir}/llama-cpp/models/${cfg.llama-cpp.model}.gguf";
package = inputs.llama-cpp.packages.${system}.rocm;
extraFlags = [
"--fit"
"on"
"--seed"
"3407"
"--temp"
"0.7"
"--top-p"
"0.9"
"--min-p"
"0.05"
"--top-k"
"30"
"--jinja"
"--chat-template-kwargs"
"{\"enable_thinking\":true}"
"--temp"
"1.0"
"--top-p"
"0.95"
"--top-k"
"64"
"--ctx-size"
"131072"
"32768"
"--threads"
"8"
"--batch-size"
@@ -81,7 +74,7 @@ let
"--gpu-layers"
"999"
"--flash-attn"
"auto"
"on"
"--mlock"
];
};
@@ -120,16 +113,16 @@ let
# Systemd service for automatic model updates
systemd = {
services = {
update-qwen-model = {
description = "Update Qwen3-Coder-Next model from HuggingFace";
update-gemma-model = {
description = "Update Gemma 4 model from HuggingFace";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.writeShellScript "update-qwen-model" ''
ExecStart = "${pkgs.writeShellScript "update-gemma-model" ''
set -euo pipefail
MODEL_DIR="${cfg.configDir}/llama-cpp/models"
MODEL_NAME="${cfg.llama-cpp.model}.gguf"
REPO_ID="unsloth/Qwen3-Coder-Next-GGUF"
REPO_ID="unsloth/gemma-4-26B-A4B-it-GGUF"
# Create model directory if it doesn't exist
mkdir -p "$MODEL_DIR"
@@ -147,13 +140,13 @@ let
Group = "jallen-nas";
EnvironmentFile = [ config.sops.templates."ntfy.env".path ];
};
unitConfig.OnFailure = "update-qwen-model-notify-failure.service";
unitConfig.OnFailure = "update-gemma-model-notify-failure.service";
# Run daily at 3 AM
startAt = "*-*-* 03:00:00";
};
update-qwen-model-notify-failure = {
description = "Notify ntfy on update-qwen-model failure";
update-gemma-model-notify-failure = {
description = "Notify ntfy on update-gemma-model failure";
serviceConfig = {
Type = "oneshot";
ExecStart = "${ntfyModelFailScript}";
@@ -163,8 +156,8 @@ let
# Ensure model is available before llama-cpp starts
llama-cpp = {
after = [ "update-qwen-model.service" ];
wants = [ "update-qwen-model.service" ];
after = [ "update-gemma-model.service" ];
wants = [ "update-gemma-model.service" ];
};
};
};

View File

@@ -7,6 +7,7 @@
}:
with lib;
let
inherit (lib.${namespace}) mkOpt mkBoolOpt;
name = "attic";
cfg = config.${namespace}.services.${name};
@@ -24,7 +25,12 @@ let
atticConfig = lib.${namespace}.mkModule {
inherit config name;
description = "attic Service";
options = { };
options = {
ntfy = {
enable = mkBoolOpt false "Enable ntfy notifications for cache rebuild failures";
envFile = mkOpt types.str "" "Path to the environment file containing NTFY_USER and NTFY_PASSWORD";
};
};
moduleConfig = {
services.atticd = {
inherit (cfg) environmentFile;
@@ -71,9 +77,13 @@ let
StandardError = "journal+console";
Restart = "no";
TimeoutStartSec = "2h";
EnvironmentFile = [ config.sops.templates."ntfy.env".path ];
}
// lib.optionalAttrs cfg.ntfy.enable {
EnvironmentFile = [ cfg.ntfy.envFile ];
};
unitConfig = lib.optionalAttrs cfg.ntfy.enable {
OnFailure = "nix-rebuild-cache-notify-failure.service";
};
unitConfig.OnFailure = "nix-rebuild-cache-notify-failure.service";
path = with pkgs; [
nix
git
@@ -125,13 +135,14 @@ let
fi;
'';
};
}
// lib.optionalAttrs cfg.ntfy.enable {
nix-rebuild-cache-notify-failure = {
description = "Notify ntfy on nix-rebuild-cache failure";
serviceConfig = {
Type = "oneshot";
ExecStart = "${ntfyFailScript}";
EnvironmentFile = [ config.sops.templates."ntfy.env".path ];
EnvironmentFile = [ cfg.ntfy.envFile ];
};
};
};

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
namespace,
...
}:
with lib;
let
name = "caddy-internal";
cfg = config.${namespace}.services.${name};
net = lib.${namespace}.network;
caddyPackage = pkgs.caddy.withPlugins {
plugins = [
"github.com/caddy-dns/cloudflare@v0.2.3"
];
hash = "sha256-20o+14cn/eeLuf1c8uGE1ODRZGC0oxocaIVlv4tFSvA=";
};
# Build a virtual-host block for one proxy entry.
# Access is restricted to LAN + Nebula subnets; all other clients get 403.
mkProxyBlock =
entryName: proxyCfg:
let
fqdn = "${proxyCfg.subdomain}.${net.domain}";
in
''
@${entryName} host ${fqdn}
handle @${entryName} {
@${entryName}_internal {
remote_ip ${net.subnet.lan} ${net.subnet.nebula}
host ${fqdn}
}
handle @${entryName}_internal {
reverse_proxy ${proxyCfg.upstream}
${proxyCfg.extraCaddyConfig}
}
handle {
respond "Forbidden" 403
}
}
'';
proxyBlocks = lib.concatStringsSep "\n" (
lib.mapAttrsToList mkProxyBlock (lib.filterAttrs (_: p: p.enable) cfg.proxies)
);
caddy-internal = lib.${namespace}.mkModule {
inherit config name;
description = "Internal-only Caddy reverse proxy with HTTPS via Cloudflare DNS challenge";
options = {
proxies = mkOption {
type = types.attrsOf (
types.submodule {
options = {
enable = lib.${namespace}.mkBoolOpt true "Whether to enable this proxy entry";
subdomain = lib.${namespace}.mkOpt types.str "" "Subdomain under ${net.domain}";
upstream = lib.${namespace}.mkOpt types.str "" "Upstream address (e.g. http://127.0.0.1:8123)";
extraCaddyConfig =
lib.${namespace}.mkOpt types.lines ""
"Extra Caddyfile directives for this entry";
};
}
);
default = { };
description = "Internal services to proxy, each restricted to LAN + Nebula subnets";
};
};
moduleConfig = {
services.caddy = {
enable = true;
package = caddyPackage;
environmentFile = config.sops.templates."caddy-internal.env".path;
email = "jalle008@proton.me";
enableReload = true;
globalConfig = ''
metrics
http_port 80
https_port 443
default_bind 0.0.0.0
'';
virtualHosts."*.${net.domain}" = {
extraConfig = ''
tls {
dns cloudflare {$CLOUDFLARE_DNS_API_TOKEN}
}
${proxyBlocks}
'';
};
};
networking.firewall.allowedTCPPorts = [
80
443
];
};
};
in
{
imports = [
caddy-internal
./sops.nix
];
}

View File

@@ -0,0 +1,39 @@
{
config,
lib,
namespace,
...
}:
let
cfg = config.${namespace}.services.caddy-internal;
caddyUser = config.users.users.caddy.name;
caddyGroup = config.users.users.caddy.group;
caddySecret = {
owner = caddyUser;
group = caddyGroup;
sopsFile = lib.snowfall.fs.get-file "secrets/nuc-secrets.yaml";
restartUnits = [ "caddy.service" ];
};
in
{
config = lib.mkIf cfg.enable {
sops = {
secrets = {
# Add this key to secrets/nuc-secrets.yaml:
# nuc/caddy/cloudflare-dns-api-token: <token>
"nuc/caddy/cloudflare-dns-api-token" = caddySecret;
};
templates."caddy-internal.env" = {
content = ''
CLOUDFLARE_DNS_API_TOKEN=${config.sops.placeholder."nuc/caddy/cloudflare-dns-api-token"}
'';
owner = caddyUser;
group = caddyGroup;
restartUnits = [ "caddy.service" ];
};
};
};
}

View File

@@ -156,9 +156,53 @@ let
bouncerName = "nas-bouncer";
};
# secrets.apiKeyPath = config.sops.secrets."jallen-nas/crowdsec-firewall-bouncer-api-key".path;
settings = {
# The default api_url is derived from the LAPI's listen_uri, which is
# "0.0.0.0:8181" — a valid bind address but not a connectable URL.
# Override to the loopback address the bouncer should actually connect to.
api_url = "http://127.0.0.1:${toString cfg.port}";
};
};
};
# During activation (which runs as root), check whether the machine credential in
# client.yaml exists in the crowdsec SQLite DB. If not (e.g. after a DB wipe),
# clear client.yaml so the subsequent crowdsec-setup ExecStartPre re-registers.
# This runs before switch-to-configuration starts/restarts services, breaking the
# boot-time cycle where the ExecStartPre fix can't apply until the service succeeds.
system.activationScripts.crowdsec-check-machine-creds =
let
machineName = config.services.crowdsec.name;
in
{
text = ''
clientYaml="${cfg.configDir}/crowdsec/client.yaml"
dbPath="/var/lib/crowdsec/state/crowdsec.db"
if [ -f "$dbPath" ]; then
if [ -s "$clientYaml" ]; then
login=$(${pkgs.gnugrep}/bin/grep -oP '(?<=login: ).*' "$clientYaml" || true)
if [ -n "$login" ]; then
found=$(${pkgs.sqlite}/bin/sqlite3 "$dbPath" \
"SELECT COUNT(*) FROM machines WHERE machine_id='$login';" 2>/dev/null || echo "0")
if [ "$found" = "0" ]; then
echo "crowdsec activation: machine '$login' missing from DB resetting credentials"
${pkgs.coreutils}/bin/rm -f "$clientYaml"
# Also remove any stale entry under the configured machine name so
# 'cscli machines add ${machineName} --auto' doesn't fail with "user already exist"
${pkgs.sqlite}/bin/sqlite3 "$dbPath" \
"DELETE FROM machines WHERE machine_id='${machineName}';" 2>/dev/null || true
fi
fi
else
# client.yaml absent/empty ensure no stale name entry blocks re-registration
${pkgs.sqlite}/bin/sqlite3 "$dbPath" \
"DELETE FROM machines WHERE machine_id='${machineName}';" 2>/dev/null || true
fi
fi
'';
deps = [ ];
};
# The upstream crowdsec module uses ReadWritePaths (not StateDirectory) on
# crowdsec.service, meaning it expects /var/lib/crowdsec to exist as a real
# directory (created by tmpfiles). However, crowdsec-firewall-bouncer-register
@@ -181,7 +225,64 @@ let
services = {
crowdsec = {
serviceConfig = lib.mkMerge [
{ DynamicUser = lib.mkForce false; }
{
DynamicUser = lib.mkForce false;
# ProtectSystem=strict (set upstream) makes all paths read-only except
# those in ReadWritePaths. The credentials file lives on the NAS mount
# which is not listed by default, so cscli machines add fails with EROFS.
ReadWritePaths = [ "${cfg.configDir}/crowdsec" ];
# If the machine credentials in client.yaml don't match any machine in the
# SQLite DB (e.g. after a DB wipe while client.yaml persists), crowdsec
# fatals on startup. Detect this mismatch before crowdsec-setup runs:
# read the machine login from client.yaml, query the DB directly, and
# delete client.yaml if the machine is absent so the next crowdsec-setup
# invocation re-registers a fresh machine.
# Use mkBefore so this runs before the upstream crowdsec-setup ExecStartPre
# entries, giving crowdsec-setup a cleared client.yaml to re-register from.
ExecStartPre = lib.mkBefore [
(
"+"
+ (
let
machineName = config.services.crowdsec.name;
in
pkgs.writeShellScript "crowdsec-check-machine-creds" ''
set -euo pipefail
clientYaml="${cfg.configDir}/crowdsec/client.yaml"
dbPath="/var/lib/crowdsec/state/crowdsec.db"
sqlite="${pkgs.sqlite}/bin/sqlite3"
rm="${pkgs.coreutils}/bin/rm"
[ -f "$dbPath" ] || exit 0 # No DB yet; fresh install, nothing to fix
if [ -s "$clientYaml" ]; then
# Credentials file exists verify the login it contains is in the DB
login=$(${pkgs.gnugrep}/bin/grep -oP '(?<=login: ).*' "$clientYaml" || true)
if [ -n "$login" ]; then
found=$("$sqlite" "$dbPath" \
"SELECT COUNT(*) FROM machines WHERE machine_id='$login';" 2>/dev/null || echo "0")
if [ "$found" = "0" ]; then
echo "crowdsec: machine '$login' missing from DB resetting credentials"
"$rm" -f "$clientYaml"
"$sqlite" "$dbPath" \
"DELETE FROM machines WHERE machine_id='${machineName}';" 2>/dev/null || true
fi
fi
else
# Credentials file absent ensure no stale name row blocks machines add
stale=$("$sqlite" "$dbPath" \
"SELECT COUNT(*) FROM machines WHERE machine_id='${machineName}';" 2>/dev/null || echo "0")
if [ "$stale" != "0" ]; then
echo "crowdsec: client.yaml absent but '${machineName}' in DB removing stale row"
"$sqlite" "$dbPath" \
"DELETE FROM machines WHERE machine_id='${machineName}';" 2>/dev/null || true
fi
fi
''
)
)
];
}
(lib.mkIf (cfg.ntfy.enable && cfg.ntfy.envFile != "") {
EnvironmentFile = [ cfg.ntfy.envFile ];
})

View File

@@ -52,7 +52,7 @@ let
name = "node-exporter-full.json";
path = patchDashboard "node-exporter-full.json" (pkgs.fetchurl {
url = "https://grafana.com/api/dashboards/1860/revisions/latest/download";
sha256 = "sha256-mEWSdsTn1EKpW6xoJv/s0XST46EOoUPbDugQwyngIss=";
sha256 = "sha256-GExrdAnzBtp1Ul13cvcZRbEM6iOtFrXXjEaY6g6lGYY=";
}) "ds_prometheus";
}
{
@@ -890,7 +890,24 @@ let
restartUnits = [ "grafana.service" ];
};
systemd.services.grafana.serviceConfig.EnvironmentFile = config.sops.templates."grafana.env".path;
systemd.services.grafana.serviceConfig = {
EnvironmentFile = config.sops.templates."grafana.env".path;
# Grafana downloads plugins at runtime and occasionally creates subdirectories
# with overly restrictive permissions (e.g. 0700 for locales/*), which causes
# the next startup to fail with "permission denied" during plugin discovery.
# Fix any such directories before Grafana starts.
ExecStartPre = [
(
"+"
+ pkgs.writeShellScript "grafana-fix-plugin-perms" ''
pluginDir="${cfg.configDir}/grafana/plugins"
if [ -d "$pluginDir" ]; then
${pkgs.coreutils}/bin/chmod -R a+rX "$pluginDir"
fi
''
)
];
};
# The redis exporter needs AF_INET to reach TCP Redis instances.
# The default systemd hardening only allows AF_UNIX.

View File

@@ -0,0 +1,137 @@
{
config,
lib,
namespace,
pkgs,
...
}:
with lib;
let
name = "nebula-ui";
cfg = config.${namespace}.services.${name};
statsListenAddr = "${cfg.statsListenAddress}:${toString cfg.statsPort}";
nebulaUiConfig = lib.${namespace}.mkModule {
inherit config name;
description = "Nebula network web UI (stats + cert signing)";
options = {
# Override mkModule defaults: bind to localhost only; firewall closed by
# default since this service sits behind a Caddy reverse proxy.
listenAddress = lib.${namespace}.mkOpt types.str "127.0.0.1" "Address nebula-ui listens on";
openFirewall =
lib.${namespace}.mkBoolOpt false
"Open firewall for nebula-ui (not needed behind a reverse proxy)";
# ── Stats endpoint ───────────────────────────────────────────────────────
statsListenAddress =
lib.${namespace}.mkOpt types.str "127.0.0.1"
"Address nebula's stats HTTP endpoint listens on";
statsPort = lib.${namespace}.mkOpt types.port 8474 "Port nebula's stats HTTP endpoint listens on";
# ── CA secrets ───────────────────────────────────────────────────────────
# The CA cert/key are already decrypted by the nebula sops.nix.
# We need a *separate* sops secret for the CA key exposed to nebula-ui
# because the nebula module only exposes it to nebula-<network>.
caCertSecretKey =
lib.${namespace}.mkOpt types.str ""
"SOPS secret key for the CA certificate (e.g. \"pi5/nebula/ca-cert\")";
caKeySecretKey =
lib.${namespace}.mkOpt types.str ""
"SOPS secret key for the CA private key (e.g. \"pi5/nebula/ca-key\")";
secretsFile =
lib.${namespace}.mkOpt types.str ""
"Path to the SOPS secrets YAML that holds the CA cert + key";
# ── Network identity ─────────────────────────────────────────────────────
networkName =
lib.${namespace}.mkOpt types.str "jallen-nebula"
"Nebula network name (must match services.nebula.networkName)";
};
moduleConfig = {
assertions = [
{
assertion = cfg.caCertSecretKey != "";
message = "mjallen.services.nebula-ui.caCertSecretKey must be set";
}
{
assertion = cfg.caKeySecretKey != "";
message = "mjallen.services.nebula-ui.caKeySecretKey must be set";
}
{
assertion = cfg.secretsFile != "";
message = "mjallen.services.nebula-ui.secretsFile must be set";
}
];
# ── SOPS secrets ─────────────────────────────────────────────────────────
# ca-cert: already declared by the nebula module (owned by nebula-<network>,
# mode 0440). We only append restartUnits here; access is via group membership.
sops.secrets."${cfg.caCertSecretKey}" = {
restartUnits = [ "nebula-ui.service" ];
};
# ca-key: only used by nebula-ui, so we own it outright.
sops.secrets."${cfg.caKeySecretKey}" = {
sopsFile = cfg.secretsFile;
owner = name;
group = name;
restartUnits = [ "nebula-ui.service" ];
};
# ── User / group ────────────────────────────────────────────────────────
users.users.${name} = {
isSystemUser = true;
group = name;
# Grant read access to the nebula CA secrets (owned by nebula-<network>)
extraGroups = [ "nebula-${cfg.networkName}" ];
description = "Nebula UI service user";
};
users.groups.${name} = { };
# ── Systemd service ─────────────────────────────────────────────────────
systemd.services.${name} = {
description = "Nebula network web UI";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"sops-nix.service"
];
environment = {
NEBULA_UI_CA_CERT_PATH = config.sops.secrets."${cfg.caCertSecretKey}".path;
NEBULA_UI_CA_KEY_PATH = config.sops.secrets."${cfg.caKeySecretKey}".path;
NEBULA_UI_STATS_URL = "http://${statsListenAddr}";
NEBULA_UI_NETWORK_NAME = cfg.networkName;
NEBULA_UI_LISTEN_HOST = cfg.listenAddress;
NEBULA_UI_LISTEN_PORT = toString cfg.port;
};
serviceConfig = {
ExecStart = "${pkgs.${namespace}.nebula-ui}/bin/nebula-ui";
User = name;
GlistenAddressroup = name;
Restart = "on-failure";
RestartSec = "5s";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadOnlyPaths = [
config.sops.secrets."${cfg.caCertSecretKey}".path
config.sops.secrets."${cfg.caKeySecretKey}".path
];
};
};
};
};
in
{
imports = [ nebulaUiConfig ];
}

View File

@@ -95,6 +95,17 @@ let
host = "any";
}
] "Nebula outbound firewall rules";
# -----------------------------------------------------------------------
# Stats / metrics HTTP endpoint
# -----------------------------------------------------------------------
stats = {
enable = lib.${namespace}.mkBoolOpt false "Enable the Nebula HTTP stats endpoint";
listenAddress = lib.${namespace}.mkOpt types.str "127.0.0.1" "Address the stats endpoint binds to";
statsPort = lib.${namespace}.mkOpt types.port 8474 "Port the stats endpoint listens on";
};
};
moduleConfig = {
environment.systemPackages = with pkgs; [ nebula ];
@@ -136,6 +147,12 @@ let
inbound = cfg.inboundRules;
outbound = cfg.outboundRules;
};
settings.stats = lib.mkIf cfg.stats.enable {
type = "json";
listen = "${cfg.stats.listenAddress}:${toString cfg.stats.statsPort}";
interval = "10s";
};
};
};
};

View File

@@ -17,6 +17,9 @@ let
group = nebulaUser;
restartUnits = [ nebulaUnit ];
};
# CA cert/key are group-readable so nebula-ui (a group member) can access them
mkCaSecret = _key: (mkSecret _key) // { mode = "0440"; };
in
{
config = mkIf cfg.enable {
@@ -32,7 +35,7 @@ in
];
sops.secrets = {
"${cfg.secretsPrefix}/ca-cert" = mkSecret "ca-cert";
"${cfg.secretsPrefix}/ca-cert" = mkCaSecret "ca-cert";
"${cfg.secretsPrefix}/${cfg.hostSecretName}-cert" = mkSecret "host-cert";
"${cfg.secretsPrefix}/${cfg.hostSecretName}-key" = mkSecret "host-key";
};

View File

@@ -23,7 +23,7 @@ let
enable = true;
package = pkgs.nextcloud33;
hostName = "cloud.mjallen.dev";
home = "${cfg.configDir}/nextcloud";
home = "${cfg.dataDir}/nextcloud";
datadir = "${cfg.dataDir}/nextcloud";
configureRedis = true;
enableImagemagick = true;
@@ -147,12 +147,144 @@ let
# "Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing."
nextcloud = lib.mkForce { };
nextcloud-setup = {
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
# Also require the NAS bcachefs mount so that ExecStartPre can create
# the store-apps and config directories on the actual NAS filesystem.
# Without this, the dirs are created on the root tmpfs overlay before
# the mount comes up, and the real NAS store-apps path never exists.
after = [
"postgresql.service"
"media-nas-main.mount"
];
requires = [
"postgresql.service"
"media-nas-main.mount"
];
serviceConfig =
let
# Extract the override.config.php store-path from the already-evaluated
# tmpfiles rules list at Nix eval time, so we never have to parse files at
# runtime. The upstream module emits exactly one rule of the form:
# "L+ <dest> - - - - <storepath>"
overrideLine = lib.findFirst (
r: lib.hasInfix "override.config.php" r
) null config.systemd.tmpfiles.rules;
overrideStorePath =
if overrideLine != null then lib.last (lib.splitString " " overrideLine) else null;
# Bootstrap config.php written when the file is absent/empty.
# Satisfies Nextcloud's Config.php writeData() guard (needs 'version')
# and the setup script's `-s` check (needs non-empty file).
# passwordsalt/secret/instanceid are intentionally left empty here —
# they must be populated manually or via SOPS before first use.
bootstrapConfig = pkgs.writeText "nextcloud-bootstrap-config.php" (
"<?php\n"
+ "$"
+ "CONFIG = [\n"
+ " 'installed' => true,\n"
+ " 'version' => '${config.services.nextcloud.package.version}',\n"
+ " 'datadirectory' => '${cfg.dataDir}/nextcloud/data',\n"
+ " 'dbtype' => 'pgsql',\n"
+ " 'dbname' => 'nextcloud',\n"
+ " 'dbhost' => '/run/postgresql',\n"
+ " 'dbuser' => 'nextcloud',\n"
+ " 'dbpassword' => "
+ "''"
+ ",\n"
+ " 'instanceid' => "
+ "''"
+ ",\n"
+ " 'passwordsalt' => "
+ "''"
+ ",\n"
+ " 'secret' => "
+ "''"
+ ",\n"
+ "];\n"
);
in
lib.mkIf (overrideStorePath != null) {
# systemd-tmpfiles refuses to create paths under /media/nas/main because
# of an "unsafe path transition" (owned by nix-apps, not root/nextcloud).
# Work around by creating the required dirs/symlinks as root ('+' prefix)
# before the setup script's ownership check runs.
ExecStartPre = [
(
"+"
+ pkgs.writeShellScript "nextcloud-fix-override-config" ''
ncdir="${cfg.dataDir}/nextcloud"
# Ensure required directories exist with correct ownership
for dir in "$ncdir" "$ncdir/config" "$ncdir/data" "$ncdir/store-apps"; do
if [ ! -d "$dir" ]; then
${pkgs.coreutils}/bin/mkdir -p "$dir"
fi
${pkgs.coreutils}/bin/chown nextcloud:nextcloud "$dir"
${pkgs.coreutils}/bin/chmod 0750 "$dir"
done
# override.config.php symlink (updated each generation)
dest="$ncdir/config/override.config.php"
echo "Creating symlink: $dest -> ${overrideStorePath}"
${pkgs.coreutils}/bin/ln -sf "${overrideStorePath}" "$dest"
# If config.php is absent or empty, copy in a bootstrap stub.
# Nextcloud's Config.php writeData() guard requires 'version' in the
# merged cache, and the setup script's -s check requires a non-empty
# file. The real runtime settings come from override.config.php via
# array_replace_recursive; this stub just satisfies those two guards.
cfgfile="$ncdir/config/config.php"
if [ ! -s "$cfgfile" ]; then
echo "Writing bootstrap config.php"
${pkgs.coreutils}/bin/cp ${bootstrapConfig} "$cfgfile"
${pkgs.coreutils}/bin/chown nextcloud:nextcloud "$cfgfile"
${pkgs.coreutils}/bin/chmod 0640 "$cfgfile"
fi
''
)
];
};
};
nextcloud-update-db = {
after = [
"postgresql.service"
"media-nas-main.mount"
];
requires = [
"postgresql.service"
"media-nas-main.mount"
];
};
nextcloud-cron = {
after = [ "media-nas-main.mount" ];
requires = [ "media-nas-main.mount" ];
};
phpfpm-nextcloud = {
after = [ "media-nas-main.mount" ];
requires = [ "media-nas-main.mount" ];
};
# One-shot repair for the oc_filecache_extended duplicate key constraint
# violation that causes nextcloud-cron to fail with:
# "duplicate key value violates unique constraint oc_filecache_extended_pkey"
# Runs as the postgres user before nextcloud-setup so that the DB is clean
# before Nextcloud starts. Idempotent: only removes rows whose fileid does
# not exist in oc_filecache (true orphans). Remove this service once the
# underlying Nextcloud bug is fixed and a clean run confirms cron succeeds.
nextcloud-repair-filecache = {
description = "Repair orphan rows in oc_filecache_extended";
wantedBy = [ "nextcloud-setup.service" ];
before = [ "nextcloud-setup.service" ];
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
serviceConfig = {
Type = "oneshot";
User = "postgres";
ExecStart = pkgs.writeShellScript "nextcloud-repair-filecache" ''
${pkgs.postgresql}/bin/psql -d nextcloud -c "
DELETE FROM oc_filecache_extended
WHERE fileid NOT IN (SELECT fileid FROM oc_filecache);
"
'';
};
};
};
};

View File

@@ -60,6 +60,11 @@ in
};
config = mkIf cfg.enable {
systemd.services."podman-${cfg.name}".unitConfig.RequiresMountsFor = [
cfg.configPath
cfg.dataPath
];
virtualisation.oci-containers.containers."${cfg.name}" = {
inherit (cfg) autoStart image;
ports = [ "${cfg.httpPort}:9200" ];

View File

@@ -31,7 +31,6 @@ in
nil
nix-output-monitor
nixos-anywhere
qemu
udisks2
unzip
]

View File

@@ -109,6 +109,8 @@ in
hashedPasswordFile = resolvedPasswordFile;
extraGroups = [
"dialout"
"uucp"
"wheel"
"keys"
"networkmanager"

View File

@@ -1,8 +1,12 @@
{ lib, ... }:
{ lib, pkgs, ... }:
{
# Virtualisation configuration
virtualisation = {
libvirtd.enable = lib.mkDefault true;
libvirtd = {
enable = lib.mkForce false;
package = pkgs.stable.libvirt;
qemu.package = pkgs.stable.qemu;
};
podman.enable = lib.mkDefault true;
waydroid.enable = lib.mkDefault false;
};

View File

@@ -1,125 +0,0 @@
{ ... }:
_final: prev: {
home-assistant = prev.home-assistant.override {
packageOverrides = _self: super: {
nice-go = super.nice-go.overridePythonAttrs (_old: {
doCheck = false;
});
cfn-lint = super.cfn-lint.overridePythonAttrs (_old: {
doCheck = false;
});
aiobotocore = super.aiobotocore.overridePythonAttrs (_old: {
doCheck = false;
});
connect-box = super.connect-box.overridePythonAttrs (_old: {
doCheck = false;
});
motionblindsble = super.motionblindsble.overridePythonAttrs (_old: {
doCheck = false;
});
pyinsteon = super.pyinsteon.overridePythonAttrs (_old: {
doCheck = false;
});
psnawp = super.psnawp.overridePythonAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ super.pythonRelaxDepsHook ];
pythonRelaxDeps = [ "pycountry" ];
});
radios = super.radios.overridePythonAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ super.pythonRelaxDepsHook ];
pythonRelaxDeps = [ "pycountry" ];
});
# Several packages are gated behind pythonAtLeast "3.13" or "3.14" guards
# in nixpkgs, but are required as transitive dependencies of HA components
# when using availableComponents. Override them all to allow Python 3.14
# until upstream catches up.
aiounittest = super.aiounittest.overridePythonAttrs (_old: {
disabled = false;
doCheck = false;
});
# aiokef tests rely on asyncio.get_event_loop() auto-creating a loop,
# which was removed in Python 3.10+.
aiokef = super.aiokef.overridePythonAttrs (_old: {
doCheck = false;
});
# pyads 3.5.1 adopted a src/ layout; the nixpkgs patchPhase references
# the old flat path, and setup.py always tries to compile adslib from
# source via make. Skip the compile step by making platform_is_unix()
# return False, pre-place the nixpkgs adslib.so in src/ so the wheel
# installs it, and disable the import check since pyads searches sys.path
# for adslib.so at import time (requires the real ADS hardware library).
pyads = super.pyads.overridePythonAttrs (old: {
patchPhase = ''
substituteInPlace setup.py \
--replace-fail "sys.platform.startswith(\"linux\") or sys.platform.startswith(\"darwin\")" \
"False"
'';
preBuild = ''
mkdir -p src
cp ${builtins.head old.buildInputs}/lib/adslib.so src/adslib.so
'';
doCheck = false;
pythonImportsCheck = [ ];
});
apischema = super.apischema.overridePythonAttrs (_old: {
disabled = false;
doCheck = false;
});
aws-sam-translator = super.aws-sam-translator.overridePythonAttrs (_old: {
# https://github.com/aws/serverless-application-model/issues/3831
disabled = false;
doCheck = false;
});
# future uses lib2to3 in past.translation, which was removed in Python 3.14.
# That module is only needed for Python 2→3 source translation at runtime,
# which HA components don't use. Drop past.translation from the import check.
future = super.future.overridePythonAttrs (_old: {
disabled = false;
pythonImportsCheck = [
"future.builtins"
"future.moves"
"future.standard_library"
"past.builtins"
];
});
raincloudy = super.raincloudy.overridePythonAttrs (_old: {
disabled = false;
doCheck = false;
});
reactivex = super.reactivex.overridePythonAttrs (_old: {
disabled = false;
doCheck = false;
});
# Several packages are marked broken due to version constraints on upstream
# deps that have been bumped in nixpkgs (pysnmp 7.x, xmltodict 1.x).
# They work fine at runtime for HA's usage.
# aio-georss-client is marked broken because xmltodict >= 1.0 changed how
# namespace-annotated XML elements are returned (dict instead of string),
# which breaks _process_coordinates. The GDACS integration is not used here
# and the package installs fine; skip tests to unblock the build.
aio-georss-client = super.aio-georss-client.overridePythonAttrs (_old: {
meta = (super.aio-georss-client.meta or { }) // {
broken = false;
};
doCheck = false;
});
atenpdu = super.atenpdu.overridePythonAttrs (_old: {
meta = (super.atenpdu.meta or { }) // {
broken = false;
};
nativeBuildInputs = (_old.nativeBuildInputs or [ ]) ++ [ super.pythonRelaxDepsHook ];
pythonRelaxDeps = [ "async-timeout" ];
});
bimmer-connected = super.bimmer-connected.overridePythonAttrs (old: {
meta = (old.meta or { }) // {
broken = false;
};
doCheck = false;
# pillow is an optional dep (China market only) but the runtime dep check
# flags it as missing; add it to propagatedBuildInputs to satisfy it.
propagatedBuildInputs = (old.propagatedBuildInputs or [ ]) ++ [ super.pillow ];
});
};
};
}

View File

@@ -36,7 +36,7 @@ let
in
stdenv.mkDerivation (finalAttrs: {
pname = "bcachefs-tools";
version = "1.37.2";
version = "1.37.5";
src = sources.bcachefs-tools;

View File

@@ -4,11 +4,11 @@
"bcachefs-tools": {
"fetcher": "git",
"url": "https://github.com/koverstreet/bcachefs-tools",
"rev": "v1.37.4",
"hash": "sha256-VlljE+xoKg6GryVuebUA1v9x2shMBUb7veCtD68MBJw="
"rev": "v1.37.5",
"hash": "sha256-x/hpOTvLYyNIsC8ZM0zmTwL8GsiqaTg0ZjQNiS7Y2Wo="
},
"cargoDeps": {
"hash": "sha256-t6ghsIRJFR2Kqism4tdizhnJ8qcE2ZZwH6c3nYogHlo="
"hash": "sha256-+9vy+StbFxN+2sNrObLUhvn0BhPZLW6zXuw0uUbaRmw="
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,32 @@
{
stdenv,
writeShellScriptBin,
appimage-run
}:
let
src = ./HueForge_Linux_v0.9.3.1.AppImage;
wrapperScript = writeShellScriptBin "hueforge" ''
exec ${appimage-run}/bin/appimage-run ${src} "$@"
'';
in
stdenv.mkDerivation rec {
inherit src;
pname = "hueforge";
version = "0.9.3.1";
dontUnpack = true;
dontConfigure = true;
dontPatch = true;
dontFixup = true;
dontBuild = true;
installPhase = ''
runHook preInstall
mkdir -p $out/{share,bin}
cp $src $out/share/HueForge_Linux_v0.9.3.1.AppImage
cp ${wrapperScript}/bin/hueforge $out/bin/hueforge
runHook postInstall
'';
}

View File

@@ -1,21 +1,12 @@
{
lib,
stdenvNoCC,
fetchurl,
}:
let
imageUrl = "https://scontent.ffcm1-2.fna.fbcdn.net/v/t39.30808-6/387733576_7270226619673057_585200616645530081_n.jpg?_nc_cat=107&ccb=1-7&_nc_sid=1d70fc&_nc_ohc=KeQIDYtXccwQ7kNvwGkF57h&_nc_oc=AdqzTQBFHCfCUmy1iF-ATon_uMr6n0nMSXFyYTmSbT07kVw3D5nCvgfqp3UNhjkO0AM&_nc_zt=23&_nc_ht=scontent.ffcm1-2.fna&_nc_gid=6gw6tJoXB9eWOy5en5irxg&_nc_ss=7a32e&oh=00_Afwr4AsNifG3vViXZ_x-wNC98bznQ2vFnVrKl2sDPEl9oA&oe=69CCE530";
in
stdenvNoCC.mkDerivation {
pname = "profile-pic";
version = "1.0.0";
src = fetchurl {
url = imageUrl;
hash = "sha256-n7yayMkNow6IaKVk8mkUWEqa0STR8UKNJD2K1PtPfW8=";
};
sourceRoot = ".";
src = ./profile-pic.jpg;
dontUnpack = true;
@@ -33,7 +24,7 @@ stdenvNoCC.mkDerivation {
meta = with lib; {
description = "Profile Pic for Matt";
homepage = "https://authentik.mjallen.dev";
platforms = platforms.linux;
platforms = platforms.all;
maintainers = [ ];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

6
packages/raspberrypi/linux-rpi/version.json Executable file → Normal file
View File

@@ -25,12 +25,12 @@
},
"unstable": {
"variables": {
"modDirVersion": "7.0.0-rc6"
"modDirVersion": "7.0.0-rc7"
},
"sources": {
"unstable": {
"rev": "cab660d9a1cbe0098db32e660fadb051b5c7fc12",
"hash": "sha256-AAgvct6nGxZcCjkaS8fd/WYR22pj3mJheAj87ivovDE="
"rev": "8771a522e74c97a183b0a3a98ae9c2a3160c3cbb",
"hash": "sha256-M48A581MdEDZr/sPq8VFAH5ij8GfBJYcQct0HDxEHnw="
}
}
}

View File

@@ -0,0 +1,252 @@
"""
nebula-ui — FastAPI web UI for managing a Nebula overlay network.
Reads configuration from environment variables (set by the NixOS module):
NEBULA_UI_CA_CERT_PATH — path to the decrypted CA cert file (from sops-nix)
NEBULA_UI_CA_KEY_PATH — path to the decrypted CA key file (from sops-nix)
NEBULA_UI_STATS_URL — nebula stats HTTP endpoint, e.g. http://127.0.0.1:8472
NEBULA_UI_NETWORK_NAME — nebula network name, e.g. "jallen-nebula"
NEBULA_UI_LISTEN_HOST — host to bind to (default: 127.0.0.1)
NEBULA_UI_LISTEN_PORT — port to listen on (default: 8472)
"""
from __future__ import annotations
import base64
import io
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Annotated
import httpx
import qrcode
import qrcode.image.svg
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
# ---------------------------------------------------------------------------
# Config from environment
# ---------------------------------------------------------------------------
CA_CERT_PATH = os.environ.get("NEBULA_UI_CA_CERT_PATH", "")
CA_KEY_PATH = os.environ.get("NEBULA_UI_CA_KEY_PATH", "")
STATS_URL = os.environ.get("NEBULA_UI_STATS_URL", "http://127.0.0.1:8472")
NETWORK_NAME = os.environ.get("NEBULA_UI_NETWORK_NAME", "nebula")
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
app = FastAPI(title="Nebula UI", docs_url=None, redoc_url=None)
_templates_dir = Path(__file__).parent / "templates"
# When installed via Nix the templates are copied into the same package dir
# as __init__.py (i.e. nebula_ui/templates/). __file__ resolves correctly.
templates = Jinja2Templates(directory=str(_templates_dir))
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _qr_svg(data: str) -> str:
"""Return an inline SVG string for a QR code encoding *data*."""
factory = qrcode.image.svg.SvgPathImage
img = qrcode.make(
data, image_factory=factory, error_correction=qrcode.constants.ERROR_CORRECT_L
)
buf = io.BytesIO()
img.save(buf)
return buf.getvalue().decode()
def _nebula_cert_bin() -> str:
"""Return the path to nebula-cert (must be on PATH)."""
path = shutil.which("nebula-cert")
if path is None:
raise RuntimeError("nebula-cert not found on PATH")
return path
def _sign_cert(name: str, ip: str, groups: str, duration: str) -> tuple[str, str]:
"""
Sign a new Nebula host certificate using the CA files pointed to by
NEBULA_UI_CA_CERT_PATH / NEBULA_UI_CA_KEY_PATH.
Returns (cert_pem, key_pem) as strings.
Raises RuntimeError on failure.
"""
if not CA_CERT_PATH or not CA_KEY_PATH:
raise RuntimeError(
"CA cert/key paths are not configured "
"(NEBULA_UI_CA_CERT_PATH / NEBULA_UI_CA_KEY_PATH)"
)
ca_cert = Path(CA_CERT_PATH)
ca_key = Path(CA_KEY_PATH)
if not ca_cert.exists():
raise RuntimeError(f"CA cert not found: {ca_cert}")
if not ca_key.exists():
raise RuntimeError(f"CA key not found: {ca_key}")
with tempfile.TemporaryDirectory(prefix="nebula-ui-") as tmp:
tmp_path = Path(tmp)
out_crt = tmp_path / "host.crt"
out_key = tmp_path / "host.key"
cmd = [
_nebula_cert_bin(),
"sign",
"-name",
name,
"-ip",
ip,
"-ca-crt",
str(ca_cert),
"-ca-key",
str(ca_key),
"-out-crt",
str(out_crt),
"-out-key",
str(out_key),
]
if groups.strip():
cmd += ["-groups", groups.strip()]
if duration.strip():
cmd += ["-duration", duration.strip()]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"nebula-cert sign failed:\n{result.stderr}")
cert_pem = out_crt.read_text()
key_pem = out_key.read_text()
return cert_pem, key_pem
def _cert_info(cert_pem: str) -> dict:
"""Return parsed cert info by running nebula-cert print."""
with tempfile.NamedTemporaryFile(suffix=".crt", mode="w", delete=False) as f:
f.write(cert_pem)
f.flush()
result = subprocess.run(
[_nebula_cert_bin(), "print", "-json", "-path", f.name],
capture_output=True,
text=True,
)
Path(f.name).unlink(missing_ok=True)
if result.returncode != 0:
return {}
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return {}
def _fetch_stats() -> dict | None:
"""Fetch the nebula stats JSON. Returns None on error."""
try:
resp = httpx.get(f"{STATS_URL}/v1/stats", timeout=3.0)
resp.raise_for_status()
return resp.json()
except Exception:
pass
# Some nebula versions use /stats
try:
resp = httpx.get(f"{STATS_URL}/stats", timeout=3.0)
resp.raise_for_status()
return resp.json()
except Exception:
return None
def _fetch_hostmap() -> dict | None:
"""Fetch the nebula hostmap JSON."""
try:
resp = httpx.get(f"{STATS_URL}/v1/hostmap", timeout=3.0)
resp.raise_for_status()
return resp.json()
except Exception:
pass
try:
resp = httpx.get(f"{STATS_URL}/hostmap", timeout=3.0)
resp.raise_for_status()
return resp.json()
except Exception:
return None
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
stats = _fetch_stats()
hostmap = _fetch_hostmap()
return templates.TemplateResponse(
"index.html",
{
"request": request,
"network_name": NETWORK_NAME,
"stats": stats,
"hostmap": hostmap,
},
)
@app.get("/sign", response_class=HTMLResponse)
async def sign_form(request: Request):
return templates.TemplateResponse(
"sign.html",
{
"request": request,
"network_name": NETWORK_NAME,
"error": None,
"result": None,
},
)
@app.post("/sign", response_class=HTMLResponse)
async def sign_submit(
request: Request,
name: Annotated[str, Form()],
ip: Annotated[str, Form()],
groups: Annotated[str, Form()] = "",
duration: Annotated[str, Form()] = "",
):
error = None
result = None
try:
cert_pem, key_pem = _sign_cert(name, ip, groups, duration)
cert_info = _cert_info(cert_pem)
cert_qr = _qr_svg(cert_pem)
key_qr = _qr_svg(key_pem)
result = {
"name": name,
"ip": ip,
"cert_pem": cert_pem,
"key_pem": key_pem,
"cert_qr": cert_qr,
"key_qr": key_qr,
"cert_info": cert_info,
}
except Exception as exc:
error = str(exc)
return templates.TemplateResponse(
"sign.html",
{
"request": request,
"network_name": NETWORK_NAME,
"error": error,
"result": result,
},
)

View File

@@ -0,0 +1,52 @@
{
lib,
python3,
nebula,
runCommand,
writeShellApplication,
...
}:
let
pythonEnv = python3.withPackages (
ps: with ps; [
fastapi
uvicorn
httpx
qrcode
pillow # qrcode SVG path image factory dependency
jinja2
python-multipart # needed for FastAPI Form() parsing
]
);
# Install the app source as a proper Python package so uvicorn can import it.
appPkg = runCommand "nebula-ui-app" { } ''
pkgdir=$out/lib/python${python3.pythonVersion}/site-packages/nebula_ui
mkdir -p "$pkgdir"
cp ${./app.py} "$pkgdir/__init__.py"
cp -r ${./templates} "$pkgdir/templates"
'';
in
writeShellApplication {
name = "nebula-ui";
runtimeInputs = [
pythonEnv
nebula # provides nebula-cert on PATH
];
text = ''
export PYTHONPATH="${appPkg}/lib/python${python3.pythonVersion}/site-packages''${PYTHONPATH:+:$PYTHONPATH}"
exec uvicorn nebula_ui:app \
--host "''${NEBULA_UI_LISTEN_HOST:-127.0.0.1}" \
--port "''${NEBULA_UI_LISTEN_PORT:-8473}"
'';
meta = {
description = "Web UI for managing a Nebula overlay network stats, cert signing, QR codes";
mainProgram = "nebula-ui";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
};
}

View File

@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nebula UI — {{ network_name }}</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117;
--surface: #1a1d27;
--border: #2a2d3a;
--accent: #6c8ef5;
--accent2: #4ade80;
--text: #e2e4ef;
--muted: #7a7f9a;
--danger: #f87171;
--radius: 8px;
--font: 'Inter', ui-sans-serif, system-ui, sans-serif;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
font-size: 14px;
min-height: 100vh;
}
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 24px;
display: flex;
align-items: center;
gap: 32px;
height: 52px;
}
header .logo {
font-weight: 700;
font-size: 16px;
color: var(--accent);
letter-spacing: -.3px;
}
header nav a {
color: var(--muted);
text-decoration: none;
font-size: 13px;
padding: 6px 10px;
border-radius: var(--radius);
transition: color .15s, background .15s;
}
header nav a:hover,
header nav a.active {
color: var(--text);
background: var(--border);
}
main {
max-width: 960px;
margin: 0 auto;
padding: 32px 24px;
}
h1 { font-size: 22px; font-weight: 600; margin-bottom: 24px; }
h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
margin-bottom: 20px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: .3px;
}
.badge-green { background: #14532d; color: var(--accent2); }
.badge-blue { background: #1e3a5f; color: var(--accent); }
.badge-red { background: #450a0a; color: var(--danger); }
.badge-gray { background: var(--border); color: var(--muted); }
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
text-align: left;
padding: 8px 12px;
color: var(--muted);
border-bottom: 1px solid var(--border);
font-weight: 500;
}
td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,.02); }
.mono { font-family: ui-monospace, 'Cascadia Code', monospace; font-size: 12px; }
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-box {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
}
.stat-box .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
.stat-box .value { font-size: 22px; font-weight: 700; color: var(--accent); }
.stat-box .unit { font-size: 11px; color: var(--muted); margin-left: 3px; }
form .field { margin-bottom: 16px; }
form label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px; font-weight: 500; }
form input[type="text"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 9px 12px;
color: var(--text);
font-size: 14px;
outline: none;
transition: border-color .15s;
}
form input[type="text"]:focus { border-color: var(--accent); }
form input[type="text"]::placeholder { color: var(--muted); }
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 18px;
border-radius: var(--radius);
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: opacity .15s;
}
.btn:hover { opacity: .85; }
.btn-primary { background: var(--accent); color: #fff; }
.alert {
padding: 12px 16px;
border-radius: var(--radius);
margin-bottom: 20px;
font-size: 13px;
}
.alert-error { background: #450a0a; border: 1px solid #7f1d1d; color: var(--danger); }
.qr-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
@media (max-width: 640px) { .qr-grid { grid-template-columns: 1fr; } }
.qr-box {
background: #fff;
border-radius: var(--radius);
padding: 16px;
text-align: center;
}
.qr-box .qr-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: .5px;
}
.qr-box svg { max-width: 100%; height: auto; }
details { margin-top: 16px; }
summary { cursor: pointer; color: var(--muted); font-size: 12px; }
pre {
margin-top: 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
font-family: ui-monospace, monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
color: var(--text);
}
.no-data { color: var(--muted); font-size: 13px; padding: 20px 0; text-align: center; }
</style>
</head>
<body>
<header>
<span class="logo">nebula-ui</span>
<nav>
<a href="/" {% if request.url.path == "/" %}class="active"{% endif %}>Stats</a>
<a href="/sign" {% if request.url.path == "/sign" %}class="active"{% endif %}>Sign Certificate</a>
</nav>
<span style="margin-left:auto; color:var(--muted); font-size:12px;">{{ network_name }}</span>
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block content %}
<h1>Network Overview</h1>
{% if stats is none %}
<div class="card">
<div class="no-data">
Could not reach the Nebula stats endpoint at <code class="mono">{{ request.app.state.stats_url if request.app.state is defined else "configured URL" }}</code>.
Make sure <code class="mono">stats.enabled</code> and <code class="mono">stats.listen</code> are set in your Nebula config.
</div>
</div>
{% else %}
{# ── top-level counters ── #}
{% set meta = stats.get("meta", {}) %}
{% set network = stats.get("network", {}) %}
<div class="stat-grid">
<div class="stat-box">
<div class="label">Nebula Version</div>
<div class="value" style="font-size:15px; padding-top:4px;">{{ meta.get("version", "—") }}</div>
</div>
{% set peers = hostmap.get("Hosts", {}) if hostmap else {} %}
<div class="stat-box">
<div class="label">Active Peers</div>
<div class="value">{{ peers | length }}</div>
</div>
{% set counters = stats.get("counters", {}) %}
<div class="stat-box">
<div class="label">Tx Bytes</div>
<div class="value">{{ "{:,}".format(counters.get("send_bytes", 0)) }}</div>
</div>
<div class="stat-box">
<div class="label">Rx Bytes</div>
<div class="value">{{ "{:,}".format(counters.get("recv_bytes", 0)) }}</div>
</div>
<div class="stat-box">
<div class="label">Tx Packets</div>
<div class="value">{{ "{:,}".format(counters.get("send_packets", 0)) }}</div>
</div>
<div class="stat-box">
<div class="label">Rx Packets</div>
<div class="value">{{ "{:,}".format(counters.get("recv_packets", 0)) }}</div>
</div>
</div>
{# ── raw stats JSON ── #}
<details style="margin-bottom:20px;">
<summary>Raw stats JSON</summary>
<pre>{{ stats | tojson(indent=2) }}</pre>
</details>
{% endif %}
{# ── hostmap ── #}
<div class="card">
<h2>Peer Hostmap</h2>
{% if hostmap is none %}
<div class="no-data">Hostmap unavailable — stats endpoint not reachable.</div>
{% else %}
{% set hosts = hostmap.get("Hosts", {}) %}
{% if not hosts %}
<div class="no-data">No peers connected.</div>
{% else %}
<table>
<thead>
<tr>
<th>Overlay IP</th>
<th>Remote Addrs</th>
<th>Index</th>
<th>Relay?</th>
</tr>
</thead>
<tbody>
{% for overlay_ip, host in hosts.items() %}
<tr>
<td class="mono">{{ overlay_ip }}</td>
<td>
{% for addr in host.get("RemoteAddrs", []) %}
<span class="mono badge badge-gray" style="margin-right:4px;">{{ addr }}</span>
{% else %}
<span class="badge badge-gray">none</span>
{% endfor %}
</td>
<td class="mono">{{ host.get("LocalIndex", "—") }}</td>
<td>
{% if host.get("Relay") %}
<span class="badge badge-blue">relay</span>
{% else %}
<span class="badge badge-gray">no</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<details style="margin-top:16px;">
<summary>Raw hostmap JSON</summary>
<pre>{{ hostmap | tojson(indent=2) }}</pre>
</details>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block content %}
<h1>Sign Certificate</h1>
<div class="card">
<h2>New Host Certificate</h2>
<p style="color:var(--muted); font-size:13px; margin-bottom:20px;">
Fill in the details below. The CA on this host will sign a new certificate.
Scan the QR codes with the mobile Nebula app or copy the PEM values.
</p>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="post" action="/sign">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="field">
<label for="name">Host Name *</label>
<input type="text" id="name" name="name" placeholder="e.g. laptop" required
value="{{ result.name if result else '' }}">
</div>
<div class="field">
<label for="ip">Overlay IP / Mask *</label>
<input type="text" id="ip" name="ip" placeholder="e.g. 10.1.1.5/24" required
value="{{ result.ip if result else '' }}">
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="field">
<label for="groups">Groups <span style="color:var(--muted)">(comma-separated, optional)</span></label>
<input type="text" id="groups" name="groups" placeholder="e.g. laptops,users">
</div>
<div class="field">
<label for="duration">Duration <span style="color:var(--muted)">(optional, e.g. 8760h0m0s)</span></label>
<input type="text" id="duration" name="duration" placeholder="default: CA lifetime">
</div>
</div>
<button type="submit" class="btn btn-primary">Sign Certificate</button>
</form>
</div>
{% if result %}
<div class="card">
<h2>Certificate Issued</h2>
{# cert info summary #}
{% if result.cert_info %}
{% set details = result.cert_info.get("details", {}) %}
<div class="stat-grid" style="margin-bottom:20px;">
<div class="stat-box">
<div class="label">Name</div>
<div class="value" style="font-size:14px; padding-top:2px;">{{ details.get("name", result.name) }}</div>
</div>
<div class="stat-box">
<div class="label">IP</div>
<div class="value" style="font-size:14px; padding-top:2px;">{{ (details.get("ips", [result.ip]) | first) }}</div>
</div>
{% if details.get("groups") %}
<div class="stat-box">
<div class="label">Groups</div>
<div class="value" style="font-size:14px; padding-top:2px;">{{ details.get("groups") | join(", ") }}</div>
</div>
{% endif %}
{% if details.get("notAfter") %}
<div class="stat-box">
<div class="label">Expires</div>
<div class="value" style="font-size:13px; padding-top:2px; word-break:break-all;">{{ details.get("notAfter") }}</div>
</div>
{% endif %}
</div>
{% endif %}
<p style="color:var(--muted); font-size:12px; margin-bottom:16px;">
Scan each QR code separately with the Nebula mobile app, or use the PEM text below.
The private key is shown only once — store it securely.
</p>
<div class="qr-grid">
<div>
<div class="qr-box">
<div class="qr-label">Host Certificate</div>
{{ result.cert_qr | safe }}
</div>
<details style="margin-top:10px;">
<summary>Certificate PEM</summary>
<pre>{{ result.cert_pem }}</pre>
</details>
</div>
<div>
<div class="qr-box">
<div class="qr-label">Host Key (Private)</div>
{{ result.key_qr | safe }}
</div>
<details style="margin-top:10px;">
<summary>Key PEM</summary>
<pre>{{ result.key_pem }}</pre>
</details>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -23,7 +23,10 @@ jallen-nas:
redis_nextcloud: ENC[AES256_GCM,data:mllmOV98zpqLPhI=,iv:Vl52jKAfzy+aCqpGYUQ80Ye3sGDAR/3ULSStEjAi1hg=,tag:DwUT2cdQ/BA1TwdnXoElXA==,type:str]
nextcloud:
dbpassword: ENC[AES256_GCM,data:kvBa4tUglGHuLH0=,iv:IUWHIH6vx6QlXaKNyq2l0lZGZvudskCW9+jCZ2oENVs=,tag:2UC565EXzGxb2N4XO43ydw==,type:str]
adminpassword: ENC[AES256_GCM,data:UJ4LBpif5TIOJqY=,iv:28UgOD1LIoHOVBPTQ3MpofjdkBwY3ghLK2vIbTwZQaA=,tag:F/77ApazlfppzQG6UdTQdQ==,type:str]
adminpassword: ENC[AES256_GCM,data:3Fa9ryPgmWKH9A8=,iv:/iji/991Ijb4r0VLA4QlQBbt18evzGUtoJbQEo+hHqM=,tag:RKyS79eM0Pp4aFt6Z7RmWw==,type:str]
instanceid: ENC[AES256_GCM,data:3wNXF951lMRkpxBdpfA=,iv:97sxghu9Yp8lxOUQti4lR1UPXnBP1gpMJwIV5bw4TKc=,tag:ZoI4FHyQA+71bOM9J0vzLw==,type:str]
passwordsalt: ENC[AES256_GCM,data:mg7arfQVC7WnO3/g7cXxf4t9fW+5cev1AbhmuN+QJu3aCuQJCD56,iv:AtM9sbmOLKns3EEErIe+VKzjS/8sCLeKr804G51gAzU=,tag:cf/jRMSKXUC4L4gc4bgs0A==,type:str]
secret: ENC[AES256_GCM,data:5oDcVaF1fYLy/XhwkXrM/vwA2+94sTK9Xjg4se23sxvgupGnzlAoEw6WCDJGZHnb1PqGLSiNDFBgHsMR,iv:8U5aVb9gv12lMB/GMezPkNgwOX/ylBMFQAqvyr59Cig=,tag:MWzDd/SDl0OUp1m0lsNNAw==,type:str]
smtp_settings: ENC[AES256_GCM,data:8KyWmx+e/DouS8OucMrd2AMFE9w1b4WKHr8RuJ8kMqSlL7MP5GLwW4WZhPCJto6kTzAsy4WPFb23oG6UbQg9DK7b2CiUoC5S5mcDxX3lnzX7clyrosDvIHqVlwjamtgfCuNJWcT+RB6a91T84HEDTnKMTsiJvLxpSEHehzL3ItndV9p4+JTs5tZFX+dOZgqVIMN9syQqFsSpI6fk8LNJh90rXXg4jkh7+EFjmBk3QoSOuI1z4JiRpiauVP23VwTtTqiL1Aw8NpDhX1bU0RL+DAWQIZd8o9k/oedziCfKuea73yq4Z8uDaZJ3BW5m1Z1QA44Yu08qvKzBiAAnEb3bQ83ltWI2mfygupjm3tfIVfQoMB8scivaN+5Nlkx3u8f9SAzxyuvqYxZx8TDsyWGEG6p2frHxX72qxu/xb+DYrXBpyDkVA6ng,iv:CqS+/0kL4JmPXRc05Zr2+CFksP1RhOvzIr732z6A1bw=,tag:ux0ja/g1Y40c21hslYnorA==,type:str]
gitea:
mail-key: ENC[AES256_GCM,data:kfurnrj8eNKGw+KZcijs4jhJgQ==,iv:mFNVcxVyOj5Se/SyLOcKh5ja/QJmL8wfejzSGOzq6yo=,tag:XBU3urrqarICO7niqBRNbA==,type:str]
@@ -98,6 +101,9 @@ jallen-nas:
secret-key: ENC[AES256_GCM,data:dPHXEAKGrbbM36uH3W4yzm3GmJI=,iv:yHMAMJ8w+uoH/IvLSbxyQm6dEml0MWvwfRIIVHmc6LE=,tag:AyDLS20gUH4gupRGrtGReQ==,type:str]
kavita:
token: ENC[AES256_GCM,data:XurnehEZ/jCn+lxtTyty3WkDb17nQ7X3dIIys8O1l17gNzBMyqCLuzQaKLvqAV13PIaGBRTSnlNe7hDs5XYsI2nyL/l0ptPPXgKGEujEtTL8ei0rxTAYnA==,iv:Guinyj+EQNSUE+z+yu3HTF+leoxk7LWXBX/HGcLEki4=,tag:hfkgnjFAwUEVXtDT5HFJTg==,type:str]
actual:
client-id: ENC[AES256_GCM,data:bgImXku1RYZADFOPGKScHyC07iB6NRO+OfL477Fmxvp2/MGdahhjXQ==,iv:PsAomwKXxJjHEDrkIuzOAoyxhmKM9Yqmj3xVYx8iUns=,tag:nPd9+jPqcc06DqVb+bg79Q==,type:str]
client-secret: ENC[AES256_GCM,data:PN5RA3gd5NM8m8FbpeJR5SROU9BaE78jTDHBBrVTkXZQq+7HQ6Tq9X6YDv755qwES2WOZyWohAlPSGBJ7/z1NY2dzDhBJTekVXKDpWYpVZhk3AlaT/XjaFP6HNRyAHjBUjLvp1hgCC/QcipzSjfVYdRduRV5b5nMhbz+AITlo6w=,iv:MpgHI1bmd4fS1MILDtmTeFShi61M/dqQsalmTnxT4q8=,tag:+lTfIfRIy7MxFRQu06GtgQ==,type:str]
sops:
shamir_threshold: 1
age:
@@ -245,8 +251,8 @@ sops:
L0gwQm5takNjMkVGNzVlSStJYlUwWDAKP8QA3rRUHYbyyhPC/k0Eq2EIKfjyc7Co
7BkHH3msC6h9g42BB5iIYe6KQ+UGxMQBFvp+qSB27jaIfajN5MP0BA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-30T01:56:41Z"
mac: ENC[AES256_GCM,data:U7xBMz0kJd2fF81rCRD0x4g573lEUnn2Q8e6V/0zgwzoaC39VyZ3RBAUVBS+5d4GSTjrU82sucSNYBZphOGODdgnmL7jSBwaKxg4co9fjuABFmn5WFKJgsw07F48/F2DraftSlRoq1YYkzOhiravgVyWU6aC7o5av+nFVhhfcrM=,iv:bnGpEY6mjPrmeJ+bQdZkXQPTzkBxmIy+5B8/dtJDLmM=,tag:OqvH+y5vjQryzyMUof4isA==,type:str]
lastmodified: "2026-04-13T16:30:32Z"
mac: ENC[AES256_GCM,data:7IQ7jAVbIy2U8WZu7jMCHO13uz8Q+3w4hwEZP8QSOzN45EwjnZSKUoHtIbeS7FEkB/EwkfUdN9trezbEhcPRb7cF6hg2xNdEU8dnx8MmzOoRvirqCf2/c0AD1rVzkEYgYU6RZDIDGOck+WPHf95+2jR2h+P5zz47QCKnej+nlXA=,iv:FhqWxLvGbmMgywIvnXJC2xi6BP7LLhS3mwJIcsUhL84=,tag:NoCqjuZNESUXtX/V31frZg==,type:str]
pgp:
- created_at: "2026-02-06T15:34:30Z"
enc: |-

View File

@@ -6,6 +6,8 @@ nuc:
nuc-nixos-cert: ENC[AES256_GCM,data:lRB9M1D7xMjf/XNxljM7wPitZzY8105Hu6GmmaBgenWIsewIoSfk3tTMwFEe8nzp2jraOzcurTEujl++YOy46S0FDg3bPzdlXBwWZw12F6akfgGTGl8XX7BB7vu9UiQ8stpgUd+6G8NhNmbmVw/0oDnEsSfd4KiJgyf8Hfd9wOBhw+xZEVzHJ+D98ixChH5RB1CyFDpK1qrU9+K7GhsRFEQQdIkr4Xv6ZKnQfcB/uuwQ9Po1vFyfZRmRbRrcYFWIhJSoBr7YY5Kn5LXJxpWPDLZGNe7zibE11J09gXBRXB2Oni2G7TEsNiMlY2w4SA4aBpVngyitw9So7hzwSPX+JCYZnHr3nBCDvkWaECvju9i6pclnuuEN4PFXghaxy3QOweWA4l1DF1jrgdQ+XU8VJk4H,iv:ZdGsr5AldRJYvoG+tXW2cnS7iEs4C14qjp7hQRNdKcI=,tag:fC/DbuFeSWA0vtn9fTV7bg==,type:str]
nuc-nixos-key: ENC[AES256_GCM,data:7VhntGkRDoXulB9FNelvF0YwxriuVCpUCb43V33LlCRI8dizGIGtayr4g4hwg88rdgGBZMIzpG0MrR65DhLriR+yJAuvgHBuNHb/IOt9x0Jjo4a5g9r4wwMjcj8TBFM0FiFh5oSysY/S6VYJif7aBTnqGWvrwyfWIuDOD7MWfQ==,iv:rVlATexxX7k9jrk6i11+Wgy6GhWKxTYkBBlGAqOAtMQ=,tag:YHELFmDNyHq1amjYQgTWTQ==,type:str]
ca-cert: ENC[AES256_GCM,data:ZXmAZdQ0BSGJB5IZ2VJx9IxrrNTqmEYGYKua0gk61/EOnebMp5yg+swKl94+pmFWtqwKlaH+jaChAqYchONHGxOt55AAAlhGzM7BzoUseWdjTf0mFRq4Kr49Tjsz1iOK5XHr8aLESF2E3RkBRi5r0MzstutuagSO59Dj74ZT176sMYWiT7yjPXBgxlLuROQGHBV1/l+N8AMt9M2OLp/0+QYcwSrDh3u8Ts82d9YMODcbNbnCaeo64xmHLW7jJBkDTeH89rfWA5hE+haqUDv/BWe1mBvkV0YIJceyfPZdku0+hOdUIw1iXOTH9Q3KTL5FR9i7lRrCz/ZJzOVrNA==,iv:Td7TBKn+5/1V3WblVwaWjYTOZXMvJ/SMSoUnrNXdAOU=,tag:6YDpTM9kADs2KHKERNeVFQ==,type:str]
caddy:
cloudflare-dns-api-token: ENC[AES256_GCM,data:QMbo5KFej5MVIpWeWr+B4msS1dUPTtinKNJER44FPiKMNiFzkfGa4w==,iv:h18CzpdvPNOx/IWKEyuNlGmltdBeTUx4i8VMs0Dz8z0=,tag:Ke8nakp2VA/YSAH5Mvf9sQ==,type:str]
sops:
shamir_threshold: 1
age:
@@ -153,8 +155,8 @@ sops:
WFJONHNsUjJuditvVEgxQ0Y1RVhXQ1kKwBM8ljdCTTbjdasCdtLj4wZ+fX2XQIXf
IMgacJ5kxYHaYpNpY5wyK2kHzPY9Ovz75WyXicPj0SCojhoKvMAWXQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-24T14:25:23Z"
mac: ENC[AES256_GCM,data:H50AiSWZ3gzFw4EOfwQE2Z7D2Uj4ZqMbMdcOEY3umoA0wzN2JRBJuIU599tV+bRI4SqlJ/vsSDuoLEqxbDR1ZJfLTLzFxFOfr8kkj7bir1tkRWZPY6pkNBsIteWeaJktXyCodCRxaDuDiKNWlD3+tA5+X3Wjhg4RcAAQ+F18gkk=,iv:SffLSph1FL9Yg915VctG+TRJ/3aoJ3D1wBFiPS2MbAc=,tag:JQJ+rRPftBAeyKtUD+JQ2A==,type:str]
lastmodified: "2026-04-09T19:46:52Z"
mac: ENC[AES256_GCM,data:sQm090y8Txfg/DATQZxhYTPFSKGne5tk9534EOmddLQo4PWFRy5FdssCU7L9gCrcWy2x/hRPq57vNgOSvXrU/JNS3Q/lC2xR8G5cFiwFTg+efXoiS4dnJnAkO8i+IROuh3kbAy0rsECcW8yQS2A1aEXi52hfEkMFrCd9nfX0BmY=,iv:HdjMqLqcztzntb2ChVxcj91KcnP3jYFNhHR8ezgoOkk=,tag:4yPrEKw+I+ECAWrCq0M+2Q==,type:str]
pgp:
- created_at: "2026-02-06T15:34:31Z"
enc: |-
@@ -177,4 +179,4 @@ sops:
-----END PGP MESSAGE-----
fp: CBCB9B18A6B8930B0B6ABFD1CCB8CBEB30633684
unencrypted_suffix: _unencrypted
version: 3.12.1
version: 3.12.2

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,25 @@
setupAsahiSound = true;
};
# Workaround for Apple BCM Bluetooth firmware not sending completion ACKs.
# Without this, WirePlumber spams "Missing completion reports for packet:
# Bluetooth adapter firmware bug?" and audio over BT is unreliable.
hardware.bluetooth.settings = {
Policy = {
AutoEnable = true;
};
};
services.pipewire.wireplumber.extraConfig = {
"51-bluetooth-apple-fix" = {
"monitor.bluez.properties" = {
"bluez5.msbc-support" = false;
"bluez5.sbc-xq-support" = false;
"bluez5.hw-offload-sco" = false;
};
};
};
${namespace} = {
headless.enable = false;
@@ -107,7 +126,7 @@
nixpkgs.config.allowUnsupportedSystem = true;
virtualisation = {
libvirtd.enable = true;
libvirtd.enable = false;
# efi = {
# OVMF = (pkgs.OVMF.override {
# secureBoot = true;

View File

@@ -56,6 +56,75 @@ in
ra_allow_slaac = false;
};
};
filtering = {
rewrites = [
{
domain = "actual.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "adguard.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "cache.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "chat.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "code.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "esphome.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "grafana.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "manyfold.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "orca.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "sabnzbd.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "sonarr.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "orca.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
{
domain = "tunarr.mjallen.dev";
answer = "10.0.1.4";
enabled = true;
}
];
};
};
};
}

View File

@@ -107,6 +107,14 @@ in
# ###################################################
services = {
attic = {
enable = true;
port = net.ports.pi5.attic;
listenAddress = "[::]";
environmentFile = "/run/secrets/pi5/attic-key";
configDir = "/mnt/attic-store";
};
nebula = {
enable = true;
isLighthouse = true;
@@ -114,6 +122,26 @@ in
secretsPrefix = "pi5/nebula";
secretsFile = lib.snowfall.fs.get-file "secrets/pi5-secrets.yaml";
hostSecretName = "lighthouse";
stats = {
enable = true;
listenAddress = "0.0.0.0";
statsPort = 8474;
};
};
nebula-ui = {
enable = true;
port = 8473;
networkName = "jallen-nebula";
caCertSecretKey = "pi5/nebula/ca-cert";
caKeySecretKey = "pi5/nebula/ca-key";
secretsFile = lib.snowfall.fs.get-file "secrets/pi5-secrets.yaml";
statsListenAddress = "0.0.0.0";
statsPort = 8474;
reverseProxy = {
enable = true;
subdomain = "nebula";
};
};
};
@@ -133,7 +161,10 @@ in
firewall = {
enable = true;
allowPing = true;
allowedTCPPorts = [ 53 ];
allowedTCPPorts = [
53
net.ports.pi5.attic
];
allowedUDPPorts = [ 53 ];
};
networkmanager.profiles = {
@@ -149,6 +180,29 @@ in
# # FileSystems # #
# ###################################################
fileSystems."/mnt/attic-store" = {
device = "/dev/disk/by-uuid/E81DBFCC-066E-46A3-8018-AA3BF4573074";
fsType = "xfs";
options = [
"defaults"
"noatime"
"nofail"
];
};
users = {
users = {
atticd = {
isSystemUser = true;
group = "atticd";
extraGroups = [ "keys" ];
};
};
groups = {
atticd = { };
};
};
boot.supportedFilesystems = [ "bcachefs" ];
programs.seahorse.enable = false;

View File

@@ -9,6 +9,17 @@ in
# Secrets
# ------------------------------
secrets = {
# ------------------------------
# Attic
# ------------------------------
"pi5/attic-key" = {
sopsFile = defaultSops;
mode = "0400";
owner = "atticd";
group = "atticd";
restartUnits = [ "atticd.service" ];
};
# ------------------------------
# SSH keys
# ------------------------------

View File

@@ -7,11 +7,11 @@
}:
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-calamares-plasma6.nix"
];
${namespace} = {
bootloader.lanzaboote.enable = true;
desktop.plasma.enable = true;
hardware.disko = {
enable = true;
@@ -43,9 +43,10 @@
};
};
specialisation.graphical.configuration = {
${namespace}.desktop.plasma.enable = true;
};
# The installer base module (installation-cd-graphical-calamares-plasma6.nix)
# sets plasma-login-manager for auto-login; override with SDDM from the plasma
# module disabled to avoid display-manager conflicts on the live ISO.
services.displayManager.sddm.enable = lib.mkForce false;
boot = {
kernelPackages = lib.mkForce pkgs.linuxPackages_latest;

View File

@@ -4,7 +4,7 @@
...
}:
let
kernel = pkgs.cachyosKernels.linuxPackages-cachyos-latest-lto-zen4;
kernel = pkgs.linuxPackages_latest;
in
{
# Configure bootloader with lanzaboot and secureboot

View File

@@ -26,10 +26,14 @@ in
};
arrs.enable = true;
attic = {
enable = true;
enable = false;
port = 9012;
listenAddress = "[::]";
environmentFile = "/run/secrets/jallen-nas/attic-key";
ntfy = {
enable = true;
envFile = config.sops.templates."ntfy.env".path;
};
reverseProxy = {
enable = true;
subdomain = "cache";

View File

@@ -3,13 +3,14 @@
...
}:
let
kernelPackages = pkgs.cachyosKernels.linuxPackages-cachyos-latest-lto-zen4;
kernelPackages = pkgs.linuxPackages_latest;
in
{
boot = {
inherit kernelPackages;
kernelParams = [ "amd_iommu=pgtbl_v2" ];
# https://github.com/artmoty-dev/n5pro-jmb585-fix
kernelParams = [ "amd_iommu=on" ];
initrd = {
supportedFilesystems = {

View File

@@ -361,7 +361,7 @@ in
# Configure programs
programs = {
kdeconnect.enable = lib.mkForce false;
virt-manager.enable = true;
virt-manager.enable = config.virtualisation.libvirtd.enable;
steam.gamescopeSession.enable = lib.mkForce false;
};
}

View File

@@ -123,6 +123,14 @@ in
# nextcloud
# ------------------------------
# instanceid / passwordsalt / secret are written into config.php at
# install time. Store them here so the bootstrap stub in the nextcloud
# module can restore config.php if it ever gets zeroed again.
# Add to nas-secrets.yaml via: sops secrets/nas-secrets.yaml
# jallen-nas/nextcloud/instanceid: <value from config.php>
# jallen-nas/nextcloud/passwordsalt: <value from config.php>
# jallen-nas/nextcloud/secret: <value from config.php>
"jallen-nas/nextcloud/dbpassword" = sopsSettings // {
mode = "0650";
restartUnits = [ "nextcloud.service" ];

View File

@@ -4,7 +4,7 @@
...
}:
let
kernel = pkgs.cachyosKernels.linuxPackages-cachyos-latest-lto-zen4;
kernel = pkgs.linuxPackages_latest;
pkgsVersion = pkgs; # .unstable;
in
{

View File

@@ -1,4 +1,4 @@
{ pkgs, ... }:
{
boot.kernelPackages = pkgs.cachyosKernels.linuxPackages-cachyos-latest-lto-x86_64-v4;
boot.kernelPackages = pkgs.linuxPackages_latest;
}

View File

@@ -64,6 +64,78 @@ in
security.tpm.enable = true;
services = {
caddy-internal = {
enable = true;
proxies = {
esphome = {
subdomain = "esphome";
upstream = "http://127.0.0.1:${toString net.ports.nuc.esphome}";
};
otbr = {
subdomain = "otbr";
upstream = "http://127.0.0.1:${toString net.ports.nuc.otbr}";
};
actual = {
subdomain = "actual";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.actual}";
};
cache = {
subdomain = "cache";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.attic}";
};
manyfold = {
subdomain = "manyfold";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.manyfold}";
};
chat = {
subdomain = "chat";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.llamaCpp}";
};
grafana = {
subdomain = "grafana";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.grafana}";
};
orca = {
subdomain = "orca";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.orcaSlicer}";
};
sabnzbd = {
subdomain = "sabnzbd";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.sabnzbd}";
};
sonarr = {
subdomain = "sonarr";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.sonarr}";
};
radarr = {
subdomain = "radarr";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.radarr}";
};
tunarr = {
subdomain = "tunarr";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.tunarr}";
};
unmanic = {
subdomain = "unmanic";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.unmanic}";
};
code = {
subdomain = "code";
upstream = "http://${net.hosts.nas.lan}:${toString net.ports.nas.codeServer}";
};
adguard = {
subdomain = "adguard";
upstream = "http://${net.hosts.pi5.lan}:${toString net.ports.pi5.adguard}";
};
# hass is currently proxied by the NAS Caddy (modules/nixos/services/caddy).
# To migrate it here, remove the @hass block from that module and add:
# hass = {
# subdomain = "hass";
# upstream = "http://127.0.0.1:${toString net.ports.nuc.homeAssistant}";
# };
};
};
home-assistant = {
enable = true;
automation = {