Compare commits
257 Commits
34d652acbb
...
staging-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413093d43 | ||
|
|
1a8a1736bf | ||
|
|
51c7d172b3 | ||
|
|
6999ae5680 | ||
|
|
0c3f74e7de | ||
|
|
d2703ff84b | ||
|
|
1a9e0825fc | ||
|
|
284a861927 | ||
|
|
02b4e6b5b4 | ||
|
|
60084c292e | ||
|
|
fa22a080b9 | ||
|
|
70f0af98f6 | ||
|
|
cd4df316ae | ||
|
|
ff55dce746 | ||
|
|
5a86c03f74 | ||
| 1c2df46ac4 | |||
| 8839620e63 | |||
| c03126e8f8 | |||
|
|
10ef36859d | ||
|
|
4acb75f2bd | ||
|
|
77e2fb2537 | ||
|
|
c7bbb97a68 | ||
|
|
6d1c360c02 | ||
|
|
3b73eb3bd1 | ||
|
|
6ffcc056ad | ||
|
|
742f680d0d | ||
|
|
c872f1c6b0 | ||
|
|
bc5a40f143 | ||
|
|
c2bd3f6273 | ||
|
|
343dee3576 | ||
|
|
ebcafd3c6d | ||
|
|
5231b5ca4b | ||
|
|
1195456bee | ||
|
|
48de6b9821 | ||
|
|
cd4a17fe31 | ||
|
|
d3a5b3e6ef | ||
|
|
3c4c6c7389 | ||
|
|
876f728aa2 | ||
|
|
950a6dabd8 | ||
|
|
1d9589a186 | ||
|
|
b13fa7dc05 | ||
|
|
069f6c3ec7 | ||
|
|
5a27b79b51 | ||
|
|
72453c80bf | ||
| 14800ffb1e | |||
| e2f36d01bc | |||
| 55b231b456 | |||
|
|
b4b2607df1 | ||
|
|
ac9ba4776c | ||
|
|
85aca0d022 | ||
|
|
80c74b2d1a | ||
|
|
d28f224ad5 | ||
|
|
f2a808ed13 | ||
|
|
4ef420651d | ||
|
|
65ce66a541 | ||
|
|
deae53b721 | ||
|
|
f459e83861 | ||
|
|
badab99242 | ||
|
|
84124ba1b1 | ||
|
|
2ad0d2072d | ||
|
|
ff1632dcda | ||
|
|
531b8c1d09 | ||
|
|
a8128cef8d | ||
|
|
3baffb2a69 | ||
|
|
06bdf999a6 | ||
|
|
76ff1f4d4f | ||
|
|
2360b4147c | ||
|
|
37874ff58e | ||
|
|
aef13155fc | ||
|
|
1d4f104524 | ||
|
|
11ec4b4816 | ||
|
|
2bd899848d | ||
|
|
18a6e8d24c | ||
|
|
13c686a8a1 | ||
|
|
7a172c0306 | ||
|
|
7fc04fcf20 | ||
|
|
a40ea61415 | ||
| eba517d34d | |||
|
|
38257492bd | ||
| 93592c984d | |||
|
|
7a08bc0b2b | ||
|
|
25e8cac613 | ||
|
|
02eaea85d8 | ||
|
|
6c433d642d | ||
|
|
7aed3e09e8 | ||
|
|
e0e6ab0de6 | ||
|
|
7a1cd8a6f6 | ||
|
|
9407d500c8 | ||
| 9f1dd7def1 | |||
|
|
480f188d86 | ||
| e2bd366bb3 | |||
|
|
f80c8a0481 | ||
| 7e996fffa1 | |||
|
|
d14e25c29f | ||
|
|
1ed7ab9776 | ||
|
|
dd8867b52f | ||
|
|
3668eb2829 | ||
|
|
e751dfc1b2 | ||
|
|
6c3bbbf72b | ||
|
|
9dcb45a017 | ||
|
|
b9069433b1 | ||
|
|
739f6a08da | ||
|
|
2fc8b64964 | ||
|
|
6e133b6b59 | ||
|
|
01e3e02a62 | ||
|
|
85af70e2ee | ||
| b21d9bef87 | |||
|
|
26b89dae76 | ||
|
|
b2a2ef70a4 | ||
|
|
8286e00eb3 | ||
|
|
c7487c9763 | ||
|
|
e910d0a8a7 | ||
|
|
a3b9608887 | ||
|
|
09002cfe22 | ||
|
|
8a3b5c031f | ||
|
|
b441515f89 | ||
|
|
b7c1632bb8 | ||
| a7d40fb138 | |||
|
|
8f4ec83104 | ||
|
|
fba4ab13cf | ||
|
|
5ba1a256fe | ||
|
|
5ee0ef4d58 | ||
|
|
5a77a03ac0 | ||
|
|
5349c2408a | ||
| 9ea4fb32f4 | |||
|
|
9e1673ef7f | ||
|
|
44a7b2a8ab | ||
|
|
aa24505314 | ||
|
|
d0bf878555 | ||
|
|
2e5be9816e | ||
|
|
87ecccff9e | ||
| d60a44b438 | |||
|
|
9413bb6403 | ||
|
|
28bcddb957 | ||
|
|
1737e93c68 | ||
|
|
90fdbdea70 | ||
|
|
90ffadf2ea | ||
|
|
f6c9080cea | ||
|
|
8e40faad75 | ||
|
|
4978d44ba2 | ||
|
|
6fefed2909 | ||
| 1a48266cde | |||
|
|
639d39108a | ||
|
|
185ed4e3d8 | ||
|
|
8240b9af3c | ||
|
|
deb66c9cb7 | ||
|
|
8a49a3d04e | ||
|
|
cdccb8138c | ||
|
|
09a817f02d | ||
|
|
4edcf066ca | ||
|
|
0d49b67c7b | ||
|
|
24bf72ef69 | ||
|
|
8459061968 | ||
|
|
b3f1c35995 | ||
|
|
27f27b1503 | ||
|
|
f108abd7ae | ||
|
|
2be8fe65d8 | ||
|
|
a0c1628461 | ||
|
|
06615a3541 | ||
|
|
328b2a3ee8 | ||
|
|
5123287ef7 | ||
|
|
13e3b76c88 | ||
|
|
27502c6997 | ||
|
|
6b467525cf | ||
|
|
b1b0e85db7 | ||
|
|
e84dd7cb91 | ||
| 94d94fb7a2 | |||
|
|
e67b4fecc4 | ||
|
|
f7539dc9b6 | ||
|
|
5632068ca8 | ||
|
|
99df7a097b | ||
|
|
cc17c3fb42 | ||
|
|
d51919ec69 | ||
|
|
ad80b9d8c3 | ||
|
|
3c4495c066 | ||
|
|
4765a18224 | ||
|
|
6bd11be8b5 | ||
|
|
f294828409 | ||
|
|
953271eeee | ||
|
|
70f3cef03a | ||
|
|
c9a5a4dec9 | ||
|
|
37cc35bb1b | ||
|
|
25b758304d | ||
|
|
2385466b1a | ||
|
|
37ad4fd2ad | ||
|
|
35569e6ec0 | ||
|
|
536eb0deb1 | ||
|
|
a390c4711a | ||
|
|
86605bda05 | ||
|
|
d6cdfcf31a | ||
|
|
a592b270af | ||
|
|
4be9be34e0 | ||
|
|
cb7b097ce0 | ||
|
|
53a0010e47 | ||
|
|
6d36023222 | ||
|
|
7c1dbeac27 | ||
|
|
9386b073ba | ||
|
|
cf09845431 | ||
|
|
d65193d7d3 | ||
|
|
0e0e91d1f8 | ||
|
|
a59165c31e | ||
|
|
df2768c6fc | ||
|
|
109c92a33a | ||
|
|
ca11cbbc79 | ||
|
|
6584b63c36 | ||
|
|
88cac6c1e9 | ||
|
|
ef39040919 | ||
|
|
ca275c45de | ||
|
|
fee6035de0 | ||
|
|
c42962f6da | ||
|
|
7c047a16b7 | ||
|
|
a3b34ef74b | ||
| 8dc3066f06 | |||
| 04fd3c523b | |||
|
|
6f88d0726b | ||
|
|
53ea704e57 | ||
|
|
4253518ceb | ||
|
|
1c4382e655 | ||
|
|
5b1454adf6 | ||
|
|
6d3dbf497e | ||
|
|
0526945114 | ||
|
|
400a59f06a | ||
|
|
48826590de | ||
|
|
61cf06b4c7 | ||
|
|
f4a644dc05 | ||
|
|
fc847a17cd | ||
|
|
6bb4aaf3ba | ||
|
|
beca9756ea | ||
|
|
54ed1db6cd | ||
|
|
abaae7f360 | ||
|
|
8ca1ea8e78 | ||
|
|
a0426b2fee | ||
|
|
4fd8bd7534 | ||
|
|
b6046e63c5 | ||
|
|
84d76f0436 | ||
|
|
9664c59523 | ||
|
|
265f34b8aa | ||
|
|
0d6e7e6381 | ||
| 4144198e4b | |||
|
|
e5d3b9236c | ||
|
|
53c2371c45 | ||
|
|
48fb59b862 | ||
|
|
64744d1d93 | ||
|
|
a1d83e731a | ||
| f9ecdaec96 | |||
|
|
e8a7b2c7ab | ||
|
|
3e855af8d5 | ||
|
|
9672d30de4 | ||
|
|
78d3559e7b | ||
|
|
b8956ebf72 | ||
|
|
4bda2f1aae | ||
|
|
9897c2a991 | ||
|
|
af31c60be8 | ||
|
|
2b89969a96 | ||
|
|
88d0f8ffb8 | ||
|
|
58966646c2 | ||
|
|
c28de5def9 |
7
app/icons/bisq.svg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
111
app/icons/sparrow.svg
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Created with Vectornator (http://vectornator.io/) -->
|
||||
<svg stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="8.789 8.791 32.422 32.418" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Untitled">
|
||||
<path d="M38.8519 23.6353L38.3818 24.2217L41.2105 25.0446L41.2105 24.8132L39.3844 23.4968L38.8519 23.6353Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M37.9843 25.2648L38.7188 25.2376L40.119 25.1551L41.2106 25.0446L38.3819 24.2217L37.5381 23.9764L37.9843 25.2648Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M38.8519 23.6353L38.3818 24.2217L37.5381 23.9764L38.0403 23.846L38.8519 23.6353Z" fill="#ababab" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M37.9843 25.2648L33.5662 25.2648L36.0929 24.3388L37.9843 25.2648Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M38.0403 23.846L37.145 23.3355L36.8724 23.18L38.7803 21.9829L39.3844 23.4968L38.8519 23.6353L38.0403 23.846Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M37.0092 23.5521L36.7613 23.8298L36.3913 23.3554L36.3959 23.3517L37.0092 23.5521Z" fill="#ffffff" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.8724 23.1801L37.1451 23.3355L37.0092 23.5521L36.3959 23.3517L36.6823 23.1455L36.8724 23.1801Z" fill="#ffffff" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M38.7803 21.9829L36.8724 23.18L36.2058 20.8919L38.7665 21.949L38.7803 21.9829Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.7613 23.8298L37.0092 23.5521L37.1451 23.3355L38.0403 23.846L37.5381 23.9764L36.7172 23.8792L36.7613 23.8298Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.3913 23.3554L36.7613 23.8298L36.7172 23.8792L35.9653 23.6626L36.3913 23.3554Z" fill="#808080" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.7172 23.8792L37.5381 23.9764L36.103 24.3351L36.069 24.3278L36.0681 24.3271L33.4046 23.7318L35.9653 23.6626L36.7172 23.8792Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.3958 23.3517L36.3913 23.3554L36.1535 23.0497L36.6823 23.1455L36.3958 23.3517Z" fill="#efefef" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.1535 23.0497L36.3913 23.3554L35.9653 23.6626L35.9139 23.2419L36.1535 23.0497Z" fill="#d4d4d4" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.2058 20.8919L36.8724 23.1801L36.6823 23.1454L36.1535 23.0497L35.6779 22.9627L36.1342 20.8956L36.2058 20.8919Z" fill="#d8d8d8" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.1342 20.8956L35.6779 22.9627L34.4972 20.9745L36.1342 20.8956Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.103 24.3351L37.5381 23.9764L37.9843 25.2648L36.0929 24.3388L36.103 24.3351Z" fill="#d8d8d8" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M36.0928 24.3388L33.5661 25.2648L33.5092 25.2648L33.3183 23.734L33.4046 23.7318L36.0681 24.3271L36.069 24.3278L36.0928 24.3388Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.5092 25.2648L35.7366 27.2015L35.3005 27.5949L35.2785 27.6664L35.2638 27.6745L31.5591 26.1025L33.5092 25.2648Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M34.4972 20.9745L35.6779 22.9627L33.3202 23.7046L34.4972 20.9745Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M35.2748 27.6789L34.8397 29.0874L34.0712 31.1582L31.3848 29.8823L35.2639 27.6745L35.2748 27.6789Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M35.2638 27.6745L31.3847 29.8823L31.5591 26.1025L35.2638 27.6745Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.2366 23.6942L31.0303 23.4143L32.3322 22.3697L32.705 22.0706L34.4972 20.9744L33.3202 23.7045L33.2366 23.6942Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M34.0712 31.1582L33.9959 31.2459L33.995 31.2459L29.0407 31.307L29.0398 31.307L29.0306 31.2916L29.0003 31.2399L31.3848 29.8823L34.0712 31.1582Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.9949 31.2459L32.3276 33.1745L30.6869 33.9126L29.0407 31.307L33.9949 31.2459Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.5661 25.2648L37.9842 25.2648L37.112 25.8822L36.5152 26.4988L35.7366 27.2015L33.5092 25.2648L33.5661 25.2648Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.3183 23.734L33.5092 25.2648L31.5591 26.1024L31.0303 23.4143L33.2366 23.6942L33.3155 23.7119L33.3183 23.734Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M33.3183 23.734L33.4046 23.7318L33.3156 23.712L33.3183 23.734Z" fill="#3a3a3a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M35.6779 22.9627L36.1535 23.0497L35.9139 23.2419L35.9653 23.6626L33.4046 23.7318L33.3156 23.7119L33.2366 23.6942L33.3202 23.7046L35.6779 22.9627Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<g opacity="1">
|
||||
<path d="M34.0372 36.0387C34.0712 36.1212 34.0023 36.4077 34.0023 36.4077L33.5773 36.8313L33.6938 36.4622L33.8655 36.1594L33.6598 35.7359L32.671 35.3941L32.9253 35.7904L33.1998 36.4077L32.8904 36.6L32.4305 37.0795L32.5682 36.628L32.8904 36.2972C32.8904 36.2972 32.705 35.9834 32.671 35.8729C32.6361 35.7631 32.0394 35.3661 32.0394 35.3661L31.8677 35.1186L32.8106 35.0124L32.8115 35.0124L34.14 35.3116L34.14 35.8729L33.9683 35.5038L33.3706 35.3388L33.8655 35.6526C33.8655 35.6526 34.0023 35.9554 34.0372 36.0387" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M32.8106 35.0124L31.8677 35.1186L30.2958 34.3251L32.4994 34.9425L32.8106 35.0124Z" fill="#7f7f7f" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.9016 15.918L32.4305 17.3559L30.3894 21.9646L29.6981 23.5248L29.9561 20.9605L30.0994 19.5358L30.2352 18.1869L30.2958 18.2348L30.2728 18.1354L31.9016 15.918Z" fill="#585858" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M32.4305 17.3559L31.9016 20.8647L31.0303 23.4143L30.3894 21.9645L32.4305 17.3559Z" fill="#545454" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.9016 20.8647L32.3322 22.3697L31.0303 23.4143L31.9016 20.8647Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.9016 15.918L30.2728 18.1354L29.89 16.4808L29.89 16.4801L29.935 14.4225C30.3986 14.7769 30.6943 15.0111 30.7219 15.0539C30.8724 15.2851 31.9016 15.918 31.9016 15.918" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.8677 35.1186L30.8935 34.9145L29.5613 34.4467L30.2958 34.3251L31.8677 35.1186Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.5592 26.1024L31.3848 29.8823L27.8912 29.3739L27.1521 28.1289L31.5142 26.123L31.5592 26.1024Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.4894 26.0891L31.5141 26.123L27.1521 28.1289L29.3363 26.07L29.3896 26.0199L31.4894 26.0891Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.3848 29.8823L29.0003 31.2399L27.8912 29.374L31.3848 29.8823Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.7898 36.0659L30.9275 35.5428L31.8677 35.1186L32.0394 35.3661L31.1331 35.6806L31.0303 36.0107L31.202 36.4077L30.7898 36.0659Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.3894 21.9645L31.0303 23.4143L29.6981 23.5248L30.3894 21.9645Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.5282 24.895L29.6981 23.5248L31.0303 23.4143L29.3987 25.9462L29.5282 24.895Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.6869 33.9126L30.2958 34.3251L29.5613 34.4467L28.6496 34.0754L30.6869 33.9126Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.159 37.7234L29.7669 37.162L29.2179 36.804L30.7181 37.0648L30.9615 37.2445C30.9615 37.2445 31.236 37.5584 31.2709 37.6961C31.3048 37.8339 31.3737 38.1477 31.3737 38.2582C31.3737 38.368 30.8247 38.7098 30.8247 38.7098L31.0992 38.396L30.9275 37.7234L30.2269 37.327C30.2269 37.327 30.5703 38.038 30.6043 38.1477C30.6392 38.2582 30.4675 38.5168 30.3298 38.5448C30.1976 38.5713 29.7936 38.8564 29.7679 38.874L30.2958 38.2582L30.159 37.7234Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.7181 37.0648L29.2179 36.804L29.127 36.1167L29.1353 36.1211L29.5952 36.3252L30.3298 36.7768L30.7181 37.0648Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.2352 18.1869L30.0994 19.5358L22.4568 15.5216L20.4039 14.2052L26.5673 16.175L30.181 18.1449L30.1828 18.1456L30.2352 18.1869Z" fill="#9c9c9c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.181 18.1449L26.5673 16.175L22.5045 13.9599L20.9189 12.4372L20.2533 11.7653L20.2533 11.3137L20.6444 11.0935L22.353 11.9966L30.181 18.1449Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M30.0993 19.5358L29.9561 20.9605L23.1912 17.2491L23.1224 17.2344L20.9189 15.918L20.816 15.2741L21.1383 15.0811L22.4567 15.5216L30.0993 19.5358Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.935 14.4225L29.89 16.4801L29.89 16.4808L29.8881 16.5773L22.4788 10.1284L29.935 14.4225Z" fill="#9c9c9c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.935 14.4225L22.4788 10.1284L21.6874 9.07799L21.9417 8.79141L22.5596 8.95646C22.5596 8.95646 27.8866 12.8571 29.935 14.4225" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M22.5247 10.1904L22.4789 10.1284L29.8882 16.5773L29.89 16.4808L30.2728 18.1353L30.2352 18.1869L30.1829 18.1457L30.181 18.1449L22.3531 11.9966L21.0218 10.5042L20.884 9.97009L21.3789 9.63932L22.5247 10.1904Z" fill="#585858" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.6871 23.5197L23.9258 19.2617L23.1564 18.6171L22.7598 18.069L22.7607 18.069L29.9561 20.9605L29.6981 23.5248L29.6871 23.5197Z" fill="#545454" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.6981 23.5248L29.5282 24.895L26.0605 21.9771L29.6871 23.5197L29.6981 23.5248Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.6871 23.5197L26.0605 21.9771L24.1452 20.3851L23.5485 19.658L23.5135 19.1792L23.9258 19.2617L29.6871 23.5197Z" fill="#3a3a3a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M31.4894 26.0891L29.3896 26.0199L29.3987 25.9462L31.0303 23.4143L31.5592 26.1024L31.5141 26.123L31.4894 26.0891Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.3896 26.0199L29.3364 26.07L29.2399 26.067L29.3896 26.0199Z" fill="#a3a3a3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.3896 26.0199L29.2399 26.067L23.1637 25.8792L24.8265 22.8566L29.3896 26.0199Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.3363 26.07L27.152 28.129L24.237 29.1773L24.0626 29.0491L22.7451 28.9386L22.1823 28.6955L25.8198 27.1434L29.2399 26.0671L29.3363 26.07Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.2399 26.0671L25.8199 27.1434L23.1637 25.88L23.1637 25.8792L29.2399 26.0671Z" fill="#c8c8c8" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.9985 36.4902L28.118 35.5996L29.127 36.1167L29.2179 36.804L28.8268 37.2998L28.7579 37.7786L29.1353 38.3407L28.5862 38.01L28.4357 37.3661L28.6551 36.997L28.9985 36.4902Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.1261 36.1108L29.127 36.1167L28.118 35.5996L28.0234 34.832L29.1261 36.1108Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.6147 34.0769L28.5743 34.071L29.0398 31.307L29.0407 31.307L30.6869 33.9126L28.6496 34.0754L28.6211 34.0776L28.6156 34.0769L28.6147 34.0769Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.0398 31.307L28.5743 34.071L22.8819 33.268L29.0398 31.307Z" fill="#a2a2a2" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.0398 31.307L22.8819 33.268L26.2331 30.0856L29.0398 31.307Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M29.0398 31.307L26.2331 30.0856L27.8912 29.374L29.0003 31.2399L28.9984 31.2407L29.0306 31.2916L29.0398 31.307Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.6211 34.0776L28.6496 34.0754L29.5613 34.4467L28.0234 34.832L28.6211 34.0776Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.6211 34.0776L28.0234 34.832L28.118 35.5996L28.0234 35.5038L27.2889 34.9425L28.6211 34.0776Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.6211 34.0776L27.2889 34.9425L26.8639 35.0361L25.9411 34.0864L28.5735 34.0769L28.6147 34.0769L28.6156 34.0769L28.6211 34.0776Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M28.5743 34.071L28.5734 34.0769L25.9411 34.0864L25.9383 34.0864L23.2858 34.0953L23.2849 34.0953L22.8819 33.268L28.5743 34.071Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M27.1521 28.129L27.8912 29.374L26.2331 30.0856L27.1521 28.129Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M26.0862 35.1288L26.8639 35.036C26.8639 35.036 24.7972 38.9285 24.3619 39.7816L26.0862 35.1288Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M22.5045 13.9599L26.5673 16.175L20.4039 14.2052L20.1156 13.6711L20.2533 13.2858L20.7472 13.2858L22.5596 14.0129L22.5045 13.9599Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M26.2331 30.0856L22.8819 33.268L23.5135 31.1309L24.2232 29.2148L24.2407 29.2192L26.2331 30.0856Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M26.0862 35.1288L24.3619 39.7816C24.3142 39.8759 24.2867 39.9326 24.283 39.9437C24.2481 40.0535 23.5485 40.5603 23.5485 40.5603L22.848 40.5876L26.0862 35.1288Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M25.9383 34.0864L25.9411 34.0864L26.8638 35.0361L26.0861 35.1288L24.5565 35.3116L25.9383 34.0864Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M25.9383 34.0864L24.5565 35.3116L24.1076 35.0559L23.6164 34.7767L23.2849 34.0961L23.2858 34.0953L25.9383 34.0864Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M25.8199 27.1434L22.1823 28.6955L22.1814 28.6955L22.0445 28.6358L21.5368 28.1784L23.1637 25.88L25.8199 27.1434Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.4198 22.5745L24.8265 22.8566L21.0998 22.6128L19.7033 22.5214L19.6308 22.506L16.4063 19.8805L22.6845 21.415L24.4198 22.5745Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.5565 35.3116L22.69 40.7334C22.5403 40.8661 22.3227 41.0399 22.2162 41.0399C22.0445 41.0399 21.1732 41.1769 21.1732 41.1769L20.8014 41.1865L24.5565 35.3116Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.5565 35.3116L20.8014 41.1865L19.986 41.2086L19.3121 41.1496L19.1809 40.9906L24.1076 35.0559L24.5565 35.3116Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.0552 20.3608L24.4197 22.5745L22.6844 21.415L19.0744 19.0024L24.0552 20.3608Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.2407 29.2192L24.2233 29.2148L24.237 29.1772L27.1521 28.1289L26.2331 30.0856L24.2407 29.2192Z" fill="#8d8d8d" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.1452 20.3852L26.0605 21.9771L29.5283 24.895L29.3988 25.9463L29.3896 26.02L24.8265 22.8567L24.4198 22.5745L24.0552 20.3608L24.1452 20.3852Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.1076 35.0559L19.1809 40.9905L18.5777 41.0399L17.8092 40.8078L17.5347 40.2848L24.1076 35.0559Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.1076 35.0559L17.5347 40.2848L23.2849 34.0961L23.6164 34.7767L24.1076 35.0559Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M23.2858 34.0953L23.2849 34.0961L23.2849 34.0953L23.2858 34.0953Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M23.2849 34.0953L23.2849 34.0961L16.3603 39.3853L19.0036 36.8313L22.8819 33.268L23.2849 34.0953Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M23.2849 34.0961L17.5346 40.2848L16.869 40.3673L16.2373 40.0535L16.3062 39.4369L16.3585 39.3861L16.3603 39.3853L23.2849 34.0961Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M24.8265 22.8567L23.1637 25.8792L23.1628 25.8792L23.138 25.8674L21.0998 22.6128L24.8265 22.8567Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M23.1628 25.8792L23.1637 25.8792L23.1637 25.88L21.5368 28.1784L21.4817 28.129L20.4039 28.157L19.9788 27.9368L19.5187 27.5677L19.1754 27.1434L19.2699 27.1132L23.1628 25.8792Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M23.138 25.8674L23.1628 25.8792L19.2698 27.1132L20.3441 24.5385L23.138 25.8674Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M22.8479 40.5876C22.8479 40.5876 22.7818 40.6524 22.69 40.7334L24.5565 35.3116L26.0861 35.1288L22.8479 40.5876Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M22.7102 18.0005L22.6762 17.2071L23.1224 17.2344L23.1913 17.2491L29.9561 20.9605L22.7606 18.0691L22.7598 18.0691L22.7102 18.0005Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M19.0744 19.0024L22.6844 21.415L16.4063 19.8805L11.2674 17.3287L19.0734 19.0024L19.0744 19.0024Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M20.3442 24.5385L20.256 24.5385L14.6995 24.5385C14.6995 24.5385 12.7365 23.6626 12.7016 23.5794C12.6676 23.4969 12.3591 22.9628 12.3591 22.9628L12.4271 22.6048L17.9763 23.6118L18.0745 23.6295L20.3442 24.5385Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M20.3442 24.5385L19.2699 27.1132L19.1754 27.1434L18.3381 27.0881L17.6724 26.8958L17.2464 26.526L17.0885 26.0104L20.256 24.5385L20.3442 24.5385Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M20.256 24.5385L17.0884 26.0104L17.0747 25.9647L15.2971 25.7717L14.8712 25.2376L14.6995 24.5385L20.256 24.5385Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M16.4063 19.8805L19.6307 22.506L19.5114 22.4802L11.24 19.1166L10.0381 18.3305L9.06393 17.3007L8.82343 16.673L8.99512 16.3703L9.86642 16.7666L11.2675 17.3287L16.4063 19.8805Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M17.9763 23.6117L12.4271 22.6047L10.8552 22.2356L19.2993 22.4353L19.5114 22.4802L19.6307 22.506L19.7033 22.5214L17.9763 23.6117Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M17.9763 23.6117L19.7033 22.5214L21.0998 22.6128L23.1381 25.8674L20.3442 24.5385L18.0745 23.6294L17.9763 23.6117Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M19.0735 19.0024L11.2675 17.3287L9.45504 15.5216L9.38617 15.0538L9.62673 14.8226L12.4959 16.5625C12.4959 16.5625 16.2373 18.0278 16.443 18.1383C16.6487 18.248 18.1664 18.7549 18.1664 18.7549L19.0735 19.0024Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M19.2993 22.4353L11.463 20.7601L11.542 20.7704L9.59278 19.9608L8.99511 19.2617L8.78939 18.6724L9.06398 18.4411L11.3354 19.1792L11.24 19.1166L19.5114 22.4802L19.2993 22.4353Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
<path d="M11.4621 20.7601L11.4631 20.7601L19.2993 22.4353L10.8552 22.2357L10.073 21.5918L9.79847 20.8367L9.90129 20.5502L11.4621 20.7601Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
236
app/icons/update.svg
Normal file
@@ -0,0 +1,236 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="128px"
|
||||
height="128px"
|
||||
viewBox="0 0 128 128"
|
||||
version="1.1"
|
||||
id="svg96"
|
||||
sodipodi:docname="Sovran_SystemsOS_Updater_Iconv3.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview98"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.2149125"
|
||||
inkscape:cx="9.0126153"
|
||||
inkscape:cy="64.430611"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1352"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2" /><defs
|
||||
id="defs67"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient936"><stop
|
||||
style="stop-color:#1e8e11;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop932" /><stop
|
||||
style="stop-color:#1bff00;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop934" /></linearGradient><linearGradient
|
||||
id="linearGradient1028"
|
||||
inkscape:swatch="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop1026" /></linearGradient><linearGradient
|
||||
id="linearGradient998"
|
||||
inkscape:swatch="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop996" /></linearGradient><radialGradient
|
||||
id="radial0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="131.914749"
|
||||
cy="55.927143"
|
||||
fx="131.914749"
|
||||
fy="55.927143"
|
||||
r="160"
|
||||
gradientTransform="matrix(0.232034,-0.541475,-0.368794,-0.0298398,4.277749,118.95849)"><stop
|
||||
offset="0"
|
||||
style="stop-color:#00ff39;stop-opacity:1;"
|
||||
id="stop2" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#004a19;stop-opacity:1;"
|
||||
id="stop4" /></radialGradient><radialGradient
|
||||
id="radial1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="525.587769"
|
||||
cy="638.591797"
|
||||
fx="525.587769"
|
||||
fy="638.591797"
|
||||
r="192"
|
||||
gradientTransform="matrix(-0.107656,-0.225172,-0.327748,0.258343,373.87973,30.205086)"><stop
|
||||
offset="0"
|
||||
style="stop-color:#43b60b;stop-opacity:1;"
|
||||
id="stop7" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#0b88ff;stop-opacity:0.00829875;"
|
||||
id="stop9" /></radialGradient><clipPath
|
||||
id="clip1"><path
|
||||
d="M 7 46 L 57 46 L 57 93 L 7 93 Z M 7 46 "
|
||||
id="path12" /></clipPath><clipPath
|
||||
id="clip2"><path
|
||||
d="M 32.25 46.957031 C 19.6875 46.96875 9.085938 56.636719 7.503906 69.53125 C 9.0625 82.445312 19.667969 92.144531 32.25 92.160156 C 44.816406 92.148438 55.414062 82.480469 57 69.585938 C 55.441406 56.671875 44.835938 46.972656 32.25 46.957031 Z M 32.25 46.957031 "
|
||||
id="path15" /></clipPath><radialGradient
|
||||
id="radial2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="131.914749"
|
||||
cy="55.927143"
|
||||
fx="131.914749"
|
||||
fy="55.927143"
|
||||
r="160"
|
||||
gradientTransform="matrix(0.485163,-1.148584,-0.771115,-0.0632965,-47.124961,203.98857)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(92.941177%,20%,23.137255%);stop-opacity:1;"
|
||||
id="stop18" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(63.921571%,27.843139%,72.941178%);stop-opacity:1;"
|
||||
id="stop20" /></radialGradient><radialGradient
|
||||
id="radial3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="525.587769"
|
||||
cy="638.591797"
|
||||
fx="525.587769"
|
||||
fy="638.591797"
|
||||
r="192"
|
||||
gradientTransform="matrix(-0.225099,-0.477638,-0.685291,0.548001,725.67923,15.723794)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(10.980392%,44.313726%,84.705883%);stop-opacity:1;"
|
||||
id="stop23" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(20.784314%,51.764709%,89.411765%);stop-opacity:0.00829876;"
|
||||
id="stop25" /></radialGradient><linearGradient
|
||||
id="linear0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="22"
|
||||
y1="37"
|
||||
x2="62"
|
||||
y2="37"
|
||||
gradientTransform="matrix(1.4,0,0,1.4,-26.799973,2.491745)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(58.039218%,57.647061%,56.470591%);stop-opacity:1;"
|
||||
id="stop28" /><stop
|
||||
offset="0.0908155"
|
||||
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
|
||||
id="stop30" /><stop
|
||||
offset="0.336093"
|
||||
style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"
|
||||
id="stop32" /><stop
|
||||
offset="0.844326"
|
||||
style="stop-color:rgb(76.47059%,75.294119%,72.941178%);stop-opacity:1;"
|
||||
id="stop34" /><stop
|
||||
offset="0.930505"
|
||||
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
|
||||
id="stop36" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"
|
||||
id="stop38" /></linearGradient><radialGradient
|
||||
id="radial4"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="-172.560638"
|
||||
cy="28.569126"
|
||||
fx="-172.560638"
|
||||
fy="28.569126"
|
||||
r="15.85742"
|
||||
gradientTransform="matrix(1.560712,0,0,1.4252,300.69366,13.349996)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.358268;"
|
||||
id="stop41" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.0944882;"
|
||||
id="stop43" /></radialGradient><filter
|
||||
id="alpha"
|
||||
filterUnits="objectBoundingBox"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1"
|
||||
height="1"><feColorMatrix
|
||||
type="matrix"
|
||||
in="SourceGraphic"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
id="feColorMatrix46" /></filter><mask
|
||||
id="mask0"><g
|
||||
filter="url(#alpha)"
|
||||
id="g51"><rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="128"
|
||||
height="128"
|
||||
style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"
|
||||
id="rect49" /></g></mask><clipPath
|
||||
id="clip3"><rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="192"
|
||||
height="152"
|
||||
id="rect54" /></clipPath><g
|
||||
id="surface382"
|
||||
clip-path="url(#clip3)"><path
|
||||
style=" stroke:none;fill-rule:nonzero;fill:rgb(27.058825%,21.176471%,21.568628%);fill-opacity:1;"
|
||||
d="M 40 59.957031 C 26.191406 59.957031 15 71.152344 15 84.957031 C 15.011719 85.996094 15.085938 86.777344 15.222656 87.804688 C 15.222656 75.957031 27.421875 65.96875 40 65.957031 C 52.597656 65.972656 64.777344 75.957031 64.777344 87.859375 C 64.917969 86.816406 64.992188 86.011719 65 84.957031 C 65 71.152344 53.808594 59.957031 40 59.957031 Z M 40 59.957031 "
|
||||
id="path57" /></g><radialGradient
|
||||
id="radial5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="40"
|
||||
cy="227"
|
||||
fx="40"
|
||||
fy="227"
|
||||
r="28"
|
||||
gradientTransform="matrix(0.575553,0,1.60551e-8,1.540703,8.977913,-280.78108)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"
|
||||
id="stop60" /><stop
|
||||
offset="0.744626"
|
||||
style="stop-color:rgb(98.039216%,98.039216%,98.039216%);stop-opacity:1;"
|
||||
id="stop62" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(87.450981%,87.450981%,87.450981%);stop-opacity:1;"
|
||||
id="stop64" /></radialGradient>
|
||||
|
||||
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient936"
|
||||
id="linearGradient938"
|
||||
x1="-48.519272"
|
||||
y1="18.511358"
|
||||
x2="287.07454"
|
||||
y2="18.511358"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1020247,0,0,1.1097375,37.198581,-10.424856)" /></defs><path
|
||||
style="fill:#f5f5f3;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 20,11.957031 h 88 c 4.41797,0 8,3.582031 8,8 V 108 c 0,4.41797 -3.58203,8 -8,8 H 20 c -4.417969,0 -8,-3.58203 -8,-8 V 19.957031 c 0,-4.417969 3.582031,-8 8,-8 z m 0,0"
|
||||
id="path69" /><path
|
||||
style="fill:url(#radial0);fill-rule:nonzero;stroke:none"
|
||||
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
|
||||
id="path71" /><path
|
||||
style="fill:none;fill-rule:nonzero;stroke:none;fill-opacity:1"
|
||||
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
|
||||
id="path73" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 1"
|
||||
transform="matrix(0.1816,0,0,0.1816,35.224187,79.037164)"><ellipse
|
||||
fill="#54c147"
|
||||
cx="168.64549"
|
||||
cy="10.117889"
|
||||
id="circle8314"
|
||||
rx="184.91634"
|
||||
ry="179.91556"
|
||||
style="fill:url(#linearGradient938);fill-opacity:1;stroke-width:1.71591" /><polygon
|
||||
fill="#ffffff"
|
||||
points="46.678,120.299 63.562,96.402 96.977,121.718 145.084,50.79 168.752,69.02 103.583,164.647 "
|
||||
id="polygon8316"
|
||||
transform="matrix(1.7395866,0,0,1.6925423,-18.737581,-172.19767)" /></g></svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -18,6 +18,8 @@ BACKUP_LOG="/var/log/sovran-hub-backup.log"
|
||||
BACKUP_STATUS="/var/log/sovran-hub-backup.status"
|
||||
MEDIA_ROOT="/run/media"
|
||||
MIN_FREE_GB=10
|
||||
HUB_CONFIG_JSON="/var/lib/sovran-hub/config.json"
|
||||
ROLE_STATE_NIX="/etc/nixos/role-state.nix"
|
||||
|
||||
# ── Internal drive labels/paths to NEVER use as backup targets ───
|
||||
INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos")
|
||||
@@ -125,6 +127,40 @@ for d in flatten(data.get('blockdevices', [])):
|
||||
echo "$target"
|
||||
}
|
||||
|
||||
# ── Detect the configured system role ───────────────────────────
|
||||
#
|
||||
# Priority:
|
||||
# 1. Hub config JSON (/var/lib/sovran-hub/config.json) — "role" key
|
||||
# 2. role-state.nix (/etc/nixos/role-state.nix) — grep for true flag
|
||||
# 3. Default: server_plus_desktop
|
||||
|
||||
detect_role() {
|
||||
local role="server_plus_desktop"
|
||||
|
||||
# 1. Try the Hub config JSON
|
||||
if [[ -f "$HUB_CONFIG_JSON" ]] && command -v python3 &>/dev/null; then
|
||||
local r
|
||||
r=$(python3 -c \
|
||||
"import json,sys; d=json.load(open(sys.argv[1])); print(d.get('role',''))" \
|
||||
"$HUB_CONFIG_JSON" 2>/dev/null || true)
|
||||
if [[ -n "$r" ]]; then
|
||||
echo "$r"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Fall back to parsing role-state.nix
|
||||
if [[ -f "$ROLE_STATE_NIX" ]]; then
|
||||
if grep -q 'roles\.desktop = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
|
||||
role="desktop"
|
||||
elif grep -q 'roles\.node = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
|
||||
role="node"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$role"
|
||||
}
|
||||
|
||||
# ── Initialise log file ──────────────────────────────────────────
|
||||
|
||||
: > "$BACKUP_LOG"
|
||||
@@ -133,6 +169,17 @@ set_status "RUNNING"
|
||||
log "=== Sovran_SystemsOS External Hub Backup ==="
|
||||
log "Starting backup process…"
|
||||
|
||||
# ── Detect system role ───────────────────────────────────────────
|
||||
|
||||
ROLE="$(detect_role)"
|
||||
case "$ROLE" in
|
||||
desktop) ROLE_LABEL="Desktop Only" ;;
|
||||
node) ROLE_LABEL="Node (Bitcoin-only)" ;;
|
||||
server_plus_desktop) ROLE_LABEL="Server + Desktop" ;;
|
||||
*) ROLE_LABEL="$ROLE" ;;
|
||||
esac
|
||||
log "Detected role: $ROLE_LABEL"
|
||||
|
||||
# ── Detect target drive ──────────────────────────────────────────
|
||||
|
||||
if [[ -n "${BACKUP_TARGET:-}" ]]; then
|
||||
@@ -190,16 +237,29 @@ log ""
|
||||
log "── Stage 2/4: Secrets ───────────────────────────────────────"
|
||||
mkdir -p "$BACKUP_DIR/secrets"
|
||||
|
||||
for SRC in /etc/nix-bitcoin-secrets /var/lib/domains; do
|
||||
if [[ -e "$SRC" ]]; then
|
||||
rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||
log "WARNING: Could not copy $SRC — continuing."
|
||||
else
|
||||
log " (not found: $SRC — skipping)"
|
||||
fi
|
||||
done
|
||||
if [[ "$ROLE" == "desktop" ]]; then
|
||||
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
|
||||
# /var/lib/domains is still backed up if present (hub state)
|
||||
for SRC in /var/lib/domains; do
|
||||
if [[ -e "$SRC" ]]; then
|
||||
rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||
log "WARNING: Could not copy $SRC — continuing."
|
||||
else
|
||||
log " (not found: $SRC — skipping)"
|
||||
fi
|
||||
done
|
||||
else
|
||||
for SRC in /etc/nix-bitcoin-secrets /var/lib/domains; do
|
||||
if [[ -e "$SRC" ]]; then
|
||||
rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||
log "WARNING: Could not copy $SRC — continuing."
|
||||
else
|
||||
log " (not found: $SRC — skipping)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Hub state files from /var/lib/secrets/
|
||||
# Hub state files from /var/lib/secrets/ (backed up for all roles)
|
||||
if [[ -d /var/lib/secrets ]]; then
|
||||
mkdir -p "$BACKUP_DIR/secrets/hub-state"
|
||||
rsync -a --info=progress2 /var/lib/secrets/ "$BACKUP_DIR/secrets/hub-state/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||
@@ -230,7 +290,9 @@ fi
|
||||
|
||||
log ""
|
||||
log "── Stage 4/4: Wallet and node data (/var/lib/lnd) ──────────"
|
||||
if [[ -d /var/lib/lnd ]]; then
|
||||
if [[ "$ROLE" == "desktop" ]]; then
|
||||
log "Skipping Stage 4 (LND wallet data) — not applicable for Desktop Only role."
|
||||
elif [[ -d /var/lib/lnd ]]; then
|
||||
rsync -a --info=progress2 \
|
||||
--exclude='logs/' \
|
||||
/var/lib/lnd/ "$BACKUP_DIR/lnd/" 2>&1 | tee -a "$BACKUP_LOG" || \
|
||||
@@ -248,6 +310,7 @@ log "Generating BACKUP_MANIFEST.txt …"
|
||||
echo "Sovran_SystemsOS Backup Manifest"
|
||||
echo "Generated: $(date)"
|
||||
echo "Hostname: $(hostname)"
|
||||
echo "Role: $ROLE_LABEL"
|
||||
echo "Target: $TARGET"
|
||||
echo ""
|
||||
echo "Contents:"
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
@@ -57,6 +58,12 @@ ZEUS_CONNECT_FILE = "/var/lib/secrets/zeus-connect-url"
|
||||
REBOOT_COMMAND = ["reboot"]
|
||||
|
||||
ONBOARDING_FLAG = "/var/lib/sovran/onboarding-complete"
|
||||
AUTOLAUNCH_DISABLE_FLAG = "/var/lib/sovran/hub-autolaunch-disabled"
|
||||
|
||||
# ── Legacy security check constants ──────────────────────────────
|
||||
|
||||
SECURITY_STATUS_FILE = "/var/lib/sovran/security-status"
|
||||
SECURITY_WARNING_FILE = "/var/lib/sovran/security-warning"
|
||||
|
||||
# ── Tech Support constants ────────────────────────────────────────
|
||||
|
||||
@@ -192,6 +199,35 @@ FEATURE_REGISTRY = [
|
||||
"conflicts_with": ["bip110"],
|
||||
"port_requirements": [],
|
||||
},
|
||||
{
|
||||
"id": "sshd",
|
||||
"name": "SSH Remote Access",
|
||||
"description": "Enable SSH for remote terminal access. Required for Tech Support. Disabled by default for security — enable only when needed.",
|
||||
"category": "support",
|
||||
"needs_domain": False,
|
||||
"domain_name": None,
|
||||
"needs_ddns": False,
|
||||
"extra_fields": [],
|
||||
"conflicts_with": [],
|
||||
"port_requirements": [
|
||||
{"port": "22", "protocol": "TCP", "description": "SSH"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "btcpay-web",
|
||||
"name": "BTCPay Server Web Access",
|
||||
"description": "Expose BTCPay Server to the internet via your domain. When disabled, BTCPay Server still runs locally but is not accessible from the web.",
|
||||
"category": "bitcoin",
|
||||
"needs_domain": True,
|
||||
"domain_name": "btcpayserver",
|
||||
"needs_ddns": True,
|
||||
"extra_fields": [],
|
||||
"conflicts_with": [],
|
||||
"port_requirements": [
|
||||
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
||||
{"port": "443", "protocol": "TCP", "description": "HTTPS"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# Map feature IDs to their systemd units in config.json
|
||||
@@ -202,6 +238,8 @@ FEATURE_SERVICE_MAP = {
|
||||
"mempool": "mempool.service",
|
||||
"bip110": None,
|
||||
"bitcoin-core": None,
|
||||
"btcpay-web": "btcpayserver.service",
|
||||
"sshd": "sshd.service",
|
||||
}
|
||||
|
||||
# Port requirements for service tiles (keyed by unit name or icon)
|
||||
@@ -233,6 +271,8 @@ SERVICE_PORT_REQUIREMENTS: dict[str, list[dict]] = {
|
||||
"phpfpm-nextcloud.service": _PORTS_WEB,
|
||||
"phpfpm-wordpress.service": _PORTS_WEB,
|
||||
"haven-relay.service": _PORTS_WEB,
|
||||
# SSH (only open when feature is enabled)
|
||||
"sshd.service": [{"port": "22", "protocol": "TCP", "description": "SSH"}],
|
||||
}
|
||||
|
||||
# Maps service unit names to their domain file name in DOMAINS_DIR.
|
||||
@@ -259,6 +299,20 @@ ROLE_LABELS = {
|
||||
"node": "Bitcoin Node",
|
||||
}
|
||||
|
||||
# Categories shown per role (None = show all)
|
||||
ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
||||
"server_plus_desktop": None,
|
||||
"desktop": {"infrastructure", "support", "feature-manager"},
|
||||
"node": {"infrastructure", "bitcoin-base", "bitcoin-apps", "support", "feature-manager"},
|
||||
}
|
||||
|
||||
# Features shown per role (None = show all)
|
||||
ROLE_FEATURES: dict[str, set[str] | None] = {
|
||||
"server_plus_desktop": None,
|
||||
"desktop": {"rdp", "sshd"},
|
||||
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
||||
}
|
||||
|
||||
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"bitcoind.service": (
|
||||
"The foundation of your financial sovereignty. Your node independently verifies "
|
||||
@@ -340,10 +394,37 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||
"Manage your system visually without being physically present. "
|
||||
"Sovran_SystemsOS sets up secure remote access with generated credentials — connect and go."
|
||||
),
|
||||
"sshd.service": (
|
||||
"Secure Shell (SSH) remote access. When enabled, authorized users can connect "
|
||||
"to your machine over the network via encrypted terminal sessions. "
|
||||
"Sovran_SystemsOS keeps SSH disabled by default for maximum security — "
|
||||
"enable it only when you need remote access or Tech Support."
|
||||
),
|
||||
"root-password-setup.service": (
|
||||
"Your system account credentials. These are the keys to your Sovran_SystemsOS machine — "
|
||||
"root access, user accounts, and SSH passphrases. Keep them safe."
|
||||
),
|
||||
"sparrow-autoconnect.service": (
|
||||
"Sparrow Wallet is a privacy-focused Bitcoin desktop wallet for sending, receiving, "
|
||||
"and managing your Bitcoin. Sovran_SystemsOS automatically connects it to your local "
|
||||
"Electrs server on first boot — your address lookups, balances, and transactions "
|
||||
"never touch a third-party server. Full privacy, zero configuration."
|
||||
),
|
||||
"bisq-autoconnect.service": (
|
||||
"Bisq is a decentralized, peer-to-peer Bitcoin exchange — buy and sell Bitcoin "
|
||||
"with no KYC and no middleman. Sovran_SystemsOS automatically connects it to your "
|
||||
"local Bitcoin node on first boot, routing all traffic through Tor. Your trades are "
|
||||
"verified by your own node, keeping you fully sovereign."
|
||||
),
|
||||
}
|
||||
|
||||
SERVICE_DESKTOP_LINKS: dict[str, list[dict[str, str]]] = {
|
||||
"sparrow-autoconnect.service": [
|
||||
{"label": "Open Sparrow Wallet", "desktop_file": "sparrow-desktop.desktop"},
|
||||
],
|
||||
"bisq-autoconnect.service": [
|
||||
{"label": "Open Bisq", "desktop_file": "Bisq.desktop"},
|
||||
],
|
||||
}
|
||||
|
||||
# ── App setup ────────────────────────────────────────────────────
|
||||
@@ -610,7 +691,10 @@ def _check_port_status(
|
||||
for pt in ports_set
|
||||
)
|
||||
|
||||
if is_listening and is_allowed:
|
||||
# A process bound to the port is the authoritative signal; firewall
|
||||
# detection (nft/iptables) is only used as a secondary hint when nothing
|
||||
# is listening yet.
|
||||
if is_listening:
|
||||
return "listening"
|
||||
if is_allowed:
|
||||
return "firewall_open"
|
||||
@@ -621,7 +705,7 @@ def _check_port_status(
|
||||
|
||||
def _generate_qr_base64(data: str) -> str | None:
|
||||
"""Generate a QR code PNG and return it as a base64-encoded data URI.
|
||||
Uses qrencode CLI (available on the system via credentials-pdf.nix)."""
|
||||
Uses qrencode CLI (available on the system via credentials.nix)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["qrencode", "-o", "-", "-t", "PNG", "-s", "6", "-m", "2", "-l", "H", data],
|
||||
@@ -918,6 +1002,11 @@ def _read_hub_overrides() -> tuple[dict, str | None]:
|
||||
section,
|
||||
):
|
||||
features[m.group(1)] = m.group(2) == "true"
|
||||
for m in re.finditer(
|
||||
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
||||
section,
|
||||
):
|
||||
features["btcpay-web"] = m.group(1) == "true"
|
||||
m2 = re.search(
|
||||
r'sovran_systemsOS\.nostr_npub\s*=\s*(?:lib\.mkForce\s+)?"([^"]*)"',
|
||||
section,
|
||||
@@ -934,7 +1023,10 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None) -> None:
|
||||
lines = []
|
||||
for feat_id, enabled in features.items():
|
||||
val = "true" if enabled else "false"
|
||||
lines.append(f" sovran_systemsOS.features.{feat_id} = lib.mkForce {val};")
|
||||
if feat_id == "btcpay-web":
|
||||
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
|
||||
else:
|
||||
lines.append(f" sovran_systemsOS.features.{feat_id} = lib.mkForce {val};")
|
||||
if nostr_npub:
|
||||
lines.append(f' sovran_systemsOS.nostr_npub = lib.mkForce "{nostr_npub}";')
|
||||
hub_block = (
|
||||
@@ -976,6 +1068,8 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None) -> None:
|
||||
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
||||
"""Check if a feature's service appears as enabled in the running config.json.
|
||||
Returns True/False if found, None if the feature has no mapped service."""
|
||||
if feature_id == "btcpay-web":
|
||||
return False # Default off in Node role; only on via explicit hub toggle
|
||||
unit = FEATURE_SERVICE_MAP.get(feature_id)
|
||||
if unit is None:
|
||||
return None # bip110, bitcoin-core — can't determine from config
|
||||
@@ -986,6 +1080,15 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
||||
return None
|
||||
|
||||
|
||||
def _is_sshd_feature_enabled() -> bool:
|
||||
"""Check if the sshd feature is enabled via hub overrides or config."""
|
||||
overrides, _ = _read_hub_overrides()
|
||||
if "sshd" in overrides:
|
||||
return bool(overrides["sshd"])
|
||||
config_state = _is_feature_enabled_in_config("sshd")
|
||||
return bool(config_state) if config_state is not None else False
|
||||
|
||||
|
||||
# ── Tech Support helpers ──────────────────────────────────────────
|
||||
|
||||
def _is_support_active() -> bool:
|
||||
@@ -1318,18 +1421,361 @@ async def api_onboarding_complete():
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Auto-launch endpoints ─────────────────────────────────────────
|
||||
|
||||
@app.get("/api/autolaunch/status")
|
||||
async def api_autolaunch_status():
|
||||
"""Check if Hub auto-launch on login is enabled."""
|
||||
disabled = os.path.exists(AUTOLAUNCH_DISABLE_FLAG)
|
||||
return {"enabled": not disabled}
|
||||
|
||||
|
||||
class AutolaunchToggleRequest(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
@app.post("/api/autolaunch/toggle")
|
||||
async def api_autolaunch_toggle(req: AutolaunchToggleRequest):
|
||||
"""Enable or disable Hub auto-launch on login."""
|
||||
if req.enabled:
|
||||
# Remove the disable flag to enable auto-launch
|
||||
try:
|
||||
os.remove(AUTOLAUNCH_DISABLE_FLAG)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
# Create the disable flag to suppress auto-launch
|
||||
os.makedirs(os.path.dirname(AUTOLAUNCH_DISABLE_FLAG), exist_ok=True)
|
||||
try:
|
||||
with open(AUTOLAUNCH_DISABLE_FLAG, "w") as f:
|
||||
f.write("")
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Could not write flag file: {exc}")
|
||||
return {"ok": True, "enabled": req.enabled}
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def api_config():
|
||||
cfg = load_config()
|
||||
role = cfg.get("role", "server_plus_desktop")
|
||||
allowed_cats = ROLE_CATEGORIES.get(role)
|
||||
cats = CATEGORY_ORDER if allowed_cats is None else [
|
||||
c for c in CATEGORY_ORDER if c[0] in allowed_cats
|
||||
]
|
||||
return {
|
||||
"role": role,
|
||||
"role_label": ROLE_LABELS.get(role, role),
|
||||
"category_order": CATEGORY_ORDER,
|
||||
"category_order": cats,
|
||||
"feature_manager": True,
|
||||
}
|
||||
|
||||
|
||||
ROLE_STATE_NIX = """\
|
||||
# THIS FILE IS AUTO-GENERATED. DO NOT EDIT.
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault true;
|
||||
sovran_systemsOS.roles.desktop = lib.mkDefault false;
|
||||
sovran_systemsOS.roles.node = lib.mkDefault false;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@app.post("/api/role/upgrade-to-server")
|
||||
async def api_upgrade_to_server():
|
||||
"""Upgrade from Node role to Server+Desktop role by writing role-state.nix and rebuilding."""
|
||||
cfg = load_config()
|
||||
if cfg.get("role", "server_plus_desktop") != "node":
|
||||
raise HTTPException(status_code=400, detail="Upgrade is only available for the Node role.")
|
||||
|
||||
try:
|
||||
with open("/etc/nixos/role-state.nix", "w") as f:
|
||||
f.write(ROLE_STATE_NIX)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to write role-state.nix: {exc}")
|
||||
|
||||
# Reset onboarding so the wizard runs for the newly unlocked services
|
||||
try:
|
||||
os.remove(ONBOARDING_FLAG)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Clear stale rebuild log
|
||||
try:
|
||||
open(REBUILD_LOG, "w").close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
await asyncio.create_subprocess_exec(
|
||||
"systemctl", "reset-failed", REBUILD_UNIT,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"systemctl", "start", "--no-block", REBUILD_UNIT,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await proc.wait()
|
||||
|
||||
return {"ok": True, "status": "rebuilding"}
|
||||
|
||||
|
||||
# ── Bitcoin IBD sync helper ───────────────────────────────────────
|
||||
|
||||
BITCOIN_DATADIR = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"
|
||||
|
||||
# Simple in-process cache: (timestamp, result)
|
||||
_btc_sync_cache: tuple[float, dict | None] = (0.0, None)
|
||||
_BTC_SYNC_CACHE_TTL = 5 # seconds
|
||||
|
||||
_btc_version_cache: tuple[float, dict | None] = (0.0, None)
|
||||
_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
|
||||
|
||||
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
|
||||
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
|
||||
|
||||
|
||||
# ── Generic service version detection (NixOS store path) ─────────
|
||||
|
||||
# Regex to extract the version from a Nix store ExecStart path.
|
||||
# Pattern: /nix/store/<32-char-hash>-<name-segments>-<version>/...
|
||||
# Name segments may begin with a letter or digit (e.g. 'python3', 'gtk3',
|
||||
# 'lib32-foo') and consist of alphanumeric characters only (no underscores,
|
||||
# since Nix store paths use hyphens as separators).
|
||||
# The version is identified as the first token starting with digit.digit.
|
||||
_NIX_STORE_VERSION_RE = re.compile(
|
||||
r"/nix/store/[a-z0-9]{32}-" # hash prefix
|
||||
r"(?:[a-zA-Z0-9][a-zA-Z0-9]*(?:-[a-zA-Z0-9][a-zA-Z0-9]*)*)+" # package name
|
||||
r"-(\d+\.\d+(?:\.\d+)*(?:[+-][a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)?)/" # version (group 1)
|
||||
)
|
||||
|
||||
# Nix path suffixes that indicate a wrapper environment, not a real package version.
|
||||
_NIX_WRAPPER_SUFFIX_RE = re.compile(
|
||||
r"-(?:env|wrapper|wrapped|script|hook|setup|compat)$"
|
||||
)
|
||||
|
||||
# Cache: unit → (monotonic_timestamp, version_str | None)
|
||||
_svc_version_cache: dict[str, tuple[float, str | None]] = {}
|
||||
_SVC_VERSION_CACHE_TTL = 300 # 5 minutes — versions only change on system update
|
||||
|
||||
|
||||
def _get_service_version(unit: str) -> str | None:
|
||||
"""Extract the version of a service from its Nix store ExecStart path.
|
||||
|
||||
Runs ``systemctl show <unit> --property=ExecStart --value`` and parses
|
||||
the Nix store path embedded in the output to obtain the package version.
|
||||
|
||||
Results are cached for _SVC_VERSION_CACHE_TTL seconds so that repeated
|
||||
/api/services polls don't spawn extra processes on every request.
|
||||
Returns None if the version cannot be determined.
|
||||
"""
|
||||
now = time.monotonic()
|
||||
cached = _svc_version_cache.get(unit)
|
||||
if cached is not None:
|
||||
cached_at, cached_val = cached
|
||||
if now - cached_at < _SVC_VERSION_CACHE_TTL:
|
||||
return cached_val
|
||||
|
||||
version: str | None = None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "show", unit, "--property=ExecStart", "--value"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
m = _NIX_STORE_VERSION_RE.search(result.stdout)
|
||||
if m:
|
||||
ver = m.group(1)
|
||||
# Strip a single trailing period (defensive; shouldn't appear in store paths)
|
||||
ver = ver[:-1] if ver.endswith(".") else ver
|
||||
# Skip Nix environment/wrapper suffixes that are not real versions
|
||||
if not _NIX_WRAPPER_SUFFIX_RE.search(ver):
|
||||
version = ver if ver.startswith("v") else f"v{ver}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_svc_version_cache[unit] = (now, version)
|
||||
return version
|
||||
|
||||
|
||||
def _parse_bitcoin_subversion(subversion: str) -> str:
|
||||
"""Parse a subversion string like '/Bitcoin Knots:27.1.0/' into 'v27.1.0'.
|
||||
|
||||
Examples:
|
||||
'/Bitcoin Knots:27.1.0/' → 'v27.1.0'
|
||||
'/Satoshi:27.0.0/' → 'v27.0.0'
|
||||
'/Bitcoin Knots:27.1.0(bip110)/' → 'v27.1.0 (bip110)'
|
||||
Falls back to the raw subversion string if parsing fails.
|
||||
"""
|
||||
m = re.search(r":(\d+\.\d+(?:\.\d+)*)", subversion)
|
||||
if m:
|
||||
ver = "v" + m.group(1)
|
||||
if "(bip110)" in subversion.lower():
|
||||
ver += " (bip110)"
|
||||
return ver
|
||||
return subversion
|
||||
|
||||
|
||||
def _get_bitcoin_version_info() -> dict | None:
|
||||
"""Call bitcoin-cli getnetworkinfo and return parsed JSON, or None on error.
|
||||
|
||||
Results are cached for _BTC_VERSION_CACHE_TTL seconds since the version
|
||||
does not change while the service is running.
|
||||
"""
|
||||
global _btc_version_cache
|
||||
now = time.monotonic()
|
||||
cached_at, cached_val = _btc_version_cache
|
||||
if now - cached_at < _BTC_VERSION_CACHE_TTL:
|
||||
return cached_val
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getnetworkinfo"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_btc_version_cache = (now, None)
|
||||
return None
|
||||
info = json.loads(result.stdout)
|
||||
_btc_version_cache = (now, info)
|
||||
return info
|
||||
except Exception:
|
||||
_btc_version_cache = (now, None)
|
||||
return None
|
||||
|
||||
|
||||
def _get_bitcoind_version() -> str | None:
|
||||
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
|
||||
|
||||
Parses the first output line to extract the token after "version ".
|
||||
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1"
|
||||
returns "v29.3.knots20260210+bip110-v0.4.1".
|
||||
|
||||
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
|
||||
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
|
||||
"""
|
||||
global _btcd_version_cache
|
||||
now = time.monotonic()
|
||||
cached_at, cached_val = _btcd_version_cache
|
||||
if now - cached_at < _BTC_VERSION_CACHE_TTL:
|
||||
return cached_val
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bitcoind", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
first_line = result.stdout.splitlines()[0]
|
||||
m = re.search(r"version\s+(v?\S+)", first_line, re.IGNORECASE)
|
||||
if m:
|
||||
ver = m.group(1)
|
||||
_btcd_version_cache = (now, ver)
|
||||
return ver
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_btcd_version_cache = (now, None)
|
||||
return None
|
||||
|
||||
|
||||
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
|
||||
"""Format a raw version string from ``bitcoind --version`` for tile display.
|
||||
|
||||
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown
|
||||
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1" → "v29.3.knots20260210").
|
||||
For the BIP110 tile (icon == "bip110") a " (bip110 vX.Y.Z)" tag is appended
|
||||
including the patch version.
|
||||
"""
|
||||
# Extract the BIP110 patch version before stripping the suffix
|
||||
bip110_ver = ""
|
||||
bip_match = re.search(r"\+bip110-v(\S+)", raw_version)
|
||||
if bip_match:
|
||||
bip110_ver = bip_match.group(1)
|
||||
|
||||
# Strip the +bip110... suffix for the base Knots version
|
||||
display = re.sub(r"\+bip110\S*", "", raw_version)
|
||||
|
||||
# For BIP110 tile, append both the tag and the patch version
|
||||
if icon == "bip110":
|
||||
if bip110_ver:
|
||||
display += f" (bip110 v{bip110_ver})"
|
||||
elif "(bip110)" not in display.lower():
|
||||
display += " (bip110)"
|
||||
return display
|
||||
|
||||
|
||||
def _get_bitcoin_sync_info() -> dict | None:
|
||||
"""Call bitcoin-cli getblockchaininfo and return parsed JSON, or None on error.
|
||||
|
||||
Results are cached for _BTC_SYNC_CACHE_TTL seconds to avoid hammering
|
||||
bitcoin-cli on every /api/services poll cycle.
|
||||
"""
|
||||
global _btc_sync_cache
|
||||
now = time.monotonic()
|
||||
cached_at, cached_val = _btc_sync_cache
|
||||
if now - cached_at < _BTC_SYNC_CACHE_TTL:
|
||||
return cached_val
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getblockchaininfo"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_btc_sync_cache = (now, None)
|
||||
return None
|
||||
info = json.loads(result.stdout)
|
||||
_btc_sync_cache = (now, info)
|
||||
return info
|
||||
except Exception:
|
||||
_btc_sync_cache = (now, None)
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/api/bitcoin/sync")
|
||||
async def api_bitcoin_sync():
|
||||
"""Return Bitcoin blockchain sync status directly from bitcoin-cli."""
|
||||
loop = asyncio.get_event_loop()
|
||||
info = await loop.run_in_executor(None, _get_bitcoin_sync_info)
|
||||
if info is None:
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"error": "bitcoin-cli unavailable or bitcoind not running"},
|
||||
)
|
||||
return {
|
||||
"blocks": info.get("blocks", 0),
|
||||
"headers": info.get("headers", 0),
|
||||
"verificationprogress": info.get("verificationprogress", 0),
|
||||
"initialblockdownload": info.get("initialblockdownload", False),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/bitcoin/version")
|
||||
async def api_bitcoin_version():
|
||||
"""Return the version string of the active bitcoind implementation."""
|
||||
loop = asyncio.get_event_loop()
|
||||
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
|
||||
if raw_ver is None:
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"error": "bitcoind --version failed or bitcoind not on PATH"},
|
||||
)
|
||||
return {
|
||||
"version": _format_bitcoin_version(raw_ver),
|
||||
"raw_version": raw_ver,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/services")
|
||||
async def api_services():
|
||||
cfg = load_config()
|
||||
@@ -1391,6 +1837,10 @@ async def api_services():
|
||||
domain = None
|
||||
|
||||
# Compute composite health
|
||||
sync_progress: float | None = None
|
||||
sync_blocks: int | None = None
|
||||
sync_headers: int | None = None
|
||||
sync_ibd: bool | None = None
|
||||
if not enabled:
|
||||
health = "disabled"
|
||||
elif status == "active":
|
||||
@@ -1411,6 +1861,15 @@ async def api_services():
|
||||
if not domain:
|
||||
has_domain_issues = True
|
||||
health = "needs_attention" if (has_port_issues or has_domain_issues) else "healthy"
|
||||
# Check Bitcoin IBD state
|
||||
if unit == "bitcoind.service":
|
||||
sync = await loop.run_in_executor(None, _get_bitcoin_sync_info)
|
||||
if sync and sync.get("initialblockdownload"):
|
||||
health = "syncing"
|
||||
sync_progress = sync.get("verificationprogress", 0)
|
||||
sync_blocks = sync.get("blocks", 0)
|
||||
sync_headers = sync.get("headers", 0)
|
||||
sync_ibd = True
|
||||
elif status == "inactive":
|
||||
health = "inactive"
|
||||
elif status == "failed":
|
||||
@@ -1418,7 +1877,7 @@ async def api_services():
|
||||
else:
|
||||
health = status # loading states, etc.
|
||||
|
||||
return {
|
||||
service_data: dict = {
|
||||
"name": entry.get("name", ""),
|
||||
"unit": unit,
|
||||
"type": scope,
|
||||
@@ -1432,6 +1891,18 @@ async def api_services():
|
||||
"needs_domain": needs_domain,
|
||||
"domain": domain,
|
||||
}
|
||||
if sync_ibd is not None:
|
||||
service_data["sync_ibd"] = sync_ibd
|
||||
service_data["sync_progress"] = sync_progress
|
||||
service_data["sync_blocks"] = sync_blocks
|
||||
service_data["sync_headers"] = sync_headers
|
||||
if unit == "bitcoind.service" and enabled:
|
||||
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
|
||||
if raw_ver is not None:
|
||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||
service_data["bitcoin_version"] = btc_ver # backwards compat
|
||||
service_data["version"] = btc_ver
|
||||
return service_data
|
||||
|
||||
results = await asyncio.gather(*[get_status(s) for s in services])
|
||||
return list(results)
|
||||
@@ -1613,6 +2084,10 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
})
|
||||
|
||||
# Compute composite health
|
||||
sync_progress: float | None = None
|
||||
sync_blocks: int | None = None
|
||||
sync_headers: int | None = None
|
||||
sync_ibd: bool | None = None
|
||||
if not enabled:
|
||||
health = "disabled"
|
||||
elif status == "active":
|
||||
@@ -1624,6 +2099,15 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
elif domain_status and domain_status.get("status") not in ("connected", None):
|
||||
has_domain_issues = True
|
||||
health = "needs_attention" if (has_port_issues or has_domain_issues) else "healthy"
|
||||
# Check Bitcoin IBD state
|
||||
if unit == "bitcoind.service":
|
||||
sync = await loop.run_in_executor(None, _get_bitcoin_sync_info)
|
||||
if sync and sync.get("initialblockdownload"):
|
||||
health = "syncing"
|
||||
sync_progress = sync.get("verificationprogress", 0)
|
||||
sync_blocks = sync.get("blocks", 0)
|
||||
sync_headers = sync.get("headers", 0)
|
||||
sync_ibd = True
|
||||
elif status == "inactive":
|
||||
health = "inactive"
|
||||
elif status == "failed":
|
||||
@@ -1666,7 +2150,7 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"port_requirements": feat_meta.get("port_requirements", []),
|
||||
}
|
||||
|
||||
return {
|
||||
service_detail: dict = {
|
||||
"name": entry.get("name", ""),
|
||||
"unit": unit,
|
||||
"icon": icon,
|
||||
@@ -1678,6 +2162,7 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"credentials": resolved_creds,
|
||||
"needs_domain": needs_domain,
|
||||
"domain": domain,
|
||||
"domain_name": domain_key,
|
||||
"domain_status": domain_status,
|
||||
"port_requirements": port_requirements,
|
||||
"port_statuses": port_statuses,
|
||||
@@ -1685,6 +2170,46 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
||||
"internal_ip": internal_ip,
|
||||
"feature": feature_entry,
|
||||
}
|
||||
if sync_ibd is not None:
|
||||
service_detail["sync_ibd"] = sync_ibd
|
||||
service_detail["sync_progress"] = sync_progress
|
||||
service_detail["sync_blocks"] = sync_blocks
|
||||
service_detail["sync_headers"] = sync_headers
|
||||
if unit == "bitcoind.service" and enabled:
|
||||
loop = asyncio.get_event_loop()
|
||||
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
|
||||
if raw_ver is not None:
|
||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
||||
service_detail["version"] = btc_ver
|
||||
desktop_links = SERVICE_DESKTOP_LINKS.get(unit, [])
|
||||
if desktop_links:
|
||||
service_detail["desktop_links"] = desktop_links
|
||||
return service_detail
|
||||
|
||||
|
||||
@app.post("/api/desktop/launch/{desktop_file}")
|
||||
async def api_desktop_launch(desktop_file: str):
|
||||
"""Launch a desktop application via gtk-launch on the local GNOME session."""
|
||||
import re as _re
|
||||
if not _re.match(r'^[a-zA-Z0-9_.-]+\.desktop$', desktop_file):
|
||||
raise HTTPException(status_code=400, detail="Invalid desktop file name")
|
||||
|
||||
try:
|
||||
env = dict(os.environ)
|
||||
env["DISPLAY"] = ":0"
|
||||
result = subprocess.run(
|
||||
["gtk-launch", desktop_file],
|
||||
capture_output=True, text=True, timeout=10, env=env,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to launch: {result.stderr.strip()}")
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=500, detail="gtk-launch not found on this system")
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=500, detail="Launch command timed out")
|
||||
|
||||
return {"ok": True, "launched": desktop_file}
|
||||
|
||||
|
||||
@app.get("/api/network")
|
||||
@@ -1901,11 +2426,13 @@ async def api_support_status():
|
||||
"""Check if tech support SSH access is currently enabled."""
|
||||
loop = asyncio.get_event_loop()
|
||||
active = await loop.run_in_executor(None, _is_support_active)
|
||||
sshd_enabled = await loop.run_in_executor(None, _is_sshd_feature_enabled)
|
||||
session = await loop.run_in_executor(None, _get_support_session_info)
|
||||
unlock_info = await loop.run_in_executor(None, _get_wallet_unlock_info)
|
||||
wallet_unlocked = bool(unlock_info)
|
||||
return {
|
||||
"active": active,
|
||||
"sshd_enabled": sshd_enabled,
|
||||
"enabled_at": session.get("enabled_at"),
|
||||
"enabled_at_human": session.get("enabled_at_human"),
|
||||
"wallet_protected": session.get("wallet_protected", False),
|
||||
@@ -1919,8 +2446,18 @@ async def api_support_status():
|
||||
|
||||
@app.post("/api/support/enable")
|
||||
async def api_support_enable():
|
||||
"""Add the Sovran support SSH key to allow remote tech support."""
|
||||
"""Add the Sovran support SSH key to allow remote tech support.
|
||||
Requires the sshd feature to be enabled first."""
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Gate: SSH feature must be enabled before support can be activated
|
||||
sshd_on = await loop.run_in_executor(None, _is_sshd_feature_enabled)
|
||||
if not sshd_on:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="SSH must be enabled first. Please enable SSH Remote Access, then try again.",
|
||||
)
|
||||
|
||||
ok = await loop.run_in_executor(None, _enable_support)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=500, detail="Failed to enable support access")
|
||||
@@ -2082,8 +2619,14 @@ async def api_features():
|
||||
ssl_email_path = os.path.join(DOMAINS_DIR, "sslemail")
|
||||
ssl_email_configured = os.path.exists(ssl_email_path)
|
||||
|
||||
role = load_config().get("role", "server_plus_desktop")
|
||||
allowed_features = ROLE_FEATURES.get(role)
|
||||
registry = FEATURE_REGISTRY if allowed_features is None else [
|
||||
f for f in FEATURE_REGISTRY if f["id"] in allowed_features
|
||||
]
|
||||
|
||||
features = []
|
||||
for feat in FEATURE_REGISTRY:
|
||||
for feat in registry:
|
||||
feat_id = feat["id"]
|
||||
|
||||
# Determine enabled state:
|
||||
@@ -2160,7 +2703,7 @@ async def api_features_toggle(req: FeatureToggleRequest):
|
||||
status_code=400,
|
||||
detail=(
|
||||
"Element Calling requires a Matrix domain to be configured. "
|
||||
"Please run `sovran-setup-domains` first or configure the Matrix domain."
|
||||
"Element Calling requires a Matrix domain to be configured. Please configure it through the Sovran Hub web interface."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2380,6 +2923,196 @@ async def api_domains_check(req: DomainCheckRequest):
|
||||
return {"domains": list(check_results)}
|
||||
|
||||
|
||||
# ── Legacy security check ─────────────────────────────────────────
|
||||
|
||||
@app.get("/api/security/status")
|
||||
async def api_security_status():
|
||||
"""Return the legacy security status and warning message, if present.
|
||||
|
||||
Reads /var/lib/sovran/security-status and /var/lib/sovran/security-warning.
|
||||
Returns {"status": "legacy", "warning": "<message>"} for legacy machines,
|
||||
or {"status": "ok", "warning": ""} when the files are absent.
|
||||
"""
|
||||
try:
|
||||
with open(SECURITY_STATUS_FILE, "r") as f:
|
||||
status = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
status = "ok"
|
||||
|
||||
warning = ""
|
||||
if status == "legacy":
|
||||
try:
|
||||
with open(SECURITY_WARNING_FILE, "r") as f:
|
||||
warning = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
warning = (
|
||||
"This machine was manufactured before the factory-seal process. "
|
||||
"The default system password may be known to the factory. "
|
||||
"Please change your system and application passwords immediately."
|
||||
)
|
||||
elif status == "unsealed":
|
||||
try:
|
||||
with open(SECURITY_WARNING_FILE, "r") as f:
|
||||
warning = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
warning = (
|
||||
"This machine was set up without the factory seal process. "
|
||||
"Factory test data — including SSH keys, database contents, and wallet information — "
|
||||
"may still be present on this system."
|
||||
)
|
||||
|
||||
return {"status": status, "warning": warning}
|
||||
|
||||
|
||||
def _is_free_password_default() -> bool:
|
||||
"""Check /etc/shadow directly to see if 'free' still has a factory default password.
|
||||
|
||||
Hashes each known factory default against the current shadow hash so that
|
||||
password changes made via GNOME, passwd, or any method other than the Hub
|
||||
are detected correctly.
|
||||
"""
|
||||
import subprocess
|
||||
import re as _re
|
||||
|
||||
FACTORY_DEFAULTS = ["free", "gosovransystems"]
|
||||
# Map shadow algorithm IDs to openssl passwd flags (SHA-512 and SHA-256 only,
|
||||
# matching the shell-script counterpart in factory-seal.nix)
|
||||
ALGO_FLAGS = {"6": "-6", "5": "-5"}
|
||||
try:
|
||||
with open("/etc/shadow", "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(":")
|
||||
if parts[0] == "free" and len(parts) > 1:
|
||||
current_hash = parts[1]
|
||||
if not current_hash or current_hash in ("!", "*", "!!"):
|
||||
return True # locked/no password — treat as default
|
||||
# Parse hash: $id$[rounds=N$]salt$hash
|
||||
hash_fields = current_hash.split("$")
|
||||
# hash_fields: ["", id, salt_or_rounds, ...]
|
||||
if len(hash_fields) < 4:
|
||||
return True # unrecognized format — assume default for safety
|
||||
algo_id = hash_fields[1]
|
||||
salt_field = hash_fields[2]
|
||||
if algo_id not in ALGO_FLAGS:
|
||||
return True # unrecognized algorithm — assume default for safety
|
||||
if salt_field.startswith("rounds="):
|
||||
return True # can't extract real salt simply — assume default for safety
|
||||
# Validate salt contains only safe characters (alphanumeric, '.', '/', '-', '_')
|
||||
# to guard against unexpected shadow file content before passing to subprocess
|
||||
if not _re.fullmatch(r"[A-Za-z0-9./\-_]+", salt_field):
|
||||
return True # unexpected salt format — assume default for safety
|
||||
openssl_flag = ALGO_FLAGS[algo_id]
|
||||
for default_pw in FACTORY_DEFAULTS:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["openssl", "passwd", openssl_flag, "-salt", salt_field, default_pw],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip() == current_hash:
|
||||
return True
|
||||
except Exception:
|
||||
return True # if openssl fails, assume default for safety
|
||||
return False
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
return True # if /etc/shadow is unreadable, assume default for safety
|
||||
|
||||
|
||||
@app.get("/api/security/password-is-default")
|
||||
async def api_password_is_default():
|
||||
"""Check if the free account password is still the factory default.
|
||||
|
||||
Uses /etc/shadow as the authoritative source so that password changes made
|
||||
via GNOME Settings, the passwd command, or any other method are detected
|
||||
correctly — not just changes made through the Hub or change-free-password.
|
||||
"""
|
||||
return {"is_default": _is_free_password_default()}
|
||||
|
||||
|
||||
# ── System password change ────────────────────────────────────────
|
||||
|
||||
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
new_password: str
|
||||
confirm_password: str
|
||||
|
||||
|
||||
@app.post("/api/change-password")
|
||||
async def api_change_password(req: ChangePasswordRequest):
|
||||
"""Change the system 'free' user password.
|
||||
|
||||
Updates /etc/shadow via chpasswd and writes the new password to
|
||||
/var/lib/secrets/free-password so the Hub credentials view stays in sync.
|
||||
Also clears the legacy security-status and security-warning files so the
|
||||
security banner disappears after a successful change.
|
||||
"""
|
||||
if not req.new_password:
|
||||
raise HTTPException(status_code=400, detail="New password must not be empty.")
|
||||
if req.new_password != req.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match.")
|
||||
if len(req.new_password) < 8:
|
||||
raise HTTPException(status_code=400, detail="Password must be at least 8 characters long.")
|
||||
|
||||
# Locate chpasswd binary (NixOS puts it in the Nix store, not /usr/bin)
|
||||
chpasswd_bin = (
|
||||
shutil.which("chpasswd")
|
||||
or ("/run/current-system/sw/bin/chpasswd"
|
||||
if os.path.isfile("/run/current-system/sw/bin/chpasswd") else None)
|
||||
)
|
||||
if chpasswd_bin is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="chpasswd binary not found. Cannot update system password.",
|
||||
)
|
||||
|
||||
# Update /etc/shadow via chpasswd
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[chpasswd_bin],
|
||||
input=f"free:{req.new_password}",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout).strip() or "chpasswd failed."
|
||||
raise HTTPException(status_code=500, detail=detail)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update system password: {exc}")
|
||||
|
||||
# Write new password to secrets file so Hub credentials stay in sync
|
||||
try:
|
||||
os.makedirs(os.path.dirname(FREE_PASSWORD_FILE), exist_ok=True)
|
||||
with open(FREE_PASSWORD_FILE, "w") as f:
|
||||
f.write(req.new_password)
|
||||
os.chmod(FREE_PASSWORD_FILE, 0o600)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}")
|
||||
|
||||
# Clear legacy security status so the warning banner is removed — but only
|
||||
# for "legacy" machines (pre-seal era). For "unsealed" machines, changing
|
||||
# passwords is not enough; the factory residue (SSH keys, wallet data,
|
||||
# databases) remains until a proper re-seal or re-install is performed.
|
||||
try:
|
||||
with open(SECURITY_STATUS_FILE, "r") as f:
|
||||
current_status = f.read().strip()
|
||||
if current_status == "legacy":
|
||||
os.remove(SECURITY_STATUS_FILE)
|
||||
try:
|
||||
os.remove(SECURITY_WARNING_FILE)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Matrix user management ────────────────────────────────────────
|
||||
|
||||
MATRIX_USERS_FILE = "/var/lib/secrets/matrix-users"
|
||||
@@ -2567,3 +3300,55 @@ async def _startup_save_ip():
|
||||
loop = asyncio.get_event_loop()
|
||||
ip = await loop.run_in_executor(None, _get_internal_ip)
|
||||
_save_internal_ip(ip)
|
||||
|
||||
|
||||
# ── Startup: recover stale RUNNING status files ──────────────────
|
||||
|
||||
_SAFE_UNIT_RE = re.compile(r'^[a-zA-Z0-9@._\-]+\.service$')
|
||||
|
||||
|
||||
def _recover_stale_status(status_file: str, log_file: str, unit_name: str):
|
||||
"""If status_file says RUNNING but the systemd unit is not active, reset to FAILED."""
|
||||
if not _SAFE_UNIT_RE.match(unit_name):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(status_file, "r") as f:
|
||||
status = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if status != "RUNNING":
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", unit_name],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
active = result.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = False
|
||||
|
||||
if not active:
|
||||
try:
|
||||
with open(status_file, "w") as f:
|
||||
f.write("FAILED")
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with open(log_file, "a") as f:
|
||||
f.write(
|
||||
"\n[Hub] Process was interrupted (stale RUNNING status detected"
|
||||
" on startup). Marking as failed.\n"
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def _startup_recover_stale_status():
|
||||
"""Reset stale RUNNING status files left by interrupted update/rebuild jobs."""
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, _recover_stale_status, UPDATE_STATUS, UPDATE_LOG, UPDATE_UNIT)
|
||||
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
.header-bar {
|
||||
background-color: var(--surface-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 16px 24px;
|
||||
padding: 8px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 140px;
|
||||
height: 80px;
|
||||
width: auto;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-bar .title {
|
||||
|
||||
@@ -82,6 +82,62 @@
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* ── Sidebar: Upgrade button (Node role) ────────────────────────── */
|
||||
|
||||
.sidebar-upgrade-btn {
|
||||
border-color: var(--accent-color);
|
||||
background-color: rgba(137, 180, 250, 0.06);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-upgrade-btn:hover {
|
||||
background-color: rgba(137, 180, 250, 0.14);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.sidebar-upgrade-btn .sidebar-support-hint {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ── Upgrade modal ──────────────────────────────────────────────── */
|
||||
|
||||
.upgrade-dialog {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.upgrade-info-box {
|
||||
background-color: var(--card-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.upgrade-info-title {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upgrade-info-list {
|
||||
padding-left: 20px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upgrade-info-list a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.upgrade-rebuild-note {
|
||||
font-style: italic;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* ── Tiles area ─────────────────────────────────────────────────── */
|
||||
|
||||
#tiles-area {
|
||||
|
||||
@@ -147,6 +147,17 @@ button.btn-reboot:hover:not(:disabled) {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.creds-title-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
vertical-align: middle;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creds-close-btn {
|
||||
|
||||
@@ -146,17 +146,6 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.onboarding-card--scroll {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
}
|
||||
|
||||
.onboarding-card--ports {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Body text */
|
||||
|
||||
.onboarding-body-text {
|
||||
@@ -228,7 +217,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 4px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.onboarding-btn-next {
|
||||
@@ -575,6 +566,110 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Password step (Step 2) ─────────────────────────────────────── */
|
||||
|
||||
.onboarding-password-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.onboarding-password-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.onboarding-password-input {
|
||||
flex: 1;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-btn);
|
||||
background-color: var(--card-color);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.88rem;
|
||||
font-family: 'Cantarell', 'Inter', 'Segoe UI', sans-serif;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.onboarding-password-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.onboarding-password-toggle {
|
||||
padding: 6px 10px;
|
||||
background-color: var(--card-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-btn);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.onboarding-password-toggle:hover {
|
||||
background-color: rgba(137, 180, 250, 0.12);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.onboarding-password-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.onboarding-password-warning {
|
||||
padding: 10px 14px;
|
||||
background-color: rgba(229, 165, 10, 0.1);
|
||||
border: 1px solid rgba(229, 165, 10, 0.35);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--yellow);
|
||||
line-height: 1.5;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.onboarding-password-success {
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(166, 227, 161, 0.1);
|
||||
border: 1px solid rgba(166, 227, 161, 0.35);
|
||||
border-radius: 8px;
|
||||
font-size: 0.92rem;
|
||||
color: var(--green);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.onboarding-password-optional {
|
||||
margin-top: 12px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.onboarding-password-optional > summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.onboarding-password-optional > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-password-optional > summary::before {
|
||||
content: '▶ ';
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
.onboarding-password-optional[open] > summary::before {
|
||||
content: '▼ ';
|
||||
}
|
||||
|
||||
/* ── Reboot overlay ─────────────────────────────────────────────── */
|
||||
|
||||
.reboot-overlay {
|
||||
|
||||
107
app/sovran_systemsos_web/static/css/security.css
Normal file
@@ -0,0 +1,107 @@
|
||||
/* ── Legacy security inline warning banner ───────────────────────── */
|
||||
|
||||
.security-inline-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
background-color: rgba(180, 100, 0, 0.12);
|
||||
border-left: 3px solid #c97a00;
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.security-inline-icon {
|
||||
font-size: 1rem;
|
||||
color: #e69000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.security-inline-text {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.security-inline-link {
|
||||
display: inline-block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: #e69000;
|
||||
text-decoration: none;
|
||||
border: 1px solid #c97a00;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
align-self: flex-start;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.security-inline-link:hover {
|
||||
background-color: rgba(180, 100, 0, 0.22);
|
||||
}
|
||||
|
||||
/* ── System change-password form extras ──────────────────────────── */
|
||||
|
||||
.sys-chpw-header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sys-chpw-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sys-chpw-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pw-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pw-input-wrap .matrix-form-input {
|
||||
padding-right: 2.4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pw-toggle-btn {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.pw-toggle-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pw-hint {
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pw-credentials-note {
|
||||
font-size: 0.78rem;
|
||||
color: #c97a00;
|
||||
background-color: rgba(180, 100, 0, 0.10);
|
||||
border-left: 2px solid #c97a00;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
.support-active-icon {
|
||||
animation: pulse-badge 2s ease-in-out infinite;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.support-heading {
|
||||
|
||||
@@ -71,6 +71,13 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tile-version {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -85,6 +92,65 @@
|
||||
.status-dot.failed { background-color: var(--red); }
|
||||
.status-dot.disabled { background-color: var(--grey); }
|
||||
.status-dot.needs-attention { background-color: var(--yellow); }
|
||||
.status-dot.syncing { background-color: #f5a623; animation: pulse-badge 1.5s infinite; }
|
||||
|
||||
/* ── Bitcoin IBD sync progress bar ──────────────────────────────── */
|
||||
|
||||
.tile-sync-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.tile-sync-label {
|
||||
font-size: 0.72rem;
|
||||
color: #f5a623;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tile-sync-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tile-sync-bar-track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background-color: var(--border-color);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tile-sync-bar-fill {
|
||||
height: 100%;
|
||||
background-color: #f5a623;
|
||||
border-radius: 3px;
|
||||
transition: width 0.6s ease;
|
||||
min-width: 2px;
|
||||
}
|
||||
|
||||
.tile-sync-percent {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: #f5a623;
|
||||
white-space: nowrap;
|
||||
min-width: 2.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tile-sync-eta {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Service detail modal sections ───────────────────────────────── */
|
||||
|
||||
@@ -244,6 +310,12 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Service detail: Domain configure button ─────────────────────── */
|
||||
|
||||
.svc-detail-domain-btn {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ── Service detail: Addon feature toggle ────────────────────────── */
|
||||
|
||||
.svc-detail-addon-row {
|
||||
@@ -277,3 +349,18 @@
|
||||
color: var(--yellow);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
/* ── Desktop launch buttons ──────────────────────────────────────── */
|
||||
|
||||
.svc-detail-launch-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.svc-detail-launch-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
236
app/sovran_systemsos_web/static/icons/update.svg
Normal file
@@ -0,0 +1,236 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="128px"
|
||||
height="128px"
|
||||
viewBox="0 0 128 128"
|
||||
version="1.1"
|
||||
id="svg96"
|
||||
sodipodi:docname="Sovran_SystemsOS_Updater_Iconv3.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview98"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.2149125"
|
||||
inkscape:cx="9.0126153"
|
||||
inkscape:cy="64.430611"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1352"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2" /><defs
|
||||
id="defs67"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient936"><stop
|
||||
style="stop-color:#1e8e11;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop932" /><stop
|
||||
style="stop-color:#1bff00;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop934" /></linearGradient><linearGradient
|
||||
id="linearGradient1028"
|
||||
inkscape:swatch="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop1026" /></linearGradient><linearGradient
|
||||
id="linearGradient998"
|
||||
inkscape:swatch="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop996" /></linearGradient><radialGradient
|
||||
id="radial0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="131.914749"
|
||||
cy="55.927143"
|
||||
fx="131.914749"
|
||||
fy="55.927143"
|
||||
r="160"
|
||||
gradientTransform="matrix(0.232034,-0.541475,-0.368794,-0.0298398,4.277749,118.95849)"><stop
|
||||
offset="0"
|
||||
style="stop-color:#00ff39;stop-opacity:1;"
|
||||
id="stop2" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#004a19;stop-opacity:1;"
|
||||
id="stop4" /></radialGradient><radialGradient
|
||||
id="radial1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="525.587769"
|
||||
cy="638.591797"
|
||||
fx="525.587769"
|
||||
fy="638.591797"
|
||||
r="192"
|
||||
gradientTransform="matrix(-0.107656,-0.225172,-0.327748,0.258343,373.87973,30.205086)"><stop
|
||||
offset="0"
|
||||
style="stop-color:#43b60b;stop-opacity:1;"
|
||||
id="stop7" /><stop
|
||||
offset="1"
|
||||
style="stop-color:#0b88ff;stop-opacity:0.00829875;"
|
||||
id="stop9" /></radialGradient><clipPath
|
||||
id="clip1"><path
|
||||
d="M 7 46 L 57 46 L 57 93 L 7 93 Z M 7 46 "
|
||||
id="path12" /></clipPath><clipPath
|
||||
id="clip2"><path
|
||||
d="M 32.25 46.957031 C 19.6875 46.96875 9.085938 56.636719 7.503906 69.53125 C 9.0625 82.445312 19.667969 92.144531 32.25 92.160156 C 44.816406 92.148438 55.414062 82.480469 57 69.585938 C 55.441406 56.671875 44.835938 46.972656 32.25 46.957031 Z M 32.25 46.957031 "
|
||||
id="path15" /></clipPath><radialGradient
|
||||
id="radial2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="131.914749"
|
||||
cy="55.927143"
|
||||
fx="131.914749"
|
||||
fy="55.927143"
|
||||
r="160"
|
||||
gradientTransform="matrix(0.485163,-1.148584,-0.771115,-0.0632965,-47.124961,203.98857)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(92.941177%,20%,23.137255%);stop-opacity:1;"
|
||||
id="stop18" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(63.921571%,27.843139%,72.941178%);stop-opacity:1;"
|
||||
id="stop20" /></radialGradient><radialGradient
|
||||
id="radial3"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="525.587769"
|
||||
cy="638.591797"
|
||||
fx="525.587769"
|
||||
fy="638.591797"
|
||||
r="192"
|
||||
gradientTransform="matrix(-0.225099,-0.477638,-0.685291,0.548001,725.67923,15.723794)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(10.980392%,44.313726%,84.705883%);stop-opacity:1;"
|
||||
id="stop23" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(20.784314%,51.764709%,89.411765%);stop-opacity:0.00829876;"
|
||||
id="stop25" /></radialGradient><linearGradient
|
||||
id="linear0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="22"
|
||||
y1="37"
|
||||
x2="62"
|
||||
y2="37"
|
||||
gradientTransform="matrix(1.4,0,0,1.4,-26.799973,2.491745)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(58.039218%,57.647061%,56.470591%);stop-opacity:1;"
|
||||
id="stop28" /><stop
|
||||
offset="0.0908155"
|
||||
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
|
||||
id="stop30" /><stop
|
||||
offset="0.336093"
|
||||
style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"
|
||||
id="stop32" /><stop
|
||||
offset="0.844326"
|
||||
style="stop-color:rgb(76.47059%,75.294119%,72.941178%);stop-opacity:1;"
|
||||
id="stop34" /><stop
|
||||
offset="0.930505"
|
||||
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
|
||||
id="stop36" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"
|
||||
id="stop38" /></linearGradient><radialGradient
|
||||
id="radial4"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="-172.560638"
|
||||
cy="28.569126"
|
||||
fx="-172.560638"
|
||||
fy="28.569126"
|
||||
r="15.85742"
|
||||
gradientTransform="matrix(1.560712,0,0,1.4252,300.69366,13.349996)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.358268;"
|
||||
id="stop41" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.0944882;"
|
||||
id="stop43" /></radialGradient><filter
|
||||
id="alpha"
|
||||
filterUnits="objectBoundingBox"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1"
|
||||
height="1"><feColorMatrix
|
||||
type="matrix"
|
||||
in="SourceGraphic"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
id="feColorMatrix46" /></filter><mask
|
||||
id="mask0"><g
|
||||
filter="url(#alpha)"
|
||||
id="g51"><rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="128"
|
||||
height="128"
|
||||
style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"
|
||||
id="rect49" /></g></mask><clipPath
|
||||
id="clip3"><rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="192"
|
||||
height="152"
|
||||
id="rect54" /></clipPath><g
|
||||
id="surface382"
|
||||
clip-path="url(#clip3)"><path
|
||||
style=" stroke:none;fill-rule:nonzero;fill:rgb(27.058825%,21.176471%,21.568628%);fill-opacity:1;"
|
||||
d="M 40 59.957031 C 26.191406 59.957031 15 71.152344 15 84.957031 C 15.011719 85.996094 15.085938 86.777344 15.222656 87.804688 C 15.222656 75.957031 27.421875 65.96875 40 65.957031 C 52.597656 65.972656 64.777344 75.957031 64.777344 87.859375 C 64.917969 86.816406 64.992188 86.011719 65 84.957031 C 65 71.152344 53.808594 59.957031 40 59.957031 Z M 40 59.957031 "
|
||||
id="path57" /></g><radialGradient
|
||||
id="radial5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="40"
|
||||
cy="227"
|
||||
fx="40"
|
||||
fy="227"
|
||||
r="28"
|
||||
gradientTransform="matrix(0.575553,0,1.60551e-8,1.540703,8.977913,-280.78108)"><stop
|
||||
offset="0"
|
||||
style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"
|
||||
id="stop60" /><stop
|
||||
offset="0.744626"
|
||||
style="stop-color:rgb(98.039216%,98.039216%,98.039216%);stop-opacity:1;"
|
||||
id="stop62" /><stop
|
||||
offset="1"
|
||||
style="stop-color:rgb(87.450981%,87.450981%,87.450981%);stop-opacity:1;"
|
||||
id="stop64" /></radialGradient>
|
||||
|
||||
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient936"
|
||||
id="linearGradient938"
|
||||
x1="-48.519272"
|
||||
y1="18.511358"
|
||||
x2="287.07454"
|
||||
y2="18.511358"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1020247,0,0,1.1097375,37.198581,-10.424856)" /></defs><path
|
||||
style="fill:#f5f5f3;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 20,11.957031 h 88 c 4.41797,0 8,3.582031 8,8 V 108 c 0,4.41797 -3.58203,8 -8,8 H 20 c -4.417969,0 -8,-3.58203 -8,-8 V 19.957031 c 0,-4.417969 3.582031,-8 8,-8 z m 0,0"
|
||||
id="path69" /><path
|
||||
style="fill:url(#radial0);fill-rule:nonzero;stroke:none"
|
||||
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
|
||||
id="path71" /><path
|
||||
style="fill:none;fill-rule:nonzero;stroke:none;fill-opacity:1"
|
||||
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
|
||||
id="path73" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 1"
|
||||
transform="matrix(0.1816,0,0,0.1816,35.224187,79.037164)"><ellipse
|
||||
fill="#54c147"
|
||||
cx="168.64549"
|
||||
cy="10.117889"
|
||||
id="circle8314"
|
||||
rx="184.91634"
|
||||
ry="179.91556"
|
||||
style="fill:url(#linearGradient938);fill-opacity:1;stroke-width:1.71591" /><polygon
|
||||
fill="#ffffff"
|
||||
points="46.678,120.299 63.562,96.402 96.977,121.718 145.084,50.79 168.752,69.02 103.583,164.647 "
|
||||
id="polygon8316"
|
||||
transform="matrix(1.7395866,0,0,1.6925423,-18.737581,-172.19767)" /></g></svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
// ── Event listeners ───────────────────────────────────────────────
|
||||
|
||||
if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal);
|
||||
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
|
||||
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
|
||||
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
||||
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
||||
@@ -33,6 +33,43 @@ if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $mod
|
||||
if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); });
|
||||
if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); });
|
||||
|
||||
// Upgrade modal
|
||||
if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeModal);
|
||||
if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal);
|
||||
if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) closeUpgradeModal(); });
|
||||
|
||||
// ── Upgrade modal functions ───────────────────────────────────────
|
||||
|
||||
function openUpgradeModal() {
|
||||
if ($upgradeModal) $upgradeModal.classList.add("open");
|
||||
}
|
||||
|
||||
function closeUpgradeModal() {
|
||||
if ($upgradeModal) $upgradeModal.classList.remove("open");
|
||||
}
|
||||
|
||||
async function doUpgradeToServer() {
|
||||
var confirmBtn = $upgradeConfirmBtn;
|
||||
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; }
|
||||
closeUpgradeModal();
|
||||
|
||||
// Reuse the rebuild modal to show progress
|
||||
_rebuildFeatureName = "Server + Desktop";
|
||||
_rebuildIsEnabling = true;
|
||||
openRebuildModal();
|
||||
|
||||
try {
|
||||
await apiFetch("/api/role/upgrade-to-server", { method: "POST" });
|
||||
} catch (err) {
|
||||
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Upgrade failed: " + err.message;
|
||||
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
||||
if ($rebuildClose) $rebuildClose.disabled = false;
|
||||
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = "Yes, Upgrade"; }
|
||||
}
|
||||
}
|
||||
|
||||
if ($upgradeConfirmBtn) $upgradeConfirmBtn.addEventListener("click", doUpgradeToServer);
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────
|
||||
|
||||
async function init() {
|
||||
@@ -47,8 +84,12 @@ async function init() {
|
||||
// If we can't reach the endpoint, continue to normal dashboard
|
||||
}
|
||||
|
||||
// Check for legacy machine security warning
|
||||
await checkLegacySecurity();
|
||||
|
||||
try {
|
||||
var cfg = await apiFetch("/api/config");
|
||||
_currentRole = cfg.role || "server_plus_desktop";
|
||||
if (cfg.category_order) {
|
||||
for (var i = 0; i < cfg.category_order.length; i++) {
|
||||
_categoryLabels[cfg.category_order[i][0]] = cfg.category_order[i][1];
|
||||
@@ -67,12 +108,14 @@ async function init() {
|
||||
if (cfg.feature_manager) {
|
||||
loadFeatureManager();
|
||||
}
|
||||
loadAutolaunchToggle();
|
||||
} catch (_) {
|
||||
await refreshServices();
|
||||
loadNetwork();
|
||||
checkUpdates();
|
||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||
loadAutolaunchToggle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -579,3 +579,86 @@ function buildFeatureCard(feat) {
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// ── Auto-launch toggle ────────────────────────────────────────────
|
||||
|
||||
async function loadAutolaunchToggle() {
|
||||
try {
|
||||
var data = await apiFetch("/api/autolaunch/status");
|
||||
renderAutolaunchToggle(data.enabled);
|
||||
} catch (err) {
|
||||
console.warn("Failed to load autolaunch status:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAutolaunchToggle(enabled) {
|
||||
// Remove existing section if any
|
||||
var old = $sidebarFeatures.querySelector(".autolaunch-section");
|
||||
if (old) old.parentNode.removeChild(old);
|
||||
|
||||
var section = document.createElement("div");
|
||||
section.className = "category-section autolaunch-section";
|
||||
|
||||
var securityBanner = "";
|
||||
if (_securityIsLegacy) {
|
||||
var msg = _securityWarningMessage || "Your system may have factory default passwords. Please change your passwords to secure your system.";
|
||||
var linkText, linkAction;
|
||||
if (_securityStatus === "unsealed") {
|
||||
linkText = "Contact Support";
|
||||
linkAction = "openSupportModal(); return false;";
|
||||
} else {
|
||||
linkText = "Change Passwords";
|
||||
linkAction = "openServiceDetailModal('root-password-setup.service', 'System Passwords', 'passwords'); return false;";
|
||||
}
|
||||
securityBanner =
|
||||
'<div class="security-inline-banner">' +
|
||||
'<span class="security-inline-icon">⚠</span>' +
|
||||
'<span class="security-inline-text">' + msg + '</span>' +
|
||||
'<a class="security-inline-link" href="#" onclick="' + linkAction + '">' + linkText + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
section.innerHTML =
|
||||
'<div class="section-header">Preferences</div>' +
|
||||
'<hr class="section-divider" />' +
|
||||
securityBanner +
|
||||
'<div class="feature-card">' +
|
||||
'<div class="feature-card-top">' +
|
||||
'<div class="feature-card-info">' +
|
||||
'<div class="feature-card-name">Auto-launch Hub on Login</div>' +
|
||||
'<div class="feature-card-desc">Automatically open the Sovran Hub dashboard in your browser when you log in to the desktop.</div>' +
|
||||
'</div>' +
|
||||
'<label class="feature-toggle' + (enabled ? " active" : "") + '" id="autolaunch-toggle-label" title="Toggle auto-launch">' +
|
||||
'<input type="checkbox" class="feature-toggle-input" id="autolaunch-toggle-input"' + (enabled ? " checked" : "") + ' />' +
|
||||
'<span class="feature-toggle-slider"></span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$sidebarFeatures.appendChild(section);
|
||||
|
||||
var input = document.getElementById("autolaunch-toggle-input");
|
||||
var label = document.getElementById("autolaunch-toggle-label");
|
||||
if (!input || !label) return;
|
||||
|
||||
input.addEventListener("change", async function() {
|
||||
var newEnabled = input.checked;
|
||||
// Revert visually until confirmed
|
||||
input.checked = !newEnabled;
|
||||
if (newEnabled) { label.classList.remove("active"); } else { label.classList.add("active"); }
|
||||
input.disabled = true;
|
||||
try {
|
||||
await apiFetch("/api/autolaunch/toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: newEnabled }),
|
||||
});
|
||||
input.checked = newEnabled;
|
||||
if (newEnabled) { label.classList.add("active"); } else { label.classList.remove("active"); }
|
||||
} catch (err) {
|
||||
alert("Failed to update auto-launch setting. Please try again.");
|
||||
} finally {
|
||||
input.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ function statusClass(health) {
|
||||
if (health === "inactive") return "inactive";
|
||||
if (health === "failed") return "failed";
|
||||
if (health === "disabled") return "disabled";
|
||||
if (health === "syncing") return "syncing";
|
||||
if (STATUS_LOADING_STATES.has(health)) return "loading";
|
||||
return "unknown";
|
||||
}
|
||||
@@ -23,6 +24,7 @@ function statusText(health, enabled) {
|
||||
if (health === "active") return "Active";
|
||||
if (health === "inactive") return "Inactive";
|
||||
if (health === "failed") return "Failed";
|
||||
if (health === "syncing") return "Syncing\u2026";
|
||||
if (!health || health === "unknown") return "Unknown";
|
||||
if (STATUS_LOADING_STATES.has(health)) return health;
|
||||
return health;
|
||||
|
||||
16
app/sovran_systemsos_web/static/js/security.js
Normal file
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
// ── Legacy security warning ───────────────────────────────────────
|
||||
|
||||
async function checkLegacySecurity() {
|
||||
try {
|
||||
var data = await apiFetch("/api/security/status");
|
||||
if (data && (data.status === "legacy" || data.status === "unsealed")) {
|
||||
_securityIsLegacy = true;
|
||||
_securityStatus = data.status;
|
||||
_securityWarningMessage = data.warning || "This machine may have a security issue. Please review your system security.";
|
||||
}
|
||||
} catch (_) {
|
||||
// Non-fatal — silently ignore if the endpoint is unreachable
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,20 @@ function _attachCopyHandlers(container) {
|
||||
|
||||
async function openServiceDetailModal(unit, name, icon) {
|
||||
if (!$credsModal) return;
|
||||
if ($credsTitle) $credsTitle.textContent = name;
|
||||
if ($credsTitle) {
|
||||
$credsTitle.innerHTML = '';
|
||||
if (icon) {
|
||||
var iconImg = document.createElement("img");
|
||||
iconImg.className = "creds-title-icon";
|
||||
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
|
||||
iconImg.alt = name;
|
||||
iconImg.onerror = function() { this.style.display = "none"; };
|
||||
$credsTitle.appendChild(iconImg);
|
||||
}
|
||||
var nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = name;
|
||||
$credsTitle.appendChild(nameSpan);
|
||||
}
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
|
||||
$credsModal.classList.add("open");
|
||||
|
||||
@@ -72,6 +85,21 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Section: Desktop Launch (only for services with desktop apps)
|
||||
if (data.desktop_links && data.desktop_links.length > 0) {
|
||||
var launchBtns = '';
|
||||
data.desktop_links.forEach(function(link) {
|
||||
launchBtns += '<button class="btn btn-primary svc-detail-launch-btn" data-desktop="' + escHtml(link.desktop_file) + '">' +
|
||||
'🖥️ ' + escHtml(link.label) +
|
||||
'</button>';
|
||||
});
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Open on Desktop</div>' +
|
||||
'<p class="svc-detail-desc" style="margin-bottom:12px">If you are accessing this machine locally on the GNOME desktop, click below to launch the app directly.</p>' +
|
||||
'<div class="svc-detail-launch-row">' + launchBtns + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Section B: Status
|
||||
// When a feature override is present, use the feature's enabled state so the
|
||||
// modal matches what the dashboard tile shows (feature toggle is authoritative).
|
||||
@@ -216,8 +244,8 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<li>Find the domain you purchased for this service</li>' +
|
||||
'<li>Create a Dynamic DNS record pointing to your external IP: <code>' + escHtml(ds.expected_ip || "—") + '</code></li>' +
|
||||
'<li>Copy the DDNS curl command from Njal.la\'s dashboard</li>' +
|
||||
'<li>You can re-enter it in the Feature Manager to update your configuration</li>' +
|
||||
'</ol>' +
|
||||
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
domainBadge = '<span class="svc-detail-domain-value">' + escHtml(data.domain) + '</span>';
|
||||
@@ -229,9 +257,9 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<p style="margin-top:8px">To get this service working:</p>' +
|
||||
'<ol>' +
|
||||
'<li>Purchase a subdomain at <a href="https://njal.la" target="_blank">njal.la</a> (if you haven\'t already)</li>' +
|
||||
'<li>Go to the <strong>Feature Manager</strong> in the sidebar</li>' +
|
||||
'<li>Find this service and configure your domain through the setup wizard</li>' +
|
||||
'<li>Use the button below to configure your domain through the setup wizard</li>' +
|
||||
'</ol>' +
|
||||
'<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-config-domain-btn">🌐 Configure Domain</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -252,6 +280,10 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<button class="matrix-action-btn" id="matrix-add-user-btn">➕ Add New User</button>' +
|
||||
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
|
||||
'</div>' : "") +
|
||||
(unit === "root-password-setup.service" ?
|
||||
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
|
||||
'<button class="matrix-action-btn" id="sys-change-pw-btn">🔑 Change Password</button>' +
|
||||
'</div>' : "") +
|
||||
'</div>';
|
||||
} else if (!data.enabled && !data.feature) {
|
||||
html += '<div class="svc-detail-section">' +
|
||||
@@ -312,6 +344,25 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
$credsBody.innerHTML = html;
|
||||
_attachCopyHandlers($credsBody);
|
||||
|
||||
// Desktop launch button handlers
|
||||
$credsBody.querySelectorAll(".svc-detail-launch-btn").forEach(function(btn) {
|
||||
btn.addEventListener("click", async function() {
|
||||
var desktopFile = btn.dataset.desktop;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Launching…";
|
||||
try {
|
||||
await apiFetch("/api/desktop/launch/" + encodeURIComponent(desktopFile), {
|
||||
method: "POST"
|
||||
});
|
||||
btn.textContent = "✓ Launched!";
|
||||
setTimeout(function() { btn.textContent = "🖥️ " + btn.textContent; btn.disabled = false; }, 2000);
|
||||
} catch (err) {
|
||||
btn.textContent = "❌ Failed";
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (unit === "matrix-synapse.service") {
|
||||
var addBtn = document.getElementById("matrix-add-user-btn");
|
||||
var changePwBtn = document.getElementById("matrix-change-pw-btn");
|
||||
@@ -319,6 +370,11 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name, icon); });
|
||||
}
|
||||
|
||||
if (unit === "root-password-setup.service") {
|
||||
var sysPwBtn = document.getElementById("sys-change-pw-btn");
|
||||
if (sysPwBtn) sysPwBtn.addEventListener("click", function() { openSystemChangePasswordModal(unit, name, icon); });
|
||||
}
|
||||
|
||||
if (data.feature) {
|
||||
var addonBtn = document.getElementById("svc-detail-addon-btn");
|
||||
if (addonBtn) {
|
||||
@@ -329,6 +385,26 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Domain button (for non-feature services that need a domain)
|
||||
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
|
||||
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
|
||||
var domainBtn = configDomainBtn || reconfigDomainBtn;
|
||||
if (domainBtn && data.needs_domain && data.domain_name) {
|
||||
var pseudoFeat = {
|
||||
id: data.domain_name,
|
||||
name: name,
|
||||
domain_name: data.domain_name,
|
||||
needs_ddns: true,
|
||||
extra_fields: []
|
||||
};
|
||||
domainBtn.addEventListener("click", function() {
|
||||
closeCredsModal();
|
||||
openDomainSetupModal(pseudoFeat, function() {
|
||||
openServiceDetailModal(unit, name, icon);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
|
||||
}
|
||||
@@ -336,9 +412,22 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
|
||||
// ── Credentials info modal ────────────────────────────────────────
|
||||
|
||||
async function openCredsModal(unit, name) {
|
||||
async function openCredsModal(unit, name, icon) {
|
||||
if (!$credsModal) return;
|
||||
if ($credsTitle) $credsTitle.textContent = name + " — Connection Info";
|
||||
if ($credsTitle) {
|
||||
$credsTitle.innerHTML = '';
|
||||
if (icon) {
|
||||
var iconImg = document.createElement("img");
|
||||
iconImg.className = "creds-title-icon";
|
||||
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
|
||||
iconImg.alt = name;
|
||||
iconImg.onerror = function() { this.style.display = "none"; };
|
||||
$credsTitle.appendChild(iconImg);
|
||||
}
|
||||
var nameSpan = document.createElement("span");
|
||||
nameSpan.textContent = name + " — Connection Info";
|
||||
$credsTitle.appendChild(nameSpan);
|
||||
}
|
||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
|
||||
$credsModal.classList.add("open");
|
||||
try {
|
||||
@@ -475,4 +564,104 @@ function openMatrixChangePasswordModal(unit, name, icon) {
|
||||
});
|
||||
}
|
||||
|
||||
function openSystemChangePasswordModal(unit, name, icon) {
|
||||
if (!$credsBody) return;
|
||||
$credsBody.innerHTML =
|
||||
'<div class="sys-chpw-header">' +
|
||||
'<div class="sys-chpw-title">🔑 Change \'free\' Account Password</div>' +
|
||||
'<div class="sys-chpw-desc">This updates the system login password for the <strong>free</strong> user account on this device.</div>' +
|
||||
'</div>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-new">New Password</label>' +
|
||||
'<div class="pw-input-wrap">' +
|
||||
'<input class="matrix-form-input" type="password" id="sys-chpw-new" placeholder="New strong password" autocomplete="new-password">' +
|
||||
'<button type="button" class="pw-toggle-btn" id="sys-chpw-new-toggle" aria-label="Toggle password visibility">👁</button>' +
|
||||
'</div>' +
|
||||
'<div class="pw-hint">Password must be at least 8 characters.</div></div>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-confirm">Confirm Password</label>' +
|
||||
'<div class="pw-input-wrap">' +
|
||||
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password">' +
|
||||
'<button type="button" class="pw-toggle-btn" id="sys-chpw-confirm-toggle" aria-label="Toggle password visibility">👁</button>' +
|
||||
'</div></div>' +
|
||||
'<div class="pw-credentials-note">⚠ After changing, your updated password will appear in the System Passwords credentials tile. Make sure to remember it.</div>' +
|
||||
'<div class="matrix-form-actions">' +
|
||||
'<button class="matrix-form-back" id="sys-chpw-back-btn">← Back</button>' +
|
||||
'<button class="matrix-form-submit" id="sys-chpw-submit-btn">Change Password</button>' +
|
||||
'</div>' +
|
||||
'<div class="matrix-form-result" id="sys-chpw-result"></div>';
|
||||
|
||||
document.getElementById("sys-chpw-back-btn").addEventListener("click", function() {
|
||||
openServiceDetailModal(unit, name, icon);
|
||||
});
|
||||
|
||||
document.getElementById("sys-chpw-new-toggle").addEventListener("click", function() {
|
||||
var inp = document.getElementById("sys-chpw-new");
|
||||
var isHidden = inp.type === "password";
|
||||
inp.type = isHidden ? "text" : "password";
|
||||
this.textContent = isHidden ? "👁🗨" : "👁";
|
||||
});
|
||||
|
||||
document.getElementById("sys-chpw-confirm-toggle").addEventListener("click", function() {
|
||||
var inp = document.getElementById("sys-chpw-confirm");
|
||||
var isHidden = inp.type === "password";
|
||||
inp.type = isHidden ? "text" : "password";
|
||||
this.textContent = isHidden ? "👁🗨" : "👁";
|
||||
});
|
||||
|
||||
document.getElementById("sys-chpw-submit-btn").addEventListener("click", async function() {
|
||||
var submitBtn = document.getElementById("sys-chpw-submit-btn");
|
||||
var resultEl = document.getElementById("sys-chpw-result");
|
||||
var newPassword = document.getElementById("sys-chpw-new").value || "";
|
||||
var confirmPassword = document.getElementById("sys-chpw-confirm").value || "";
|
||||
|
||||
if (!newPassword || !confirmPassword) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "Both password fields are required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "Password must be at least 8 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "Passwords do not match.";
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "Changing…";
|
||||
resultEl.className = "matrix-form-result";
|
||||
resultEl.textContent = "";
|
||||
|
||||
try {
|
||||
await apiFetch("/api/change-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ new_password: newPassword, confirm_password: confirmPassword })
|
||||
});
|
||||
resultEl.className = "matrix-form-result success";
|
||||
resultEl.textContent = "✅ System password changed successfully.";
|
||||
submitBtn.textContent = "Change Password";
|
||||
submitBtn.disabled = false;
|
||||
// Hide the legacy security banner if it's visible — but only for
|
||||
// "legacy" status machines. For "unsealed" machines, changing passwords
|
||||
// is not enough; the factory residue remains until a proper re-seal or re-install.
|
||||
if (typeof _securityIsLegacy !== "undefined" && _securityIsLegacy &&
|
||||
(typeof _securityStatus === "undefined" || _securityStatus !== "unsealed")) {
|
||||
_securityIsLegacy = false;
|
||||
var banner = document.querySelector(".security-inline-banner");
|
||||
if (banner) banner.remove();
|
||||
}
|
||||
} catch (err) {
|
||||
resultEl.className = "matrix-form-result error";
|
||||
resultEl.textContent = "❌ " + (err.message || "Failed to change password.");
|
||||
submitBtn.textContent = "Change Password";
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); }
|
||||
|
||||
@@ -15,6 +15,9 @@ let _supportStatus = null; // last fetched /api/support/status payload
|
||||
let _walletUnlockTimerInt = null;
|
||||
let _cachedExternalIp = null;
|
||||
|
||||
// Current role (set during init from /api/config)
|
||||
let _currentRole = "server_plus_desktop";
|
||||
|
||||
// Feature Manager state
|
||||
let _featuresData = null;
|
||||
let _rebuildLog = "";
|
||||
@@ -31,8 +34,9 @@ let _rebuildIsEnabling = true;
|
||||
const $tilesArea = document.getElementById("tiles-area");
|
||||
const $sidebarSupport = document.getElementById("sidebar-support");
|
||||
const $sidebarFeatures = document.getElementById("sidebar-features");
|
||||
const $updateBtn = document.getElementById("btn-update");
|
||||
const $updateBadge = document.getElementById("update-badge");
|
||||
// No longer needed — Update System moved to sidebar
|
||||
// const $updateBtn = document.getElementById("btn-update");
|
||||
// const $updateBadge = document.getElementById("update-badge");
|
||||
const $internalIp = document.getElementById("ip-internal");
|
||||
const $externalIp = document.getElementById("ip-external");
|
||||
|
||||
@@ -89,5 +93,16 @@ const $portReqModal = document.getElementById("port-requirements-modal");
|
||||
const $portReqBody = document.getElementById("port-req-body");
|
||||
const $portReqClose = document.getElementById("port-req-close-btn");
|
||||
|
||||
// Upgrade modal (Node → Server+Desktop)
|
||||
const $upgradeModal = document.getElementById("upgrade-modal");
|
||||
const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn");
|
||||
const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn");
|
||||
const $upgradeCloseBtn = document.getElementById("upgrade-close-btn");
|
||||
|
||||
// Legacy security warning state (populated by checkLegacySecurity in security.js)
|
||||
var _securityIsLegacy = false;
|
||||
var _securityStatus = "ok"; // "ok", "legacy", or "unsealed"
|
||||
var _securityWarningMessage = "";
|
||||
|
||||
// System status banner
|
||||
// (removed — health is now shown per-tile via the composite health field)
|
||||
@@ -10,12 +10,84 @@ async function openSupportModal() {
|
||||
var status = await apiFetch("/api/support/status");
|
||||
_supportStatus = status;
|
||||
if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(status); }
|
||||
else if (!status.sshd_enabled) { renderSupportSshdOff(); }
|
||||
else { renderSupportInactive(); }
|
||||
} catch (err) {
|
||||
$supportBody.innerHTML = '<p class="creds-empty">Could not check support status.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSupportSshdOff() {
|
||||
stopSupportTimer();
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">🛟</div>',
|
||||
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
|
||||
'<p class="support-desc">To get Tech Support, SSH must be enabled first. SSH is <strong>off by default</strong> for maximum security — it only needs to be on during a support session.</p>',
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">SSH is Off</span></div>',
|
||||
'<p class="support-wallet-desc">SSH (remote login) is <strong>disabled by default</strong> on your Sovran Pro. Clicking the button below will enable SSH and trigger a system rebuild. Once complete, you can then grant support access.</p>',
|
||||
'<p class="support-wallet-desc">When you end the support session, you\'ll be able to disable SSH to return to the default secure state.</p>',
|
||||
'</div>',
|
||||
'<div class="support-steps"><div class="support-steps-title">Steps:</div><ol>',
|
||||
'<li>Enable SSH (triggers a system rebuild — takes a few minutes)</li>',
|
||||
'<li>Grant Sovran Systems temporary support access</li>',
|
||||
'<li>End the session when done — you\'ll be prompted to disable SSH</li>',
|
||||
'</ol></div>',
|
||||
'<button class="btn support-btn-enable" id="btn-sshd-enable">Enable SSH</button>',
|
||||
'<p class="support-fine-print">This will trigger a NixOS rebuild. Your machine will remain operational during the rebuild.</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-sshd-enable").addEventListener("click", enableSshd);
|
||||
}
|
||||
|
||||
async function enableSshd() {
|
||||
var btn = document.getElementById("btn-sshd-enable");
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Enabling SSH…"; }
|
||||
try {
|
||||
await apiFetch("/api/features/toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ feature: "sshd", enabled: true }),
|
||||
});
|
||||
// Poll until rebuild completes and sshd_enabled is true
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">⚙️</div>',
|
||||
'<h3 class="support-heading">Enabling SSH…</h3>',
|
||||
'<p class="support-desc">A system rebuild is in progress. This may take a few minutes. The page will update automatically when SSH is ready.</p>',
|
||||
'<p class="creds-loading" id="sshd-rebuild-status">Rebuilding system…</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
pollForSshdReady();
|
||||
} catch (err) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = "Enable SSH"; }
|
||||
alert("Failed to enable SSH. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
function pollForSshdReady() {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 60; // 5 minutes (5s interval)
|
||||
var interval = setInterval(async function() {
|
||||
attempts++;
|
||||
try {
|
||||
var status = await apiFetch("/api/support/status");
|
||||
var el = document.getElementById("sshd-rebuild-status");
|
||||
if (status.sshd_enabled) {
|
||||
clearInterval(interval);
|
||||
_supportStatus = status;
|
||||
renderSupportInactive();
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
if (el) el.textContent = "Rebuild is taking longer than expected. Please close this dialog and try again.";
|
||||
} else {
|
||||
if (el) el.textContent = "Rebuilding system… (" + attempts * 5 + "s)";
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function renderSupportInactive() {
|
||||
stopSupportTimer();
|
||||
var ip = _cachedExternalIp || "loading…";
|
||||
@@ -24,6 +96,10 @@ function renderSupportInactive() {
|
||||
'<div class="support-icon-big">🛟</div>',
|
||||
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
|
||||
'<p class="support-desc">This will temporarily grant our support team SSH access to your machine so we can help diagnose and fix issues.</p>',
|
||||
'<div class="support-wallet-box support-wallet-protected">',
|
||||
'<div class="support-wallet-header"><span class="support-wallet-icon">✅</span><span class="support-wallet-title">SSH is Active</span></div>',
|
||||
'<p class="support-wallet-desc">SSH is enabled on your machine. You can now grant Sovran Systems temporary access below.</p>',
|
||||
'</div>',
|
||||
'<div class="support-info-box">',
|
||||
'<div class="support-info-row"><span class="support-info-label">Your IP</span><span class="support-info-value">' + escHtml(ip) + '</span></div>',
|
||||
'<div class="support-info-hint">This IP will be shared with Sovran Systems support</div>',
|
||||
@@ -40,7 +116,7 @@ function renderSupportInactive() {
|
||||
'<li>All session events are logged for your audit</li>',
|
||||
'</ol></div>',
|
||||
'<button class="btn support-btn-enable" id="btn-support-enable">Enable Support Access</button>',
|
||||
'<p class="support-fine-print">You can revoke access at any time. Wallet files are protected unless you unlock them.</p>',
|
||||
'<p class="support-fine-print">You can revoke access at any time. When you end the session, you\'ll be able to disable SSH to return to the default secure state.</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-support-enable").addEventListener("click", enableSupport);
|
||||
@@ -131,8 +207,22 @@ function renderSupportRemoved(verified) {
|
||||
var msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot to ensure it is gone.";
|
||||
var vclass = verified ? "verified-gone" : "verify-warning";
|
||||
var vlabel = verified ? "✓ Removed — No access" : "⚠ Verify by rebooting";
|
||||
$supportBody.innerHTML = '<div class="support-section"><div class="support-icon-big">' + icon + '</div><h3 class="support-heading">Support Session Ended</h3><p class="support-desc">' + escHtml(msg) + '</p><div class="support-verify-box"><span class="support-verify-label">SSH Key Status:</span><span class="support-verify-value ' + vclass + '">' + vlabel + '</span></div><button class="btn support-btn-done" id="btn-support-done">Done</button></div>';
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">' + icon + '</div>',
|
||||
'<h3 class="support-heading">Support Session Ended</h3>',
|
||||
'<p class="support-desc">' + escHtml(msg) + '</p>',
|
||||
'<div class="support-verify-box"><span class="support-verify-label">SSH Key Status:</span><span class="support-verify-value ' + vclass + '">' + vlabel + '</span></div>',
|
||||
'<div class="support-wallet-box support-wallet-protected" style="margin-top:12px;">',
|
||||
'<div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">Disable SSH When Done</span></div>',
|
||||
'<p class="support-wallet-desc">SSH is still enabled on your machine. Click below to turn it off and return to the default secure state.</p>',
|
||||
'<button class="btn support-btn-enable" id="btn-sshd-disable">Disable SSH</button>',
|
||||
'</div>',
|
||||
'<button class="btn support-btn-done" id="btn-support-done">Done</button>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
|
||||
document.getElementById("btn-sshd-disable").addEventListener("click", disableSshd);
|
||||
}
|
||||
|
||||
async function enableSupport() {
|
||||
@@ -162,6 +252,59 @@ async function disableSupport() {
|
||||
}
|
||||
}
|
||||
|
||||
async function disableSshd() {
|
||||
var btn = document.getElementById("btn-sshd-disable");
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Disabling SSH…"; }
|
||||
try {
|
||||
await apiFetch("/api/features/toggle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ feature: "sshd", enabled: false }),
|
||||
});
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">⚙️</div>',
|
||||
'<h3 class="support-heading">Disabling SSH…</h3>',
|
||||
'<p class="support-desc">A system rebuild is in progress to turn off SSH. This may take a few minutes.</p>',
|
||||
'<p class="creds-loading" id="sshd-disable-status">Rebuilding system…</p>',
|
||||
'</div>',
|
||||
].join("");
|
||||
pollForSshdDisabled();
|
||||
} catch (err) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = "Disable SSH"; }
|
||||
alert("Failed to disable SSH. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
function pollForSshdDisabled() {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 60; // 5 minutes (5s interval)
|
||||
var interval = setInterval(async function() {
|
||||
attempts++;
|
||||
try {
|
||||
var status = await apiFetch("/api/support/status");
|
||||
var el = document.getElementById("sshd-disable-status");
|
||||
if (!status.sshd_enabled) {
|
||||
clearInterval(interval);
|
||||
$supportBody.innerHTML = [
|
||||
'<div class="support-section">',
|
||||
'<div class="support-icon-big">🔐</div>',
|
||||
'<h3 class="support-heading">SSH is Off</h3>',
|
||||
'<p class="support-desc">SSH has been disabled. Your machine is back to its default secure state.</p>',
|
||||
'<button class="btn support-btn-done" id="btn-support-done">Done</button>',
|
||||
'</div>',
|
||||
].join("");
|
||||
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
if (el) el.textContent = "Rebuild is taking longer than expected. Please close this dialog and try again.";
|
||||
} else {
|
||||
if (el) el.textContent = "Rebuilding system… (" + attempts * 5 + "s)";
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function walletUnlock() {
|
||||
var btn = document.getElementById("btn-wallet-unlock");
|
||||
var sel = document.getElementById("wallet-unlock-duration");
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
// ── Bitcoin IBD sync state (for ETA calculation) ──────────────────
|
||||
// Keyed by tileId: { progress: float, timestamp: ms }
|
||||
var _btcSyncPrev = {};
|
||||
|
||||
// ── Render: initial build ─────────────────────────────────────────
|
||||
|
||||
function buildTiles(services, categoryLabels) {
|
||||
@@ -45,6 +49,20 @@ function buildTiles(services, categoryLabels) {
|
||||
|
||||
function renderSidebarSupport(supportServices) {
|
||||
$sidebarSupport.innerHTML = "";
|
||||
|
||||
// ── Update System button (above Tech Help)
|
||||
var sidebarUpdateBtn = document.createElement("button");
|
||||
sidebarUpdateBtn.className = "sidebar-support-btn";
|
||||
sidebarUpdateBtn.id = "sidebar-btn-update";
|
||||
sidebarUpdateBtn.innerHTML =
|
||||
'<img class="sidebar-support-icon" src="/static/icons/update.svg" alt="Update" style="width:1.5rem;height:1.5rem;">' +
|
||||
'<span class="sidebar-support-text">' +
|
||||
'<span class="sidebar-support-title">Update System</span>' +
|
||||
'<span class="sidebar-support-hint" id="sidebar-update-hint">Check for updates</span>' +
|
||||
'</span>';
|
||||
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
|
||||
$sidebarSupport.appendChild(sidebarUpdateBtn);
|
||||
|
||||
for (var i = 0; i < supportServices.length; i++) {
|
||||
var svc = supportServices[i];
|
||||
var btn = document.createElement("button");
|
||||
@@ -71,6 +89,20 @@ function renderSidebarSupport(supportServices) {
|
||||
backupBtn.addEventListener("click", function() { openBackupModal(); });
|
||||
$sidebarSupport.appendChild(backupBtn);
|
||||
|
||||
// ── Upgrade button (Node role only)
|
||||
if (_currentRole === "node") {
|
||||
var upgradeBtn = document.createElement("button");
|
||||
upgradeBtn.className = "sidebar-support-btn sidebar-upgrade-btn";
|
||||
upgradeBtn.innerHTML =
|
||||
'<span class="sidebar-support-icon">🚀</span>' +
|
||||
'<span class="sidebar-support-text">' +
|
||||
'<span class="sidebar-support-title">Upgrade to Full Server</span>' +
|
||||
'<span class="sidebar-support-hint">Unlock all services</span>' +
|
||||
'</span>';
|
||||
upgradeBtn.addEventListener("click", function() { openUpgradeModal(); });
|
||||
$sidebarSupport.appendChild(upgradeBtn);
|
||||
}
|
||||
|
||||
var hr = document.createElement("hr");
|
||||
hr.className = "sidebar-divider";
|
||||
$sidebarSupport.appendChild(hr);
|
||||
@@ -95,7 +127,35 @@ function buildTile(svc) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
||||
if (svc.sync_ibd) {
|
||||
var pct = Math.round((svc.sync_progress || 0) * 100);
|
||||
var id = tileId(svc);
|
||||
var eta = _calcBtcEta(id, svc.sync_progress || 0);
|
||||
var ver = svc.version || svc.bitcoin_version || '';
|
||||
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
||||
tile.innerHTML =
|
||||
'<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">' +
|
||||
'<div class="tile-icon-fallback" style="display:none">?</div>' +
|
||||
'<div class="tile-name">' + escHtml(svc.name) + '</div>' +
|
||||
versionLabel +
|
||||
'<div class="tile-sync-container">' +
|
||||
'<div class="tile-sync-label">\u23F3 Syncing Timechain</div>' +
|
||||
'<div class="tile-sync-bar-row">' +
|
||||
'<div class="tile-sync-bar-track"><div class="tile-sync-bar-fill" style="width:' + pct + '%"></div></div>' +
|
||||
'<span class="tile-sync-percent">' + pct + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="tile-sync-eta">' + escHtml(eta) + '</div>' +
|
||||
'</div>';
|
||||
tile.style.cursor = "pointer";
|
||||
tile.addEventListener("click", function() {
|
||||
openServiceDetailModal(svc.unit, svc.name, svc.icon);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
|
||||
var ver = svc.version || svc.bitcoin_version || '';
|
||||
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
||||
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
||||
|
||||
tile.style.cursor = "pointer";
|
||||
tile.addEventListener("click", function() {
|
||||
@@ -107,6 +167,23 @@ function buildTile(svc) {
|
||||
|
||||
// ── Render: live update ───────────────────────────────────────────
|
||||
|
||||
// Calculate ETA text for Bitcoin IBD and track progress history.
|
||||
function _calcBtcEta(id, progress) {
|
||||
var now = Date.now();
|
||||
var prev = _btcSyncPrev[id];
|
||||
// Only update the cache when progress has actually advanced
|
||||
if (!prev || prev.progress < progress) {
|
||||
_btcSyncPrev[id] = { progress: progress, timestamp: now };
|
||||
}
|
||||
if (!prev || prev.progress >= progress) return "Estimating\u2026";
|
||||
var elapsed = (now - prev.timestamp) / 1000; // seconds
|
||||
if (elapsed <= 0) return "Estimating\u2026";
|
||||
var rate = (progress - prev.progress) / elapsed; // progress per second
|
||||
if (rate <= 0) return "Estimating\u2026";
|
||||
var remaining = (1.0 - progress) / rate;
|
||||
return "\u007E" + formatDuration(remaining) + " remaining";
|
||||
}
|
||||
|
||||
function updateTiles(services) {
|
||||
_servicesCache = services;
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
@@ -115,12 +192,70 @@ function updateTiles(services) {
|
||||
var id = CSS.escape(tileId(svc));
|
||||
var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]');
|
||||
if (!tile) continue;
|
||||
var sc = statusClass(svc.health || svc.status);
|
||||
var st = statusText(svc.health || svc.status, svc.enabled);
|
||||
var dot = tile.querySelector(".status-dot");
|
||||
var text = tile.querySelector(".status-text");
|
||||
if (dot) dot.className = "status-dot " + sc;
|
||||
if (text) text.textContent = st;
|
||||
|
||||
if (svc.sync_ibd) {
|
||||
// If tile was previously normal, rebuild it with the sync layout
|
||||
if (!tile.querySelector(".tile-sync-container")) {
|
||||
var newTile = buildTile(svc);
|
||||
tile.parentNode.replaceChild(newTile, tile);
|
||||
continue;
|
||||
}
|
||||
// Update progress bar values in-place
|
||||
var pct = Math.round((svc.sync_progress || 0) * 100);
|
||||
var etaText = _calcBtcEta(tileId(svc), svc.sync_progress || 0);
|
||||
var fill = tile.querySelector(".tile-sync-bar-fill");
|
||||
var pctEl = tile.querySelector(".tile-sync-percent");
|
||||
var etaEl = tile.querySelector(".tile-sync-eta");
|
||||
if (fill) fill.style.width = pct + "%";
|
||||
if (pctEl) pctEl.textContent = pct + "%";
|
||||
if (etaEl) etaEl.textContent = etaText;
|
||||
// Update or insert version label
|
||||
var syncVer = svc.version || svc.bitcoin_version || '';
|
||||
if (syncVer) {
|
||||
var syncVerEl = tile.querySelector(".tile-version");
|
||||
if (syncVerEl) {
|
||||
syncVerEl.textContent = syncVer;
|
||||
} else {
|
||||
var syncNameEl = tile.querySelector(".tile-name");
|
||||
if (syncNameEl) {
|
||||
var newSyncVerEl = document.createElement("div");
|
||||
newSyncVerEl.className = "tile-version";
|
||||
newSyncVerEl.textContent = syncVer;
|
||||
syncNameEl.insertAdjacentElement("afterend", newSyncVerEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// IBD finished or not syncing — if tile had sync layout rebuild it normally
|
||||
if (tile.querySelector(".tile-sync-container")) {
|
||||
delete _btcSyncPrev[tileId(svc)];
|
||||
var normalTile = buildTile(svc);
|
||||
tile.parentNode.replaceChild(normalTile, tile);
|
||||
continue;
|
||||
}
|
||||
var sc = statusClass(svc.health || svc.status);
|
||||
var st = statusText(svc.health || svc.status, svc.enabled);
|
||||
var dot = tile.querySelector(".status-dot");
|
||||
var text = tile.querySelector(".status-text");
|
||||
if (dot) dot.className = "status-dot " + sc;
|
||||
if (text) text.textContent = st;
|
||||
// Update or insert version label for all service tiles
|
||||
var tileVer = svc.version || svc.bitcoin_version || '';
|
||||
if (tileVer) {
|
||||
var verEl = tile.querySelector(".tile-version");
|
||||
if (verEl) {
|
||||
verEl.textContent = tileVer;
|
||||
} else {
|
||||
var nameEl = tile.querySelector(".tile-name");
|
||||
if (nameEl) {
|
||||
var newVerEl = document.createElement("div");
|
||||
newVerEl.className = "tile-version";
|
||||
newVerEl.textContent = tileVer;
|
||||
nameEl.insertAdjacentElement("afterend", newVerEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +291,18 @@ async function checkUpdates() {
|
||||
try {
|
||||
var data = await apiFetch("/api/updates/check");
|
||||
var hasUpdates = !!data.available;
|
||||
if ($updateBadge) $updateBadge.classList.toggle("visible", hasUpdates);
|
||||
if ($updateBtn) $updateBtn.classList.toggle("has-updates", hasUpdates);
|
||||
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
||||
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
||||
if (sidebarUpdateBtn) {
|
||||
if (hasUpdates) {
|
||||
sidebarUpdateBtn.style.borderColor = "#2ec27e";
|
||||
sidebarUpdateBtn.style.backgroundColor = "rgba(46, 194, 126, 0.08)";
|
||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
|
||||
} else {
|
||||
sidebarUpdateBtn.style.borderColor = "";
|
||||
sidebarUpdateBtn.style.backgroundColor = "";
|
||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
38
app/sovran_systemsos_web/static/logo-light.svg
Normal file → Executable file
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
@@ -9,6 +11,9 @@
|
||||
sodipodi:docname="sovran_systems_2.svg"
|
||||
width="218.44057"
|
||||
height="109.75845"
|
||||
inkscape:export-filename="sovran_systems_2.svg"
|
||||
inkscape:export-xdpi="169.6163"
|
||||
inkscape:export-ydpi="169.6163"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -32,59 +37,59 @@
|
||||
<g
|
||||
id="g30"
|
||||
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)"
|
||||
style="fill:#ffffff;fill-opacity:1">
|
||||
style="fill:#dedede;fill-opacity:1">
|
||||
<path
|
||||
d="m 354.93,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.69,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 412.05,528.85 c 0,1.81 -0.27,3.46 -0.8,4.94 -0.53,1.48 -1.48,2.74 -2.84,3.78 -1.36,1.04 -3.23,1.86 -5.62,2.45 -2.39,0.59 -5.41,0.89 -9.08,0.89 -3.67,0 -6.7,-0.29 -9.11,-0.89 -2.4,-0.59 -4.29,-1.41 -5.65,-2.45 -1.36,-1.04 -2.31,-2.31 -2.84,-3.78 -0.53,-1.48 -0.8,-3.12 -0.8,-4.94 v -20.17 c 0,-1.81 0.27,-3.46 0.8,-4.94 0.53,-1.48 1.48,-2.75 2.84,-3.81 1.36,-1.06 3.24,-1.89 5.65,-2.48 2.4,-0.59 5.44,-0.89 9.11,-0.89 3.67,0 6.69,0.3 9.08,0.89 2.39,0.59 4.26,1.42 5.62,2.48 1.36,1.06 2.31,2.34 2.84,3.81 0.53,1.48 0.8,3.13 0.8,4.94 z m -23.3,-2.13 c 0,0.79 0.3,1.45 0.89,1.98 0.59,0.53 1.95,0.8 4.08,0.8 2.13,0 3.49,-0.27 4.08,-0.8 0.59,-0.53 0.89,-1.19 0.89,-1.98 v -15.91 c 0,-0.75 -0.3,-1.39 -0.89,-1.92 -0.59,-0.53 -1.95,-0.8 -4.08,-0.8 -2.13,0 -3.49,0.27 -4.08,0.8 -0.59,0.53 -0.89,1.17 -0.89,1.92 z"
|
||||
id="path6"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 447.12,540.02 h -15.38 c -0.75,0 -1.37,-0.14 -1.86,-0.41 -0.49,-0.28 -0.88,-0.79 -1.15,-1.54 l -5.8,-14.49 c -0.35,-0.87 -0.65,-1.63 -0.89,-2.28 -0.24,-0.65 -0.43,-1.27 -0.59,-1.86 -0.16,-0.59 -0.27,-1.21 -0.33,-1.86 -0.06,-0.65 -0.09,-1.45 -0.09,-2.4 v -15.61 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.4 c 0.75,0 1.32,0.17 1.71,0.5 0.39,0.34 0.59,0.88 0.59,1.63 v 16.32 c 0,0.39 0.04,0.79 0.12,1.18 0.08,0.39 0.2,0.81 0.35,1.24 l 2.78,8.28 c 0.12,0.39 0.26,0.66 0.41,0.8 0.16,0.14 0.39,0.21 0.71,0.21 h 0.65 c 0.31,0 0.55,-0.07 0.71,-0.21 0.16,-0.14 0.3,-0.4 0.41,-0.8 l 2.78,-8.34 c 0.16,-0.43 0.28,-0.85 0.35,-1.24 0.08,-0.39 0.12,-0.79 0.12,-1.18 v -16.26 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.28 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 15.61 c 0,0.95 -0.03,1.74 -0.09,2.4 -0.06,0.65 -0.17,1.27 -0.33,1.86 -0.16,0.59 -0.35,1.21 -0.59,1.86 -0.24,0.65 -0.53,1.41 -0.89,2.28 l -5.8,14.49 c -0.28,0.75 -0.66,1.26 -1.15,1.54 -0.45,0.28 -1.07,0.41 -1.82,0.41 z"
|
||||
id="path8"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 478.69,540.02 h -9.11 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 19.69 c 4.41,0 7.46,0.92 9.14,2.75 1.68,1.83 2.51,4.21 2.51,7.13 v 2.72 c 0,1.66 -0.25,3.07 -0.74,4.23 -0.49,1.16 -1.35,2 -2.57,2.51 2.13,0.24 3.85,1.1 5.17,2.6 1.32,1.5 1.98,3.49 1.98,5.97 v 12.54 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -9.17 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -9.05 c 0,-0.87 -0.17,-1.51 -0.5,-1.92 -0.34,-0.41 -0.92,-0.62 -1.74,-0.62 h -8.28 v 11.59 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.32,0.34 -0.87,0.5 -1.62,0.5 z m 2.13,-31.93 v 7.57 h 4.44 c 1.02,0 1.71,-0.27 2.07,-0.8 0.36,-0.53 0.53,-1.19 0.53,-1.98 v -2.01 c 0,-0.79 -0.18,-1.45 -0.53,-1.98 -0.35,-0.53 -1.04,-0.8 -2.07,-0.8 z"
|
||||
id="path10"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 525.83,537.89 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 H 515 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -15.61 c 0,-1.18 0.19,-2.54 0.56,-4.08 0.38,-1.54 0.96,-3.33 1.75,-5.38 l 5.15,-13.42 c 0.24,-0.67 0.6,-1.16 1.09,-1.48 0.49,-0.31 1.13,-0.47 1.92,-0.47 h 15.91 c 0.75,0 1.37,0.16 1.86,0.47 0.49,0.32 0.86,0.81 1.09,1.48 l 5.14,13.42 c 0.79,2.05 1.37,3.84 1.75,5.38 0.37,1.54 0.56,2.9 0.56,4.08 v 15.61 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -8.93 c -0.79,0 -1.37,-0.17 -1.74,-0.5 -0.38,-0.33 -0.56,-0.88 -0.56,-1.63 v -8.28 h -10.47 v 8.28 z m 3.37,-28.03 -2.78,8.99 h 9.29 l -2.78,-8.99 c -0.16,-0.35 -0.33,-0.61 -0.5,-0.77 -0.18,-0.16 -0.38,-0.24 -0.62,-0.24 h -1.48 c -0.24,0 -0.44,0.08 -0.62,0.24 -0.19,0.16 -0.36,0.42 -0.51,0.77 z"
|
||||
id="path12"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 570.29,540.02 h -8.87 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 6.15 c 0.75,0 1.39,0.12 1.92,0.35 0.53,0.24 1.05,0.65 1.57,1.24 l 11.47,13.13 v -12.6 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 8.87 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 38.32 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -8.87 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.27 l -10.11,-12.24 v 19.51 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.35 -0.88,0.51 -1.63,0.51 z"
|
||||
id="path14"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 641.61,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.17,2.69 -8.96,2.69 z"
|
||||
id="path16"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 683.66,540.02 h -9.58 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.57 L 662.9,518.2 c -0.91,-1.22 -1.51,-2.29 -1.8,-3.19 -0.3,-0.91 -0.44,-2.27 -0.44,-4.08 v -11.35 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.11 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 9.7 c 0,0.39 0.02,0.81 0.06,1.24 0.04,0.43 0.2,0.85 0.47,1.24 l 2.72,4.26 c 0.2,0.35 0.4,0.61 0.62,0.77 0.22,0.16 0.48,0.24 0.8,0.24 h 0.59 c 0.31,0 0.58,-0.08 0.8,-0.24 0.22,-0.16 0.42,-0.41 0.62,-0.77 l 2.72,-4.26 c 0.28,-0.39 0.43,-0.81 0.47,-1.24 0.04,-0.43 0.06,-0.85 0.06,-1.24 v -9.7 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 8.81 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 11.35 c 0,1.81 -0.16,3.17 -0.47,4.08 -0.32,0.91 -0.91,1.97 -1.77,3.19 l -8.99,12.18 v 7.51 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.35,0.34 -0.9,0.5 -1.64,0.5 z"
|
||||
id="path18"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 725.88,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
|
||||
id="path20"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 766.44,540.02 h -9.58 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -29.04 h -8.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.16 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 31.22 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.16 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.33,0.34 -0.88,0.5 -1.63,0.5 h -8.69 v 29.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.33,0.34 -0.88,0.5 -1.63,0.5 z"
|
||||
id="path22"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 817.06,540.02 h -27.44 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 27.44 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 6.92 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -16.32 v 4.55 h 11.53 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.33 0.5,0.88 0.5,1.63 v 6.33 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.33 -0.88,0.5 -1.63,0.5 h -11.53 v 5.08 h 16.32 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.33 0.5,0.88 0.5,1.63 v 6.92 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 z"
|
||||
id="path24"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 839.48,540.02 h -8.81 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.52 c 0.63,0 1.15,0.14 1.57,0.41 0.41,0.28 0.8,0.73 1.15,1.36 l 5.32,9.64 c 0.2,0.35 0.36,0.61 0.5,0.77 0.14,0.16 0.33,0.24 0.56,0.24 h 0.53 c 0.24,0 0.42,-0.08 0.56,-0.24 0.14,-0.16 0.3,-0.41 0.5,-0.77 l 5.26,-9.64 c 0.36,-0.63 0.74,-1.08 1.15,-1.36 0.41,-0.28 0.94,-0.41 1.57,-0.41 h 9.58 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 38.32 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -9.11 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -20.82 l -3.49,6.45 c -0.35,0.67 -0.78,1.15 -1.27,1.45 -0.49,0.3 -1.12,0.44 -1.86,0.44 h -2.37 c -0.75,0 -1.37,-0.15 -1.86,-0.44 -0.49,-0.29 -0.92,-0.78 -1.27,-1.45 l -3.49,-6.45 v 20.82 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.32,0.34 -0.87,0.5 -1.61,0.5 z"
|
||||
id="path26"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
<path
|
||||
d="m 900.86,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
|
||||
id="path28"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
style="fill:#dedede;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g34"
|
||||
@@ -104,7 +109,10 @@
|
||||
</g>
|
||||
<g
|
||||
id="g42"
|
||||
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)">
|
||||
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)"
|
||||
inkscape:export-filename="./g42.svg"
|
||||
inkscape:export-xdpi="169.6163"
|
||||
inkscape:export-ydpi="169.6163">
|
||||
<path
|
||||
class="st0"
|
||||
d="m 399.59,672.5 c -88.63,0 -160.73,-72.11 -160.73,-160.73 0,-88.62 72.11,-160.73 160.73,-160.73 75.66,0 139.05,52.63 156.04,123.16 h 5.06 C 543.61,400.93 478,346.1 399.6,346.1 c -91.36,0 -165.68,74.32 -165.68,165.68 0,91.36 74.32,165.68 165.68,165.68 56.18,0 105.83,-28.17 135.77,-71.07 -1.21,-1.21 -2.42,-2.42 -3.64,-3.63 -29,42.03 -77.32,69.74 -132.14,69.74 z"
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,10 +1,24 @@
|
||||
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
|
||||
Drives the 4-step post-install setup flow. */
|
||||
Drives the 5-step post-install setup flow. */
|
||||
"use strict";
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
const TOTAL_STEPS = 5;
|
||||
|
||||
// Steps to skip per role (steps 3 and 4 involve domain/port setup)
|
||||
// Step 2 (password) is NEVER skipped — all roles need it.
|
||||
const ROLE_SKIP_STEPS = {
|
||||
"desktop": [3, 4],
|
||||
"node": [3, 4],
|
||||
};
|
||||
|
||||
// ── Role state (loaded at init) ───────────────────────────────────
|
||||
|
||||
var _onboardingRole = "server_plus_desktop";
|
||||
|
||||
// Password default state (loaded at step 2)
|
||||
var _passwordIsDefault = true;
|
||||
|
||||
// Domains that may need configuration, with service unit mapping for enabled check
|
||||
const DOMAIN_DEFS = [
|
||||
@@ -81,6 +95,24 @@ function showStep(step) {
|
||||
// Lazy-load step content
|
||||
if (step === 2) loadStep2();
|
||||
if (step === 3) loadStep3();
|
||||
if (step === 4) loadStep4();
|
||||
// Step 5 (Complete) is static — no lazy-load needed
|
||||
}
|
||||
|
||||
// Return the next step number, skipping over role-excluded steps
|
||||
function nextStep(current) {
|
||||
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
|
||||
var next = current + 1;
|
||||
while (next < TOTAL_STEPS && skip.indexOf(next) !== -1) next++;
|
||||
return next;
|
||||
}
|
||||
|
||||
// Return the previous step number, skipping over role-excluded steps
|
||||
function prevStep(current) {
|
||||
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
|
||||
var prev = current - 1;
|
||||
while (prev > 1 && skip.indexOf(prev) !== -1) prev--;
|
||||
return prev;
|
||||
}
|
||||
|
||||
// ── Step 1: Welcome ───────────────────────────────────────────────
|
||||
@@ -93,12 +125,135 @@ async function loadStep1() {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Step 2: Domain Configuration ─────────────────────────────────
|
||||
// ── Step 2: Create Your Password ─────────────────────────────────
|
||||
|
||||
async function loadStep2() {
|
||||
var body = document.getElementById("step-2-body");
|
||||
if (!body) return;
|
||||
|
||||
var nextBtn = document.getElementById("step-2-next");
|
||||
|
||||
try {
|
||||
var result = await apiFetch("/api/security/password-is-default");
|
||||
_passwordIsDefault = result.is_default !== false;
|
||||
} catch (_) {
|
||||
_passwordIsDefault = true;
|
||||
}
|
||||
|
||||
if (_passwordIsDefault) {
|
||||
// Factory-sealed scenario: password must be set before continuing
|
||||
if (nextBtn) nextBtn.textContent = "Set Password & Continue \u2192";
|
||||
|
||||
body.innerHTML =
|
||||
'<div class="onboarding-password-group">' +
|
||||
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
|
||||
'<div class="onboarding-password-input-wrap">' +
|
||||
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
|
||||
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
|
||||
'</div>' +
|
||||
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
|
||||
'</div>' +
|
||||
'<div class="onboarding-password-group">' +
|
||||
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
|
||||
'<div class="onboarding-password-input-wrap">' +
|
||||
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
|
||||
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>';
|
||||
|
||||
// Wire show/hide toggles
|
||||
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
var inp = document.getElementById(btn.dataset.target);
|
||||
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
// DIY install scenario: password already set by installer
|
||||
if (nextBtn) nextBtn.textContent = "Continue \u2192";
|
||||
|
||||
body.innerHTML =
|
||||
'<div class="onboarding-password-success">✅ Your password was already set during installation.</div>' +
|
||||
'<details class="onboarding-password-optional">' +
|
||||
'<summary>Change it anyway</summary>' +
|
||||
'<div style="margin-top:14px;">' +
|
||||
'<div class="onboarding-password-group">' +
|
||||
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
|
||||
'<div class="onboarding-password-input-wrap">' +
|
||||
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
|
||||
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
|
||||
'</div>' +
|
||||
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
|
||||
'</div>' +
|
||||
'<div class="onboarding-password-group">' +
|
||||
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
|
||||
'<div class="onboarding-password-input-wrap">' +
|
||||
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
|
||||
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>' +
|
||||
'</div>' +
|
||||
'</details>';
|
||||
|
||||
// Wire show/hide toggles
|
||||
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
var inp = document.getElementById(btn.dataset.target);
|
||||
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStep2() {
|
||||
var newPw = document.getElementById("pw-new");
|
||||
var confirmPw = document.getElementById("pw-confirm");
|
||||
|
||||
// If no fields visible or both empty and password already set → skip
|
||||
if (!newPw || !newPw.value.trim()) {
|
||||
if (!_passwordIsDefault) return true; // already set, no change requested
|
||||
setStatus("step-2-status", "⚠ Please enter a password.", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
var pw = newPw.value;
|
||||
var cpw = confirmPw ? confirmPw.value : "";
|
||||
|
||||
if (pw.length < 8) {
|
||||
setStatus("step-2-status", "⚠ Password must be at least 8 characters.", "error");
|
||||
return false;
|
||||
}
|
||||
if (pw !== cpw) {
|
||||
setStatus("step-2-status", "⚠ Passwords do not match.", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
setStatus("step-2-status", "Saving password…", "info");
|
||||
try {
|
||||
await apiFetch("/api/change-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ new_password: pw, confirm_password: cpw }),
|
||||
});
|
||||
} catch (err) {
|
||||
setStatus("step-2-status", "⚠ " + err.message, "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
setStatus("step-2-status", "✓ Password saved", "ok");
|
||||
_passwordIsDefault = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Step 3: Domain Configuration ─────────────────────────────────
|
||||
|
||||
async function loadStep3() {
|
||||
var body = document.getElementById("step-3-body");
|
||||
if (!body) return;
|
||||
|
||||
try {
|
||||
// Fetch services, domains, and network info in parallel
|
||||
var results = await Promise.all([
|
||||
@@ -153,6 +308,8 @@ async function loadStep2() {
|
||||
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
|
||||
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl "https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&k=abc123&auto"" />';
|
||||
html += '<p class="onboarding-hint" style="margin-top:4px;">ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
|
||||
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
||||
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
@@ -163,13 +320,82 @@ async function loadStep2() {
|
||||
html += '<label class="onboarding-domain-label">📧 SSL Certificate Email</label>';
|
||||
html += '<p class="onboarding-hint onboarding-hint--inline">Let\'s Encrypt uses this for certificate expiry notifications.</p>';
|
||||
html += '<input class="onboarding-domain-input domain-field-input" type="email" id="ssl-email-input" placeholder="you@example.com" value="' + escHtml(emailVal) + '" />';
|
||||
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-email="true" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
|
||||
html += '<span class="onboarding-domain-save-status" id="domain-save-status-email" style="font-size:0.82rem;min-height:1.2em;"></span>';
|
||||
html += '</div>';
|
||||
|
||||
body.innerHTML = html;
|
||||
|
||||
// Wire per-field save buttons for domains
|
||||
body.querySelectorAll('[data-save-domain]').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
var domainName = btn.dataset.saveDomain;
|
||||
var domainInput = document.getElementById('domain-input-' + domainName);
|
||||
var ddnsInput = document.getElementById('ddns-input-' + domainName);
|
||||
var statusEl = document.getElementById('domain-save-status-' + domainName);
|
||||
var domainVal = domainInput ? domainInput.value.trim() : '';
|
||||
var ddnsVal = ddnsInput ? ddnsInput.value.trim() : '';
|
||||
|
||||
if (!domainVal) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ Enter a domain first'; statusEl.style.color = 'var(--red)'; }
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
if (statusEl) { statusEl.textContent = ''; }
|
||||
|
||||
try {
|
||||
await apiFetch('/api/domains/set', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domain_name: domainName, domain: domainVal, ddns_url: ddnsVal }),
|
||||
});
|
||||
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save';
|
||||
});
|
||||
});
|
||||
|
||||
// Wire save button for SSL email
|
||||
body.querySelectorAll('[data-save-email]').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
var emailInput = document.getElementById('ssl-email-input');
|
||||
var statusEl = document.getElementById('domain-save-status-email');
|
||||
var emailVal = emailInput ? emailInput.value.trim() : '';
|
||||
|
||||
if (!emailVal) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ Enter an email first'; statusEl.style.color = 'var(--red)'; }
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Saving…';
|
||||
if (statusEl) { statusEl.textContent = ''; }
|
||||
|
||||
try {
|
||||
await apiFetch('/api/domains/set-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailVal }),
|
||||
});
|
||||
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
|
||||
} catch (err) {
|
||||
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Save';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveStep2() {
|
||||
setStatus("step-2-status", "Saving domains…", "info");
|
||||
async function saveStep3() {
|
||||
setStatus("step-3-status", "Saving domains…", "info");
|
||||
var errors = [];
|
||||
|
||||
// Save each domain input
|
||||
@@ -209,18 +435,18 @@ async function saveStep2() {
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
setStatus("step-2-status", "⚠ Some errors: " + errors.join("; "), "error");
|
||||
setStatus("step-3-status", "⚠ Some errors: " + errors.join("; "), "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
setStatus("step-2-status", "✓ Saved", "ok");
|
||||
setStatus("step-3-status", "✓ Saved", "ok");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Step 3: Port Forwarding ───────────────────────────────────────
|
||||
// ── Step 4: Port Forwarding ───────────────────────────────────────
|
||||
|
||||
async function loadStep3() {
|
||||
var body = document.getElementById("step-3-body");
|
||||
async function loadStep4() {
|
||||
var body = document.getElementById("step-4-body");
|
||||
if (!body) return;
|
||||
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>';
|
||||
|
||||
@@ -301,10 +527,10 @@ async function loadStep3() {
|
||||
body.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Step 4: Complete ──────────────────────────────────────────────
|
||||
// ── Step 5: Complete ──────────────────────────────────────────────
|
||||
|
||||
async function completeOnboarding() {
|
||||
var btn = document.getElementById("step-4-finish");
|
||||
var btn = document.getElementById("step-5-finish");
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
|
||||
|
||||
try {
|
||||
@@ -319,33 +545,45 @@ async function completeOnboarding() {
|
||||
// ── Event wiring ──────────────────────────────────────────────────
|
||||
|
||||
function wireNavButtons() {
|
||||
// Step 1 → 2
|
||||
// Step 1 → next
|
||||
var s1next = document.getElementById("step-1-next");
|
||||
if (s1next) s1next.addEventListener("click", function() { showStep(2); });
|
||||
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
|
||||
|
||||
// Step 2 → 3 (save first)
|
||||
// Step 2 → 3 (save password first)
|
||||
var s2next = document.getElementById("step-2-next");
|
||||
if (s2next) s2next.addEventListener("click", async function() {
|
||||
s2next.disabled = true;
|
||||
var origText = s2next.textContent;
|
||||
s2next.textContent = "Saving…";
|
||||
await saveStep2();
|
||||
var ok = await saveStep2();
|
||||
s2next.disabled = false;
|
||||
s2next.textContent = "Save & Continue →";
|
||||
showStep(3);
|
||||
s2next.textContent = origText;
|
||||
if (ok) showStep(nextStep(2));
|
||||
});
|
||||
|
||||
// Step 3 → 4 (Complete)
|
||||
// Step 3 → 4 (save domains first)
|
||||
var s3next = document.getElementById("step-3-next");
|
||||
if (s3next) s3next.addEventListener("click", function() { showStep(4); });
|
||||
if (s3next) s3next.addEventListener("click", async function() {
|
||||
s3next.disabled = true;
|
||||
s3next.textContent = "Saving…";
|
||||
await saveStep3();
|
||||
s3next.disabled = false;
|
||||
s3next.textContent = "Save & Continue →";
|
||||
showStep(nextStep(3));
|
||||
});
|
||||
|
||||
// Step 4: finish
|
||||
var s4finish = document.getElementById("step-4-finish");
|
||||
if (s4finish) s4finish.addEventListener("click", completeOnboarding);
|
||||
// Step 4 → 5 (Complete)
|
||||
var s4next = document.getElementById("step-4-next");
|
||||
if (s4next) s4next.addEventListener("click", function() { showStep(nextStep(4)); });
|
||||
|
||||
// Step 5: finish
|
||||
var s5finish = document.getElementById("step-5-finish");
|
||||
if (s5finish) s5finish.addEventListener("click", completeOnboarding);
|
||||
|
||||
// Back buttons
|
||||
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
|
||||
var prev = parseInt(btn.dataset.prev, 10);
|
||||
btn.addEventListener("click", function() { showStep(prev); });
|
||||
btn.addEventListener("click", function() { showStep(prevStep(prev + 1)); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,6 +599,12 @@ document.addEventListener("DOMContentLoaded", async function() {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Load role so step-skipping is applied before wiring nav buttons
|
||||
try {
|
||||
var cfg = await apiFetch("/api/config");
|
||||
if (cfg.role) _onboardingRole = cfg.role;
|
||||
} catch (_) {}
|
||||
|
||||
wireNavButtons();
|
||||
updateProgress(1);
|
||||
loadStep1();
|
||||
|
||||
54
app/sovran_systemsos_web/static/sovran-hub-icon.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -14,6 +14,7 @@
|
||||
<link rel="stylesheet" href="/static/css/onboarding.css" />
|
||||
<link rel="stylesheet" href="/static/css/support.css" />
|
||||
<link rel="stylesheet" href="/static/css/domain-setup.css" />
|
||||
<link rel="stylesheet" href="/static/css/security.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -23,10 +24,6 @@
|
||||
<span class="title">Sovran_SystemsOS Hub</span>
|
||||
<div class="header-buttons">
|
||||
<span class="role-badge" id="role-badge">Loading…</span>
|
||||
<button class="btn btn-update" id="btn-update" title="Run system update">
|
||||
<span class="update-badge" id="update-badge"></span>
|
||||
Update System
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -172,6 +169,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upgrade Modal (Node → Server+Desktop) -->
|
||||
<div class="modal-overlay" id="upgrade-modal" role="dialog" aria-modal="true" aria-labelledby="upgrade-modal-title">
|
||||
<div class="creds-dialog upgrade-dialog">
|
||||
<div class="creds-header">
|
||||
<span class="creds-title" id="upgrade-modal-title">🚀 Upgrade to Server + Desktop</span>
|
||||
<button class="creds-close-btn" id="upgrade-close-btn" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="creds-body">
|
||||
<p class="support-desc">
|
||||
Upgrading to the full <strong>Server + Desktop</strong> experience will unlock all services —
|
||||
encrypted messaging, password management, cloud storage, website hosting, and more.
|
||||
</p>
|
||||
<div class="upgrade-info-box">
|
||||
<p class="upgrade-info-title">⚠ What you should know:</p>
|
||||
<ul class="upgrade-info-list">
|
||||
<li>You will need to purchase domains for your services (we recommend <a href="https://njal.la" target="_blank" rel="noopener noreferrer">njal.la</a>)</li>
|
||||
<li>Some services require ports to be opened on your router</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="upgrade-info-box">
|
||||
<p class="upgrade-info-title">ℹ️ Good to know:</p>
|
||||
<ul class="upgrade-info-list">
|
||||
<li>Your services will be accessible via your home internet connection. Your approximate geographic area may be visible through DNS records — this is normal for all self-hosted services</li>
|
||||
<li>Your Bitcoin node remains fully private over Tor</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="support-desc">
|
||||
<strong>Don't worry</strong> — the Hub will walk you through every step after the upgrade.
|
||||
Domain setup, port forwarding, and configuration are all guided.
|
||||
</p>
|
||||
<p class="support-desc upgrade-rebuild-note">
|
||||
The system will rebuild after upgrading. This may take several minutes.
|
||||
</p>
|
||||
<div class="domain-field-actions">
|
||||
<button class="btn btn-close-modal" id="upgrade-cancel-btn">Cancel</button>
|
||||
<button class="btn btn-primary" id="upgrade-confirm-btn">Yes, Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reboot overlay -->
|
||||
<div class="reboot-overlay" id="reboot-overlay">
|
||||
<div class="reboot-card">
|
||||
@@ -199,6 +237,7 @@
|
||||
<script src="/static/js/update.js"></script>
|
||||
<script src="/static/js/rebuild.js"></script>
|
||||
<script src="/static/js/features.js"></script>
|
||||
<script src="/static/js/security.js"></script>
|
||||
<script src="/static/js/events.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,6 +34,8 @@
|
||||
<span class="onboarding-step-dot" data-step="3">3</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="4">4</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="5">5</span>
|
||||
</div>
|
||||
|
||||
<!-- Step panels -->
|
||||
@@ -70,8 +72,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Domain Configuration ── -->
|
||||
<!-- ── Step 2: Create Your Password ── -->
|
||||
<div class="onboarding-panel" id="step-2" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🔒</span>
|
||||
<h2 class="onboarding-step-title">Create Your Password</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Choose a strong password for your <strong>'free'</strong> user account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card" id="step-2-body">
|
||||
<p class="onboarding-loading">Checking password status…</p>
|
||||
</div>
|
||||
<div id="step-2-status" class="onboarding-save-status"></div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
|
||||
Set Password & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Domain Configuration ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🌐</span>
|
||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||
@@ -82,20 +105,20 @@
|
||||
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--scroll" id="step-2-body">
|
||||
<div class="onboarding-card" id="step-3-body">
|
||||
<p class="onboarding-loading">Loading service information…</p>
|
||||
</div>
|
||||
<div id="step-2-status" class="onboarding-save-status"></div>
|
||||
<div id="step-3-status" class="onboarding-save-status"></div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
Save & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<!-- ── Step 4: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🔌</span>
|
||||
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
|
||||
@@ -104,19 +127,19 @@
|
||||
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--ports" id="step-3-body">
|
||||
<div class="onboarding-card" id="step-4-body">
|
||||
<p class="onboarding-loading">Checking ports…</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-4-next">
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 4: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<!-- ── Step 5: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-5" style="display:none">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">✅</div>
|
||||
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
|
||||
@@ -128,13 +151,14 @@
|
||||
monitor your services, manage credentials, and make changes at any time.
|
||||
</p>
|
||||
<ul class="onboarding-checklist" id="onboarding-checklist">
|
||||
<li>✅ Password configured</li>
|
||||
<li>✅ Domain configuration saved</li>
|
||||
<li>✅ Port forwarding reviewed</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary" id="step-4-finish">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
|
||||
<button class="btn btn-primary" id="step-5-finish">
|
||||
Go to Dashboard →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,6 @@
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 8448 3051 ];
|
||||
networking.firewall.allowedUDPPorts = [ 80 443 8448 3051 5353 ];
|
||||
networking.firewall.allowedUDPPortRanges = [
|
||||
{ from = 49152; to = 65535; }
|
||||
];
|
||||
|
||||
# ── Avahi (mDNS) ───────────────────────────────────────────
|
||||
# Advertise as sovransystemsos.local on the LAN without changing the system
|
||||
@@ -100,6 +97,7 @@
|
||||
nixpkgs.config.permittedInsecurePackages = [ "jitsi-meet-1.0.8043" ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
nftables
|
||||
git wget fish htop btop
|
||||
gnomeExtensions.transparent-top-bar-adjustable-transparency
|
||||
gnomeExtensions.dash-to-dock
|
||||
@@ -170,24 +168,8 @@ backup /etc/nix-bitcoin-secrets/ localhost/
|
||||
# ── Tor ────────────────────────────────────────────────────
|
||||
services.tor = { enable = true; client.enable = true; torsocks.enable = true; };
|
||||
|
||||
# ── SSH ────────────────────────────────────────────────────
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
# ── Fail2Ban ───────────────────────────────────────────────
|
||||
services.fail2ban = {
|
||||
enable = true;
|
||||
ignoreIP = [ "127.0.0.0/8" "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "8.8.8.8" ];
|
||||
};
|
||||
|
||||
# ── Garbage Collection ─────────────────────────────────────
|
||||
nix.gc = { automatic = true; dates = "weekly"; options = "--delete-older-than 7d"; };
|
||||
|
||||
system.stateVersion = "22.05";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,123 +5,37 @@
|
||||
# #
|
||||
# Sovran_SystemsOS — custom.nix #
|
||||
# #
|
||||
# This is YOUR configuration file. Edit it to customize #
|
||||
# which services and features run on your machine. #
|
||||
# Services, features, and roles are managed by the #
|
||||
# Sovran Hub. Any changes you make through the Hub #
|
||||
# will appear in a Hub Managed section added #
|
||||
# automatically below. #
|
||||
# #
|
||||
# If you want to add your own NixOS modules or #
|
||||
# configuration, place them here — outside of the #
|
||||
# Hub Managed section. #
|
||||
# #
|
||||
# After making changes, rebuild with: #
|
||||
# #
|
||||
# nixos-rebuild switch #
|
||||
# nixos-rebuild switch #
|
||||
# #
|
||||
###########################################################
|
||||
|
||||
# ─── Add your custom NixOS configuration below ───────────
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# STEP 1: CHOOSE YOUR ROLE
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# ─── Custom Caddy virtual hosts ──────────────────────────
|
||||
# Uncomment and edit below to add your own Caddy sites:
|
||||
#
|
||||
# Your initial role was selected during installation.
|
||||
# To CHANGE your role, uncomment exactly ONE of the lines below.
|
||||
# sovran_systemsOS.caddy.extraVirtualHosts = ''
|
||||
# mysite.example.com {
|
||||
# encode gzip zstd
|
||||
# root * /var/lib/www/mysite
|
||||
# php_fastcgi unix//run/phpfpm/mypool.sock
|
||||
# file_server browse
|
||||
# }
|
||||
#
|
||||
# Server+Desktop: Full server + desktop environment
|
||||
# Desktop Only: Desktop environment, no server services
|
||||
# Node (Bitcoin Only): Bitcoin ecosystem
|
||||
#
|
||||
# ───────────────────────────────────────────────────────────
|
||||
# anotherdomain.com {
|
||||
# reverse_proxy localhost:9090
|
||||
# }
|
||||
# '';
|
||||
|
||||
# sovran_systemsOS.roles.server_plus_desktop = true;
|
||||
# sovran_systemsOS.roles.desktop = true;
|
||||
# sovran_systemsOS.roles.node = true;
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# STEP 2: SERVICES (default: ON)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
#
|
||||
# These are all ON by default in the Server+Desktop role.
|
||||
# Set any to "false" to disable it.
|
||||
#
|
||||
# ┌─────────────────────┬────────────────────────────────┐
|
||||
# │ Service │ What it does │
|
||||
# ├─────────────────────┼────────────────────────────────┤
|
||||
# │ synapse │ Matrix Synapse homeserver │
|
||||
# │ bitcoin │ Bitcoin ecosystem (bitcoind, │
|
||||
# │ │ electrs, lnd, rtl, btcpay) │
|
||||
# │ vaultwarden │ Vaultwarden password manager │
|
||||
# │ wordpress │ WordPress website │
|
||||
# │ nextcloud │ Nextcloud file hosting │
|
||||
# └─────────────────────┴────────────────────────────────┘
|
||||
#
|
||||
# Example — disable WordPress and Nextcloud:
|
||||
#
|
||||
# sovran_systemsOS.services.wordpress = false;
|
||||
# sovran_systemsOS.services.nextcloud = false;
|
||||
#
|
||||
# ───────────────────────────────────────────────────────────
|
||||
|
||||
# sovran_systemsOS.services.wordpress = false;
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# STEP 3: FEATURES (default: OFF)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
#
|
||||
# These are OFF by default. Set to "true" to enable.
|
||||
#
|
||||
# ┌─────────────────────┬────────────────────────────────┐
|
||||
# │ Feature │ What it does │
|
||||
# ├─────────────────────┼────────────────────────────────┤
|
||||
# │ haven │ Haven NOSTR relay & Blossom │
|
||||
# │ bip110 │ BIP-110 Bitcoin Better Money │
|
||||
# │ mempool │ Mempool.space block explorer │
|
||||
# │ element-calling │ LiveKit server for Matrix │
|
||||
# │ rdp │ GNOME Remote Desktop (RDP) │
|
||||
# │ bitcoin-core │ Bitcoin Core GUI desktop app │
|
||||
# └─────────────────────┴─────<E29480><E29480><EFBFBD>──────────────────────────┘
|
||||
#
|
||||
# Example — enable element video calling:
|
||||
#
|
||||
# sovran_systemsOS.features.element-calling = true;
|
||||
#
|
||||
# ───────────────────────────────────────────────────────────
|
||||
|
||||
# sovran_systemsOS.features.element-calling = true;
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# STEP 4: WEB EXPOSURE (default: ON)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
#
|
||||
# Controls whether Caddy serves this application to the web.
|
||||
# (Does not stop the application itself from running).
|
||||
#
|
||||
# ┌─────────────────────┬────────────────────────────────┐
|
||||
# │ Option │ Default │
|
||||
# ├─────────────────────┼────────────────────────────────┤
|
||||
# │ btcpayserver │ true (false in Node role) │
|
||||
# └─────────────────────┴────────────────────────────────┘
|
||||
#
|
||||
# Example — hide BTCPay from the web:
|
||||
#
|
||||
# sovran_systemsOS.web.btcpayserver = false;
|
||||
#
|
||||
# ───────────────────────────────────────────────────────────
|
||||
|
||||
# sovran_systemsOS.web.btcpayserver = false;
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# STEP 5: NOSTR PUBLIC KEY (required for Haven)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
#
|
||||
# If you enabled Haven above, paste your npub here.
|
||||
# Haven will NOT start without a valid npub.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# sovran_systemsOS.nostr_npub = "npub1abc123...";
|
||||
#
|
||||
# ───────────────────────────────────────────────────────────
|
||||
|
||||
# sovran_systemsOS.nostr_npub = "";
|
||||
|
||||
}
|
||||
}
|
||||
93
docs/manual-backup.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Sovran Hub — Manual Backup
|
||||
|
||||
The manual backup service copies critical system data from your Sovran Pro to an external USB drive, providing a third copy of your data (your Sovran Pro already maintains an automatic internal backup on its second drive).
|
||||
|
||||
Backups are written to:
|
||||
|
||||
```
|
||||
<USB drive>/Sovran_SystemsOS_Backup/<timestamp>/
|
||||
```
|
||||
|
||||
where `<timestamp>` is formatted as `YYYYMMDD_HHMMSS`.
|
||||
|
||||
---
|
||||
|
||||
## Backup Stages
|
||||
|
||||
The script always attempts all four stages, but skips stages that are irrelevant to the system's configured role (see [Per-Role Breakdown](#per-role-breakdown) below).
|
||||
|
||||
| Stage | Directory | Contents |
|
||||
|-------|-----------|----------|
|
||||
| **1/4 — NixOS config** | `/etc/nixos/` | Full NixOS system configuration: `role-state.nix`, `custom.nix`, flake files, and any other config managed by the Hub |
|
||||
| **2/4 — Secrets** | `/etc/nix-bitcoin-secrets`, `/var/lib/domains`, `/var/lib/secrets` | Bitcoin/LND secrets, domain configurations for all web services, and Hub state files |
|
||||
| **3/4 — Home directory** | `/home/` | All user home directories (`.cache/` and Trash are excluded) |
|
||||
| **4/4 — LND wallet data** | `/var/lib/lnd/` | Lightning Network node wallet and channel data (log files excluded) |
|
||||
|
||||
---
|
||||
|
||||
## Per-Role Breakdown
|
||||
|
||||
The script detects the system role at runtime by reading `/var/lib/sovran-hub/config.json` (falling back to `/etc/nixos/role-state.nix`) and adjusts its behaviour accordingly.
|
||||
|
||||
### Server + Desktop (default)
|
||||
|
||||
All services are enabled: Bitcoin, Matrix Synapse, Vaultwarden, WordPress, Nextcloud.
|
||||
|
||||
| Stage | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Stage 1 — NixOS config | ✅ Backed up | Full server configuration |
|
||||
| Stage 2 — Secrets | ✅ Backed up | Bitcoin secrets, domain configs, and Hub state |
|
||||
| Stage 3 — Home directory | ✅ Backed up | Desktop user data |
|
||||
| Stage 4 — LND wallet | ✅ Backed up | Lightning wallet and channel data |
|
||||
|
||||
This produces the largest backup. All four stages generate meaningful data.
|
||||
|
||||
### Desktop Only
|
||||
|
||||
All server services are disabled (`bitcoin = false`, `synapse = false`, `vaultwarden = false`, `wordpress = false`, `nextcloud = false`). Only GNOME desktop is active.
|
||||
|
||||
| Stage | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Stage 1 — NixOS config | ✅ Backed up | Simpler config (no server services) |
|
||||
| Stage 2 — Secrets | ⚠️ Partial | `/etc/nix-bitcoin-secrets` is **skipped** (not applicable for Desktop Only role). `/var/lib/domains` and `/var/lib/secrets` (Hub state) are still backed up if present |
|
||||
| Stage 3 — Home directory | ✅ Backed up | **The most important data for this role** |
|
||||
| Stage 4 — LND wallet | ⏭️ Skipped | Explicitly skipped — not applicable for Desktop Only role |
|
||||
|
||||
This produces the smallest and fastest backup. Stages 1 and 3 are the primary sources of meaningful data.
|
||||
|
||||
### Node (Bitcoin-only)
|
||||
|
||||
Only the Bitcoin ecosystem is active: `bitcoind`, `electrs`, `lnd`, `rtl`, `btcpay`, `mempool`, and `bip110`. All other server services are disabled.
|
||||
|
||||
| Stage | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Stage 1 — NixOS config | ✅ Backed up | Node-specific configuration |
|
||||
| Stage 2 — Secrets | ✅ Backed up | Bitcoin secrets and Hub state. `/var/lib/domains` may be minimal (BTCPay runs but is not exposed via Caddy) |
|
||||
| Stage 3 — Home directory | ✅ Backed up | User data |
|
||||
| Stage 4 — LND wallet | ✅ Backed up | **Critical** — Lightning wallet and channel data |
|
||||
|
||||
All four stages run, matching Server + Desktop behaviour. The `/var/lib/domains` directory may be sparsely populated since non-Bitcoin web services are not configured.
|
||||
|
||||
---
|
||||
|
||||
## Backup Manifest
|
||||
|
||||
After all stages complete, the script writes a `BACKUP_MANIFEST.txt` file inside the timestamped backup directory. This file records the date, hostname, detected role, target drive, and a directory listing of everything that was backed up.
|
||||
|
||||
---
|
||||
|
||||
## Running the Backup
|
||||
|
||||
The backup is triggered from the Sovran Hub web UI. You can also run it directly:
|
||||
|
||||
```bash
|
||||
# Auto-detect the first external USB drive
|
||||
sudo bash /path/to/sovran-hub-backup.sh
|
||||
|
||||
# Specify a target drive explicitly
|
||||
sudo BACKUP_TARGET=/run/media/<user>/<drive> bash /path/to/sovran-hub-backup.sh
|
||||
```
|
||||
|
||||
The script requires at least **10 GB** of free space on the target drive and will refuse to write to internal system drives.
|
||||
|
||||
Logs are written to `/var/log/sovran-hub-backup.log` and the current status (`RUNNING`, `SUCCESS`, or `FAILED`) is tracked in `/var/log/sovran-hub-backup.status`.
|
||||
@@ -37,21 +37,26 @@ FILE=/var/lib/beacons/file_fixes_and_new_services/add-custom-nix/completed
|
||||
touch /etc/nixos/custom.nix
|
||||
|
||||
/run/current-system/sw/bin/cat > /etc/nixos/custom.nix <<- "EOF"
|
||||
{ config, lib, ... }:
|
||||
|
||||
{config, pkgs, lib, ...}:
|
||||
|
||||
# Add custom NixOS modules here.
|
||||
|
||||
let
|
||||
personalization = import ./personalization.nix;
|
||||
|
||||
in
|
||||
{
|
||||
###########################################################
|
||||
# #
|
||||
# Sovran_SystemsOS — custom.nix #
|
||||
# #
|
||||
# Services, features, and roles are managed by the #
|
||||
# Sovran Hub. Any changes you make through the Hub #
|
||||
# will appear in the "Hub Managed" section below. #
|
||||
# #
|
||||
# If you want to add your own NixOS modules or #
|
||||
# configuration, place them here — outside of the #
|
||||
# Hub Managed section. #
|
||||
# #
|
||||
###########################################################
|
||||
|
||||
|
||||
# ─── Add your custom NixOS configuration below ───────────
|
||||
|
||||
}
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
|
||||
36
flake.lock
generated
@@ -5,11 +5,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773169138,
|
||||
"narHash": "sha256-6X41z8o2z8KjF4gMzLTPD41WjvCDGXTc0muPGmwcOMk=",
|
||||
"lastModified": 1775155316,
|
||||
"narHash": "sha256-4H8aEChZ6rra9jd8OcVHgHs3IuzKzpDt4PPtsPJrkyM=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "bitcoin-knots-bip-110-nix",
|
||||
"rev": "b9d018b71e20ce8c1567cbc2401b6edc2c1c7793",
|
||||
"rev": "663ea34f6f846f48c385a73d4581ba599bb5bbc0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -24,11 +24,11 @@
|
||||
"oldNixpkgs": "oldNixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774797058,
|
||||
"narHash": "sha256-URUOiKNjG3s7vDkTj554+3yzQ0qqNQoQwHdc7vs63X0=",
|
||||
"lastModified": 1775155241,
|
||||
"narHash": "sha256-J8dGfHLXlpJgDSgbBuQtll9chM9Hsv5hhVWglSRLgIE=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "btc-clients-nix",
|
||||
"rev": "a10dae067da04b7b170eed73efc665d27fc0e0c5",
|
||||
"rev": "e76e32fef6d0b8a8e9a32318835ba09915232a5c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -127,11 +127,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772380631,
|
||||
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
|
||||
"lastModified": 1775054576,
|
||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
|
||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -191,11 +191,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772380631,
|
||||
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
|
||||
"lastModified": 1775054576,
|
||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
|
||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -222,11 +222,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"lastModified": 1775423009,
|
||||
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -259,11 +259,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774802402,
|
||||
"narHash": "sha256-L1UJ/zxKTyyaGGmytH6OYlgQ0HGSMhvPkvU+iz4Mkb8=",
|
||||
"lastModified": 1775307257,
|
||||
"narHash": "sha256-y9hEecHH4ennFwIcw1n480YCGh73DkEmizmQnyXuvgg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "cbd8536a05d1aae2593cb5c9ace1010c8c5845cb",
|
||||
"rev": "2e008bb941f72379d5b935d5bfe70ed8b7c793ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
19
flake.nix
@@ -25,26 +25,17 @@
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
self.nixosModules.Sovran_SystemsOS
|
||||
/etc/nixos/role-state.nix
|
||||
/etc/nixos/custom.nix
|
||||
./hardware-configuration.nix
|
||||
./role-state.nix
|
||||
./custom.nix
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.sovran-iso-desktop = nixpkgs.lib.nixosSystem {
|
||||
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
||||
./iso/desktop.nix
|
||||
nix-bitcoin.nixosModules.default
|
||||
nixvim.nixosModules.nixvim
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.sovran-iso-server = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
||||
./iso/server.nix
|
||||
./iso/common.nix
|
||||
nix-bitcoin.nixosModules.default
|
||||
nixvim.nixosModules.nixvim
|
||||
];
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 31 KiB |
@@ -55,7 +55,6 @@ in
|
||||
gsettings-desktop-schemas
|
||||
adwaita-icon-theme
|
||||
util-linux
|
||||
disko
|
||||
parted
|
||||
dosfstools
|
||||
e2fsprogs
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{ device ? "/dev/sda", dataDevice ? "", ... }:
|
||||
|
||||
{
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
type = "disk";
|
||||
device = builtins.toString device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
priority = 1;
|
||||
name = "ESP";
|
||||
start = "1M";
|
||||
end = "512M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot/efi";
|
||||
mountOptions = [ "umask=0077" "defaults" ];
|
||||
};
|
||||
};
|
||||
root = {
|
||||
name = "root";
|
||||
start = "512M";
|
||||
end = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
extraArgs = [ "-L" "sovran_systemsos" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} // (if dataDevice != "" then {
|
||||
data = {
|
||||
type = "disk";
|
||||
device = builtins.toString dataDevice;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
primary = {
|
||||
name = "primary";
|
||||
start = "1M";
|
||||
end = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/run/media/Second_Drive";
|
||||
extraArgs = [ "-L" "BTCEcoandBackup" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} else {});
|
||||
};
|
||||
}
|
||||
658
iso/installer.py
@@ -14,6 +14,28 @@ LOGO = "/etc/sovran/logo.png"
|
||||
LOG = "/tmp/sovran-install.log"
|
||||
FLAKE = "/etc/sovran/flake"
|
||||
|
||||
DEPLOYED_FLAKE = """\
|
||||
{
|
||||
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
||||
|
||||
inputs = {
|
||||
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
|
||||
};
|
||||
|
||||
outputs = { self, Sovran_Systems, ... }@inputs: {
|
||||
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
||||
./hardware-configuration.nix
|
||||
./role-state.nix
|
||||
./custom.nix
|
||||
Sovran_Systems.nixosModules.Sovran_SystemsOS
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
logfile = open(LOG, "a")
|
||||
atexit.register(logfile.close)
|
||||
@@ -38,7 +60,8 @@ def run(cmd):
|
||||
|
||||
def run_stream(cmd, buf):
|
||||
log(f"$ {' '.join(cmd)}")
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.DEVNULL, text=True)
|
||||
for line in proc.stdout:
|
||||
log(line.rstrip())
|
||||
GLib.idle_add(append_text, buf, line)
|
||||
@@ -86,7 +109,7 @@ def symbolic_icon(name):
|
||||
return icon
|
||||
|
||||
|
||||
# ── Application ────────────────────────────────────────────────────────────────
|
||||
# ── Application ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerApp(Adw.Application):
|
||||
def __init__(self):
|
||||
@@ -98,7 +121,7 @@ class InstallerApp(Adw.Application):
|
||||
self.win.present()
|
||||
|
||||
|
||||
# ── Main Window ────────────────────────────────────────────────────────────────
|
||||
# ── Main Window ──────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerWindow(Adw.ApplicationWindow):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -117,11 +140,8 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
self.nav = Adw.NavigationView()
|
||||
self.set_content(self.nav)
|
||||
|
||||
# Check for internet before anything else
|
||||
if check_internet():
|
||||
self.push_welcome()
|
||||
else:
|
||||
self.push_no_internet()
|
||||
# Always show the landing/welcome page first
|
||||
self.push_landing()
|
||||
|
||||
# ── Navigation helpers ─────────────────────────────────────────────────
|
||||
|
||||
@@ -148,7 +168,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
break
|
||||
self.push_page(title, child)
|
||||
|
||||
# ── Shared widgets ───────────────<EFBFBD><EFBFBD>─────────────────────────────────────
|
||||
# ── Shared widgets ────────────────────────────────────────────────────
|
||||
|
||||
def make_scrolled_log(self):
|
||||
sw = Gtk.ScrolledWindow()
|
||||
@@ -197,54 +217,93 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
|
||||
return box
|
||||
|
||||
# ── No Internet Screen ─────────────────────────────────────────────────
|
||||
# ── Landing / Welcome Screen ───────────────────────────────────────────
|
||||
|
||||
def push_no_internet(self):
|
||||
def push_landing(self):
|
||||
"""First screen: always shown. Welcomes the user and checks connectivity."""
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
status = Adw.StatusPage()
|
||||
status.set_title("No Internet Connection")
|
||||
status.set_description(
|
||||
"An active internet connection is required to install Sovran_SystemsOS.\n\n"
|
||||
"Please connect an Ethernet cable or configure Wi-Fi,\n"
|
||||
"then press Retry."
|
||||
)
|
||||
status.set_icon_name("network-offline-symbolic")
|
||||
status.set_vexpand(True)
|
||||
outer.append(status)
|
||||
# Hero
|
||||
hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
hero.set_margin_top(40)
|
||||
hero.set_margin_bottom(16)
|
||||
hero.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
if os.path.exists(LOGO):
|
||||
try:
|
||||
img = Gtk.Image.new_from_file(LOGO)
|
||||
img.set_pixel_size(320)
|
||||
hero.append(img)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
title = Gtk.Label()
|
||||
title.set_markup("<span size='xx-large' weight='heavy'>Welcome to Sovran SystemsOS</span>")
|
||||
title.set_margin_top(8)
|
||||
hero.append(title)
|
||||
|
||||
sub = Gtk.Label()
|
||||
sub.set_markup("<span size='large' style='italic' foreground='#888888'>Be Digitally Sovereign</span>")
|
||||
hero.append(sub)
|
||||
|
||||
outer.append(hero)
|
||||
|
||||
sep = Gtk.Separator()
|
||||
sep.set_margin_start(40)
|
||||
sep.set_margin_end(40)
|
||||
sep.set_margin_top(8)
|
||||
outer.append(sep)
|
||||
|
||||
# Internet requirement notice
|
||||
notice = Gtk.Label()
|
||||
notice.set_markup(
|
||||
"<span size='medium'>"
|
||||
"Before installation begins, please ensure you have an <b>active internet connection</b>.\n"
|
||||
"Sovran SystemsOS downloads packages during installation and requires internet access\n"
|
||||
"to complete the process. Connect via <b>Ethernet cable</b> or configure <b>Wi-Fi</b> now."
|
||||
"</span>"
|
||||
)
|
||||
notice.set_justify(Gtk.Justification.CENTER)
|
||||
notice.set_wrap(True)
|
||||
notice.set_margin_top(20)
|
||||
notice.set_margin_start(48)
|
||||
notice.set_margin_end(48)
|
||||
outer.append(notice)
|
||||
|
||||
# Inline offline warning banner (hidden by default)
|
||||
self._offline_banner = Adw.Banner()
|
||||
self._offline_banner.set_title(
|
||||
"No internet connection detected. Please connect Ethernet or Wi-Fi and try again."
|
||||
)
|
||||
self._offline_banner.set_revealed(False)
|
||||
self._offline_banner.set_margin_top(12)
|
||||
self._offline_banner.set_margin_start(40)
|
||||
self._offline_banner.set_margin_end(40)
|
||||
outer.append(self._offline_banner)
|
||||
|
||||
outer.append(Gtk.Label(label="", vexpand=True))
|
||||
|
||||
# Check & Continue button
|
||||
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
btn_box.set_halign(Gtk.Align.CENTER)
|
||||
btn_box.set_margin_bottom(32)
|
||||
|
||||
retry_btn = Gtk.Button(label="Retry")
|
||||
retry_btn.add_css_class("suggested-action")
|
||||
retry_btn.add_css_class("pill")
|
||||
retry_btn.connect("clicked", self.on_retry_internet)
|
||||
btn_box.append(retry_btn)
|
||||
connect_btn = Gtk.Button(label="Check Connection & Continue →")
|
||||
connect_btn.add_css_class("suggested-action")
|
||||
connect_btn.add_css_class("pill")
|
||||
connect_btn.connect("clicked", self._on_landing_connect)
|
||||
btn_box.append(connect_btn)
|
||||
|
||||
outer.append(btn_box)
|
||||
|
||||
self.push_page("No Internet", outer)
|
||||
self.push_page("Sovran_SystemsOS Installer", outer)
|
||||
|
||||
def on_retry_internet(self, btn):
|
||||
def _on_landing_connect(self, btn):
|
||||
if check_internet():
|
||||
# Pop the no-internet page and proceed to welcome
|
||||
try:
|
||||
self.nav.pop()
|
||||
except Exception:
|
||||
pass
|
||||
self._offline_banner.set_revealed(False)
|
||||
self.push_welcome()
|
||||
else:
|
||||
dlg = Adw.MessageDialog()
|
||||
dlg.set_transient_for(self)
|
||||
dlg.set_heading("Still Offline")
|
||||
dlg.set_body(
|
||||
"Could not reach the internet.\n"
|
||||
"Please check your network connection and try again."
|
||||
)
|
||||
dlg.add_response("ok", "OK")
|
||||
dlg.present()
|
||||
self._offline_banner.set_revealed(True)
|
||||
|
||||
# ── Step 1: Welcome & Role ─────────────────────────────────────────────
|
||||
|
||||
@@ -302,6 +361,23 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
"Node (Bitcoin-only)"),
|
||||
]
|
||||
|
||||
# Detect internal (non-USB) drives to gate role availability
|
||||
try:
|
||||
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
|
||||
internal_disks = []
|
||||
for line in raw.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
|
||||
tran = parts[4] if len(parts) >= 5 else ""
|
||||
if tran != "usb":
|
||||
internal_disks.append(parts[0])
|
||||
except Exception:
|
||||
internal_disks = []
|
||||
|
||||
has_second_drive = len(internal_disks) >= 2
|
||||
|
||||
NEEDS_DATA_DRIVE = {"Server+Desktop", "Node (Bitcoin-only)"}
|
||||
|
||||
self._role_radios = []
|
||||
radio_group = None
|
||||
cards_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
@@ -311,14 +387,22 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
for label, desc, key in roles:
|
||||
card = Adw.ActionRow()
|
||||
card.set_title(label)
|
||||
card.set_subtitle(desc)
|
||||
|
||||
available = has_second_drive or key not in NEEDS_DATA_DRIVE
|
||||
if not available:
|
||||
card.set_subtitle(desc + "\n⚠ Requires a second internal drive (not detected)")
|
||||
card.set_sensitive(False)
|
||||
else:
|
||||
card.set_subtitle(desc)
|
||||
|
||||
radio = Gtk.CheckButton()
|
||||
radio.set_name(key)
|
||||
if radio_group is None:
|
||||
radio.set_sensitive(available)
|
||||
|
||||
if radio_group is None and available:
|
||||
radio_group = radio
|
||||
radio.set_active(True)
|
||||
else:
|
||||
elif radio_group is not None:
|
||||
radio.set_group(radio_group)
|
||||
|
||||
card.add_prefix(radio)
|
||||
@@ -341,144 +425,12 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
if radio.get_active():
|
||||
self.role = radio.get_name()
|
||||
break
|
||||
self.push_port_requirements()
|
||||
self.push_disk_detect()
|
||||
|
||||
# ── Step 1b: Port Requirements Notice ─────────────────────────────────
|
||||
# ── Step 2a: Disk Detect ──────────────────────────────────────────────
|
||||
|
||||
def push_port_requirements(self):
|
||||
"""Inform the user about required router/firewall ports before install."""
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# Detect internal IP at install time
|
||||
internal_ip = "this machine's LAN IP"
|
||||
try:
|
||||
import subprocess as _sp
|
||||
_r = _sp.run(["hostname", "-I"], capture_output=True, text=True, timeout=5)
|
||||
if _r.returncode == 0:
|
||||
_parts = _r.stdout.strip().split()
|
||||
if _parts:
|
||||
internal_ip = _parts[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Warning banner
|
||||
banner = Adw.Banner()
|
||||
banner.set_title(
|
||||
"⚠ Port Forwarding Setup Required — configure your router before install"
|
||||
)
|
||||
banner.set_revealed(True)
|
||||
banner.set_margin_top(16)
|
||||
banner.set_margin_start(40)
|
||||
banner.set_margin_end(40)
|
||||
outer.append(banner)
|
||||
|
||||
intro = Gtk.Label()
|
||||
intro.set_markup(
|
||||
"<span foreground='#a6adc8'>"
|
||||
"Many Sovran_SystemsOS features require <b>port forwarding</b> to be configured "
|
||||
"in your router's admin panel. This means telling your router to forward "
|
||||
"specific ports to <b>this machine's internal LAN IP</b>.\n\n"
|
||||
"Services like Element Video/Audio Calling and Matrix Federation "
|
||||
"<b>will not work for clients outside your LAN</b> unless these ports are "
|
||||
"forwarded to this machine."
|
||||
"</span>"
|
||||
)
|
||||
intro.set_wrap(True)
|
||||
intro.set_justify(Gtk.Justification.FILL)
|
||||
intro.set_margin_top(14)
|
||||
intro.set_margin_start(40)
|
||||
intro.set_margin_end(40)
|
||||
outer.append(intro)
|
||||
|
||||
ip_label = Gtk.Label()
|
||||
ip_label.set_markup(
|
||||
f"<span foreground='#89b4fa' font_desc='monospace'>"
|
||||
f" Forward ports to this machine's internal IP: <b>{internal_ip}</b>"
|
||||
f"</span>"
|
||||
)
|
||||
ip_label.set_margin_top(10)
|
||||
ip_label.set_margin_start(40)
|
||||
ip_label.set_margin_end(40)
|
||||
outer.append(ip_label)
|
||||
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
sw.set_vexpand(True)
|
||||
sw.set_margin_start(40)
|
||||
sw.set_margin_end(40)
|
||||
sw.set_margin_top(12)
|
||||
sw.set_margin_bottom(8)
|
||||
|
||||
ports_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
|
||||
port_sections = [
|
||||
(
|
||||
"🌐 Web / HTTPS (all domain-based services)",
|
||||
[("80", "TCP", "HTTP (redirects to HTTPS)"),
|
||||
("443", "TCP", "HTTPS")],
|
||||
),
|
||||
(
|
||||
"💬 Matrix Federation (Matrix-Synapse)",
|
||||
[("8448", "TCP", "Server-to-server federation")],
|
||||
),
|
||||
(
|
||||
"🎥 Element Video & Audio Calling (LiveKit / Element-call)",
|
||||
[("7881", "TCP", "LiveKit WebRTC signalling"),
|
||||
("7882-7894", "UDP", "LiveKit media streams"),
|
||||
("5349", "TCP", "TURN over TLS"),
|
||||
("3478", "UDP", "TURN (STUN / relay)"),
|
||||
("30000-40000", "TCP/UDP", "TURN relay (WebRTC media)")],
|
||||
),
|
||||
(
|
||||
"🖥 Remote SSH (optional — only if you want WAN SSH access)",
|
||||
[("22", "TCP", "SSH")],
|
||||
),
|
||||
]
|
||||
|
||||
for section_title, rows in port_sections:
|
||||
group = Adw.PreferencesGroup()
|
||||
group.set_title(section_title)
|
||||
|
||||
for port, proto, desc in rows:
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(f"Port {port} ({proto})")
|
||||
row.set_subtitle(desc)
|
||||
group.add(row)
|
||||
|
||||
ports_box.append(group)
|
||||
|
||||
note = Gtk.Label()
|
||||
note.set_markup(
|
||||
"<span foreground='#6c7086' size='small'>"
|
||||
"ℹ In your router's admin panel (usually at 192.168.1.1), find the "
|
||||
"\"<b>Port Forwarding</b>\" section and add a rule for each port above with "
|
||||
"the destination set to <b>this machine's internal IP</b>. "
|
||||
"These ports only need to be forwarded to this specific machine — "
|
||||
"this does <b>NOT</b> expose your entire network.\n"
|
||||
"To verify forwarding is working, test from a device on a different network "
|
||||
"(e.g. a phone on mobile data) or check your router's port forwarding page."
|
||||
"</span>"
|
||||
)
|
||||
note.set_wrap(True)
|
||||
note.set_justify(Gtk.Justification.FILL)
|
||||
note.set_margin_top(8)
|
||||
ports_box.append(note)
|
||||
|
||||
sw.set_child(ports_box)
|
||||
outer.append(sw)
|
||||
|
||||
outer.append(self.nav_row(
|
||||
back_label="← Back",
|
||||
back_cb=lambda b: self.nav.pop(),
|
||||
next_label="I Understand →",
|
||||
next_cb=lambda b: self.push_disk_confirm(),
|
||||
))
|
||||
|
||||
self.push_page("Network Port Requirements", outer, show_back=True)
|
||||
|
||||
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
||||
|
||||
def push_disk_confirm(self):
|
||||
def push_disk_detect(self):
|
||||
"""Detect internal drives and show the interactive disk selection page."""
|
||||
try:
|
||||
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
|
||||
except Exception as e:
|
||||
@@ -491,22 +443,208 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
|
||||
tran = parts[4] if len(parts) >= 5 else ""
|
||||
if tran != "usb":
|
||||
disks.append((parts[0], int(parts[1])))
|
||||
disks.append((parts[0], int(parts[1]), tran))
|
||||
|
||||
if not disks:
|
||||
self.show_error("No valid internal drives found. USB drives are excluded.")
|
||||
return
|
||||
|
||||
disks.sort(key=lambda x: x[1])
|
||||
self.boot_disk, self.boot_size = disks[0]
|
||||
self.data_disk, self.data_size = None, None
|
||||
self.push_disk_select(disks)
|
||||
|
||||
BYTES_2TB = 2 * 1024 ** 4
|
||||
if len(disks) >= 2:
|
||||
d, s = disks[-1]
|
||||
if s >= BYTES_2TB:
|
||||
self.data_disk, self.data_size = d, s
|
||||
# ── Step 2b: Disk Select ──────────────────────────────────────────────
|
||||
|
||||
def push_disk_select(self, disks):
|
||||
"""Interactive disk-selection page: pick OS drive and (optionally) data drive."""
|
||||
|
||||
BYTES_256GB = 256 * 1024 ** 3
|
||||
BYTES_2TB = 2 * 10 ** 12
|
||||
|
||||
# Sort ascending by size so the default selection (index 0) is the smallest
|
||||
disks = sorted(disks, key=lambda x: x[1])
|
||||
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroll.set_vexpand(True)
|
||||
|
||||
inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# ── OS Drive group ────────────────────────────────────────────
|
||||
os_group = Adw.PreferencesGroup()
|
||||
os_group.set_title("OS Drive (NixOS Boot + Root)")
|
||||
os_group.set_description(
|
||||
"Choose the drive for the NixOS installation. Minimum 256 GB required."
|
||||
)
|
||||
os_group.set_margin_top(24)
|
||||
os_group.set_margin_start(40)
|
||||
os_group.set_margin_end(40)
|
||||
|
||||
self._os_disk_radios = []
|
||||
os_radio_group = None
|
||||
|
||||
for name, size, tran in disks:
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(f"/dev/{name}")
|
||||
type_label = tran.upper() if tran else "Disk"
|
||||
meets = "✓ Meets 256 GB minimum" if size >= BYTES_256GB else "✗ Below 256 GB minimum"
|
||||
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
||||
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||
|
||||
radio = Gtk.CheckButton()
|
||||
radio.set_name(name)
|
||||
if os_radio_group is None:
|
||||
os_radio_group = radio
|
||||
radio.set_active(True)
|
||||
else:
|
||||
radio.set_group(os_radio_group)
|
||||
|
||||
row.add_suffix(radio)
|
||||
row.set_activatable_widget(radio)
|
||||
self._os_disk_radios.append(radio)
|
||||
os_group.add(row)
|
||||
|
||||
inner.append(os_group)
|
||||
|
||||
# ── Data Drive group (skipped for Desktop Only) ───────────────
|
||||
self._data_disk_radios = []
|
||||
|
||||
if self.role != "Desktop Only":
|
||||
data_group = Adw.PreferencesGroup()
|
||||
data_group.set_title("Bitcoin Timechain & Backups Drive")
|
||||
data_group.set_description(
|
||||
"💡 Tip: Always assign your LARGEST drive here. "
|
||||
"The full Bitcoin timechain is over 700 GB and grows continuously — "
|
||||
"a 2 TB or larger drive is required."
|
||||
)
|
||||
data_group.set_margin_top(20)
|
||||
data_group.set_margin_start(40)
|
||||
data_group.set_margin_end(40)
|
||||
|
||||
data_radio_group = None
|
||||
|
||||
# "None" option
|
||||
none_row = Adw.ActionRow()
|
||||
none_row.set_title("None (skip data drive)")
|
||||
none_row.set_subtitle("Bitcoin node functionality will be unavailable")
|
||||
none_radio = Gtk.CheckButton()
|
||||
none_radio.set_name("")
|
||||
data_radio_group = none_radio
|
||||
none_radio.set_active(True)
|
||||
none_row.add_suffix(none_radio)
|
||||
none_row.set_activatable_widget(none_radio)
|
||||
self._data_disk_radios.append(none_radio)
|
||||
data_group.add(none_row)
|
||||
|
||||
for name, size, tran in disks:
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(f"/dev/{name}")
|
||||
type_label = tran.upper() if tran else "Disk"
|
||||
meets = "✓ Meets 2 TB minimum" if size >= BYTES_2TB else "✗ Below 2 TB minimum"
|
||||
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
||||
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||
|
||||
radio = Gtk.CheckButton()
|
||||
radio.set_name(name)
|
||||
radio.set_group(data_radio_group)
|
||||
|
||||
row.add_suffix(radio)
|
||||
row.set_activatable_widget(radio)
|
||||
self._data_disk_radios.append(radio)
|
||||
data_group.add(row)
|
||||
|
||||
inner.append(data_group)
|
||||
|
||||
scroll.set_child(inner)
|
||||
outer.append(scroll)
|
||||
outer.append(self.nav_row(
|
||||
back_label="← Back",
|
||||
back_cb=lambda b: self.nav.pop(),
|
||||
next_label="Next →",
|
||||
next_cb=lambda b: self._on_disk_select_next(disks),
|
||||
))
|
||||
|
||||
self.push_page("Select Drives", outer, show_back=True)
|
||||
|
||||
def _on_disk_select_next(self, disks):
|
||||
"""Validate the user's disk selections and advance to the ERASE confirmation."""
|
||||
BYTES_256GB = 256 * 1024 ** 3
|
||||
BYTES_2TB = 2 * 10 ** 12
|
||||
|
||||
size_map = {name: size for name, size, _ in disks}
|
||||
|
||||
# Read OS disk selection
|
||||
os_name = None
|
||||
for radio in self._os_disk_radios:
|
||||
if radio.get_active():
|
||||
os_name = radio.get_name()
|
||||
break
|
||||
|
||||
# Read data disk selection (empty string = None chosen)
|
||||
data_name = None
|
||||
if self._data_disk_radios:
|
||||
for radio in self._data_disk_radios:
|
||||
if radio.get_active():
|
||||
sel = radio.get_name()
|
||||
data_name = sel if sel else None
|
||||
break
|
||||
|
||||
os_size = size_map.get(os_name, 0)
|
||||
data_size = size_map.get(data_name, 0) if data_name else 0
|
||||
|
||||
# Validate OS drive size
|
||||
if os_size < BYTES_256GB:
|
||||
dlg = Adw.MessageDialog()
|
||||
dlg.set_transient_for(self)
|
||||
dlg.set_heading("OS Drive Too Small")
|
||||
dlg.set_body(
|
||||
f"The selected OS drive (/dev/{os_name}, {human_size(os_size)}) "
|
||||
f"does not meet the 256 GB minimum. Please choose a larger drive."
|
||||
)
|
||||
dlg.add_response("ok", "OK")
|
||||
dlg.present()
|
||||
return
|
||||
|
||||
# Validate data drive size (when one was selected)
|
||||
if data_name and data_size < BYTES_2TB:
|
||||
dlg = Adw.MessageDialog()
|
||||
dlg.set_transient_for(self)
|
||||
dlg.set_heading("Bitcoin Drive Too Small")
|
||||
dlg.set_body(
|
||||
f"The selected Bitcoin Timechain & Backups drive "
|
||||
f"(/dev/{data_name}, {human_size(data_size)}) "
|
||||
f"does not meet the 2 TB minimum. "
|
||||
f"Please choose a larger drive or select \"None\"."
|
||||
)
|
||||
dlg.add_response("ok", "OK")
|
||||
dlg.present()
|
||||
return
|
||||
|
||||
# Validate no duplicate selection
|
||||
if data_name and data_name == os_name:
|
||||
dlg = Adw.MessageDialog()
|
||||
dlg.set_transient_for(self)
|
||||
dlg.set_heading("Same Drive Selected Twice")
|
||||
dlg.set_body(
|
||||
"You cannot use the same drive for both the OS and "
|
||||
"Bitcoin Timechain & Backups. Please choose different drives."
|
||||
)
|
||||
dlg.add_response("ok", "OK")
|
||||
dlg.present()
|
||||
return
|
||||
|
||||
# Commit selections
|
||||
self.boot_disk = os_name
|
||||
self.boot_size = os_size
|
||||
self.data_disk = data_name
|
||||
self.data_size = data_size if data_name else None
|
||||
|
||||
self.push_disk_confirm()
|
||||
|
||||
# ── Step 2c: Disk Confirm (ERASE confirmation) ────────────────────────
|
||||
|
||||
def push_disk_confirm(self):
|
||||
"""Show the selected drives and ask the user to type ERASE to confirm."""
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# Disk info group
|
||||
@@ -517,23 +655,17 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
disk_group.set_margin_end(40)
|
||||
|
||||
boot_row = Adw.ActionRow()
|
||||
boot_row.set_title("Boot Disk")
|
||||
boot_row.set_title("OS Disk")
|
||||
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
|
||||
boot_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||
disk_group.add(boot_row)
|
||||
|
||||
if self.data_disk:
|
||||
data_row = Adw.ActionRow()
|
||||
data_row.set_title("Data Disk")
|
||||
data_row.set_title("Bitcoin Timechain & Backups Disk")
|
||||
data_row.set_subtitle(f"/dev/{self.data_disk} — {human_size(self.data_size)}")
|
||||
data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||
disk_group.add(data_row)
|
||||
else:
|
||||
no_row = Adw.ActionRow()
|
||||
no_row.set_title("Data Disk")
|
||||
no_row.set_subtitle("None detected (requires 2 TB or larger)")
|
||||
no_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||
disk_group.add(no_row)
|
||||
|
||||
outer.append(disk_group)
|
||||
|
||||
@@ -592,7 +724,6 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
status = Adw.StatusPage()
|
||||
status.set_title(title)
|
||||
status.set_description(subtitle)
|
||||
status.set_icon_name("emblem-synchronizing-symbolic")
|
||||
status.set_vexpand(False)
|
||||
outer.append(status)
|
||||
|
||||
@@ -624,36 +755,69 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
|
||||
def do_partition(self, buf):
|
||||
boot_path = f"/dev/{self.boot_disk}"
|
||||
data_path = f"/dev/{self.data_disk}" if self.data_disk else None
|
||||
|
||||
# ── Wipe disk(s) to clear stale GPT/MBR data before disko ──
|
||||
# ── Wipe disk(s) ──
|
||||
GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n")
|
||||
|
||||
run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf)
|
||||
run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf)
|
||||
|
||||
if self.data_disk:
|
||||
data_path = f"/dev/{self.data_disk}"
|
||||
if data_path:
|
||||
run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf)
|
||||
run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf)
|
||||
|
||||
# Inform the kernel of the wiped partition tables
|
||||
run_stream(["sudo", "partprobe", boot_path], buf)
|
||||
if self.data_disk:
|
||||
if data_path:
|
||||
run_stream(["sudo", "partprobe", data_path], buf)
|
||||
|
||||
# Short settle so the kernel finishes re-reading
|
||||
time.sleep(2)
|
||||
|
||||
# ── Now run disko on a clean disk ──
|
||||
GLib.idle_add(append_text, buf, "\n=== Partitioning drives ===\n")
|
||||
cmd = [
|
||||
"sudo", "disko", "--mode", "disko",
|
||||
f"{FLAKE}/iso/disko.nix",
|
||||
"--arg", "device", f'"{boot_path}"'
|
||||
]
|
||||
if self.data_disk:
|
||||
cmd += ["--arg", "dataDevice", f'"/dev/{self.data_disk}"']
|
||||
run_stream(cmd, buf)
|
||||
# ── Partition boot disk: 512M ESP + rest as root ──
|
||||
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk ===\n")
|
||||
run_stream(["sudo", "sgdisk",
|
||||
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
|
||||
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
|
||||
boot_path], buf)
|
||||
|
||||
run_stream(["sudo", "partprobe", boot_path], buf)
|
||||
time.sleep(2)
|
||||
|
||||
# ── Partition data disk (if selected) ──
|
||||
if data_path:
|
||||
GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n")
|
||||
run_stream(["sudo", "sgdisk",
|
||||
"-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary",
|
||||
data_path], buf)
|
||||
|
||||
run_stream(["sudo", "partprobe", data_path], buf)
|
||||
time.sleep(2)
|
||||
|
||||
# ── Format partitions ──
|
||||
GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n")
|
||||
boot_p1 = f"{boot_path}p1" if "nvme" in boot_path else f"{boot_path}1"
|
||||
boot_p2 = f"{boot_path}p2" if "nvme" in boot_path else f"{boot_path}2"
|
||||
|
||||
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
|
||||
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
|
||||
|
||||
if data_path:
|
||||
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1"
|
||||
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf)
|
||||
|
||||
# ── Mount filesystems ──
|
||||
GLib.idle_add(append_text, buf, "\n=== Mounting filesystems ===\n")
|
||||
run_stream(["sudo", "mount", boot_p2, "/mnt"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
|
||||
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
|
||||
|
||||
if data_path:
|
||||
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1"
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
|
||||
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data"], buf)
|
||||
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup"], buf)
|
||||
|
||||
GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
|
||||
run_stream(["sudo", "nixos-generate-config", "--root", "/mnt"], buf)
|
||||
@@ -692,7 +856,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
||||
|
||||
# ── Step 4: Ready to install ────────<EFBFBD><EFBFBD><EFBFBD>──────────────────────────────────
|
||||
# ── Step 4: Ready to install ──────────────────────────────────────────
|
||||
|
||||
def push_ready(self):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
@@ -700,7 +864,6 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
status = Adw.StatusPage()
|
||||
status.set_title("Drives Ready")
|
||||
status.set_description("Your drives have been partitioned successfully.")
|
||||
status.set_icon_name("emblem-ok-symbolic")
|
||||
status.set_vexpand(True)
|
||||
|
||||
details = Adw.PreferencesGroup()
|
||||
@@ -762,15 +925,54 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
if not os.path.exists(f):
|
||||
raise RuntimeError(f"Required file missing: {f}")
|
||||
|
||||
# The flake.nix imports /etc/nixos/role-state.nix and /etc/nixos/custom.nix
|
||||
# as absolute paths. With --impure, Nix resolves these on the live ISO host,
|
||||
# not under /mnt. Copy them so they exist on the host filesystem too.
|
||||
GLib.idle_add(append_text, buf, "Copying config files to host /etc/nixos for flake evaluation...\n")
|
||||
run(["sudo", "mkdir", "-p", "/etc/nixos"])
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/role-state.nix", "/etc/nixos/role-state.nix"])
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.nix", "/etc/nixos/custom.nix"])
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/hardware-configuration.nix", "/etc/nixos/hardware-configuration.nix"])
|
||||
|
||||
run_stream([
|
||||
"sudo", "nixos-install",
|
||||
"--root", "/mnt",
|
||||
"--flake", "/mnt/etc/nixos#nixos"
|
||||
"--flake", "/mnt/etc/nixos#nixos",
|
||||
"--no-root-password",
|
||||
"--impure"
|
||||
], buf)
|
||||
|
||||
# Clean up /mnt/etc/nixos — only 5 files are needed post-install.
|
||||
# configuration.nix and modules/ were needed during nixos-install
|
||||
# for flake evaluation, but are now baked into the Nix store via
|
||||
# self.nixosModules.Sovran_SystemsOS.
|
||||
GLib.idle_add(append_text, buf, "Cleaning up /mnt/etc/nixos...\n")
|
||||
keep = {"flake.nix", "flake.lock", "hardware-configuration.nix",
|
||||
"role-state.nix", "custom.nix"}
|
||||
nixos_dir = "/mnt/etc/nixos"
|
||||
for entry in os.listdir(nixos_dir):
|
||||
if entry not in keep:
|
||||
path = os.path.join(nixos_dir, entry)
|
||||
run(["sudo", "rm", "-rf", path])
|
||||
|
||||
GLib.idle_add(append_text, buf, "Writing deployed flake.nix...\n")
|
||||
proc = subprocess.run(
|
||||
["sudo", "tee", "/mnt/etc/nixos/flake.nix"],
|
||||
input=DEPLOYED_FLAKE,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
log(proc.stdout)
|
||||
if proc.returncode != 0:
|
||||
log(proc.stderr)
|
||||
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
||||
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
|
||||
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
||||
"flake", "lock", "/mnt/etc/nixos"], buf)
|
||||
|
||||
GLib.idle_add(self.push_complete)
|
||||
|
||||
# ── Step 6: Complete ───────────────────────────────────────────────────
|
||||
# ── Complete ───────────────────────────────────────────────────────────
|
||||
|
||||
def push_complete(self):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
@@ -778,11 +980,10 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
status = Adw.StatusPage()
|
||||
status.set_title("Installation Complete!")
|
||||
status.set_description("Welcome to Sovran SystemsOS.")
|
||||
status.set_icon_name("emblem-ok-symbolic")
|
||||
status.set_vexpand(True)
|
||||
|
||||
creds_group = Adw.PreferencesGroup()
|
||||
creds_group.set_title("⚠ Write down your login details before rebooting")
|
||||
creds_group.set_title("⚠ Important — read before rebooting")
|
||||
creds_group.set_margin_start(40)
|
||||
creds_group.set_margin_end(40)
|
||||
|
||||
@@ -792,15 +993,15 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
creds_group.add(user_row)
|
||||
|
||||
pass_row = Adw.ActionRow()
|
||||
pass_row.set_title("Password")
|
||||
pass_row.set_subtitle("free")
|
||||
pass_row.set_title("Default Password")
|
||||
pass_row.set_subtitle("free — you will be prompted to change it on first boot")
|
||||
creds_group.add(pass_row)
|
||||
|
||||
note_row = Adw.ActionRow()
|
||||
note_row.set_title("App Passwords")
|
||||
note_row.set_title("First Boot Setup")
|
||||
note_row.set_subtitle(
|
||||
"After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) "
|
||||
"will be saved to a secure PDF in your Documents folder."
|
||||
"After rebooting, the Sovran Hub will guide you through setting "
|
||||
"your password, domains, and all app credentials."
|
||||
)
|
||||
creds_group.add(note_row)
|
||||
|
||||
@@ -810,9 +1011,8 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
outer.append(content_box)
|
||||
|
||||
reboot_btn = Gtk.Button(label="Reboot Now")
|
||||
reboot_btn.add_css_class("suggested-action")
|
||||
reboot_btn.add_css_class("success")
|
||||
reboot_btn.add_css_class("pill")
|
||||
reboot_btn.add_css_class("destructive-action")
|
||||
reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"]))
|
||||
|
||||
nav = Gtk.Box()
|
||||
@@ -849,4 +1049,4 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = InstallerApp()
|
||||
app.run(None)
|
||||
app.run(None)
|
||||
@@ -10,15 +10,15 @@ pkgs.stdenv.mkDerivation {
|
||||
mkdir -p $out/share/plymouth/themes/sovran
|
||||
cp ${./assets/splash-logo.png} $out/share/plymouth/themes/sovran/logo.png
|
||||
|
||||
cat > $out/share/plymouth/themes/sovran/sovran.plymouth <<'EOF'
|
||||
cat > $out/share/plymouth/themes/sovran/sovran.plymouth <<EOF
|
||||
[Plymouth Theme]
|
||||
Name=Sovran Systems
|
||||
Description=Sovran Systems Splash
|
||||
ModuleName=script
|
||||
|
||||
[script]
|
||||
ImageDir=/share/plymouth/themes/sovran
|
||||
ScriptFile=/share/plymouth/themes/sovran/sovran.script
|
||||
ImageDir=$out/share/plymouth/themes/sovran
|
||||
ScriptFile=$out/share/plymouth/themes/sovran/sovran.script
|
||||
EOF
|
||||
|
||||
cat > $out/share/plymouth/themes/sovran/sovran.script <<'EOF'
|
||||
@@ -34,11 +34,6 @@ logo = Image("logo.png");
|
||||
logo_sprite = Sprite(logo);
|
||||
logo_sprite.SetX((Window.GetWidth() - logo.GetWidth()) / 2);
|
||||
logo_sprite.SetY((Window.GetHeight() - logo.GetHeight()) / 2);
|
||||
|
||||
spinner = Sprite();
|
||||
spinner.SetImage(Spinner());
|
||||
spinner.SetX((Window.GetWidth() - spinner.GetImage().GetWidth()) / 2);
|
||||
spinner.SetY((Window.GetHeight() + logo.GetHeight()) / 2 + 20);
|
||||
EOF
|
||||
'';
|
||||
}
|
||||
@@ -69,7 +69,36 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
||||
};
|
||||
|
||||
nix-bitcoin.useVersionLockedPkgs = false;
|
||||
|
||||
|
||||
systemd.services.bitcoind = {
|
||||
requires = [ "run-media-Second_Drive.mount" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
};
|
||||
|
||||
systemd.services.electrs = {
|
||||
requires = [ "run-media-Second_Drive.mount" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
};
|
||||
|
||||
systemd.services.sovran-btc-permissions = {
|
||||
description = "Fix Bitcoin/Electrs data directory ownership on second drive";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "run-media-Second_Drive.mount" ];
|
||||
before = [ "bitcoind.service" "electrs.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then
|
||||
chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node
|
||||
fi
|
||||
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then
|
||||
chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
sovran_systemsOS.domainRequirements = [
|
||||
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
|
||||
];
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
|
||||
let
|
||||
exposeBtcpay = config.sovran_systemsOS.web.btcpayserver;
|
||||
extraVhosts = config.sovran_systemsOS.caddy.extraVirtualHosts;
|
||||
in
|
||||
{
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
user = "caddy";
|
||||
group = "root";
|
||||
configFile = "/run/caddy/Caddyfile";
|
||||
};
|
||||
|
||||
# Override ExecStart + ExecReload to point at the runtime-generated Caddyfile
|
||||
systemd.services.caddy.serviceConfig = {
|
||||
ExecStart = lib.mkForce [
|
||||
""
|
||||
"${pkgs.caddy}/bin/caddy run --config /run/caddy/Caddyfile --adapter caddyfile"
|
||||
];
|
||||
ExecReload = lib.mkForce [
|
||||
""
|
||||
"${pkgs.caddy}/bin/caddy reload --config /run/caddy/Caddyfile --adapter caddyfile --force"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.caddy-generate-config = {
|
||||
@@ -170,6 +182,11 @@ EOF
|
||||
encode gzip zstd
|
||||
}
|
||||
EOF
|
||||
|
||||
# ── Custom vhosts from custom.nix ──────────────
|
||||
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
|
||||
${extraVhosts}
|
||||
CUSTOM_VHOSTS_EOF
|
||||
'';
|
||||
};
|
||||
}
|
||||
}
|
||||
292
modules/core/factory-seal.nix
Normal file
@@ -0,0 +1,292 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
sovran-factory-seal = pkgs.writeShellScriptBin "sovran-factory-seal" ''
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ SOVRAN FACTORY SEAL — WARNING ⚠ ║"
|
||||
echo "╠══════════════════════════════════════════════════════════════╣"
|
||||
echo "║ This command will PERMANENTLY DELETE: ║"
|
||||
echo "║ • All generated passwords and secrets ║"
|
||||
echo "║ • LND wallet data (seed words, channels, macaroons) ║"
|
||||
echo "║ • SSH factory login key ║"
|
||||
echo "║ • Application databases (Matrix, Nextcloud, WordPress) ║"
|
||||
echo "║ • Vaultwarden database ║"
|
||||
echo "║ ║"
|
||||
echo "║ After sealing, all credentials will be regenerated fresh ║"
|
||||
echo "║ when the customer boots the device for the first time. ║"
|
||||
echo "║ ║"
|
||||
echo "║ DO NOT run this on a customer's live system. ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo -n "Type SEAL to confirm: "
|
||||
read -r CONFIRM
|
||||
if [ "$CONFIRM" != "SEAL" ]; then
|
||||
echo "Aborted." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Sealing system..."
|
||||
|
||||
# ── 1. Delete all generated secrets ──────────────────────────────
|
||||
echo " Wiping secrets..."
|
||||
[ -d /var/lib/secrets ] && find /var/lib/secrets -mindepth 1 -delete || true
|
||||
rm -rf /var/lib/matrix-synapse/registration-secret
|
||||
rm -rf /var/lib/matrix-synapse/db-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-username
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-credentials
|
||||
rm -rf /var/lib/livekit/livekit_keyFile
|
||||
rm -rf /etc/nix-bitcoin-secrets/*
|
||||
|
||||
# ── 2. Wipe LND wallet (seed words, wallet DB, macaroons) ────────
|
||||
echo " Wiping LND wallet data..."
|
||||
rm -rf /var/lib/lnd/*
|
||||
|
||||
# ── 3. Wipe SSH factory key so it regenerates with new passphrase ─
|
||||
echo " Removing SSH factory key..."
|
||||
rm -f /home/free/.ssh/factory_login /home/free/.ssh/factory_login.pub
|
||||
if [ -f /root/.ssh/authorized_keys ]; then
|
||||
sed -i '/factory_login/d' /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# ── 4. Drop application databases ────────────────────────────────
|
||||
echo " Dropping application databases..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"matrix-synapse\";" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS nextclouddb;" 2>/dev/null || true
|
||||
mysql -u root -e "DROP DATABASE IF EXISTS wordpressdb;" 2>/dev/null || true
|
||||
|
||||
# ── 5. Remove application config files (so init services re-run) ─
|
||||
echo " Removing application config files..."
|
||||
rm -rf /var/lib/www/wordpress/wp-config.php
|
||||
rm -rf /var/lib/www/nextcloud/config/config.php
|
||||
|
||||
# ── 6. Wipe Vaultwarden database ──────────────────────────────────
|
||||
echo " Wiping Vaultwarden data..."
|
||||
rm -rf /var/lib/bitwarden_rs/*
|
||||
rm -rf /var/lib/vaultwarden/*
|
||||
|
||||
# ── 7. Set sealed flag and remove onboarded flag ─────────────────
|
||||
echo " Setting sealed flag..."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
rm -f /var/lib/sovran-customer-onboarded
|
||||
|
||||
echo ""
|
||||
echo "System sealed. Power off now or the system will shut down in 10 seconds."
|
||||
sleep 10
|
||||
poweroff
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [ sovran-factory-seal ];
|
||||
|
||||
# ── Auto-seal on first customer boot ───────────────────────────────
|
||||
systemd.services.sovran-auto-seal = {
|
||||
description = "Auto-seal Sovran system on first customer boot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "sovran-hub.service" "sovran-legacy-security-check.service" ];
|
||||
after = [ "local-fs.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.e2fsprogs pkgs.openssl pkgs.postgresql pkgs.mariadb pkgs.shadow ];
|
||||
script = ''
|
||||
# ── Idempotency check ─────────────────────────────────────────
|
||||
if [ -f /var/lib/sovran-factory-sealed ]; then
|
||||
echo "sovran-auto-seal: already sealed, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "sovran-auto-seal: seal flag missing — checking system state..."
|
||||
|
||||
# ── Safety guard 1: customer has already onboarded ────────────
|
||||
if [ -f /var/lib/sovran-customer-onboarded ]; then
|
||||
echo "sovran-auto-seal: /var/lib/sovran-customer-onboarded exists — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Safety guard 2: onboarding was completed ──────────────────
|
||||
if [ -f /var/lib/sovran/onboarding-complete ]; then
|
||||
echo "sovran-auto-seal: /var/lib/sovran/onboarding-complete exists — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Safety guard 3: password has been changed from factory defaults ──
|
||||
if [ -f /etc/shadow ]; then
|
||||
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
|
||||
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
|
||||
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
|
||||
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
|
||||
STILL_DEFAULT=false
|
||||
# If the salt field starts with "rounds=", we cannot extract the real salt
|
||||
# with a simple cut — treat as still-default for safety
|
||||
if printf '%s' "$SALT" | grep -q '^rounds='; then
|
||||
STILL_DEFAULT=true
|
||||
else
|
||||
for DEFAULT_PW in "free" "gosovransystems"; do
|
||||
case "$ALGO_ID" in
|
||||
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
*)
|
||||
# Unknown hash algorithm — treat as still-default for safety
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$STILL_DEFAULT" = "false" ]; then
|
||||
echo "sovran-auto-seal: password has been changed from factory defaults — live system detected. Restoring flag and exiting."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── All safety guards passed: this is a fresh/unsealed system ─
|
||||
echo "sovran-auto-seal: fresh system confirmed — performing auto-seal..."
|
||||
|
||||
# ── 1. Wipe generated secrets ─────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping secrets..."
|
||||
[ -d /var/lib/secrets ] && find /var/lib/secrets -mindepth 1 -delete || true
|
||||
rm -rf /var/lib/matrix-synapse/registration-secret
|
||||
rm -rf /var/lib/matrix-synapse/db-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-password
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-username
|
||||
rm -rf /var/lib/gnome-remote-desktop/rdp-credentials
|
||||
rm -rf /var/lib/livekit/livekit_keyFile
|
||||
rm -rf /etc/nix-bitcoin-secrets/*
|
||||
|
||||
# ── 2. Wipe LND wallet data ───────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping LND wallet data..."
|
||||
rm -rf /var/lib/lnd/*
|
||||
|
||||
# ── 3. Remove SSH factory key ─────────────────────────────────
|
||||
echo "sovran-auto-seal: removing SSH factory key..."
|
||||
rm -f /home/free/.ssh/factory_login /home/free/.ssh/factory_login.pub
|
||||
if [ -f /root/.ssh/authorized_keys ]; then
|
||||
sed -i '/factory_login/d' /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# ── 4. Drop application databases ────────────────────────────
|
||||
echo "sovran-auto-seal: dropping application databases..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"matrix-synapse\";" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS nextclouddb;" 2>/dev/null || true
|
||||
mysql -u root -e "DROP DATABASE IF EXISTS wordpressdb;" 2>/dev/null || true
|
||||
|
||||
# ── 5. Remove application config files ───────────────────────
|
||||
echo "sovran-auto-seal: removing application config files..."
|
||||
rm -rf /var/lib/www/wordpress/wp-config.php
|
||||
rm -rf /var/lib/www/nextcloud/config/config.php
|
||||
|
||||
# ── 6. Wipe Vaultwarden data ──────────────────────────────────
|
||||
echo "sovran-auto-seal: wiping Vaultwarden data..."
|
||||
rm -rf /var/lib/bitwarden_rs/*
|
||||
rm -rf /var/lib/vaultwarden/*
|
||||
|
||||
# ── 7. Set sealed flag and make it immutable ──────────────────
|
||||
echo "sovran-auto-seal: setting sealed flag..."
|
||||
touch /var/lib/sovran-factory-sealed
|
||||
chattr +i /var/lib/sovran-factory-sealed 2>/dev/null || true
|
||||
|
||||
# ── 8. Remove onboarded flag so onboarding runs fresh ─────────
|
||||
rm -f /var/lib/sovran-customer-onboarded
|
||||
|
||||
echo "sovran-auto-seal: auto-seal complete. Continuing boot into onboarding."
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Legacy security check: warn existing (pre-seal) machines ───────
|
||||
systemd.services.sovran-legacy-security-check = {
|
||||
description = "Check for legacy (pre-factory-seal) security status";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "local-fs.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.openssl ];
|
||||
script = ''
|
||||
# If sealed AND onboarded — fully clean, nothing to do
|
||||
[ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0
|
||||
|
||||
# If sealed but not yet onboarded — seal was run, customer hasn't finished setup yet, that's fine
|
||||
[ -f /var/lib/sovran-factory-sealed ] && exit 0
|
||||
|
||||
# If onboarded but NOT sealed — installer ran without factory seal!
|
||||
if [ -f /var/lib/sovran-customer-onboarded ] && [ ! -f /var/lib/sovran-factory-sealed ]; then
|
||||
mkdir -p /var/lib/sovran
|
||||
echo "unsealed" > /var/lib/sovran/security-status
|
||||
cat > /var/lib/sovran/security-warning << 'EOF'
|
||||
This machine was set up without the factory seal process. Factory test data — including SSH keys, database contents, and wallet information — may still be present on this system. It is strongly recommended to back up any important data and re-install using a fresh ISO, or contact Sovran Systems support for assistance.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If the user completed Hub onboarding, they've addressed security
|
||||
[ -f /var/lib/sovran/onboarding-complete ] && exit 0
|
||||
|
||||
# If the free password has been changed from ALL known factory defaults, no warning needed
|
||||
if [ -f /etc/shadow ]; then
|
||||
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
|
||||
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then
|
||||
ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2)
|
||||
SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3)
|
||||
STILL_DEFAULT=false
|
||||
# If the salt field starts with "rounds=", we cannot extract the real salt
|
||||
# with a simple cut — treat as still-default for safety
|
||||
if printf '%s' "$SALT" | grep -q '^rounds='; then
|
||||
STILL_DEFAULT=true
|
||||
else
|
||||
for DEFAULT_PW in "free" "gosovransystems"; do
|
||||
case "$ALGO_ID" in
|
||||
6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
|
||||
*)
|
||||
# Unknown hash algorithm — treat as still-default for safety
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then
|
||||
STILL_DEFAULT=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$STILL_DEFAULT" = "false" ]; then
|
||||
# Password was changed — clear any legacy warning and exit
|
||||
rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# No flags at all + secrets exist = legacy (pre-seal era) machine
|
||||
if [ -f /var/lib/secrets/root-password ]; then
|
||||
mkdir -p /var/lib/sovran
|
||||
echo "legacy" > /var/lib/sovran/security-status
|
||||
echo "This system was deployed before the factory seal feature. Your passwords may be known to the factory. Please change your passwords through the Sovran Hub." > /var/lib/sovran/security-warning
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
IP=$(dig @resolver4.opendns.com myip.opendns.com +short -4)
|
||||
|
||||
## Add DDNS entries below — one curl per line
|
||||
## Run 'sudo sovran-setup-domains' to configure automatically
|
||||
## Managed via Sovran Hub web interface
|
||||
SCRIPT
|
||||
|
||||
chmod 700 /var/lib/njalla/njalla.sh
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
||||
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
||||
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
||||
sshd = lib.mkEnableOption "SSH remote access";
|
||||
};
|
||||
|
||||
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
||||
@@ -59,6 +60,15 @@
|
||||
};
|
||||
};
|
||||
|
||||
# ── Caddy customisation ───────────────────────────────────
|
||||
caddy = {
|
||||
extraVirtualHosts = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
default = "";
|
||||
description = "Additional raw Caddyfile blocks appended to the generated Caddy config. Use this in custom.nix to add custom domains and reverse proxies.";
|
||||
};
|
||||
};
|
||||
|
||||
# ── Domain setup registry ─────────────────────────────────
|
||||
domainRequirements = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
|
||||
@@ -4,16 +4,22 @@ let
|
||||
cfg = config.sovran_systemsOS;
|
||||
|
||||
monitoredServices =
|
||||
# ── Infrastructure (always present) ────────────────────────
|
||||
# ── Infrastructure — System Passwords (always present) ─────
|
||||
[
|
||||
{ name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; category = "infrastructure"; credentials = []; }
|
||||
{ name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; category = "infrastructure"; credentials = []; }
|
||||
{ name = "System Passwords"; unit = "root-password-setup.service"; type = "system"; icon = "passwords"; enabled = true; category = "infrastructure"; credentials = [
|
||||
{ label = "Free Account — Username"; value = "free"; }
|
||||
{ label = "Free Account — Password"; file = "/var/lib/secrets/free-password"; }
|
||||
{ label = "Root Password"; file = "/var/lib/secrets/root-password"; }
|
||||
{ label = "SSH Local Access"; value = "ssh root@localhost / Passphrase: gosovransystems"; }
|
||||
{ label = "SSH Passphrase"; file = "/var/lib/secrets/ssh-passphrase"; }
|
||||
]; }
|
||||
]
|
||||
# ── Infrastructure — Caddy + Tor (NOT desktop-only) ────────
|
||||
++ lib.optionals (!cfg.roles.desktop) [
|
||||
{ name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; category = "infrastructure"; credentials = []; }
|
||||
{ name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; category = "infrastructure"; credentials = []; }
|
||||
]
|
||||
# ── Infrastructure — Remote Desktop (all roles) ─────────────
|
||||
++ [
|
||||
{ name = "Remote Desktop"; unit = "gnome-remote-desktop.service"; type = "system"; icon = "rdp"; enabled = cfg.features.rdp; category = "infrastructure"; credentials = [
|
||||
{ label = "Username"; file = "/var/lib/gnome-remote-desktop/rdp-username"; }
|
||||
{ label = "Password"; file = "/var/lib/gnome-remote-desktop/rdp-password"; }
|
||||
@@ -22,7 +28,7 @@ let
|
||||
]; }
|
||||
]
|
||||
# ── Bitcoin Base (node implementations) ────────────────────
|
||||
++ [
|
||||
++ lib.optionals cfg.services.bitcoin [
|
||||
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [
|
||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
||||
]; }
|
||||
@@ -34,7 +40,7 @@ let
|
||||
]; }
|
||||
]
|
||||
# ── Bitcoin Apps (services on top of the node) ─────────────
|
||||
++ [
|
||||
++ lib.optionals cfg.services.bitcoin [
|
||||
{ name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "Tor Address"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
|
||||
{ label = "Port"; value = "50001"; }
|
||||
@@ -53,13 +59,21 @@ let
|
||||
{ label = "Connection URL"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; }
|
||||
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus → Scan Node Config\n3. Scan the QR code above or paste the Connection URL"; }
|
||||
]; }
|
||||
{ name = "Sparrow Auto-Connect"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
||||
{ label = "Status"; value = "Auto-configured on first boot"; }
|
||||
]; }
|
||||
{ name = "Bisq Auto-Connect"; unit = "bisq-autoconnect.service"; type = "system"; icon = "bisq"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "Node"; value = "127.0.0.1:8333 (Bitcoin Core)"; }
|
||||
{ label = "Status"; value = "Auto-configured on first boot"; }
|
||||
]; }
|
||||
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
|
||||
{ label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
|
||||
{ label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
|
||||
]; }
|
||||
]
|
||||
# ── Communication ──────────────────────────────────────────
|
||||
++ [
|
||||
# ── Communication (server+desktop only) ────────────────────
|
||||
++ lib.optionals cfg.roles.server_plus_desktop [
|
||||
{ name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; credentials = [
|
||||
{ label = "Homeserver URL"; file = "/var/lib/secrets/matrix-homeserver-url"; }
|
||||
{ label = "Admin Username"; file = "/var/lib/secrets/matrix-admin-username"; }
|
||||
@@ -69,8 +83,8 @@ let
|
||||
]; }
|
||||
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
|
||||
]
|
||||
# ── Self-Hosted Apps ───────────────────────────────────────
|
||||
++ [
|
||||
# ── Self-Hosted Apps (server+desktop only) ─────────────────
|
||||
++ lib.optionals cfg.roles.server_plus_desktop [
|
||||
{ name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; credentials = [
|
||||
{ label = "URL"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; }
|
||||
{ label = "Admin Panel"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; suffix = "/admin"; }
|
||||
@@ -83,11 +97,11 @@ let
|
||||
{ label = "Credentials"; file = "/var/lib/secrets/wordpress-admin"; multiline = true; }
|
||||
]; }
|
||||
]
|
||||
# ── Nostr / Relay ──────────────────────────────────────────
|
||||
++ [
|
||||
# ── Nostr / Relay (server+desktop only) ────────────────────
|
||||
++ lib.optionals cfg.roles.server_plus_desktop [
|
||||
{ name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; category = "nostr"; credentials = []; }
|
||||
]
|
||||
# ── Support ────────────────────────────────────────────────
|
||||
# ── Support (always present) ────────────────────────────────
|
||||
++ [
|
||||
{ name = "Tech Support"; unit = "sovran-tech-support"; type = "support"; icon = "support"; enabled = true; category = "support"; credentials = []; }
|
||||
];
|
||||
@@ -197,6 +211,30 @@ let
|
||||
fi
|
||||
'';
|
||||
|
||||
# ── Hub auto-launch wrapper script ────────────────────────────────
|
||||
hub-autolaunch-script = pkgs.writeShellScript "sovran-hub-autolaunch.sh" ''
|
||||
export PATH="${lib.makeBinPath [ pkgs.curl pkgs.xdg-utils ]}:$PATH"
|
||||
|
||||
DISABLE_FLAG="/var/lib/sovran/hub-autolaunch-disabled"
|
||||
BOOT_FLAG="/run/sovran-hub-autolaunch-done"
|
||||
|
||||
# User disabled auto-launch via Hub toggle
|
||||
[ -f "$DISABLE_FLAG" ] && exit 0
|
||||
|
||||
# Already launched this boot
|
||||
[ -f "$BOOT_FLAG" ] && exit 0
|
||||
|
||||
touch "$BOOT_FLAG"
|
||||
|
||||
# Wait for Hub server to become ready (max ~15 seconds)
|
||||
for i in $(seq 1 15); do
|
||||
curl -s -o /dev/null http://localhost:8937 && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
xdg-open http://localhost:8937
|
||||
'';
|
||||
|
||||
sovran-hub-web = pkgs.python3Packages.buildPythonApplication {
|
||||
pname = "sovran-systemsos-hub-web";
|
||||
version = "1.0.0";
|
||||
@@ -224,6 +262,22 @@ let
|
||||
install -d $out/share/sovran-hub/icons
|
||||
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
|
||||
|
||||
install -d $out/share/icons/hicolor/scalable/apps
|
||||
cp sovran_systemsos_web/static/sovran-hub-icon.svg $out/share/icons/hicolor/scalable/apps/sovran-hub.svg
|
||||
|
||||
install -d $out/share/applications
|
||||
cat > $out/share/applications/sovran-hub.desktop <<DESKTOP
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Sovran Hub
|
||||
Comment=Open Sovran_SystemsOS Hub dashboard
|
||||
Exec=xdg-open http://localhost:8937
|
||||
Icon=sovran-hub
|
||||
Terminal=false
|
||||
Categories=System;
|
||||
StartupNotify=false
|
||||
DESKTOP
|
||||
|
||||
install -d $out/bin
|
||||
cat > $out/bin/sovran-hub-web <<LAUNCHER
|
||||
#!${pkgs.python3}/bin/python3
|
||||
@@ -268,7 +322,7 @@ in
|
||||
StandardError = "journal";
|
||||
};
|
||||
|
||||
path = [ pkgs.qrencode ];
|
||||
path = [ pkgs.qrencode ] ++ lib.optional cfg.services.bitcoin config.services.bitcoind.package;
|
||||
};
|
||||
|
||||
systemd.services.sovran-hub-update = {
|
||||
@@ -287,6 +341,20 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ sovran-hub-web ];
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 3051 8937 60847 ];
|
||||
|
||||
# ── Auto-launch Hub in browser on login ───────────────────────
|
||||
environment.etc."xdg/autostart/sovran-hub-autolaunch.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Sovran Hub Auto-Launch
|
||||
Exec=${hub-autolaunch-script}
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=true
|
||||
'';
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
domains = config.sovran_systemsOS.domainRequirements;
|
||||
|
||||
domainNamesList = lib.concatMapStringsSep " " (d: d.name) domains;
|
||||
|
||||
ddnsPrompt = ''
|
||||
read -p " Njal.la DDNS curl command (paste full line, or Enter to skip): " DDNS_LINE
|
||||
if [ -n "$DDNS_LINE" ]; then
|
||||
# Strip any leading "curl " if they pasted the whole command
|
||||
DDNS_LINE="''${DDNS_LINE#curl }"
|
||||
# Strip surrounding quotes
|
||||
DDNS_LINE="''${DDNS_LINE%\"}"
|
||||
DDNS_LINE="''${DDNS_LINE#\"}"
|
||||
# Replace &auto with &a=''${IP} at the end
|
||||
DDNS_LINE="''${DDNS_LINE%auto}&a=''${DOLLAR}{IP}"
|
||||
# Remove any trailing double &a= if they already had &a=
|
||||
DDNS_LINE=$(echo "$DDNS_LINE" | sed 's/&a=&a=/\&a=/g')
|
||||
'';
|
||||
|
||||
confirmDomain = name: ''
|
||||
while true; do
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" " You entered:" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Domain: $DOMAIN" "$NC"
|
||||
if [ -n "''${DDNS_DISPLAY:-}" ]; then
|
||||
printf "%b%s%b\n" "$CYAN" " DDNS URL: $DDNS_DISPLAY" "$NC"
|
||||
fi
|
||||
echo ""
|
||||
read -p " Is this correct? (y/n): " CONFIRM
|
||||
case "$CONFIRM" in
|
||||
[yY])
|
||||
echo "$DOMAIN" > "/var/lib/domains/${name}"
|
||||
printf "%b%s%b\n" "$GREEN" " Saved." "$NC"
|
||||
break
|
||||
;;
|
||||
[nN])
|
||||
echo " Let's try again."
|
||||
REDO=true
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo " Please enter y or n."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
|
||||
domainPrompts = lib.concatMapStringsSep "\n" (d: ''
|
||||
REDO=true
|
||||
while [ "$REDO" = true ]; do
|
||||
REDO=false
|
||||
DDNS_DISPLAY=""
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" "── ${d.label} ──" "$NC"
|
||||
EXISTING=""
|
||||
if [ -f "/var/lib/domains/${d.name}" ]; then
|
||||
EXISTING=$(cat "/var/lib/domains/${d.name}")
|
||||
printf "%b%s%b\n" "$CYAN" " Current: $EXISTING" "$NC"
|
||||
fi
|
||||
read -p " Subdomain (e.g. ${d.example}) or Enter to keep current: " DOMAIN_INPUT
|
||||
DOMAIN="''${DOMAIN_INPUT:-$EXISTING}"
|
||||
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
${lib.optionalString d.needsDDNS ''
|
||||
${ddnsPrompt}
|
||||
DDNS_DISPLAY="$DDNS_LINE"
|
||||
PENDING_NJALLA="curl \"$DDNS_LINE\""
|
||||
fi
|
||||
''}
|
||||
|
||||
${confirmDomain d.name}
|
||||
|
||||
if [ "$REDO" = false ] && [ -n "''${PENDING_NJALLA:-}" ]; then
|
||||
NJALLA_ENTRIES="$NJALLA_ENTRIES
|
||||
$PENDING_NJALLA"
|
||||
PENDING_NJALLA=""
|
||||
fi
|
||||
else
|
||||
echo " Skipped."
|
||||
fi
|
||||
done
|
||||
'') domains;
|
||||
|
||||
missingDomainPrompts = lib.concatMapStringsSep "\n" (d: ''
|
||||
if [ ! -f "/var/lib/domains/${d.name}" ]; then
|
||||
MISSING=true
|
||||
REDO=true
|
||||
while [ "$REDO" = true ]; do
|
||||
REDO=false
|
||||
DDNS_DISPLAY=""
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" "── ${d.label} (NEW) ──" "$NC"
|
||||
read -p " Subdomain (e.g. ${d.example}): " DOMAIN
|
||||
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
${lib.optionalString d.needsDDNS ''
|
||||
${ddnsPrompt}
|
||||
DDNS_DISPLAY="$DDNS_LINE"
|
||||
PENDING_NJALLA="curl \"$DDNS_LINE\""
|
||||
fi
|
||||
''}
|
||||
|
||||
${confirmDomain d.name}
|
||||
|
||||
if [ "$REDO" = false ] && [ -n "''${PENDING_NJALLA:-}" ]; then
|
||||
NEW_NJALLA_ENTRIES="$NEW_NJALLA_ENTRIES
|
||||
$PENDING_NJALLA"
|
||||
PENDING_NJALLA=""
|
||||
fi
|
||||
else
|
||||
echo " Skipped."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
'') domains;
|
||||
|
||||
domainSummary = lib.concatMapStringsSep "\n" (d: ''
|
||||
if [ -f "/var/lib/domains/${d.name}" ]; then
|
||||
printf "%b%s%b\n" "$NC" " ${d.label}: $(cat /var/lib/domains/${d.name})" "$NC"
|
||||
fi
|
||||
'') domains;
|
||||
|
||||
setupScript = pkgs.writeShellScriptBin "sovran-setup-domains" ''
|
||||
set -euo pipefail
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
DOLLAR='$'
|
||||
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Sovran_SystemsOS — Domain & DDNS Setup" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" "Before running this, you need:" "$NC"
|
||||
echo ""
|
||||
echo " 1. Domains/subdomains purchased on https://njal.la"
|
||||
echo " 2. For each subdomain, add a Dynamic record in"
|
||||
echo " your Njal.la dashboard."
|
||||
echo " 3. Njal.la will give you a curl command like:"
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" " curl \"https://njal.la/update/?h=sub.domain.com&k=abc123&auto\"" "$NC"
|
||||
echo ""
|
||||
echo " Have those curl commands ready."
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
|
||||
# ── Create directories ────────────────────────────
|
||||
mkdir -p /var/lib/domains
|
||||
|
||||
NJALLA_ENTRIES=""
|
||||
PENDING_NJALLA=""
|
||||
|
||||
# ── SSL Email ─────────────────────────────────────
|
||||
REDO=true
|
||||
while [ "$REDO" = true ]; do
|
||||
REDO=false
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" "── SSL Certificate Email ──" "$NC"
|
||||
echo "Let's Encrypt needs an email for certificate notifications."
|
||||
EXISTING_EMAIL=""
|
||||
if [ -f "/var/lib/domains/sslemail" ]; then
|
||||
EXISTING_EMAIL=$(cat /var/lib/domains/sslemail)
|
||||
printf "%b%s%b\n" "$CYAN" " Current: $EXISTING_EMAIL" "$NC"
|
||||
fi
|
||||
read -p " Email address (or Enter to keep current): " EMAIL_INPUT
|
||||
SSL_EMAIL="''${EMAIL_INPUT:-$EXISTING_EMAIL}"
|
||||
if [ -n "$SSL_EMAIL" ]; then
|
||||
while true; do
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" " You entered:" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Email: $SSL_EMAIL" "$NC"
|
||||
echo ""
|
||||
read -p " Is this correct? (y/n): " CONFIRM
|
||||
case "$CONFIRM" in
|
||||
[yY])
|
||||
echo "$SSL_EMAIL" > /var/lib/domains/sslemail
|
||||
printf "%b%s%b\n" "$GREEN" " Saved." "$NC"
|
||||
break
|
||||
;;
|
||||
[nN])
|
||||
echo " Let's try again."
|
||||
REDO=true
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo " Please enter y or n."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
# ── All module domains ────────────────────────────
|
||||
${domainPrompts}
|
||||
|
||||
# ── Final review ──────────────────────────────────
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Review All Entries" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " Configured domains:"
|
||||
${domainSummary}
|
||||
echo ""
|
||||
echo " DDNS entries:"
|
||||
if [ -n "$NJALLA_ENTRIES" ]; then
|
||||
echo "$NJALLA_ENTRIES"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo ""
|
||||
read -p " Does everything look correct? (y/n): " FINAL_CONFIRM
|
||||
if [ "$FINAL_CONFIRM" != "y" ] && [ "$FINAL_CONFIRM" != "Y" ]; then
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" " Setup cancelled. Run 'sudo sovran-setup-domains' to start over." "$NC"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Append curl entries to njalla.sh ──────────────
|
||||
if [ -n "$NJALLA_ENTRIES" ]; then
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" "── Updating DDNS script ──" "$NC"
|
||||
echo "$NJALLA_ENTRIES" >> /var/lib/njalla/njalla.sh
|
||||
echo " Appended entries to /var/lib/njalla/njalla.sh"
|
||||
fi
|
||||
|
||||
# ── Run DDNS update now ───────────────────────────
|
||||
echo ""
|
||||
read -p "Update Njal.la DNS records now? (y/n): " RUN_NOW
|
||||
if [ "$RUN_NOW" = "y" ]; then
|
||||
bash /var/lib/njalla/njalla.sh
|
||||
echo " DNS records updated."
|
||||
fi
|
||||
|
||||
# ── Mark setup complete ───────────────────────────
|
||||
touch /var/lib/domains/.setup-complete
|
||||
|
||||
# ── Summary ───────────────────────────────────────
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Setup Complete!" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " Domain files: /var/lib/domains/"
|
||||
echo " DDNS script: /var/lib/njalla/njalla.sh"
|
||||
echo " DDNS cron: Every 15 minutes (already configured)"
|
||||
echo ""
|
||||
|
||||
# ── Port Forwarding Reminder ──────────────────────
|
||||
INTERNAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b ⚠ Port Forwarding Reminder%b\n" "$YELLOW" "$NC"
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " For your services to be reachable from the internet, you must"
|
||||
echo " set up PORT FORWARDING in your router's admin panel."
|
||||
echo ""
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " Forward these ports to this machine's internal IP: %b%s%b\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " Forward these ports to this machine's internal LAN IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " 80/TCP — HTTP (redirects to HTTPS)"
|
||||
echo " 443/TCP — HTTPS (all domain-based services)"
|
||||
echo " 8448/TCP — Matrix federation (server-to-server)"
|
||||
echo ""
|
||||
echo " If you enabled Element Calling, also forward:"
|
||||
echo " 7881/TCP — LiveKit WebRTC signalling"
|
||||
echo " 7882-7894/UDP — LiveKit media streams"
|
||||
echo " 5349/TCP — TURN over TLS"
|
||||
echo " 3478/UDP — TURN (STUN/relay)"
|
||||
echo " 30000-40000/TCP+UDP — TURN relay"
|
||||
echo ""
|
||||
echo " How: Log into your router (usually 192.168.1.1), find the"
|
||||
echo " \"Port Forwarding\" section, and add rules for each port above"
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " with the destination set to %b%s%b.\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " with the destination set to this machine's IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " These ports only need to be forwarded to this specific machine —"
|
||||
echo " this does NOT expose your entire network."
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
read -p "Press Enter to continue with the rebuild..."
|
||||
|
||||
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
|
||||
echo ""
|
||||
nixos-rebuild switch --flake /etc/nixos#nixos
|
||||
'';
|
||||
|
||||
addDomainScript = pkgs.writeShellScriptBin "sovran-add-domains" ''
|
||||
set -euo pipefail
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
DOLLAR='$'
|
||||
|
||||
MISSING=false
|
||||
NEW_NJALLA_ENTRIES=""
|
||||
PENDING_NJALLA=""
|
||||
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Sovran_SystemsOS — New Feature Domains" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " Checking for newly enabled features that need domains..."
|
||||
|
||||
mkdir -p /var/lib/domains
|
||||
|
||||
${missingDomainPrompts}
|
||||
|
||||
if [ "$MISSING" = false ]; then
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" " All domains are already configured. Nothing to do." "$NC"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Final review ──────────────────────────────────
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " Review New Entries" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " All configured domains:"
|
||||
${domainSummary}
|
||||
echo ""
|
||||
echo " New DDNS entries:"
|
||||
if [ -n "$NEW_NJALLA_ENTRIES" ]; then
|
||||
echo "$NEW_NJALLA_ENTRIES"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo ""
|
||||
read -p " Does everything look correct? (y/n): " FINAL_CONFIRM
|
||||
if [ "$FINAL_CONFIRM" != "y" ] && [ "$FINAL_CONFIRM" != "Y" ]; then
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" " Setup cancelled. Run 'sudo sovran-add-domains' to start over." "$NC"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Append new entries to njalla.sh ───────────────
|
||||
if [ -n "$NEW_NJALLA_ENTRIES" ]; then
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$GREEN" "── Updating DDNS script ──" "$NC"
|
||||
echo "$NEW_NJALLA_ENTRIES" >> /var/lib/njalla/njalla.sh
|
||||
echo " Appended new entries to /var/lib/njalla/njalla.sh"
|
||||
|
||||
echo ""
|
||||
read -p "Update Njal.la DNS records now? (y/n): " RUN_NOW
|
||||
if [ "$RUN_NOW" = "y" ]; then
|
||||
bash /var/lib/njalla/njalla.sh
|
||||
echo " DNS records updated."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" " New Domains Added!" "$NC"
|
||||
printf "%b%s%b\n" "$CYAN" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " All configured domains:"
|
||||
${domainSummary}
|
||||
echo ""
|
||||
|
||||
# ── Port Forwarding Reminder ──────────────────────
|
||||
INTERNAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b ⚠ Port Forwarding Reminder%b\n" "$YELLOW" "$NC"
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " For your services to be reachable from the internet, you must"
|
||||
echo " set up PORT FORWARDING in your router's admin panel."
|
||||
echo ""
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " Forward these ports to this machine's internal IP: %b%s%b\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " Forward these ports to this machine's internal LAN IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " 80/TCP — HTTP (redirects to HTTPS)"
|
||||
echo " 443/TCP — HTTPS (all domain-based services)"
|
||||
echo " 8448/TCP — Matrix federation (server-to-server)"
|
||||
echo ""
|
||||
echo " If you enabled Element Calling, also forward:"
|
||||
echo " 7881/TCP — LiveKit WebRTC signalling"
|
||||
echo " 7882-7894/UDP — LiveKit media streams"
|
||||
echo " 5349/TCP — TURN over TLS"
|
||||
echo " 3478/UDP — TURN (STUN/relay)"
|
||||
echo " 30000-40000/TCP+UDP — TURN relay"
|
||||
echo ""
|
||||
echo " How: Log into your router (usually 192.168.1.1), find the"
|
||||
echo " \"Port Forwarding\" section, and add rules for each port above"
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " with the destination set to %b%s%b.\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " with the destination set to this machine's IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " These ports only need to be forwarded to this specific machine —"
|
||||
echo " this does NOT expose your entire network."
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
read -p "Press Enter to continue with the rebuild..."
|
||||
|
||||
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
|
||||
echo ""
|
||||
nixos-rebuild switch --impure
|
||||
'';
|
||||
|
||||
needsSetup = pkgs.writeShellScriptBin "sovran-domains-need-setup" ''
|
||||
# First boot — no setup done at all
|
||||
if [ ! -f /var/lib/domains/.setup-complete ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Existing machine — check for missing domain files
|
||||
for NAME in ${domainNamesList}; do
|
||||
if [ ! -f "/var/lib/domains/$NAME" ]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Everything is configured
|
||||
exit 1
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [
|
||||
setupScript
|
||||
addDomainScript
|
||||
needsSetup
|
||||
];
|
||||
|
||||
environment.etc."xdg/autostart/sovran-setup-domains.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Sovran_SystemsOS Domain Setup
|
||||
Comment=Configure domains for newly enabled features
|
||||
Exec=${pkgs.bash}/bin/bash -c 'if ${needsSetup}/bin/sovran-domains-need-setup; then if [ ! -f /var/lib/domains/.setup-complete ]; then ${pkgs.gnome-terminal}/bin/gnome-terminal -- sudo ${setupScript}/bin/sovran-setup-domains; else ${pkgs.gnome-terminal}/bin/gnome-terminal -- sudo ${addDomainScript}/bin/sovran-add-domains; fi; fi'
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
'';
|
||||
}
|
||||
@@ -12,9 +12,29 @@ lib.mkIf userExists {
|
||||
"d /home/${userName}/.ssh 0700 ${userName} users -"
|
||||
];
|
||||
|
||||
systemd.services.ssh-passphrase-setup = {
|
||||
description = "Generate per-device SSH key passphrase";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "factory-ssh-keygen.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.coreutils ];
|
||||
script = ''
|
||||
if [ ! -f "/var/lib/secrets/ssh-passphrase" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
pwgen -s 20 1 > /var/lib/secrets/ssh-passphrase
|
||||
chmod 600 /var/lib/secrets/ssh-passphrase
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.factory-ssh-keygen = {
|
||||
description = "Generate factory SSH key for ${userName} if missing";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "ssh-passphrase-setup.service" ];
|
||||
requires = [ "ssh-passphrase-setup.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
@@ -22,7 +42,8 @@ lib.mkIf userExists {
|
||||
path = [ pkgs.openssh pkgs.coreutils ];
|
||||
script = ''
|
||||
if [ ! -f "${keyPath}" ]; then
|
||||
ssh-keygen -q -N "gosovransystems" -t ed25519 -f "${keyPath}"
|
||||
PASSPHRASE=$(cat /var/lib/secrets/ssh-passphrase)
|
||||
ssh-keygen -q -N "$PASSPHRASE" -t ed25519 -f "${keyPath}"
|
||||
chown ${userName}:users "${keyPath}" "${keyPath}.pub"
|
||||
chmod 600 "${keyPath}"
|
||||
chmod 644 "${keyPath}.pub"
|
||||
|
||||
21
modules/core/sshd-localhost.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# ── Always-on localhost SSH ────────────────────────────────────
|
||||
# Provides "ssh root@localhost" for local root access and Hub
|
||||
# operations. Binds exclusively to 127.0.0.1 — zero network exposure.
|
||||
# The sshd *feature flag* in sshd.nix extends this to 0.0.0.0 and
|
||||
# opens port 22 on the firewall when the user enables remote SSH.
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
listenAddresses = lib.mkDefault [
|
||||
{ addr = "127.0.0.1"; port = 22; }
|
||||
];
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "yes";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
fonts = pkgs.liberation_ttf;
|
||||
|
||||
# ── Helper: change 'free' password and save it ─────────────
|
||||
change-free-password = pkgs.writeShellScriptBin "change-free-password" ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root (use sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "New password for free: "
|
||||
read -rs NEW_PASS
|
||||
echo
|
||||
echo -n "Confirm password: "
|
||||
read -rs CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$NEW_PASS" != "$CONFIRM" ]; then
|
||||
echo "Passwords do not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_PASS" ]; then
|
||||
echo "Password cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "free:$NEW_PASS" | ${pkgs.shadow}/bin/chpasswd
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Password for 'free' updated and saved."
|
||||
'';
|
||||
in
|
||||
{
|
||||
# ── Make helper available system-wide ───────────────────────
|
||||
environment.systemPackages = [ change-free-password ];
|
||||
|
||||
# ── Shell aliases: intercept 'passwd free' ─────────────────
|
||||
programs.bash.interactiveShellInit = ''
|
||||
passwd() {
|
||||
if [ "$1" = "free" ]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub and Magic Keys PDF will NOT be updated. ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
command passwd "$@"
|
||||
}
|
||||
'';
|
||||
|
||||
programs.fish.interactiveShellInit = ''
|
||||
function passwd --wraps passwd
|
||||
if test "$argv[1]" = "free"
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub and Magic Keys PDF will NOT be updated. ║"
|
||||
echo "╚════════════════════════════<EFBFBD><EFBFBD>═════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
end
|
||||
command passwd $argv
|
||||
end
|
||||
'';
|
||||
|
||||
# ── 1. Auto-Generate Root Password (Runs once) ─────────────
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Generate and set a random root password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/root-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
ROOT_PASS=$(pwgen -s 20 1)
|
||||
echo "root:$ROOT_PASS" | chpasswd
|
||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1b. Save 'free' password on first boot ─────────────────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Save the initial 'free' user password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "free" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1c. Save Zeus/lndconnect URL for hub credentials ────────
|
||||
systemd.services.zeus-connect-setup = {
|
||||
description = "Save Zeus lndconnect URL";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "lnd.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils "/run/current-system/sw" ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/zeus-connect-url"
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
URL=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
echo "$URL" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Zeus connect URL saved."
|
||||
else
|
||||
echo "No lndconnect URL available yet."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Refresh Zeus URL periodically (certs/macaroons may rotate)
|
||||
systemd.timers.zeus-connect-setup = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "2min";
|
||||
OnUnitActiveSec = "30min";
|
||||
Unit = "zeus-connect-setup.service";
|
||||
};
|
||||
};
|
||||
|
||||
# ── 2. Timer: Check every 5 minutes ────────────────────────
|
||||
systemd.timers.generate-credentials-pdf = {
|
||||
description = "Periodically check if Magic Keys PDF needs regenerating";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "30s";
|
||||
OnUnitActiveSec = "5min";
|
||||
Unit = "generate-credentials-pdf.service";
|
||||
};
|
||||
};
|
||||
|
||||
# ── 3. Generate the Magic Keys PDF ─────────────────────────
|
||||
systemd.services.generate-credentials-pdf = {
|
||||
description = "Generate Magic Keys PDF for Sovran_SystemsOS";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
};
|
||||
|
||||
path = [
|
||||
pkgs.pandoc
|
||||
pkgs.typst
|
||||
pkgs.coreutils
|
||||
pkgs.qrencode
|
||||
pkgs.gnugrep
|
||||
fonts
|
||||
"/run/current-system/sw"
|
||||
];
|
||||
|
||||
environment = {
|
||||
TYPST_FONT_PATHS = "${fonts}/share/fonts";
|
||||
};
|
||||
|
||||
script = ''
|
||||
DOC_DIR="/home/free/Documents"
|
||||
OUTPUT="$DOC_DIR/Sovran_SystemsOS_Magic_Keys.pdf"
|
||||
WORK_DIR="/tmp/magic_keys_build"
|
||||
FILE="$WORK_DIR/magic_keys.md"
|
||||
HASH_FILE="/var/lib/secrets/.magic-keys-hash"
|
||||
|
||||
FENCE='```'
|
||||
|
||||
# ── Collect all secret sources into a single hash ──
|
||||
SECRET_SOURCES=""
|
||||
for f in \
|
||||
/var/lib/secrets/root-password \
|
||||
/var/lib/secrets/free-password \
|
||||
/etc/nix-bitcoin-secrets/rtl-password \
|
||||
/var/lib/tor/onion/rtl/hostname \
|
||||
/var/lib/tor/onion/electrs/hostname \
|
||||
/var/lib/tor/onion/bitcoind/hostname \
|
||||
/var/lib/secrets/matrix-users \
|
||||
/var/lib/gnome-remote-desktop/rdp-credentials \
|
||||
/var/lib/secrets/nextcloud-admin \
|
||||
/var/lib/secrets/wordpress-admin \
|
||||
/var/lib/secrets/vaultwarden/vaultwarden.env \
|
||||
/var/lib/domains/vaultwarden \
|
||||
/var/lib/domains/btcpayserver \
|
||||
/var/lib/secrets/zeus-connect-url; do
|
||||
if [ -f "$f" ]; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(cat "$f")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Add lndconnect URL to hash sources (changes if certs/macaroons rotate)
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(lndconnect --url 2>/dev/null || true)"
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
SECRET_SOURCES="$SECRET_SOURCES$(lnconnect-clnrest --url 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
CURRENT_HASH=$(echo -n "$SECRET_SOURCES" | sha256sum | cut -d' ' -f1)
|
||||
OLD_HASH=""
|
||||
if [ -f "$HASH_FILE" ]; then
|
||||
OLD_HASH=$(cat "$HASH_FILE")
|
||||
fi
|
||||
|
||||
# ── Skip if PDF exists and nothing changed ──
|
||||
if [ -f "$OUTPUT" ] && [ "$CURRENT_HASH" = "$OLD_HASH" ]; then
|
||||
echo "No changes detected, skipping PDF regeneration."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes detected (or PDF missing), regenerating..."
|
||||
mkdir -p "$DOC_DIR" "$WORK_DIR"
|
||||
|
||||
# ── Read secrets (default to placeholder if missing) ──
|
||||
read_secret() { if [ -f "$1" ]; then cat "$1"; else echo "$2"; fi; }
|
||||
|
||||
ROOT_PASS=$(read_secret /var/lib/secrets/root-password "Generating...")
|
||||
FREE_PASS=$(read_secret /var/lib/secrets/free-password "free")
|
||||
RTL_PASS=$(read_secret /etc/nix-bitcoin-secrets/rtl-password "Not found")
|
||||
RTL_ONION=$(read_secret /var/lib/tor/onion/rtl/hostname "Not generated yet")
|
||||
ELECTRS_ONION=$(read_secret /var/lib/tor/onion/electrs/hostname "Not generated yet")
|
||||
BITCOIN_ONION=$(read_secret /var/lib/tor/onion/bitcoind/hostname "Not generated yet")
|
||||
|
||||
# ── Generate Zeus QR code PNG if lndconnect URL is available ──
|
||||
ZEUS_URL=""
|
||||
HAS_ZEUS_QR=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
ZEUS_URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
ZEUS_URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$ZEUS_URL" ]; then
|
||||
qrencode -o "$WORK_DIR/zeus-qr.png" -s 4 -m 1 -l H "$ZEUS_URL" 2>/dev/null && HAS_ZEUS_QR="1"
|
||||
fi
|
||||
|
||||
# ── Build the Markdown document ──
|
||||
cat > "$FILE" << ENDOFFILE
|
||||
---
|
||||
title: "Sovran SystemsOS Magic Keys"
|
||||
---
|
||||
|
||||
# Your Sovran SystemsOS Magic Keys! 🗝️
|
||||
|
||||
Welcome to your new computer! We have built a lot of cool secret forts (services) for you. To get into your forts, you need your magic keys (passwords).
|
||||
|
||||
Here are all of your keys in one place. **Keep this document safe and do not share it with strangers!**
|
||||
|
||||
> **How this document works:** This PDF is automatically generated by your computer. If any of your passwords, services, or connection details change, this document will automatically update itself within a few minutes. You can always find the latest version right here in your Documents folder. If you accidentally delete it, don't worry — your computer will recreate it for you!
|
||||
|
||||
## 🖥️ Your Computer
|
||||
These are the master keys to the actual machine.
|
||||
|
||||
### 1. Main Screen Unlock (The 'free' account)
|
||||
When you turn the computer on, it usually logs you in automatically. However, if the screen goes to sleep, or **if you enable Remote Desktop (RDP)**, you will need this to log in:
|
||||
- **Username:** \`free\`
|
||||
- **Password:** \`$FREE_PASS\`
|
||||
|
||||
🚨 **VERY IMPORTANT:** You MUST write this password down and keep it safe! If you lose it, you will be locked out of your computer!
|
||||
|
||||
### 2. The Big Boss (Root)
|
||||
Sometimes a pop-up box might ask for an Administrator (Root) password to change a setting. We created a super-secret password just for this!
|
||||
- **Root Password:** \`$ROOT_PASS\`
|
||||
|
||||
### 3. The Hacker Terminal (\`ssh root@localhost\`)
|
||||
Because your main account is so safe, you cannot just type normal commands to become the boss. If you open a black terminal box and want to make big changes, you must use your special factory key!
|
||||
|
||||
Type this exact command into the terminal:
|
||||
\`ssh root@localhost\`
|
||||
|
||||
When it asks for a passphrase, type:
|
||||
- **Terminal Password:** \`gosovransystems\`
|
||||
ENDOFFILE
|
||||
|
||||
# --- BITCOIN ECOSYSTEM ---
|
||||
if [ -f "/etc/nix-bitcoin-secrets/rtl-password" ] || [ -f "/var/lib/tor/onion/rtl/hostname" ]; then
|
||||
cat >> "$FILE" << BITCOIN
|
||||
|
||||
## ⚡ Your Bitcoin & Lightning Node
|
||||
Your computer is a real Bitcoin node! It talks to the network secretly using Tor. Here is how to connect your wallet apps to it:
|
||||
|
||||
### 1. Ride The Lightning (RTL)
|
||||
*This is the control panel for your Lightning Node.*
|
||||
Open the **Tor Browser** and go to this website. Use this password to log in:
|
||||
- **Website:** \`http://$RTL_ONION\`
|
||||
- **Password:** \`$RTL_PASS\`
|
||||
|
||||
### 2. Electrs (Your Private Bank Teller)
|
||||
*If you use a wallet app on your phone or computer (like Sparrow or BlueWallet), tell it to connect here so nobody can spy on your money!*
|
||||
- **Tor Address:** \`$ELECTRS_ONION\`
|
||||
- **Port:** \`50001\`
|
||||
|
||||
### 3. Bitcoin Core
|
||||
*This is the heartbeat of your node. It uses this address to talk to other Bitcoiners securely.*
|
||||
- **Tor Address:** \`$BITCOIN_ONION\`
|
||||
BITCOIN
|
||||
fi
|
||||
|
||||
# --- ZEUS MOBILE WALLET QR CODE ---
|
||||
if [ "$HAS_ZEUS_QR" = "1" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📱 Connect Zeus Mobile Wallet" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "Take your Bitcoin Lightning node anywhere in the world! Scan this QR code with the **Zeus** app on your phone to instantly connect your mobile wallet to your Lightning node." >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "1. Download **Zeus** from the App Store or Google Play" >> "$FILE"
|
||||
echo "2. Open Zeus and tap **\"Scan Node Config\"**" >> "$FILE"
|
||||
echo "3. Point your phone's camera at this QR code:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "{ width=200px }" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "That's it! You're now mobile. Send and receive Bitcoin anywhere in the world, powered by your very own node! ⚡" >> "$FILE"
|
||||
elif [ -n "$ZEUS_URL" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📱 Connect Zeus Mobile Wallet" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "Take your Bitcoin Lightning node anywhere in the world! Paste this connection URL into the **Zeus** app on your phone:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "1. Download **Zeus** from the App Store or Google Play" >> "$FILE"
|
||||
echo "2. Open Zeus and tap **\"Scan Node Config\"** then **\"Paste Node Config\"**" >> "$FILE"
|
||||
echo "3. Paste this URL:" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
echo "$ZEUS_URL" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "That's it! You're now mobile. Send and receive Bitcoin anywhere in the world, powered by your very own node! ⚡" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- MATRIX / ELEMENT ---
|
||||
if [ -f "/var/lib/secrets/matrix-users" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 💬 Your Private Chat (Matrix / Element)" >> "$FILE"
|
||||
echo "This is your very own private messaging app! Log in using an app like Element with these details:" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/matrix-users >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- GNOME RDP ---
|
||||
if [ -f "/var/lib/gnome-remote-desktop/rdp-credentials" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 🌎 Connect from Far Away (Remote Desktop)" >> "$FILE"
|
||||
echo "This lets you control your computer screen from another device!" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/gnome-remote-desktop/rdp-credentials >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- NEXTCLOUD ---
|
||||
if [ -f "/var/lib/secrets/nextcloud-admin" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## ☁️ Your Personal Cloud (Nextcloud)" >> "$FILE"
|
||||
echo "This is like your own private Google Drive!" >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/nextcloud-admin >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- WORDPRESS ---
|
||||
if [ -f "/var/lib/secrets/wordpress-admin" ]; then
|
||||
echo "" >> "$FILE"
|
||||
echo "## 📝 Your Website (WordPress)" >> "$FILE"
|
||||
echo "This is your very own website where you can write blogs or make pages." >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
cat /var/lib/secrets/wordpress-admin >> "$FILE"
|
||||
echo "$FENCE" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- VAULTWARDEN ---
|
||||
if [ -f "/var/lib/domains/vaultwarden" ]; then
|
||||
DOMAIN=$(cat /var/lib/domains/vaultwarden)
|
||||
VW_ADMIN_TOKEN="Not found"
|
||||
if [ -f "/var/lib/secrets/vaultwarden/vaultwarden.env" ]; then
|
||||
VW_ADMIN_TOKEN=$(grep -oP 'ADMIN_TOKEN=\K.*' /var/lib/secrets/vaultwarden/vaultwarden.env || echo "Not found")
|
||||
fi
|
||||
echo "" >> "$FILE"
|
||||
echo "## 🔐 Your Password Manager (Vaultwarden)" >> "$FILE"
|
||||
echo "This keeps all your other passwords safe! Go to this website to use it:" >> "$FILE"
|
||||
echo "- **Website:** https://$DOMAIN" >> "$FILE"
|
||||
echo "- **Admin Panel:** https://$DOMAIN/admin" >> "$FILE"
|
||||
echo "- **Admin Token:** \`$VW_ADMIN_TOKEN\`" >> "$FILE"
|
||||
echo "" >> "$FILE"
|
||||
echo "*(Create your own account on the main page. Use the Admin Token to access the admin panel and manage your server.)*" >> "$FILE"
|
||||
fi
|
||||
|
||||
# --- BTCPAY SERVER ---
|
||||
if [ -f "/var/lib/domains/btcpayserver" ]; then
|
||||
DOMAIN=$(cat /var/lib/domains/btcpayserver)
|
||||
echo "" >> "$FILE"
|
||||
echo "## ₿ Your Bitcoin Store (BTCPay Server)" >> "$FILE"
|
||||
echo "This lets you accept Bitcoin like a real shop!" >> "$FILE"
|
||||
echo "- **Website:** https://$DOMAIN" >> "$FILE"
|
||||
echo "*(You make up your own Admin Password the first time you visit!)*" >> "$FILE"
|
||||
fi
|
||||
|
||||
# ── Generate PDF (cd into work dir so Typst finds images) ──
|
||||
cd "$WORK_DIR"
|
||||
pandoc magic_keys.md -o "$OUTPUT" --pdf-engine=typst \
|
||||
-V mainfont="Liberation Sans" \
|
||||
-V monofont="Liberation Mono"
|
||||
|
||||
chown free:users "$OUTPUT"
|
||||
|
||||
# ── Save hash so we skip next time if nothing changed ──
|
||||
mkdir -p "$(dirname "$HASH_FILE")"
|
||||
echo "$CURRENT_HASH" > "$HASH_FILE"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
echo "PDF generated successfully."
|
||||
'';
|
||||
};
|
||||
}
|
||||
117
modules/credentials.nix
Normal file
@@ -0,0 +1,117 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# ── Helper: change 'free' password and save it ─────────────
|
||||
change-free-password = pkgs.writeShellScriptBin "change-free-password" ''
|
||||
set -euo pipefail
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: must be run as root (use sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "New password for free: "
|
||||
read -rs NEW_PASS
|
||||
echo
|
||||
echo -n "Confirm password: "
|
||||
read -rs CONFIRM
|
||||
echo
|
||||
|
||||
if [ "$NEW_PASS" != "$CONFIRM" ]; then
|
||||
echo "Passwords do not match." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_PASS" ]; then
|
||||
echo "Password cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "free:$NEW_PASS" | ${pkgs.shadow}/bin/chpasswd
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Password for 'free' updated and saved."
|
||||
'';
|
||||
in
|
||||
{
|
||||
# ── Make helper available system-wide ───────────────────────
|
||||
environment.systemPackages = [ change-free-password ];
|
||||
|
||||
# ── Shell aliases: intercept 'passwd free' ─────────────────
|
||||
programs.bash.interactiveShellInit = ''
|
||||
passwd() {
|
||||
if [ "$1" = "free" ]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub credentials view will NOT be updated. ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
command passwd "$@"
|
||||
}
|
||||
'';
|
||||
|
||||
programs.fish.interactiveShellInit = ''
|
||||
function passwd --wraps passwd
|
||||
if test "$argv[1]" = "free"
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ Use 'sudo change-free-password' instead. ║"
|
||||
echo "║ ║"
|
||||
echo "║ 'passwd free' only updates /etc/shadow. ║"
|
||||
echo "║ The Hub credentials view will NOT be updated. ║"
|
||||
echo "╚════════════════════════════<EFBFBD><EFBFBD>═════════════════════════╝"
|
||||
echo ""
|
||||
return 1
|
||||
end
|
||||
command passwd $argv
|
||||
end
|
||||
'';
|
||||
|
||||
# ── 1. Auto-Generate Root Password (Runs once) ─────────────
|
||||
systemd.services.root-password-setup = {
|
||||
description = "Generate and set a random root password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/root-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
ROOT_PASS=$(pwgen -s 20 1)
|
||||
echo "root:$ROOT_PASS" | chpasswd
|
||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── 1b. Save 'free' password on first boot ─────────────────
|
||||
systemd.services.free-password-setup = {
|
||||
description = "Save the initial 'free' user password";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/free-password"
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
mkdir -p /var/lib/secrets
|
||||
echo "free" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
}
|
||||
@@ -9,14 +9,15 @@
|
||||
./core/njalla.nix
|
||||
./core/ssh-bootstrap.nix
|
||||
./core/tech-support.nix
|
||||
./core/sovran-manage-domains.nix
|
||||
./core/sovran_systemsos-desktop.nix
|
||||
./core/sshd-localhost.nix
|
||||
./core/sovran-hub.nix
|
||||
./core/factory-seal.nix
|
||||
|
||||
# ── Always on (no flag) ───────────────────────────────────
|
||||
./php.nix
|
||||
./Sovran_SystemsOS_File_Fixes_And_New_Services.nix
|
||||
./credentials-pdf.nix
|
||||
./credentials.nix
|
||||
|
||||
# ── Services (default ON — disable in custom.nix) ─────────
|
||||
./synapse.nix
|
||||
@@ -24,6 +25,7 @@
|
||||
./nextcloud.nix
|
||||
./vaultwarden.nix
|
||||
./bitcoinecosystem.nix
|
||||
./wallet-autoconnect.nix
|
||||
|
||||
# ── Features (default OFF — enable in custom.nix) ─────────
|
||||
./haven.nix
|
||||
@@ -32,5 +34,6 @@
|
||||
./mempool.nix
|
||||
./bitcoin-core.nix
|
||||
./rdp.nix
|
||||
./sshd.nix
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
path = with pkgs; [ curl unzip php pwgen coreutils ];
|
||||
path = with pkgs; [ curl unzip php pwgen coreutils shadow util-linux ];
|
||||
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
@@ -109,7 +109,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
|
||||
echo "Waiting for PostgreSQL..."
|
||||
for i in $(seq 1 30); do
|
||||
if su -s /bin/sh caddy -c "php -r \"new PDO('pgsql:host=$DB_HOST;dbname=$DB_NAME', '$DB_USER', '$DB_PASS');\"" 2>/dev/null; then
|
||||
if /run/wrappers/bin/su -s /bin/sh caddy -c "php -r \"new PDO('pgsql:host=$DB_HOST;dbname=$DB_NAME', '$DB_USER', '$DB_PASS');\"" 2>/dev/null; then
|
||||
echo "Database ready."
|
||||
break
|
||||
fi
|
||||
@@ -117,7 +117,7 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
done
|
||||
|
||||
echo "Running Nextcloud installation..."
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ maintenance:install \
|
||||
--database 'pgsql' \
|
||||
--database-name '$DB_NAME' \
|
||||
@@ -129,19 +129,19 @@ lib.mkIf config.sovran_systemsOS.services.nextcloud {
|
||||
--data-dir '$DATA_DIR'
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ config:system:set trusted_domains 0 --value='$DOMAIN'
|
||||
php $INSTALL_DIR/occ config:system:set overwrite.cli.url --value='https://$DOMAIN'
|
||||
php $INSTALL_DIR/occ config:system:set overwriteprotocol --value='https'
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ config:system:set default_phone_region --value='US'
|
||||
php $INSTALL_DIR/occ config:system:set memcache.local --value='\OC\Memcache\APCu'
|
||||
php $INSTALL_DIR/occ background:cron
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
php $INSTALL_DIR/occ app:install calendar || true
|
||||
php $INSTALL_DIR/occ app:install contacts || true
|
||||
php $INSTALL_DIR/occ app:install tasks || true
|
||||
@@ -187,4 +187,4 @@ CREDS
|
||||
sovran_systemsOS.domainRequirements = [
|
||||
{ name = "nextcloud"; label = "Nextcloud"; example = "cloud.yourdomain.com"; }
|
||||
];
|
||||
}
|
||||
}
|
||||
20
modules/sshd.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
lib.mkIf config.sovran_systemsOS.features.sshd {
|
||||
|
||||
# Extend to listen on all interfaces for remote access
|
||||
services.openssh.listenAddresses = lib.mkForce [
|
||||
{ addr = "127.0.0.1"; port = 22; }
|
||||
{ addr = "0.0.0.0"; port = 22; }
|
||||
];
|
||||
|
||||
# Only open port 22 when SSH is actually enabled
|
||||
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||
|
||||
# Fail2Ban protects SSH when it's active
|
||||
services.fail2ban = {
|
||||
enable = true;
|
||||
ignoreIP = [ "127.0.0.0/8" "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" ];
|
||||
};
|
||||
|
||||
}
|
||||
124
modules/wallet-autoconnect.nix
Normal file
@@ -0,0 +1,124 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
||||
|
||||
# ── Sparrow Wallet Auto-Connect ─────────────────────────────
|
||||
systemd.services.sparrow-autoconnect = {
|
||||
description = "Auto-configure Sparrow Wallet to use local Electrs node";
|
||||
after = [ "electrs.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.iproute2 ];
|
||||
script = ''
|
||||
CONFIG_FILE="/home/free/.sparrow/config"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo "Sparrow config already exists, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait for Electrs to be ready (up to 30 attempts)
|
||||
ATTEMPTS=0
|
||||
until ss -ltn 2>/dev/null | grep -q ':50001' || [ "$ATTEMPTS" -ge 30 ]; do
|
||||
ATTEMPTS=$((ATTEMPTS + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
mkdir -p /home/free/.sparrow
|
||||
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
{
|
||||
"serverType": "ELECTRUM_SERVER",
|
||||
"electrumServer": "tcp://127.0.0.1:50001",
|
||||
"useProxy": false
|
||||
}
|
||||
EOF
|
||||
|
||||
chown -R free:users /home/free/.sparrow
|
||||
echo "Sparrow auto-configured to use local Electrs node"
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Bisq 1 Auto-Connect ─────────────────────────────────────
|
||||
systemd.services.bisq-autoconnect = {
|
||||
description = "Auto-configure Bisq to use local Bitcoin node";
|
||||
after = [ "bitcoind.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils pkgs.iproute2 ];
|
||||
script = ''
|
||||
BISQ_CONF="/home/free/.local/share/Bisq/bisq.properties"
|
||||
|
||||
if [ -f "$BISQ_CONF" ]; then
|
||||
echo "Bisq config already exists, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait for bitcoind RPC to be ready (up to 30 attempts)
|
||||
ATTEMPTS=0
|
||||
until ss -ltn 2>/dev/null | grep -q ':8333' || [ "$ATTEMPTS" -ge 30 ]; do
|
||||
ATTEMPTS=$((ATTEMPTS + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
mkdir -p /home/free/.local/share/Bisq
|
||||
|
||||
cat > "$BISQ_CONF" << 'EOF'
|
||||
btcNodes=127.0.0.1:8333
|
||||
useTorForBtc=true
|
||||
useCustomBtcNodes=true
|
||||
EOF
|
||||
|
||||
chown -R free:users /home/free/.local/share/Bisq
|
||||
echo "Bisq auto-configured to use local Bitcoin node"
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Zeus Connect (lndconnect URL for mobile wallet) ──────────
|
||||
systemd.services.zeus-connect-setup = {
|
||||
description = "Save Zeus lndconnect URL";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "lnd.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.coreutils "/run/current-system/sw" ];
|
||||
script = ''
|
||||
SECRET_FILE="/var/lib/secrets/zeus-connect-url"
|
||||
mkdir -p /var/lib/secrets
|
||||
|
||||
URL=""
|
||||
if command -v lndconnect >/dev/null 2>&1; then
|
||||
URL=$(lndconnect --url 2>/dev/null || true)
|
||||
elif command -v lnconnect-clnrest >/dev/null 2>&1; then
|
||||
URL=$(lnconnect-clnrest --url 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
echo "$URL" > "$SECRET_FILE"
|
||||
chmod 600 "$SECRET_FILE"
|
||||
echo "Zeus connect URL saved."
|
||||
else
|
||||
echo "No lndconnect URL available yet."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Refresh Zeus URL periodically (certs/macaroons may rotate)
|
||||
systemd.timers.zeus-connect-setup = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "2min";
|
||||
OnUnitActiveSec = "30min";
|
||||
Unit = "zeus-connect-setup.service";
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -60,7 +60,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
path = with pkgs; [ curl unzip wp-cli pwgen php coreutils ];
|
||||
path = with pkgs; [ curl unzip wp-cli pwgen php coreutils shadow util-linux ];
|
||||
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
@@ -97,7 +97,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
|
||||
echo "Generating wp-config.php..."
|
||||
cd "$INSTALL_DIR"
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp config create \
|
||||
--dbname='$DB_NAME' \
|
||||
--dbuser='$DB_USER' \
|
||||
@@ -108,14 +108,14 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
|
||||
echo "Waiting for database..."
|
||||
for i in $(seq 1 30); do
|
||||
if su -s /bin/sh caddy -c "wp db check" 2>/dev/null; then
|
||||
if /run/wrappers/bin/su -s /bin/sh caddy -c "wp db check" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Running WordPress core install..."
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp core install \
|
||||
--url='https://$DOMAIN' \
|
||||
--title='Sovran_SystemsOS' \
|
||||
@@ -125,7 +125,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
--skip-email
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp option update blogdescription 'Powered by Sovran_SystemsOS'
|
||||
wp option update permalink_structure '/%postname%/'
|
||||
wp option update default_ping_status 'closed'
|
||||
@@ -133,7 +133,7 @@ lib.mkIf config.sovran_systemsOS.services.wordpress {
|
||||
wp rewrite flush
|
||||
"
|
||||
|
||||
su -s /bin/sh caddy -c "
|
||||
/run/wrappers/bin/su -s /bin/sh caddy -c "
|
||||
wp config set DISALLOW_FILE_EDIT true --raw
|
||||
wp config set WP_AUTO_UPDATE_CORE true --raw
|
||||
wp config set FORCE_SSL_ADMIN true --raw
|
||||
|
||||
139
onboarding.html
@@ -1,139 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sovran_SystemsOS — First-Boot Setup</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v={{ style_css_hash }}" />
|
||||
</head>
|
||||
<body class="onboarding-body">
|
||||
|
||||
<!-- Onboarding wizard container -->
|
||||
<div class="onboarding-shell">
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="onboarding-progress-bar">
|
||||
<div class="onboarding-progress-fill" id="onboarding-progress-fill"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step indicators -->
|
||||
<div class="onboarding-steps-nav" id="onboarding-steps-nav">
|
||||
<span class="onboarding-step-dot" data-step="1">1</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="2">2</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="3">3</span>
|
||||
<span class="onboarding-step-connector"></span>
|
||||
<span class="onboarding-step-dot" data-step="4">4</span>
|
||||
</div>
|
||||
|
||||
<!-- Step panels -->
|
||||
<div class="onboarding-panel-wrap">
|
||||
|
||||
<!-- ── Step 1: Welcome ── -->
|
||||
<div class="onboarding-panel" id="step-1">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">
|
||||
<img src="/static/logo-light.svg" alt="Sovran Systems" class="onboarding-logo-img" />
|
||||
</div>
|
||||
<h1 class="onboarding-title">Welcome to Sovran_SystemsOS!</h1>
|
||||
<p class="onboarding-subtitle">Be Digitally Sovereign</p>
|
||||
</div>
|
||||
<div class="onboarding-card">
|
||||
<p class="onboarding-body-text">
|
||||
Your system is installed and ready to configure. This wizard will guide
|
||||
you through the final setup steps so everything works perfectly.
|
||||
</p>
|
||||
<div class="onboarding-role-row" id="onboarding-role-row">
|
||||
<span class="onboarding-role-label">Your Role:</span>
|
||||
<span class="onboarding-role-badge" id="onboarding-role-badge">Loading…</span>
|
||||
</div>
|
||||
<p class="onboarding-body-text onboarding-body-text--dim">
|
||||
This setup only takes a few minutes. You can always revisit these
|
||||
settings from the main Hub dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<div></div>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-1-next">
|
||||
Let's Go →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Domain Configuration ── -->
|
||||
<div class="onboarding-panel" id="step-2" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🌐</span>
|
||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
|
||||
First, create an account at <strong>Njal.la</strong> and purchase your domain.
|
||||
Then, in the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine's external IP address (shown below).
|
||||
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--scroll" id="step-2-body">
|
||||
<p class="onboarding-loading">Loading service information…</p>
|
||||
</div>
|
||||
<div id="step-2-status" class="onboarding-save-status"></div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
|
||||
Save & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 3: Port Forwarding ── -->
|
||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||
<div class="onboarding-step-header">
|
||||
<span class="onboarding-step-icon">🔌</span>
|
||||
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
|
||||
<p class="onboarding-step-desc">
|
||||
Forward these ports on your router to this machine. Each port only needs to be opened once — they are shared across all your services.
|
||||
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="onboarding-card onboarding-card--ports" id="step-3-body">
|
||||
<p class="onboarding-loading">Checking ports…</p>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 4: Complete ── -->
|
||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||
<div class="onboarding-hero">
|
||||
<div class="onboarding-logo">✅</div>
|
||||
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
|
||||
<p class="onboarding-subtitle">Setup complete</p>
|
||||
</div>
|
||||
<div class="onboarding-card">
|
||||
<p class="onboarding-body-text">
|
||||
All configuration steps are done. Head to the main Hub dashboard to
|
||||
monitor your services, manage credentials, and make changes at any time.
|
||||
</p>
|
||||
<ul class="onboarding-checklist" id="onboarding-checklist">
|
||||
<li>✅ Domain configuration saved</li>
|
||||
<li>✅ Port forwarding reviewed</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="onboarding-footer">
|
||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||
<button class="btn btn-primary" id="step-4-finish">
|
||||
Go to Dashboard →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /panel-wrap -->
|
||||
</div><!-- /shell -->
|
||||
|
||||
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
|
||||
</body>
|
||||
</html>
|
||||