Compare commits

..

180 Commits

Author SHA1 Message Date
pinpox
e1e10b2c0a Fix typography 2025-08-25 12:34:56 +02:00
Mic92
289732ad20 Merge pull request 'Replace funky utf-8 singlequotes with decent ones' (#4923) from replace-backticks into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4923
2025-08-25 10:03:46 +00:00
pinpox
a50b6f7bc7 Merge pull request 'Allow shared user password' (#4921) from TilmanBaumann/clan-core:main into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4921
Reviewed-by: pinpox <clan@pablo.tools>
2025-08-25 10:02:59 +00:00
pinpox
cdd241d8ff Replace funky utf-8 singlequotes with decent ones 2025-08-25 11:56:29 +02:00
Mic92
0803d9c864 Merge pull request 'Apply automatic ruff lints' (#4919) from ruff-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4919
2025-08-25 09:53:41 +00:00
Tilman Baumann
7171864a5e Allow shared user password
By default each user gets a new password on every host.

Now you can use a shared vars.
2025-08-25 11:46:09 +02:00
Mic92
7aa9a34168 Merge pull request 'services/user: add migration guide for root-password' (#4917) from root-password into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4917
2025-08-25 09:36:02 +00:00
Jörg Thalheim
0ec2c32ff8 ruff: apply automatic unsafe fixes 2025-08-25 11:34:41 +02:00
Jörg Thalheim
ea2d6aab65 ruff: apply automatic fixes 2025-08-25 11:34:41 +02:00
Jörg Thalheim
4101ebc45b services/user: add migration guide for root-password 2025-08-25 11:29:56 +02:00
hsjobeki
45c7c42634 Merge pull request 'tests/dirs: unify test files' (#4916) from cleanup-again into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4916
2025-08-25 09:26:12 +00:00
Johannes Kirschbauer
8baf4fcedd docs: refine autoincludes 2025-08-25 11:19:39 +02:00
Johannes Kirschbauer
a41e0ba80f nix_models: fix typo 2025-08-25 11:19:39 +02:00
DavHau
798d445f3e docs: move options search to separate page 2025-08-25 15:51:24 +07:00
Johannes Kirschbauer
00bd003be4 tests/dirs: unify test files 2025-08-25 10:14:02 +02:00
clan-bot
5841432b6f Merge pull request 'Update data-mesher' (#4914) from update-data-mesher into main 2025-08-25 05:24:10 +00:00
clan-bot
1fb91ec161 Merge pull request 'Update clan-core-for-checks in devFlake' (#4915) from update-devFlake-clan-core-for-checks into main 2025-08-25 05:23:55 +00:00
clan-bot
fc16879336 Update clan-core-for-checks in devFlake 2025-08-25 05:01:34 +00:00
clan-bot
290510ae74 Update data-mesher 2025-08-25 05:00:51 +00:00
clan-bot
7b926d43dc Merge pull request 'Update clan-core-for-checks in devFlake' (#4913) from update-devFlake-clan-core-for-checks into main 2025-08-25 00:21:50 +00:00
clan-bot
d91a44c7c5 Update clan-core-for-checks in devFlake 2025-08-25 00:01:31 +00:00
clan-bot
a47ed71bb7 Merge pull request 'Update clan-core-for-checks in devFlake' (#4911) from update-devFlake-clan-core-for-checks into main 2025-08-24 20:19:28 +00:00
clan-bot
18f9df29da Merge pull request 'Update nixpkgs-dev in devFlake' (#4912) from update-devFlake-nixpkgs-dev into main 2025-08-24 20:09:41 +00:00
clan-bot
2438dc09a2 Update nixpkgs-dev in devFlake 2025-08-24 20:01:48 +00:00
clan-bot
420412e60c Update clan-core-for-checks in devFlake 2025-08-24 20:01:29 +00:00
clan-bot
aee6bc335b Merge pull request 'Update clan-core-for-checks in devFlake' (#4910) from update-devFlake-clan-core-for-checks into main 2025-08-24 15:18:34 +00:00
clan-bot
6ae679fb3d Update clan-core-for-checks in devFlake 2025-08-24 15:01:31 +00:00
clan-bot
b40a13b4c5 Merge pull request 'Update clan-core-for-checks in devFlake' (#4906) from update-devFlake-clan-core-for-checks into main 2025-08-24 10:18:53 +00:00
clan-bot
dd2aa70efd Merge pull request 'Update nixpkgs-dev in devFlake' (#4907) from update-devFlake-nixpkgs-dev into main 2025-08-24 10:09:48 +00:00
clan-bot
2a9c9f7f2c Update nixpkgs-dev in devFlake 2025-08-24 10:01:51 +00:00
clan-bot
82001544fd Update clan-core-for-checks in devFlake 2025-08-24 10:01:32 +00:00
brianmcgee
9f352aa362 Merge pull request 'feat(ui): remove focus-visible state from readonly form inputs' (#4905) from ui/disable-active-status-readonly-input into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4905
2025-08-24 09:53:06 +00:00
Brian McGee
35177ead40 feat(ui): remove focus-visible state from readonly form inputs 2025-08-24 10:49:34 +01:00
brianmcgee
1931c17513 Merge pull request 'feat(ui): make save button clearer in sidebar section forms' (#4904) from ui/sidebar-section-save-button into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4904
2025-08-24 09:45:23 +00:00
Brian McGee
b12debf373 feat(ui): make save button clearer in sidebar section forms 2025-08-24 10:42:01 +01:00
clan-bot
0b3d362357 Merge pull request 'Update clan-core-for-checks in devFlake' (#4903) from update-devFlake-clan-core-for-checks into main 2025-08-24 05:21:48 +00:00
clan-bot
d8119f2308 Update clan-core-for-checks in devFlake 2025-08-24 05:01:46 +00:00
clan-bot
ce36894ab1 Merge pull request 'Update clan-core-for-checks in devFlake' (#4902) from update-devFlake-clan-core-for-checks into main 2025-08-24 00:19:01 +00:00
clan-bot
c5f4f2e1d6 Update clan-core-for-checks in devFlake 2025-08-24 00:01:33 +00:00
clan-bot
c861ffe07b Merge pull request 'Update clan-core-for-checks in devFlake' (#4900) from update-devFlake-clan-core-for-checks into main 2025-08-23 20:17:58 +00:00
clan-bot
6df980bc57 Merge pull request 'Update nixpkgs-dev in devFlake' (#4901) from update-devFlake-nixpkgs-dev into main 2025-08-23 20:09:30 +00:00
clan-bot
9d1d07b0ca Update nixpkgs-dev in devFlake 2025-08-23 20:01:48 +00:00
clan-bot
24a774b5d6 Update clan-core-for-checks in devFlake 2025-08-23 20:01:29 +00:00
clan-bot
442f673128 Merge pull request 'Update clan-core-for-checks in devFlake' (#4898) from update-devFlake-clan-core-for-checks into main 2025-08-23 15:20:43 +00:00
clan-bot
8905b5c5f1 Merge pull request 'Update nixpkgs-dev in devFlake' (#4899) from update-devFlake-nixpkgs-dev into main 2025-08-23 15:12:14 +00:00
clan-bot
3eff656dfa Update nixpkgs-dev in devFlake 2025-08-23 15:01:50 +00:00
clan-bot
79e6f34c9e Update clan-core-for-checks in devFlake 2025-08-23 15:01:31 +00:00
clan-bot
9c6e8f7735 Merge pull request 'Update treefmt-nix' (#4894) from update-treefmt-nix into main 2025-08-23 10:24:13 +00:00
clan-bot
cc4fd1369e Merge pull request 'Update clan-core-for-checks in devFlake' (#4895) from update-devFlake-clan-core-for-checks into main 2025-08-23 10:23:54 +00:00
clan-bot
7f32d6f81a Merge pull request 'Update treefmt-nix in devFlake' (#4897) from update-devFlake-treefmt-nix into main 2025-08-23 10:17:23 +00:00
clan-bot
a450ca10b8 Merge pull request 'Update nixpkgs-dev in devFlake' (#4896) from update-devFlake-nixpkgs-dev into main 2025-08-23 10:17:07 +00:00
clan-bot
06fbf32691 Update treefmt-nix in devFlake 2025-08-23 10:01:56 +00:00
clan-bot
d4bd297439 Update nixpkgs-dev in devFlake 2025-08-23 10:01:51 +00:00
clan-bot
acc8043f26 Update clan-core-for-checks in devFlake 2025-08-23 10:01:32 +00:00
clan-bot
35e5d0daab Update treefmt-nix 2025-08-23 10:01:29 +00:00
clan-bot
e51c9ef1ad Merge pull request 'Update clan-core-for-checks in devFlake' (#4892) from update-devFlake-clan-core-for-checks into main 2025-08-23 05:19:20 +00:00
clan-bot
cdcbe3359a Update clan-core-for-checks in devFlake 2025-08-23 05:01:32 +00:00
clan-bot
e5b51e6a2b Merge pull request 'Update clan-core-for-checks in devFlake' (#4891) from update-devFlake-clan-core-for-checks into main 2025-08-23 00:20:46 +00:00
clan-bot
694ebc5b30 Update clan-core-for-checks in devFlake 2025-08-23 00:01:30 +00:00
lassulus
ff2555cc4a Merge pull request 'Enable network configuration in iwd settings' (#4886) from RuboGubo/clan-core:fixDHCPinstaller into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4886
2025-08-22 22:34:07 +00:00
lassulus
016255459c Merge pull request 'clan_lib flake: fix handling of maybes and empty sets' (#4890) from select_fix into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4890
2025-08-22 22:31:29 +00:00
lassulus
14f03bcab0 Merge pull request 'vars: add .exists to files' (#4889) from vars_exist into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4889
2025-08-22 21:47:16 +00:00
lassulus
4dc90b3d39 clan_lib flake: fix handling of maybes and empty sets 2025-08-22 23:44:14 +02:00
lassulus
8cdce6c0c8 vars: add .exists to files 2025-08-22 23:30:19 +02:00
clan-bot
8904cf27a4 Merge pull request 'Update clan-core-for-checks in devFlake' (#4888) from update-devFlake-clan-core-for-checks into main 2025-08-22 20:19:40 +00:00
clan-bot
493194c124 Merge pull request 'Update nix-select' (#4887) from update-nix-select into main 2025-08-22 20:15:43 +00:00
clan-bot
5d1600a077 Update clan-core-for-checks in devFlake 2025-08-22 20:01:38 +00:00
clan-bot
7daaacbddf Update nix-select 2025-08-22 20:01:00 +00:00
RuboGubo
30e18bbc66 Enable network configuration in iwd settings
closes #4885
2025-08-22 20:20:37 +01:00
hsjobeki
16dffa99c0 Merge pull request 'ui/search: add search with virtualized scrolling' (#4884) from ui-search into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4884
2025-08-22 16:07:54 +00:00
Johannes Kirschbauer
58ad50b749 ui/search: add search with virtualized scrolling 2025-08-22 17:52:48 +02:00
clan-bot
bc25074f5b Merge pull request 'Update clan-core-for-checks in devFlake' (#4882) from update-devFlake-clan-core-for-checks into main 2025-08-22 15:18:46 +00:00
clan-bot
c79916d06c Merge pull request 'Update nixpkgs-dev in devFlake' (#4883) from update-devFlake-nixpkgs-dev into main 2025-08-22 15:09:43 +00:00
clan-bot
4d53542f79 Update nixpkgs-dev in devFlake 2025-08-22 15:01:50 +00:00
clan-bot
d3ef03aeb3 Update clan-core-for-checks in devFlake 2025-08-22 15:01:31 +00:00
brianmcgee
9949fac5ea Merge pull request 'feat(ui): refine spacing in NavSection' (#4881) from ui/navigation-section into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4881
2025-08-22 12:32:10 +00:00
Brian McGee
6d236a6282 feat(ui): refine spacing in NavSection 2025-08-22 13:28:06 +01:00
brianmcgee
6e6a920796 Merge pull request 'ui/navigation-section' (#4880) from ui/navigation-section into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4880
2025-08-22 11:20:14 +00:00
Brian McGee
99092a6ef2 chore(ui): lint 2025-08-22 12:16:21 +01:00
Brian McGee
1897b7bb06 feat(ui): use NavSection in installer workflow 2025-08-22 12:15:16 +01:00
Brian McGee
878789cf38 feat(ui): use NavSection in ListClansModal 2025-08-22 12:11:22 +01:00
Brian McGee
8a59cf7ea3 feat(ui): add NavSection component 2025-08-22 12:07:26 +01:00
brianmcgee
7ade9cd222 Merge pull request 'Handle error cases when switching clan' (#4879) from feat/handle-clan-switch-errors into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4879
2025-08-22 10:47:35 +00:00
Brian McGee
447f619ecc feat(ui): handle when a clan folder has been moved/renamed 2025-08-22 11:44:16 +01:00
clan-bot
657a55517b Merge pull request 'Update clan-core-for-checks in devFlake' (#4878) from update-devFlake-clan-core-for-checks into main 2025-08-22 10:20:14 +00:00
clan-bot
16a5b34ddf Update clan-core-for-checks in devFlake 2025-08-22 10:01:32 +00:00
Luis Hebendanz
23f303b6ba Merge pull request 'clan_lib: Fix run_machine_install incorrect pesist_state path' (#4877) from Qubasa/clan-core:fix_install into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4877
2025-08-22 09:36:34 +00:00
Qubasa
84bf9f3bc5 clan_lib: Fix run_machine_install incorrect pesist_state path 2025-08-22 11:21:44 +02:00
brianmcgee
48736011de Merge pull request 'fix(ui): remove custom viewboxes for some icons' (#4876) from ui/fix-icons into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4876
2025-08-22 08:14:22 +00:00
Brian McGee
cf5675b7f3 fix(ui): remove custom viewboxes for some icons 2025-08-22 09:11:09 +01:00
Brian McGee
f0bbdad9ef feat(ui): improve clan list logic in SidebarHeader 2025-08-22 08:51:27 +01:00
Brian McGee
5f83fe02a1 feat(ui): move isLoading logic into ClanContext 2025-08-22 08:51:26 +01:00
Brian McGee
8cb92e143d feat(ui): by default do not retry queries 2025-08-22 08:51:26 +01:00
Brian McGee
73f5f887f3 feat(ui): add clanURI to ClanContext 2025-08-22 08:51:25 +01:00
clan-bot
db4e6c0be5 Merge pull request 'Update nix-darwin' (#4872) from update-nix-darwin into main 2025-08-22 05:22:33 +00:00
clan-bot
c24892f865 Merge pull request 'Update clan-core-for-checks in devFlake' (#4873) from update-devFlake-clan-core-for-checks into main 2025-08-22 05:20:02 +00:00
clan-bot
6badc14936 Update clan-core-for-checks in devFlake 2025-08-22 05:01:33 +00:00
clan-bot
3d1fb401fd Update nix-darwin 2025-08-22 05:00:53 +00:00
clan-bot
f2cdac75e2 Merge pull request 'Update clan-core-for-checks in devFlake' (#4871) from update-devFlake-clan-core-for-checks into main 2025-08-22 00:19:10 +00:00
clan-bot
5d6e35832c Update clan-core-for-checks in devFlake 2025-08-22 00:01:33 +00:00
Luis Hebendanz
9aa9ba500e Merge pull request 'clan-lib: Make Flake throw more concrete errors if the flake path is invalid or non existend' (#4870) from Qubasa/clan-core:fix_ui_stuff2 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4870
2025-08-21 22:08:28 +00:00
Qubasa
2934269279 clan-lib: Make Flake throw more concrete errors if the flake path is invalid or non existend
treefmt
2025-08-21 23:53:26 +02:00
clan-bot
1c7323c90a Merge pull request 'Update clan-core-for-checks in devFlake' (#4869) from update-devFlake-clan-core-for-checks into main 2025-08-21 20:18:08 +00:00
clan-bot
e667e03832 Update clan-core-for-checks in devFlake 2025-08-21 20:01:29 +00:00
brianmcgee
7f227b232c Merge pull request 'ui/icons: update app icons' (#4867) from ui-search into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4867
Reviewed-by: brianmcgee <brian@bmcgee.ie>
2025-08-21 17:14:34 +00:00
Johannes Kirschbauer
9d887805a8 ui/icons: update app icons 2025-08-21 19:11:08 +02:00
hsjobeki
244e1c7447 Merge pull request 'ui/cubes: scene add tooltip descriptions to toolbar' (#4866) from api-modules-unify into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4866
2025-08-21 16:25:11 +00:00
Johannes Kirschbauer
78911063a6 ui/tooltip: cleanup hostfileInput
Components should not rely on any global css classes
This can have bad side effects
2025-08-21 18:22:15 +02:00
hsjobeki
d86509e97b Merge pull request 'feat(ui): history stack for stepper' (#4834) from ui/fix-backwards-nav-installer into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4834
2025-08-21 16:15:26 +00:00
Johannes Kirschbauer
6de431df2c ui/stepper: use initial step from opts 2025-08-21 18:12:06 +02:00
Johannes Kirschbauer
cda49b5b20 ui/cubes: scene add tooltip descriptions to toolbar 2025-08-21 18:04:12 +02:00
brianmcgee
678841e64c Merge pull request 'fix(ui): blurry bold variants for CommitMono' (#4864) from fix/commit-mono-bold into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4864
2025-08-21 15:37:21 +00:00
Johannes Kirschbauer
74549164e4 ui/toolbarButton: wrap in tooltip for always having more information 2025-08-21 17:36:01 +02:00
Johannes Kirschbauer
6afe8695de ui/tooltip: refactor tooltip 2025-08-21 17:35:13 +02:00
Brian McGee
460800b6fb fix(ui): blurry bold variants for CommitMono 2025-08-21 16:33:51 +01:00
clan-bot
5558bf3b9a Merge pull request 'Update clan-core-for-checks in devFlake' (#4862) from update-devFlake-clan-core-for-checks into main 2025-08-21 15:18:02 +00:00
clan-bot
62701f7730 Merge pull request 'Update nixpkgs-dev in devFlake' (#4863) from update-devFlake-nixpkgs-dev into main 2025-08-21 15:11:45 +00:00
clan-bot
a2f3e2e513 Update nixpkgs-dev in devFlake 2025-08-21 15:01:50 +00:00
clan-bot
4867d467de Update clan-core-for-checks in devFlake 2025-08-21 15:01:31 +00:00
brianmcgee
d9685acc37 Merge pull request 'feat(ui): introduce a top-level Clan context' (#4860) from feat/handle-clan-switch-errors into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4860
2025-08-21 13:57:13 +00:00
pinpox
1aaa157f20 Merge pull request 'Cleanup machine-id,postgresql test' (#4858) from cleanup-tests into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4858
2025-08-21 12:58:36 +00:00
hsjobeki
9a0ad4182f Merge pull request 'api/modules: unify frontmatter with module manifest' (#4847) from api-modules-unify into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4847
Reviewed-by: lassulus <clanlol@lassul.us>
2025-08-21 12:56:19 +00:00
pinpox
65d194af58 Cleanup machine-id,postgresql test 2025-08-21 14:52:13 +02:00
Johannes Kirschbauer
1f2f71ab03 lib/modules: make categories class method 2025-08-21 14:41:03 +02:00
Brian McGee
f985187999 feat(ui): introduce a top-level Clan context 2025-08-21 12:20:51 +01:00
clan-bot
396a8d1e5e Merge pull request 'Update clan-core-for-checks in devFlake' (#4857) from update-devFlake-clan-core-for-checks into main 2025-08-21 10:23:58 +00:00
clan-bot
651f630080 Merge pull request 'Update nix-darwin' (#4856) from update-nix-darwin into main 2025-08-21 10:21:19 +00:00
clan-bot
21de41f1c0 Update clan-core-for-checks in devFlake 2025-08-21 10:01:29 +00:00
clan-bot
98e5987e22 Update nix-darwin 2025-08-21 10:00:54 +00:00
brianmcgee
a77af2d379 Merge pull request 'ui/misc-fixes' (#4855) from ui/misc-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4855
2025-08-21 09:58:34 +00:00
Brian McGee
ccde9e0ba6 feat(ui): replace modal backdrop blur with opacity 2025-08-21 10:14:17 +01:00
Brian McGee
6f6f582fe3 feat(ui): improve spacing in sidebar dropdown 2025-08-21 10:04:21 +01:00
clan-bot
29a3140702 Merge pull request 'Update clan-core-for-checks in devFlake' (#4854) from update-devFlake-clan-core-for-checks into main 2025-08-21 05:20:25 +00:00
clan-bot
465eda24bc Update clan-core-for-checks in devFlake 2025-08-21 05:01:37 +00:00
clan-bot
2888907109 Merge pull request 'Update clan-core-for-checks in devFlake' (#4852) from update-devFlake-clan-core-for-checks into main 2025-08-21 00:19:00 +00:00
clan-bot
f770f600c6 Merge pull request 'Update nixpkgs-dev in devFlake' (#4853) from update-devFlake-nixpkgs-dev into main 2025-08-21 00:10:35 +00:00
clan-bot
729f1673b3 Update nixpkgs-dev in devFlake 2025-08-21 00:01:46 +00:00
clan-bot
7c95cb0177 Update clan-core-for-checks in devFlake 2025-08-21 00:01:30 +00:00
clan-bot
b7f159aea3 Merge pull request 'Update clan-core-for-checks in devFlake' (#4850) from update-devFlake-clan-core-for-checks into main 2025-08-20 20:20:07 +00:00
clan-bot
06a0062311 Update clan-core-for-checks in devFlake 2025-08-20 20:01:29 +00:00
Luis Hebendanz
aa840d9758 Merge pull request 'working check_machine_up_to_date' (#4754) from Qubasa/clan-core:build_is_installed_api into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4754
2025-08-20 19:12:53 +00:00
hsjobeki
d1e6da0779 Merge pull request 'api/install: set install date after install' (#4838) from install-done into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4838
Reviewed-by: brianmcgee <brian@bmcgee.ie>
2025-08-20 19:02:13 +00:00
Johannes Kirschbauer
e6981ddd72 cli/install: add --no-persist-state
Skip persisting the current date after successful install
This is a workaround due to incomplete test setup - installing a clan machine without having a clan
2025-08-20 20:46:44 +02:00
Qubasa
101c52f7c2 clan_lib: Add 'address' field to LocalHost 2025-08-20 20:28:21 +02:00
Johannes Kirschbauer
a83f301e59 docs/render: remove dead code 2025-08-20 20:19:49 +02:00
Qubasa
5120d90b85 clanService: telegraf.nix add json exporter over http 2025-08-20 20:17:49 +02:00
Qubasa
ea1e470502 clan_lib: add 'get_metrics' API endpoint 2025-08-20 20:17:14 +02:00
Johannes Kirschbauer
f4d6edc501 api/modules: unify frontmatter with module manifest 2025-08-20 20:15:41 +02:00
Johannes Kirschbauer
cbbc235570 api/modules: rename Frontmatter -> ModulesFrontmatter to make room for other disk templates metadata 2025-08-20 19:46:28 +02:00
brianmcgee
56d9256c02 Merge pull request 'chore(ui): remove close this clan button' (#4846) from chore/remove-close-clan-button into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4846
2025-08-20 15:55:50 +00:00
Brian McGee
e131d3d036 chore(ui): remove close this clan button 2025-08-20 16:52:47 +01:00
brianmcgee
7f5b7b5057 Merge pull request 'ui/clan-switching' (#4844) from ui/clan-switching into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4844
2025-08-20 15:46:11 +00:00
brianmcgee
c27fa9f56e Merge pull request 'fix(ui): inverted dividers in SectionGeneral component' (#4845) from fix/inverted-dividers-in-sidebar-pane into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4845
2025-08-20 15:41:31 +00:00
Brian McGee
1a1addb19d fix(ui): cancel loading state properly for select folder button in onboarding 2025-08-20 16:40:50 +01:00
Brian McGee
349da24b29 feat(ui): enable switching between clans 2025-08-20 16:40:49 +01:00
Brian McGee
717f66b613 fix(ui): inverted dividers in SectionGeneral component
Closes #4836
2025-08-20 16:37:33 +01:00
clan-bot
dcbc8c9a50 Merge pull request 'Update clan-core-for-checks in devFlake' (#4842) from update-devFlake-clan-core-for-checks into main 2025-08-20 15:21:49 +00:00
Brian McGee
9834f413cc feat(ui): introduce Add clan button into sidebar 2025-08-20 16:21:40 +01:00
Brian McGee
fb5645ae33 fix(ui): mock machine state in Sidebar stories 2025-08-20 16:21:39 +01:00
Brian McGee
dc311d78e2 fix(ui): add mock api client provider to Sidebar stories 2025-08-20 16:21:39 +01:00
clan-bot
f0b1d8b2af Merge pull request 'Update nixpkgs-dev in devFlake' (#4843) from update-devFlake-nixpkgs-dev into main 2025-08-20 15:12:06 +00:00
clan-bot
7f0d55ef74 Update nixpkgs-dev in devFlake 2025-08-20 15:01:51 +00:00
clan-bot
6e8860b3a0 Update clan-core-for-checks in devFlake 2025-08-20 15:01:31 +00:00
Mic92
5a5ec468c7 Merge pull request 'clan-cli: use automatic networking for vars upload and machines update' (#4792) from networking_4 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4792
2025-08-20 12:42:56 +00:00
Kenji Berthold
fbc2b889b5 Merge pull request 'docs: Fix migration docs for clan modules' (#4839) from kenji/ke-fix-migration-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4839
2025-08-20 12:27:09 +00:00
lassulus
fb094e8f3b add tor network to default template 2025-08-20 14:24:43 +02:00
lassulus
e2eb26345f networking: add documentation, unhide from CLI 2025-08-20 14:22:58 +02:00
a-kenji
6f1a94e825 docs: Fix migration docs for clan modules 2025-08-20 14:13:27 +02:00
Johannes Kirschbauer
05951ffdb9 api/install: set install date after install 2025-08-20 13:39:32 +02:00
clan-bot
69de5f10c0 Merge pull request 'Update clan-core-for-checks in devFlake' (#4833) from update-devFlake-clan-core-for-checks into main 2025-08-20 10:20:20 +00:00
Brian McGee
c01a191f3a feat(ui): history stack for stepper 2025-08-20 11:02:38 +01:00
clan-bot
dfe1a3e67f Update clan-core-for-checks in devFlake 2025-08-20 10:01:34 +00:00
brianmcgee
e975b67fad Merge pull request 'fix(ui): de-duplicate clan uris when adding to local storage' (#4831) from ui/deduplicate-clan-storage into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4831
2025-08-20 09:38:02 +00:00
Brian McGee
5c08893db0 fix(ui): de-duplicate clan uris when adding to local storage 2025-08-20 10:34:53 +01:00
brianmcgee
cb679dbee2 Merge pull request 'ui/install-workflow-refinements' (#4827) from ui/install-workflow-refinements into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4827
2025-08-20 09:23:04 +00:00
Brian McGee
f339ca0d85 feat(ui): allow installer splash screens to change modal size 2025-08-20 10:19:19 +01:00
Brian McGee
547ba4276e feat(ui): improve copy in create installer intro 2025-08-20 10:01:40 +01:00
Brian McGee
cae63cc45d fix(ui): spelling mistake 2025-08-20 10:01:39 +01:00
DavHau
527b4b2e40 vars: ensure shared generators don't depend on machine specific generators
A dependency relation like this would not make sense as it would not be clear which machines generator the shared generator would depend on
2025-08-20 15:39:17 +07:00
lassulus
448c22c280 clan-cli: use automatic networking for vars upload and machines update
This uses the networking module to find the best_host, as we already do
with ssh and install. So if we don't supply a --target-host and a
networking module is configured, the remote should be autodetected.

Since vars upload doesn't have a --target-host argument, we always try
to use get_best_remote
2025-08-19 23:40:57 +02:00
323 changed files with 4827 additions and 2818 deletions

View File

@@ -8,7 +8,7 @@ Our mission is simple: to democratize computing by providing tools that empower
## Features of Clan
- **Full-Stack System Deployment:** Utilize Clans toolkit alongside Nix's reliability to build and manage systems effortlessly.
- **Full-Stack System Deployment:** Utilize Clan's toolkit alongside Nix's reliability to build and manage systems effortlessly.
- **Overlay Networks:** Secure, private communication channels between devices.
- **Virtual Machine Integration:** Seamless operation of VM applications within the main operating system.
- **Robust Backup Management:** Long-term, self-hosted data preservation.

View File

@@ -232,6 +232,7 @@
"-i", ssh_conn.ssh_key,
"--option", "store", os.environ['CLAN_TEST_STORE'],
"--update-hardware-config", "nixos-facter",
"--no-persist-state",
]
subprocess.run(clan_cmd, check=True)
@@ -275,7 +276,7 @@
"${self.checks.x86_64-linux.clan-core-for-checks}",
"${closureInfo}"
)
# Set up SSH connection
ssh_conn = setup_ssh_connection(
target,

View File

@@ -11,7 +11,8 @@
...
}:
let
jsonpath = /tmp/telegraf.json;
jsonpath = "/tmp/telegraf.json";
auth_user = "prometheus";
in
{
@@ -19,18 +20,24 @@
builtins.listToAttrs (
map (name: {
inherit name;
value.allowedTCPPorts = [ 9273 9990 ];
value.allowedTCPPorts = [
9273
9990
];
}) settings.interfaces
)
);
systemd.services.telegsaf-json.script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath}";
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [
9273
9990
];
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [ 9273 ];
clan.core.vars.generators."telegraf" = {
clan.core.vars.generators."telegraf-password" = {
files.telegraf-password.neededFor = "users";
files.telegraf-password.restartUnits = [ "telegraf.service" ];
files.password.restartUnits = [ "telegraf.service" ];
files.password-env.restartUnits = [ "telegraf.service" ];
files.miniserve-auth.restartUnits = [ "telegraf.service" ];
runtimeInputs = [
pkgs.coreutils
@@ -40,16 +47,22 @@
script = ''
PASSWORD=$(xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n")
echo "BASIC_AUTH_PWD=$PASSWORD" > "$out"/telegraf-password
echo "BASIC_AUTH_PWD=$PASSWORD" > "$out"/password-env
echo "${auth_user}:$PASSWORD" > "$out"/miniserve-auth
echo "$PASSWORD" | tr -d "\n" > "$out"/password
'';
};
systemd.services.telegraf-json = {
enable = true;
wantedBy = [ "multi-user.target" ];
script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath} --auth-file ${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}";
};
services.telegraf = {
enable = true;
environmentFiles = [
(builtins.toString
config.clan.core.vars.generators."telegraf-password".files.telegraf-password.path
)
(builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path)
];
extraConfig = {
agent.interval = "60s";
@@ -64,32 +77,35 @@
exec =
let
currentSystemScript = pkgs.writeShellScript "current-system" ''
printf "current_system,path=%s present=0\n" $(readlink /run/current-system)
nixosSystems = pkgs.writeShellScript "current-system" ''
printf "nixos_systems,current_system=%s,booted_system=%s,current_kernel=%s,booted_kernel=%s present=0\n" \
"$(readlink /run/current-system)" "$(readlink /run/booted-system)" \
"$(basename $(echo /run/current-system/kernel-modules/lib/modules/*))" \
"$(basename $(echo /run/booted-system/kernel-modules/lib/modules/*))"
'';
in
[
{
# Expose the path to current-system as metric. We use
# this to check if the machine is up-to-date.
commands = [ currentSystemScript ];
commands = [ nixosSystems ];
data_format = "influx";
}
];
};
# sadly there doesn'T seem to exist a telegraf http_client output plugin
outputs.prometheus_client = {
listen = ":9273";
metric_version = 2;
basic_username = "${auth_user}";
basic_password = "$${BASIC_AUTH_PWD}";
};
outputs.file = {
files = [ jsonpath ];
data_format = "json";
json_timestamp_units = "1s";
};
outputs.prometheus_client = {
listen = ":9273";
metric_version = 2;
basic_username = "prometheus";
basic_password = "$${BASIC_AUTH_PWD}";
};
};
};
};

View File

@@ -17,6 +17,20 @@
};
};
# Deploy user Carol on all machines. Prompt only once and use the
# same password on all machines. (`share = true`)
user-carol = {
module = {
name = "users";
input = "clan";
};
roles.default.tags.all = { };
roles.default.settings = {
user = "carol";
share = true;
};
};
# Deploy user bob only on his laptop. Prompt for a password.
user-bob = {
module = {
@@ -29,3 +43,44 @@
};
}
```
## Migration from `root-password` module
The deprecated `clan.root-password` module has been replaced by the `users` module. Here's how to migrate:
### 1. Update your flake configuration
Replace the `root-password` module import with a `users` service instance:
```nix
# OLD - Remove this from your nixosModules:
imports = [
self.inputs.clan-core.clanModules.root-password
];
# NEW - Add to inventory.instances or machines/flake-module.nix:
instances = {
users-root = {
module.name = "users";
module.input = "clan-core";
roles.default.tags.nixos = { };
roles.default.settings = {
user = "root";
prompt = false; # Set to true if you want to be prompted
groups = [ ];
};
};
};
```
### 2. Migrate vars
The vars structure has changed from `root-password` to `user-password-root`:
```bash
# For each machine, rename the vars directories:
cd vars/per-machine/<machine-name>/
mv root-password user-password-root
mv user-password-root/password-hash user-password-root/user-password-hash
mv user-password-root/password user-password-root/user-password
```

View File

@@ -59,6 +59,17 @@
- "input" - Allows the user to access input devices.
'';
};
share = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Weather the user should have the same password on all machines.
By default, you will be prompted for a new password for every host.
Unless `generate` is set to `true`.
'';
};
};
};
@@ -82,7 +93,6 @@
};
clan.core.vars.generators."user-password-${settings.user}" = {
files.user-password-hash.neededFor = "users";
files.user-password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
files.user-password.deploy = false;
@@ -107,6 +117,8 @@
pkgs.mkpasswd
];
share = settings.share;
script =
(
if settings.prompt then

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
"""
IPv6 address allocator for WireGuard networks.
"""IPv6 address allocator for WireGuard networks.
Network layout:
- Base network: /40 ULA prefix (fd00::/8 + 32 bits from hash)
@@ -20,8 +19,7 @@ def hash_string(s: str) -> str:
def generate_ula_prefix(instance_name: str) -> ipaddress.IPv6Network:
"""
Generate a /40 ULA prefix from instance name.
"""Generate a /40 ULA prefix from instance name.
Format: fd{32-bit hash}/40
This gives us fd00:0000:0000::/40 through fdff:ffff:ff00::/40
@@ -46,10 +44,10 @@ def generate_ula_prefix(instance_name: str) -> ipaddress.IPv6Network:
def generate_controller_subnet(
base_network: ipaddress.IPv6Network, controller_name: str
base_network: ipaddress.IPv6Network,
controller_name: str,
) -> ipaddress.IPv6Network:
"""
Generate a /56 subnet for a controller from the base /40 network.
"""Generate a /56 subnet for a controller from the base /40 network.
We have 16 bits (40 to 56) to allocate controller subnets.
This allows for 65,536 possible controller subnets.
@@ -68,8 +66,7 @@ def generate_controller_subnet(
def generate_peer_suffix(peer_name: str) -> str:
"""
Generate a unique 64-bit host suffix for a peer.
"""Generate a unique 64-bit host suffix for a peer.
This suffix will be used in all controller subnets to create unique addresses.
Format: :xxxx:xxxx:xxxx:xxxx (64 bits)
@@ -86,7 +83,7 @@ def generate_peer_suffix(peer_name: str) -> str:
def main() -> None:
if len(sys.argv) < 4:
print(
"Usage: ipv6_allocator.py <output_dir> <instance_name> <controller|peer> <machine_name>"
"Usage: ipv6_allocator.py <output_dir> <instance_name> <controller|peer> <machine_name>",
)
sys.exit(1)

18
devFlake/flake.lock generated
View File

@@ -3,10 +3,10 @@
"clan-core-for-checks": {
"flake": false,
"locked": {
"lastModified": 1755649112,
"narHash": "sha256-Tk/Sjlb7W+5usS4upH0la4aGYs2velkHADnhhn2xO88=",
"lastModified": 1756081310,
"narHash": "sha256-wj1H5Pr6w4AsB+nG3K07SgSIDZ7jDCkGnh5XXWLdtk8=",
"ref": "main",
"rev": "bdab3e23af4a9715dfa6346f344f1bd428508672",
"rev": "7b926d43dc361cd8d3ad3c14a2e7e75375b7d215",
"shallow": true,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
@@ -84,11 +84,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1755628699,
"narHash": "sha256-IAM29K+Cz9pu90lDDkJpmOpWzQ9Ed0FNkRJcereY+rM=",
"lastModified": 1756050191,
"narHash": "sha256-lMtTT4rv5On7D0P4Z+k7UkvbAKKuVGRbJi/VJeRCQwI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "01b1b3809cfa3b0fbca8d75c9cf4b2efdb8182cf",
"rev": "759dcc6981cd4aa222d36069f78fe7064d563305",
"type": "github"
},
"original": {
@@ -165,11 +165,11 @@
"nixpkgs": []
},
"locked": {
"lastModified": 1754847726,
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
"lastModified": 1755934250,
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
"type": "github"
},
"original": {

2
docs/.gitignore vendored
View File

@@ -1,5 +1,5 @@
/site/reference
/site/static
/site/options-page
/site/options
/site/openapi.json
!/site/static/extra.css

View File

@@ -6,7 +6,7 @@ edit_uri: _edit/main/docs/docs/
validation:
omitted_files: warn
absolute_links: warn
absolute_links: ignore
unrecognized_links: warn
markdown_extensions:
@@ -64,7 +64,7 @@ nav:
- Disk Encryption: guides/disk-encryption.md
- Age Plugins: guides/age-plugins.md
- Secrets management: guides/secrets.md
- Target Host: guides/target-host.md
- Networking: guides/networking.md
- Zerotier VPN: guides/mesh-vpn.md
- Secure Boot: guides/secure-boot.md
- Flake-parts: guides/flake-parts.md
@@ -78,7 +78,7 @@ nav:
- Writing a Disko Template: guides/disko-templates/community.md
- Migrations:
- Migrate existing Flakes: guides/migrations/migration-guide.md
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
- Migrate from clan modules to services: guides/migrations/migrate-inventory-services.md
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
- Disk id: guides/migrations/disk-id.md
- Concepts:
@@ -88,7 +88,7 @@ nav:
- Templates: concepts/templates.md
- Reference:
- Overview: reference/index.md
- Clan Options: options.md
- Browse Options: "/options"
- Services:
- Overview:
- reference/clanServices/index.md
@@ -155,6 +155,7 @@ nav:
- 05-deployment-parameters: decisions/05-deployment-parameters.md
- Template: decisions/_template.md
- Glossary: reference/glossary.md
- Browse Options: "/options"
docs_dir: site
site_dir: out

View File

@@ -54,9 +54,9 @@ pkgs.stdenv.mkDerivation {
chmod -R +w ./site/reference
echo "Generated API documentation in './site/reference/' "
rm -r ./site/options-page || true
cp -r ${docs-options} ./site/options-page
chmod -R +w ./site/options-page
rm -rf ./site/options
cp -r ${docs-options} ./site/options
chmod -R +w ./site/options
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js

View File

@@ -25,7 +25,7 @@
serviceModules = self.clan.modules;
baseHref = "/options-page/";
baseHref = "/options/";
getRoles =
module:
@@ -126,7 +126,7 @@
nestedSettingsOption = mkOption {
type = types.raw;
description = ''
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=instances.${name}.roles.${roleName}.settings)
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=inventory.instances.${name}.roles.${roleName}.settings)
'';
};
settingsOption = mkOption {
@@ -161,6 +161,42 @@
}
];
baseModule =
# Module
{ config, ... }:
{
imports = (import (pkgs.path + "/nixos/modules/module-list.nix"));
nixpkgs.pkgs = pkgs;
clan.core.name = "dummy";
system.stateVersion = config.system.nixos.release;
# Set this to work around a bug where `clan.core.settings.machine.name`
# is forced due to `networking.interfaces` being forced
# somewhere in the nixpkgs options
facter.detected.dhcp.enable = lib.mkForce false;
};
evalClanModules =
let
evaled = lib.evalModules {
class = "nixos";
modules = [
baseModule
{
clan.core.settings.directory = self;
}
self.nixosModules.clanCore
];
};
in
evaled;
coreOptions =
(pkgs.nixosOptionsDoc {
options = (evalClanModules.options).clan.core or { };
warningsAreErrors = true;
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
}).optionsJSON;
in
{
# Uncomment for debugging
@@ -175,10 +211,17 @@
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
name = "Clan";
inherit baseHref;
name = "Flake Options (clan.nix file)";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
{
name = "Machine Options (clan.core NixOS options)";
optionsJSON = "${coreOptions}/share/doc/nixos/options.json";
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
};
};

View File

@@ -32,7 +32,7 @@ from typing import Any
from clan_lib.errors import ClanError
from clan_lib.services.modules import (
CategoryInfo,
Frontmatter,
ModuleManifest,
)
# Get environment variables
@@ -66,8 +66,7 @@ def render_option_header(name: str) -> str:
def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str:
"""
Joins multiple lines with a specified number of whitespace characters as indentation.
"""Joins multiple lines with a specified number of whitespace characters as indentation.
Args:
lines (list of str): The lines of text to join.
@@ -75,6 +74,7 @@ def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str:
Returns:
str: The indented and concatenated string.
"""
# Create the indentation string (e.g., four spaces)
indent_str = " " * indent
@@ -161,7 +161,10 @@ def render_option(
def print_options(
options_file: str, head: str, no_options: str, replace_prefix: str | None = None
options_file: str,
head: str,
no_options: str,
replace_prefix: str | None = None,
) -> str:
res = ""
with (Path(options_file) / "share/doc/nixos/options.json").open() as f:
@@ -176,9 +179,8 @@ def print_options(
return res
def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
indicator = " 🔹" if has_inventory_feature else ""
return f"# {module_name}{indicator}\n\n"
def module_header(module_name: str) -> str:
return f"# {module_name}\n\n"
clan_core_descr = """
@@ -236,7 +238,7 @@ def produce_clan_core_docs() -> None:
for submodule_name, split_options in split.items():
outfile = f"{module_name}/{submodule_name}.md"
print(
f"[clan_core.{submodule_name}] Rendering option of: {submodule_name}... {outfile}"
f"[clan_core.{submodule_name}] Rendering option of: {submodule_name}... {outfile}",
)
init_level = 1
root = options_to_tree(split_options, debug=True)
@@ -271,56 +273,9 @@ def produce_clan_core_docs() -> None:
of.write(output)
def render_roles(roles: list[str] | None, module_name: str) -> str:
if roles:
roles_list = "\n".join([f"- `{r}`" for r in roles])
return (
f"""
### Roles
This module can be used via predefined roles
{roles_list}
"""
"""
Every role has its own configuration options, which are each listed below.
For more information, see the [inventory guide](../../concepts/inventory.md).
??? Example
For example the `admin` module adds the following options globally to all machines where it is used.
`clan.admin.allowedkeys`
```nix
clan-core.lib.clan {
inventory.services = {
admin.me = {
roles.default.machines = [ "jon" ];
config.allowedkeys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD..." ];
};
};
};
```
"""
)
return ""
clan_modules_descr = """
Clan modules are [NixOS modules](https://wiki.nixos.org/wiki/NixOS_modules)
which have been enhanced with additional features provided by Clan, with
certain option types restricted to enable configuration through a graphical
interface.
!!! note "🔹"
Modules with this indicator support the [inventory](../../concepts/inventory.md) feature.
"""
def render_categories(
categories: list[str], categories_info: dict[str, CategoryInfo]
categories: list[str],
categories_info: dict[str, CategoryInfo],
) -> str:
res = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
for cat in categories:
@@ -385,10 +340,10 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
# output += f"`clan.modules.{module_name}`\n"
output += f"*{module_info['manifest']['description']}*\n"
fm = Frontmatter("")
# output += "## Categories\n\n"
output += render_categories(
module_info["manifest"]["categories"], fm.categories_info
module_info["manifest"]["categories"],
ModuleManifest.categories_info(),
)
output += f"{module_info['manifest']['readme']}\n"
@@ -397,7 +352,7 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
output += f"The {module_name} module has the following roles:\n\n"
for role_name, _ in module_info["roles"].items():
for role_name in module_info["roles"]:
output += f"- {role_name}\n"
for role_name, role_filename in module_info["roles"].items():
@@ -417,35 +372,8 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
of.write(output)
def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
"""
Build the overview index card for each reference target option.
"""
def indent_all(text: str, indent_size: int = 4) -> str:
"""
Indent all lines in a string.
"""
indent = " " * indent_size
lines = text.split("\n")
indented_text = indent + ("\n" + indent).join(lines)
return indented_text
def to_md_li(module_name: str, frontmatter: Frontmatter) -> str:
md_li = (
f"""- **[{module_name}](./{"-".join(module_name.split(" "))}.md)**\n\n"""
)
md_li += f"""{indent_all("---", 4)}\n\n"""
fmd = f"\n{frontmatter.description.strip()}" if frontmatter.description else ""
md_li += f"""{indent_all(fmd, 4)}"""
return md_li
return f"{to_md_li(module_name, frontmatter)}\n\n"
def split_options_by_root(options: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""
Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
"""Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
{
"a": { Data }
"a.b": { Data }
@@ -529,9 +457,7 @@ def option_short_name(option_name: str) -> str:
def options_to_tree(options: dict[str, Any], debug: bool = False) -> Option:
"""
Convert the options dictionary to a tree structure.
"""
"""Convert the options dictionary to a tree structure."""
# Helper function to create nested structure
def add_to_tree(path_parts: list[str], info: Any, current_node: Option) -> None:
@@ -583,22 +509,24 @@ def options_to_tree(options: dict[str, Any], debug: bool = False) -> Option:
def options_docs_from_tree(
root: Option, init_level: int = 1, prefix: list[str] | None = None
root: Option,
init_level: int = 1,
prefix: list[str] | None = None,
) -> str:
"""
eender the options from the tree structure.
"""Eender the options from the tree structure.
Args:
root (Option): The root option node.
init_level (int): The initial level of indentation.
prefix (list str): Will be printed as common prefix of all attribute names.
"""
def render_tree(option: Option, level: int = init_level) -> str:
output = ""
should_render = not option.name.startswith("<") and not option.name.startswith(
"_"
"_",
)
if should_render:
# short_name = option_short_name(option.name)
@@ -623,7 +551,7 @@ def options_docs_from_tree(
return md
if __name__ == "__main__": #
if __name__ == "__main__":
produce_clan_core_docs()
produce_clan_service_author_docs()

View File

@@ -1,15 +1,33 @@
# Auto-included Files
Clan automatically imports the following files from a directory and registers them.
Clan automatically imports specific files from each machine directory and registers them, reducing the need for manual configuration.
## Machine registration
## Machine Registration
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
Every folder under `machines/{machineName}` is automatically registered as a Clan machine.
!!! info "Automatically loaded files"
!!! info "Files loaded automatically for each machine"
The following files are loaded automatically for each Clan machine:
The following files are detected and imported for every Clan machine:
- [x] `machines/{machineName}/configuration.nix`
- [x] `machines/{machineName}/hardware-configuration.nix`
- [x] `machines/{machineName}/facter.json` Automatically configured, for further information see [nixos-facter](https://clan.lol/blog/nixos-facter/)
- [x] `machines/{machineName}/disko.nix` Automatically loaded, for further information see the [disko docs](https://github.com/nix-community/disko/blob/master/docs/quickstart.md).
- [x] `machines/{machineName}/configuration.nix`
Main configuration file for the machine.
- [x] `machines/{machineName}/hardware-configuration.nix`
Hardware-specific configuration generated by NixOS.
- [x] `machines/{machineName}/facter.json`
Contains system facts. Automatically generated — see [nixos-facter](https://clan.lol/blog/nixos-facter/) for details.
- [x] `machines/{machineName}/disko.nix`
Disk layout configuration. See the [disko quickstart](https://github.com/nix-community/disko/blob/master/docs/quickstart.md) for more info.
## Other Auto-included Files
* **`inventory.json`**
Managed by Clan's API.
Merges with `clan.inventory` to extend the inventory.
* **`.clan-flake`**
Sentinel file to be used to locate the root of a Clan repository.
Falls back to `.git`, `.hg`, `.svn`, or `flake.nix` if not found.

View File

@@ -1,6 +1,6 @@
# Using `clanServices`
Clans `clanServices` system is a composable way to define and deploy services across machines.
Clan's `clanServices` system is a composable way to define and deploy services across machines.
This guide shows how to **instantiate** a `clanService`, explains how service definitions are structured in your inventory, and how to pick or create services from modules exposed by flakes.
@@ -130,7 +130,7 @@ inventory.instances = {
## Picking a clanService
You can use services exposed by Clans core module library, `clan-core`.
You can use services exposed by Clan's core module library, `clan-core`.
🔗 See: [List of Available Services in clan-core](../reference/clanServices/index.md)
@@ -152,7 +152,7 @@ You might expose your service module from your flake — this makes it easy for
---
## Whats Next?
## What's Next?
* [Author your own clanService →](../guides/services/community.md)
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)

View File

@@ -27,7 +27,7 @@ inputs = {
## Import the Clan flake-parts Module
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](../options.md) available within `mkFlake`.
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](/options) available within `mkFlake`.
```nix
{

View File

@@ -271,7 +271,7 @@ The following table shows the migration status of each deprecated clanModule:
| `nginx` | ❌ Removed | |
| `packages` | ✅ [Migrated](../../reference/clanServices/packages.md) | |
| `postgresql` | ❌ Removed | Now an [option](../../reference/clan.core/settings.md) |
| `root-password` | ✅ [Migrated](../../reference/clanServices/users.md) | |
| `root-password` | ✅ [Migrated](../../reference/clanServices/users.md) | See [migration guide](../../reference/clanServices/users.md#migration-from-root-password-module) |
| `single-disk` | ❌ Removed | |
| `sshd` | ✅ [Migrated](../../reference/clanServices/sshd.md) | |
| `state-version` | ✅ [Migrated](../../reference/clanServices/state-version.md) | |

View File

@@ -0,0 +1,184 @@
# Connecting to Your Machines
Clan provides automatic networking with fallback mechanisms to reliably connect to your machines.
## Option 1: Automatic Networking with Fallback (Recommended)
Clan's networking module automatically manages connections through various network technologies with intelligent fallback. When you run `clan ssh` or `clan machines update`, Clan tries each configured network by priority until one succeeds.
### Basic Setup with Internet Service
For machines with public IPs or DNS names, use the `internet` service to configure direct SSH while keeping fallback options:
```{.nix title="flake.nix" hl_lines="7-10 14-16"}
{
outputs = { self, clan-core, ... }:
let
clan = clan-core.lib.clan {
inventory.instances = {
# Direct SSH with fallback support
internet = {
roles.default.machines.server1 = {
settings.address = "server1.example.com";
};
roles.default.machines.server2 = {
settings.address = "192.168.1.100";
};
};
# Fallback: Secure connections via Tor
tor = {
roles.server.tags.nixos = { };
};
};
};
in
{
inherit (clan.config) nixosConfigurations;
};
}
```
### Advanced Setup with Multiple Networks
```{.nix title="flake.nix" hl_lines="7-10 13-16 19-21"}
{
outputs = { self, clan-core, ... }:
let
clan = clan-core.lib.clan {
inventory.instances = {
# Priority 1: Try direct connection first
internet = {
roles.default.machines.publicserver = {
settings.address = "public.example.com";
};
};
# Priority 2: VPN for internal machines
zerotier = {
roles.controller.machines."controller" = { };
roles.peer.tags.nixos = { };
};
# Priority 3: Tor as universal fallback
tor = {
roles.server.tags.nixos = { };
};
};
};
in
{
inherit (clan.config) nixosConfigurations;
};
}
```
### How It Works
Clan automatically tries networks in order of priority:
1. Direct internet connections (if configured)
2. VPN networks (ZeroTier, Tailscale, etc.)
3. Tor hidden services
4. Any other configured networks
If one network fails, Clan automatically tries the next.
### Useful Commands
```bash
# View all configured networks and their status
clan network list
# Test connectivity through all networks
clan network ping machine1
# Show complete network topology
clan network overview
```
## Option 2: Manual targetHost (Bypasses Fallback!)
!!! warning
Setting `targetHost` directly **disables all automatic networking and fallback**. Only use this if you need complete control and don't want Clan's intelligent connection management.
### Using Inventory (For Static Addresses)
Use inventory-level `targetHost` when the address is **static** and doesn't depend on NixOS configuration:
```{.nix title="flake.nix" hl_lines="8"}
{
outputs = { self, clan-core, ... }:
let
clan = clan-core.lib.clan {
inventory.machines.server = {
# WARNING: This bypasses all networking modules!
# Use for: Static IPs, DNS names, known hostnames
deploy.targetHost = "root@192.168.1.100";
};
};
in
{
inherit (clan.config) nixosConfigurations;
};
}
```
**When to use inventory-level:**
- Static IP addresses: `"root@192.168.1.100"`
- DNS names: `"user@server.example.com"`
- Any address that doesn't change based on machine configuration
### Using NixOS Configuration (For Dynamic Addresses)
Use machine-level `targetHost` when you need to **interpolate values from the NixOS configuration**:
```{.nix title="flake.nix" hl_lines="7"}
{
outputs = { self, clan-core, ... }:
let
clan = clan-core.lib.clan {
machines.server = { config, ... }: {
# WARNING: This also bypasses all networking modules!
# REQUIRED for: Addresses that depend on NixOS config
clan.core.networking.targetHost = "root@${config.networking.hostName}.local";
};
};
in
{
inherit (clan.config) nixosConfigurations;
};
}
```
**When to use machine-level (NixOS config):**
- Using hostName from config: `"root@${config.networking.hostName}.local"`
- Building from multiple config values: `"${config.users.users.deploy.name}@${config.networking.hostName}"`
- Any address that depends on evaluated NixOS configuration
!!! info "Key Difference"
**Inventory-level** (`deploy.targetHost`) is evaluated immediately and works with static strings.
**Machine-level** (`clan.core.networking.targetHost`) is evaluated after NixOS configuration and can access `config.*` values.
## Quick Decision Guide
| Scenario | Recommended Approach | Why |
|----------|---------------------|-----|
| Public servers | `internet` service | Keeps fallback options |
| Mixed infrastructure | Multiple networks | Automatic failover |
| Machines behind NAT | ZeroTier/Tor | NAT traversal with fallback |
| Testing/debugging | Manual targetHost | Full control, no magic |
| Single static machine | Manual targetHost | Simple, no overhead |
## Command-Line Override
The `--target-host` flag bypasses ALL networking configuration:
```bash
# Emergency access - ignores all networking config
clan machines update server --target-host root@backup-ip.com
# Direct SSH - no fallback attempted
clan ssh laptop --target-host user@10.0.0.5
```
Use this for debugging or emergency access when automatic networking isn't working.

View File

@@ -1,84 +0,0 @@
# How to Set `targetHost` for a Machine
The `targetHost` defines where the machine can be reached for operations like SSH or deployment. You can set it in two ways, depending on your use case.
---
## ✅ Option 1: Use the Inventory (Recommended for Static Hosts)
If the hostname is **static**, like `server.example.com`, set it in the **inventory**:
```{.nix title="flake.nix" hl_lines="8"}
{
# edlided
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inventory.machines.jon = {
deploy.targetHost = "root@server.example.com";
};
};
in
{
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
# elided
};
}
```
This is fast, simple and explicit, and doesnt require evaluating the NixOS config. We can also displayed it in the clan-cli or clan-app.
---
## ✅ Option 2: Use NixOS (Only for Dynamic Hosts)
If your target host depends on a **dynamic expression** (like using the machines evaluated FQDN), set it inside the NixOS module:
```{.nix title="flake.nix" hl_lines="8"}
{
# edlided
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
machines.jon = {config, ...}: {
clan.core.networking.targetHost = "jon@${config.networking.fqdn}";
};
};
in
{
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
# elided
};
}
```
Use this **only if the value cannot be made static**, because its slower and won't be displayed in the clan-cli or clan-app yet.
---
## 📝 TL;DR
| Use Case | Use Inventory? | Example |
| ------------------------- | -------------- | -------------------------------- |
| Static hostname | ✅ Yes | `root@server.example.com` |
| Dynamic config expression | ❌ No | `jon@${config.networking.fqdn}` |
---
## 🚀 Coming Soon: Unified Networking Module
Were working on a new networking module that will automatically do all of this for you.
- Easier to use
- Sane defaults: Youll always be able to reach the machine — no need to worry about hostnames.
- ✨ Migration from **either method** will be supported and simple.
## Summary
- Ask: *Does this hostname dynamically change based on NixOS config?*
- If **no**, use the inventory.
- If **yes**, then use NixOS config.

View File

@@ -1,6 +0,0 @@
---
template: options.html
---
<iframe src="/options-page/" height="1000" width="100%"></iframe>

View File

@@ -4,7 +4,7 @@ This section of the site provides an overview of available options and commands
---
- [Clan Configuration Option](../options.md) - for defining a Clan
- [Clan Configuration Option](/options) - for defining a Clan
- Learn how to use the [Clan CLI](./cli/index.md)
- Explore available [services](./clanServices/index.md)
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.

28
flake.lock generated
View File

@@ -13,11 +13,11 @@
]
},
"locked": {
"lastModified": 1753067306,
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
"lastModified": 1756091210,
"narHash": "sha256-oEUEAZnLbNHi8ti4jY8x10yWcIkYoFc5XD+2hjmOS04=",
"rev": "eb831bca21476fa8f6df26cb39e076842634700d",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/eb831bca21476fa8f6df26cb39e076842634700d.tar.gz"
},
"original": {
"type": "tarball",
@@ -71,11 +71,11 @@
]
},
"locked": {
"lastModified": 1755275010,
"narHash": "sha256-lEApCoWUEWh0Ifc3k1JdVjpMtFFXeL2gG1qvBnoRc2I=",
"lastModified": 1755825449,
"narHash": "sha256-XkiN4NM9Xdy59h69Pc+Vg4PxkSm9EWl6u7k6D5FZ5cM=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "7220b01d679e93ede8d7b25d6f392855b81dd475",
"rev": "8df64f819698c1fee0c2969696f54a843b2231e8",
"type": "github"
},
"original": {
@@ -86,11 +86,11 @@
},
"nix-select": {
"locked": {
"lastModified": 1745005516,
"narHash": "sha256-IVaoOGDIvAa/8I0sdiiZuKptDldrkDWUNf/+ezIRhyc=",
"rev": "69d8bf596194c5c35a4e90dd02c52aa530caddf8",
"lastModified": 1755887746,
"narHash": "sha256-lzWbpHKX0WAn/jJDoCijIDss3rqYIPawe46GDaE6U3g=",
"rev": "92c2574c5e113281591be01e89bb9ddb31d19156",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/69d8bf596194c5c35a4e90dd02c52aa530caddf8.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/92c2574c5e113281591be01e89bb9ddb31d19156.tar.gz"
},
"original": {
"type": "tarball",
@@ -181,11 +181,11 @@
]
},
"locked": {
"lastModified": 1754847726,
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
"lastModified": 1755934250,
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
"type": "github"
},
"original": {

View File

@@ -328,7 +328,7 @@ rec {
# To get the type of a Deferred modules we need to know the interface of the place where it is evaluated.
# i.e. in case of a clan.service this is the interface of the service which dynamically changes depending on the service
# We assign "type" = []
# This means any value is valid — or like TypeScripts unknown.
# This means any value is valid — or like TypeScript's unknown.
# We can assign the type later, when we know the exact interface.
# tsType = "unknown" is a type that we preload for json2ts, such that it gets the correct type in typescript
(option.type.name == "deferredModule")

View File

@@ -32,11 +32,15 @@ def init_test_environment() -> None:
# Set up network bridge
subprocess.run(
["ip", "link", "add", "br0", "type", "bridge"], check=True, text=True
["ip", "link", "add", "br0", "type", "bridge"],
check=True,
text=True,
)
subprocess.run(["ip", "link", "set", "br0", "up"], check=True, text=True)
subprocess.run(
["ip", "addr", "add", "192.168.1.254/24", "dev", "br0"], check=True, text=True
["ip", "addr", "add", "192.168.1.254/24", "dev", "br0"],
check=True,
text=True,
)
# Set up minimal passwd file for unprivileged operations
@@ -111,8 +115,7 @@ def mount(
mountflags: int = 0,
data: str | None = None,
) -> None:
"""
A Python wrapper for the mount system call.
"""A Python wrapper for the mount system call.
:param source: The source of the file system (e.g., device name, remote filesystem).
:param target: The mount point (an existing directory).
@@ -129,7 +132,11 @@ def mount(
# Call the mount system call
result = libc.mount(
source_c, target_c, fstype_c, ctypes.c_ulong(mountflags), data_c
source_c,
target_c,
fstype_c,
ctypes.c_ulong(mountflags),
data_c,
)
if result != 0:
@@ -145,7 +152,7 @@ def prepare_machine_root(machinename: str, root: Path) -> None:
root.mkdir(parents=True, exist_ok=True)
root.joinpath("etc").mkdir(parents=True, exist_ok=True)
root.joinpath(".env").write_text(
"\n".join(f"{k}={v}" for k, v in os.environ.items())
"\n".join(f"{k}={v}" for k, v in os.environ.items()),
)
@@ -157,7 +164,6 @@ def retry(fn: Callable, timeout: int = 900) -> None:
"""Call the given function repeatedly, with 1 second intervals,
until it returns True or a timeout is reached.
"""
for _ in range(timeout):
if fn(False):
return
@@ -284,8 +290,7 @@ class Machine:
check_output: bool = True,
timeout: int | None = 900,
) -> subprocess.CompletedProcess:
"""
Execute a shell command, returning a list `(status, stdout)`.
"""Execute a shell command, returning a list `(status, stdout)`.
Commands are run with `set -euo pipefail` set:
@@ -316,7 +321,6 @@ class Machine:
`timeout` parameter, e.g., `execute(cmd, timeout=10)` or
`execute(cmd, timeout=None)`. The default is 900 seconds.
"""
# Always run command with shell opts
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}"
@@ -330,7 +334,9 @@ class Machine:
return proc
def nested(
self, msg: str, attrs: dict[str, str] | None = None
self,
msg: str,
attrs: dict[str, str] | None = None,
) -> _GeneratorContextManager:
if attrs is None:
attrs = {}
@@ -339,8 +345,7 @@ class Machine:
return self.logger.nested(msg, my_attrs)
def systemctl(self, q: str) -> subprocess.CompletedProcess:
"""
Runs `systemctl` commands with optional support for
"""Runs `systemctl` commands with optional support for
`systemctl --user`
```py
@@ -355,8 +360,7 @@ class Machine:
return self.execute(f"systemctl {q}")
def wait_until_succeeds(self, command: str, timeout: int = 900) -> str:
"""
Repeat a shell command with 1-second intervals until it succeeds.
"""Repeat a shell command with 1-second intervals until it succeeds.
Has a default timeout of 900 seconds which can be modified, e.g.
`wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on
command execution.
@@ -374,18 +378,17 @@ class Machine:
return output
def wait_for_open_port(
self, port: int, addr: str = "localhost", timeout: int = 900
self,
port: int,
addr: str = "localhost",
timeout: int = 900,
) -> None:
"""
Wait for a port to be open on the given address.
"""
"""Wait for a port to be open on the given address."""
command = f"nc -z {shlex.quote(addr)} {port}"
self.wait_until_succeeds(command, timeout=timeout)
def wait_for_file(self, filename: str, timeout: int = 30) -> None:
"""
Waits until the file exists in the machine's file system.
"""
"""Waits until the file exists in the machine's file system."""
def check_file(_last_try: bool) -> bool:
result = self.execute(f"test -e {filename}")
@@ -395,8 +398,7 @@ class Machine:
retry(check_file, timeout)
def wait_for_unit(self, unit: str, timeout: int = 900) -> None:
"""
Wait for a systemd unit to get into "active" state.
"""Wait for a systemd unit to get into "active" state.
Throws exceptions on "failed" and "inactive" states as well as after
timing out.
"""
@@ -441,9 +443,7 @@ class Machine:
return res.stdout
def shutdown(self) -> None:
"""
Shut down the machine, waiting for the VM to exit.
"""
"""Shut down the machine, waiting for the VM to exit."""
if self.process:
self.process.terminate()
self.process.wait()
@@ -557,7 +557,7 @@ class Driver:
rootdir=tempdir_path / container.name,
out_dir=self.out_dir,
logger=self.logger,
)
),
)
def start_all(self) -> None:
@@ -581,7 +581,7 @@ class Driver:
)
print(
f"To attach to container {machine.name} run on the same machine that runs the test:"
f"To attach to container {machine.name} run on the same machine that runs the test:",
)
print(
" ".join(
@@ -603,8 +603,8 @@ class Driver:
"-c",
"bash",
Style.RESET_ALL,
]
)
],
),
)
def test_symbols(self) -> dict[str, Any]:
@@ -623,7 +623,7 @@ class Driver:
"additionally exposed symbols:\n "
+ ", ".join(m.name for m in self.machines)
+ ",\n "
+ ", ".join(list(general_symbols.keys()))
+ ", ".join(list(general_symbols.keys())),
)
return {**general_symbols, **machine_symbols}

View File

@@ -25,14 +25,18 @@ class AbstractLogger(ABC):
@abstractmethod
@contextmanager
def subtest(
self, name: str, attributes: dict[str, str] | None = None
self,
name: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
pass
@abstractmethod
@contextmanager
def nested(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
pass
@@ -66,7 +70,7 @@ class JunitXMLLogger(AbstractLogger):
def __init__(self, outfile: Path) -> None:
self.tests: dict[str, JunitXMLLogger.TestCaseState] = {
"main": self.TestCaseState()
"main": self.TestCaseState(),
}
self.currentSubtest = "main"
self.outfile: Path = outfile
@@ -78,7 +82,9 @@ class JunitXMLLogger(AbstractLogger):
@contextmanager
def subtest(
self, name: str, attributes: dict[str, str] | None = None
self,
name: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
old_test = self.currentSubtest
self.tests.setdefault(name, self.TestCaseState())
@@ -90,7 +96,9 @@ class JunitXMLLogger(AbstractLogger):
@contextmanager
def nested(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
self.log(message)
yield
@@ -144,7 +152,9 @@ class CompositeLogger(AbstractLogger):
@contextmanager
def subtest(
self, name: str, attributes: dict[str, str] | None = None
self,
name: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
with ExitStack() as stack:
for logger in self.logger_list:
@@ -153,7 +163,9 @@ class CompositeLogger(AbstractLogger):
@contextmanager
def nested(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
with ExitStack() as stack:
for logger in self.logger_list:
@@ -200,19 +212,24 @@ class TerminalLogger(AbstractLogger):
@contextmanager
def subtest(
self, name: str, attributes: dict[str, str] | None = None
self,
name: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
with self.nested("subtest: " + name, attributes):
yield
@contextmanager
def nested(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
self._eprint(
self.maybe_prefix(
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes
)
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL,
attributes,
),
)
tic = time.time()
@@ -259,7 +276,9 @@ class XMLLogger(AbstractLogger):
return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
def maybe_prefix(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> str:
if attributes and "machine" in attributes:
return f"{attributes['machine']}: {message}"
@@ -309,14 +328,18 @@ class XMLLogger(AbstractLogger):
@contextmanager
def subtest(
self, name: str, attributes: dict[str, str] | None = None
self,
name: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
with self.nested("subtest: " + name, attributes):
yield
@contextmanager
def nested(
self, message: str, attributes: dict[str, str] | None = None
self,
message: str,
attributes: dict[str, str] | None = None,
) -> Iterator[None]:
if attributes is None:
attributes = {}

View File

@@ -1,40 +1,17 @@
{ ... }:
{
perSystem =
{ ... }:
{
clan.nixosTests.machine-id = {
perSystem.clan.nixosTests.machine-id = {
name = "service-machine-id";
name = "service-machine-id";
clan = {
directory = ./.;
# Workaround until we can use nodes.server = { };
modules."@clan/importer" = ../../../../clanServices/importer;
inventory = {
machines.server = { };
instances.importer = {
module.name = "@clan/importer";
module.input = "self";
roles.default.tags.all = { };
roles.default.extraModules = [
{
# Test machine ID generation
clan.core.settings.machine-id.enable = true;
}
];
};
};
};
# TODO: Broken. Use instead of importer after fixing.
# nodes.server = { };
# This is not an actual vm test, this is a workaround to
# generate the needed vars for the eval test.
testScript = "";
clan = {
directory = ./.;
machines.server = {
clan.core.settings.machine-id.enable = true;
};
};
# This is not an actual vm test, this is a workaround to
# generate the needed vars for the eval test.
testScript = "";
};
}

View File

@@ -10,30 +10,14 @@
clan = {
directory = ./.;
# Workaround until we can use nodes.machine = { };
modules."@clan/importer" = ../../../../clanServices/importer;
inventory = {
machines.machine = { };
instances.importer = {
module.name = "@clan/importer";
module.input = "self";
roles.default.tags.all = { };
roles.default.extraModules = [
{
clan.core.postgresql.enable = true;
clan.core.postgresql.users.test = { };
clan.core.postgresql.databases.test.create.options.OWNER = "test";
clan.core.settings.directory = ./.;
}
];
};
machines.machine = {
clan.core.postgresql.enable = true;
clan.core.postgresql.users.test = { };
clan.core.postgresql.databases.test.create.options.OWNER = "test";
clan.core.settings.directory = ./.;
};
};
# TODO: Broken. Use instead of importer after fixing.
# nodes.machine = { };
testScript =
let
runpg = "runuser -u postgres -- /run/current-system/sw/bin/psql";

View File

@@ -304,6 +304,15 @@ in
description = "The unix file mode of the file. Must be a 4-digit octal number.";
default = "0400";
};
exists = mkOption {
description = ''
Returns true if the file exists, This is used to guard against reading not set value in evaluation.
This currently only works for non secret files.
'';
type = bool;
default = if file.config.secret then throw "Cannot determine existance of secret file" else false;
defaultText = "Throws error because the existance of a secret file cannot be determined";
};
value =
mkOption {
description = ''

View File

@@ -25,7 +25,7 @@ in
);
value = mkIf (file.config.secret == false) (
# dynamically adjust priority to allow overriding with mkDefault in case the file is not found
if (pathExists file.config.flakePath) then
if file.config.exists then
# if the file is found it should have normal priority
readFile file.config.flakePath
else
@@ -34,6 +34,7 @@ in
throw "Please run `clan vars generate ${config.clan.core.settings.machine.name}` as file was not found: ${file.config.path}"
)
);
exists = mkIf (file.config.secret == false) (pathExists file.config.flakePath);
};
};
}

View File

@@ -195,7 +195,7 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
(node_id >> 16) & 0xFF,
(node_id >> 8) & 0xFF,
(node_id) & 0xFF,
]
],
)
return ipaddress.IPv6Address(bytes(addr_parts))
@@ -203,7 +203,10 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode", choices=["network", "identity"], required=True, type=str
"--mode",
choices=["network", "identity"],
required=True,
type=str,
)
parser.add_argument("--ip", type=Path, required=True)
parser.add_argument("--identity-secret", type=Path, required=True)

View File

@@ -17,7 +17,7 @@ def main() -> None:
moon_json = json.loads(Path(moon_json_path).read_text())
moon_json["roots"][0]["stableEndpoints"] = json.loads(
Path(endpoint_config).read_text()
Path(endpoint_config).read_text(),
)
with NamedTemporaryFile("w") as f:

View File

@@ -38,8 +38,7 @@ def get_gitea_api_url(remote: str = "origin") -> str:
host_and_path = remote_url.split("@")[1] # git.clan.lol:clan/clan-core.git
host = host_and_path.split(":")[0] # git.clan.lol
repo_path = host_and_path.split(":")[1] # clan/clan-core.git
if repo_path.endswith(".git"):
repo_path = repo_path[:-4] # clan/clan-core
repo_path = repo_path.removesuffix(".git") # clan/clan-core
elif remote_url.startswith("https://"):
# HTTPS format: https://git.clan.lol/clan/clan-core.git
url_parts = remote_url.replace("https://", "").split("/")
@@ -86,7 +85,10 @@ def get_repo_info_from_api_url(api_url: str) -> tuple[str, str]:
def fetch_pr_statuses(
repo_owner: str, repo_name: str, commit_sha: str, host: str
repo_owner: str,
repo_name: str,
commit_sha: str,
host: str,
) -> list[dict]:
"""Fetch CI statuses for a specific commit SHA."""
status_url = (
@@ -183,7 +185,7 @@ def run_git_command(command: list) -> tuple[int, str, str]:
def get_current_branch_name() -> str:
exit_code, branch_name, error = run_git_command(
["git", "rev-parse", "--abbrev-ref", "HEAD"]
["git", "rev-parse", "--abbrev-ref", "HEAD"],
)
if exit_code != 0:
@@ -196,7 +198,7 @@ def get_current_branch_name() -> str:
def get_latest_commit_info() -> tuple[str, str]:
"""Get the title and body of the latest commit."""
exit_code, commit_msg, error = run_git_command(
["git", "log", "-1", "--pretty=format:%B"]
["git", "log", "-1", "--pretty=format:%B"],
)
if exit_code != 0:
@@ -225,7 +227,7 @@ def get_commits_since_main() -> list[tuple[str, str]]:
"main..HEAD",
"--no-merges",
"--pretty=format:%s|%b|---END---",
]
],
)
if exit_code != 0:
@@ -263,7 +265,9 @@ def open_editor_for_pr() -> tuple[str, str]:
commits_since_main = get_commits_since_main()
with tempfile.NamedTemporaryFile(
mode="w+", suffix="COMMIT_EDITMSG", delete=False
mode="w+",
suffix="COMMIT_EDITMSG",
delete=False,
) as temp_file:
temp_file.flush()
temp_file_path = temp_file.name
@@ -280,7 +284,7 @@ def open_editor_for_pr() -> tuple[str, str]:
temp_file.write("# The first line will be used as the PR title.\n")
temp_file.write("# Everything else will be used as the PR description.\n")
temp_file.write(
"# To abort creation of the PR, close editor with an error code.\n"
"# To abort creation of the PR, close editor with an error code.\n",
)
temp_file.write("# In vim for example you can use :cq!\n")
temp_file.write("#\n")
@@ -373,7 +377,7 @@ def create_agit_push(
print(
f" Description: {description[:50]}..."
if len(description) > 50
else f" Description: {description}"
else f" Description: {description}",
)
print()
@@ -530,19 +534,26 @@ Examples:
)
create_parser.add_argument(
"-t", "--topic", help="Set PR topic (default: current branch name)"
"-t",
"--topic",
help="Set PR topic (default: current branch name)",
)
create_parser.add_argument(
"--title", help="Set the PR title (default: last commit title)"
"--title",
help="Set the PR title (default: last commit title)",
)
create_parser.add_argument(
"--description", help="Override the PR description (default: commit body)"
"--description",
help="Override the PR description (default: commit body)",
)
create_parser.add_argument(
"-f", "--force", action="store_true", help="Force push the changes"
"-f",
"--force",
action="store_true",
help="Force push the changes",
)
create_parser.add_argument(

View File

@@ -13,7 +13,9 @@ log = logging.getLogger(__name__)
def main(argv: list[str] = sys.argv) -> int:
parser = argparse.ArgumentParser(description="Clan App")
parser.add_argument(
"--content-uri", type=str, help="The URI of the content to display"
"--content-uri",
type=str,
help="The URI of the content to display",
)
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
parser.add_argument(

View File

@@ -56,18 +56,23 @@ class ApiBridge(ABC):
for middleware in self.middleware_chain:
try:
log.debug(
f"{middleware.__class__.__name__} => {request.method_name}"
f"{middleware.__class__.__name__} => {request.method_name}",
)
middleware.process(context)
except Exception as e:
# If middleware fails, handle error
self.send_api_error_response(
request.op_key or "unknown", str(e), ["middleware_error"]
request.op_key or "unknown",
str(e),
["middleware_error"],
)
return
def send_api_error_response(
self, op_key: str, error_message: str, location: list[str]
self,
op_key: str,
error_message: str,
location: list[str],
) -> None:
"""Send an error response."""
from clan_lib.api import ApiError, ErrorDataClass
@@ -80,7 +85,7 @@ class ApiBridge(ABC):
message="An internal error occured",
description=error_message,
location=location,
)
),
],
)
@@ -107,6 +112,7 @@ class ApiBridge(ABC):
thread_name: Name for the thread (for debugging)
wait_for_completion: Whether to wait for the thread to complete
timeout: Timeout in seconds when waiting for completion
"""
op_key = request.op_key or "unknown"
@@ -116,7 +122,7 @@ class ApiBridge(ABC):
try:
log.debug(
f"Processing {request.method_name} with args {request.args} "
f"and header {request.header} in thread {thread_name}"
f"and header {request.header} in thread {thread_name}",
)
self.process_request(request)
finally:
@@ -124,7 +130,9 @@ class ApiBridge(ABC):
stop_event = threading.Event()
thread = threading.Thread(
target=thread_task, args=(stop_event,), name=thread_name
target=thread_task,
args=(stop_event,),
name=thread_name,
)
thread.start()
@@ -138,5 +146,7 @@ class ApiBridge(ABC):
if thread.is_alive():
stop_event.set() # Cancel the thread
self.send_api_error_response(
op_key, "Request timeout", ["api_bridge", request.method_name]
op_key,
"Request timeout",
["api_bridge", request.method_name],
)

View File

@@ -26,8 +26,7 @@ RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
"""
Opens the clan folder using the GTK file dialog.
"""Opens the clan folder using the GTK file dialog.
Returns the path to the clan folder or an error if it fails.
"""
file_request = FileRequest(
@@ -52,7 +51,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
message="No folder selected",
description="You must select a folder to open.",
location=["get_clan_folder"],
)
),
],
)
@@ -66,7 +65,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
message="Invalid clan folder",
description=f"The selected folder '{clan_folder}' is not a valid clan folder.",
location=["get_clan_folder"],
)
),
],
)
@@ -102,8 +101,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
selected_path = remove_none([gfile.get_path()])
returns(
SuccessDataClass(
op_key=op_key, data=selected_path, status="success"
)
op_key=op_key,
data=selected_path,
status="success",
),
)
except Exception as e:
log.exception("Error opening file")
@@ -116,9 +117,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
message=e.__class__.__name__,
description=str(e),
location=["get_system_file"],
)
),
],
)
),
)
def on_file_select_multiple(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
@@ -128,8 +129,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
returns(
SuccessDataClass(
op_key=op_key, data=selected_paths, status="success"
)
op_key=op_key,
data=selected_paths,
status="success",
),
)
else:
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
@@ -144,9 +147,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
message=e.__class__.__name__,
description=str(e),
location=["get_system_file"],
)
),
],
)
),
)
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
@@ -156,8 +159,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
selected_path = remove_none([gfile.get_path()])
returns(
SuccessDataClass(
op_key=op_key, data=selected_path, status="success"
)
op_key=op_key,
data=selected_path,
status="success",
),
)
else:
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
@@ -172,9 +177,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
message=e.__class__.__name__,
description=str(e),
location=["get_system_file"],
)
),
],
)
),
)
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
@@ -184,8 +189,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
selected_path = remove_none([gfile.get_path()])
returns(
SuccessDataClass(
op_key=op_key, data=selected_path, status="success"
)
op_key=op_key,
data=selected_path,
status="success",
),
)
else:
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
@@ -200,9 +207,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
message=e.__class__.__name__,
description=str(e),
location=["get_system_file"],
)
),
],
)
),
)
dialog = Gtk.FileDialog()

View File

@@ -39,7 +39,7 @@ class ArgumentParsingMiddleware(Middleware):
except Exception as e:
log.exception(
f"Error while parsing arguments for {context.request.method_name}"
f"Error while parsing arguments for {context.request.method_name}",
)
context.bridge.send_api_error_response(
context.request.op_key or "unknown",

View File

@@ -23,7 +23,9 @@ class Middleware(ABC):
"""Process the request through this middleware."""
def register_context_manager(
self, context: MiddlewareContext, cm: AbstractContextManager[Any]
self,
context: MiddlewareContext,
cm: AbstractContextManager[Any],
) -> Any:
"""Register a context manager with the exit stack."""
return context.exit_stack.enter_context(cm)

View File

@@ -25,23 +25,26 @@ class LoggingMiddleware(Middleware):
try:
# Handle log group configuration
log_group: list[str] | None = context.request.header.get("logging", {}).get(
"group_path", None
"group_path",
None,
)
if log_group is not None:
if not isinstance(log_group, list):
msg = f"Expected log_group to be a list, got {type(log_group)}"
raise TypeError(msg) # noqa: TRY301
log.warning(
f"Using log group {log_group} for {context.request.method_name} with op_key {context.request.op_key}"
f"Using log group {log_group} for {context.request.method_name} with op_key {context.request.op_key}",
)
# Create log file
log_file = self.log_manager.create_log_file(
method, op_key=context.request.op_key or "unknown", group_path=log_group
method,
op_key=context.request.op_key or "unknown",
group_path=log_group,
).get_file_path()
except Exception as e:
log.exception(
f"Error while handling request header of {context.request.method_name}"
f"Error while handling request header of {context.request.method_name}",
)
context.bridge.send_api_error_response(
context.request.op_key or "unknown",
@@ -76,7 +79,8 @@ class LoggingMiddleware(Middleware):
line_buffering=True,
)
self.handler = setup_logging(
log.getEffectiveLevel(), log_file=handler_stream
log.getEffectiveLevel(),
log_file=handler_stream,
)
return self

View File

@@ -32,7 +32,7 @@ class MethodExecutionMiddleware(Middleware):
except Exception as e:
log.exception(
f"Error while handling result of {context.request.method_name}"
f"Error while handling result of {context.request.method_name}",
)
context.bridge.send_api_error_response(
context.request.op_key or "unknown",

View File

@@ -48,7 +48,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
# Add a log group ["clans", <dynamic_name>, "machines", <dynamic_name>]
log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
clan_log_group = LogGroupConfig("clans", "Clans").add_child(
LogGroupConfig("machines", "Machines")
LogGroupConfig("machines", "Machines"),
)
log_manager = log_manager.add_root_group_config(clan_log_group)
# Init LogManager global in log_manager_api module
@@ -89,7 +89,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
# HTTP-only mode - keep the server running
log.info("HTTP API server running...")
log.info(
f"Swagger: http://{app_opts.http_host}:{app_opts.http_port}/api/swagger"
f"Swagger: http://{app_opts.http_host}:{app_opts.http_port}/api/swagger",
)
log.info("Press Ctrl+C to stop the server")

View File

@@ -63,7 +63,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def _send_json_response_with_status(
self, data: dict[str, Any], status_code: int = 200
self,
data: dict[str, Any],
status_code: int = 200,
) -> None:
"""Send a JSON response with the given status code."""
try:
@@ -82,11 +84,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
response_dict = dataclass_to_dict(response)
self._send_json_response_with_status(response_dict, 200)
log.debug(
f"HTTP response for {response._op_key}: {json.dumps(response_dict, indent=2)}" # noqa: SLF001
f"HTTP response for {response._op_key}: {json.dumps(response_dict, indent=2)}", # noqa: SLF001
)
def _create_success_response(
self, op_key: str, data: dict[str, Any]
self,
op_key: str,
data: dict[str, Any],
) -> BackendResponse:
"""Create a successful API response."""
return BackendResponse(
@@ -98,14 +102,16 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
def _send_info_response(self) -> None:
"""Send server information response."""
response = self._create_success_response(
"info", {"message": "Clan API Server", "version": "1.0.0"}
"info",
{"message": "Clan API Server", "version": "1.0.0"},
)
self.send_api_response(response)
def _send_methods_response(self) -> None:
"""Send available API methods response."""
response = self._create_success_response(
"methods", {"methods": list(self.api.functions.keys())}
"methods",
{"methods": list(self.api.functions.keys())},
)
self.send_api_response(response)
@@ -179,7 +185,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
json_data = json.loads(file_data.decode("utf-8"))
server_address = getattr(self.server, "server_address", ("localhost", 80))
json_data["servers"] = [
{"url": f"http://{server_address[0]}:{server_address[1]}/api/v1/"}
{"url": f"http://{server_address[0]}:{server_address[1]}/api/v1/"},
]
file_data = json.dumps(json_data, indent=2).encode("utf-8")
@@ -213,7 +219,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
# Validate API path
if not path.startswith("/api/v1/"):
self.send_api_error_response(
"post", f"Path not found: {path}", ["http_bridge", "POST"]
"post",
f"Path not found: {path}",
["http_bridge", "POST"],
)
return
@@ -221,7 +229,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
method_name = path[len("/api/v1/") :]
if not method_name:
self.send_api_error_response(
"post", "Method name required", ["http_bridge", "POST"]
"post",
"Method name required",
["http_bridge", "POST"],
)
return
@@ -289,19 +299,26 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
# Create API request
api_request = BackendRequest(
method_name=method_name, args=body, header=header, op_key=op_key
method_name=method_name,
args=body,
header=header,
op_key=op_key,
)
except Exception as e:
self.send_api_error_response(
gen_op_key, str(e), ["http_bridge", method_name]
gen_op_key,
str(e),
["http_bridge", method_name],
)
return
self._process_api_request_in_thread(api_request, method_name)
def _parse_request_data(
self, request_data: dict[str, Any], gen_op_key: str
self,
request_data: dict[str, Any],
gen_op_key: str,
) -> tuple[dict[str, Any], dict[str, Any], str]:
"""Parse and validate request data components."""
header = request_data.get("header", {})
@@ -344,7 +361,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
pass
def _process_api_request_in_thread(
self, api_request: BackendRequest, method_name: str
self,
api_request: BackendRequest,
method_name: str,
) -> None:
"""Process the API request in a separate thread."""
stop_event = threading.Event()
@@ -358,7 +377,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
log.debug(
f"Processing {request.method_name} with args {request.args} "
f"and header {request.header}"
f"and header {request.header}",
)
self.process_request(request)

View File

@@ -64,7 +64,8 @@ def mock_log_manager() -> Mock:
@pytest.fixture
def http_bridge(
mock_api: MethodRegistry, mock_log_manager: Mock
mock_api: MethodRegistry,
mock_log_manager: Mock,
) -> tuple[MethodRegistry, tuple]:
"""Create HTTP bridge dependencies for testing."""
middleware_chain = (
@@ -256,7 +257,9 @@ class TestIntegration:
"""Integration tests for HTTP API components."""
def test_full_request_flow(
self, mock_api: MethodRegistry, mock_log_manager: Mock
self,
mock_api: MethodRegistry,
mock_log_manager: Mock,
) -> None:
"""Test complete request flow from server to bridge to middleware."""
server: HttpApiServer = HttpApiServer(
@@ -301,7 +304,9 @@ class TestIntegration:
server.stop()
def test_blocking_task(
self, mock_api: MethodRegistry, mock_log_manager: Mock
self,
mock_api: MethodRegistry,
mock_log_manager: Mock,
) -> None:
shared_threads: dict[str, tasks.WebThread] = {}
tasks.BAKEND_THREADS = shared_threads

View File

@@ -21,7 +21,7 @@ def _get_lib_names() -> list[str]:
machine = platform.machine().lower()
if system == "windows":
if machine == "amd64" or machine == "x86_64":
if machine in {"amd64", "x86_64"}:
return ["webview.dll", "WebView2Loader.dll"]
if machine == "arm64":
msg = "arm64 is not supported on Windows"
@@ -36,7 +36,6 @@ def _get_lib_names() -> list[str]:
def _be_sure_libraries() -> list[Path] | None:
"""Ensure libraries exist and return paths."""
lib_dir = os.environ.get("WEBVIEW_LIB_DIR")
if not lib_dir:
msg = "WEBVIEW_LIB_DIR environment variable is not set"

View File

@@ -144,7 +144,9 @@ class Webview:
)
else:
bridge = WebviewBridge(
webview=self, middleware_chain=tuple(self._middleware), threads={}
webview=self,
middleware_chain=tuple(self._middleware),
threads={},
)
self._bridge = bridge
@@ -154,7 +156,10 @@ class Webview:
def set_size(self, value: Size) -> None:
"""Set the webview size (legacy compatibility)."""
_webview_lib.webview_set_size(
self.handle, value.width, value.height, value.hint
self.handle,
value.width,
value.height,
value.hint,
)
def set_title(self, value: str) -> None:
@@ -194,7 +199,10 @@ class Webview:
self._callbacks[name] = c_callback
_webview_lib.webview_bind(
self.handle, _encode_c_string(name), c_callback, None
self.handle,
_encode_c_string(name),
c_callback,
None,
)
def bind(self, name: str, callback: Callable[..., Any]) -> None:
@@ -219,7 +227,10 @@ class Webview:
def return_(self, seq: str, status: int, result: str) -> None:
_webview_lib.webview_return(
self.handle, _encode_c_string(seq), status, _encode_c_string(result)
self.handle,
_encode_c_string(seq),
status,
_encode_c_string(result),
)
def eval(self, source: str) -> None:

View File

@@ -26,7 +26,9 @@ class WebviewBridge(ApiBridge):
def send_api_response(self, response: BackendResponse) -> None:
"""Send response back to the webview client."""
serialized = json.dumps(
dataclass_to_dict(response), indent=4, ensure_ascii=False
dataclass_to_dict(response),
indent=4,
ensure_ascii=False,
)
log.debug(f"Sending response: {serialized}")
@@ -40,7 +42,6 @@ class WebviewBridge(ApiBridge):
arg: int,
) -> None:
"""Handle a call from webview's JavaScript bridge."""
try:
op_key = op_key_bytes.decode()
raw_args = json.loads(request_data.decode())
@@ -68,7 +69,10 @@ class WebviewBridge(ApiBridge):
# Create API request
api_request = BackendRequest(
method_name=method_name, args=args, header=header, op_key=op_key
method_name=method_name,
args=args,
header=header,
op_key=op_key,
)
except Exception as e:
@@ -77,7 +81,9 @@ class WebviewBridge(ApiBridge):
)
log.exception(msg)
self.send_api_error_response(
op_key, str(e), ["webview_bridge", method_name]
op_key,
str(e),
["webview_bridge", method_name],
)
return

View File

@@ -54,8 +54,7 @@ class Command:
@pytest.fixture
def command() -> Iterator[Command]:
"""
Starts a background command. The process is automatically terminated in the end.
"""Starts a background command. The process is automatically terminated in the end.
>>> p = command.run(["some", "daemon"])
>>> print(p.pid)
"""

View File

@@ -2,12 +2,15 @@ from __future__ import annotations
import logging
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from clan_lib.custom_logger import setup_logging
from clan_lib.nix import nix_shell
if TYPE_CHECKING:
from pathlib import Path
pytest_plugins = [
"temporary_dir",
"root",

View File

@@ -13,23 +13,17 @@ else:
@pytest.fixture(scope="session")
def project_root() -> Path:
"""
Root directory the clan-cli
"""
"""Root directory the clan-cli"""
return PROJECT_ROOT
@pytest.fixture(scope="session")
def test_root() -> Path:
"""
Root directory of the tests
"""
"""Root directory of the tests"""
return TEST_ROOT
@pytest.fixture(scope="session")
def clan_core() -> Path:
"""
Directory of the clan-core flake
"""
"""Directory of the clan-core flake"""
return CLAN_CORE

View File

@@ -24,7 +24,11 @@ def app() -> Generator[GtkProc]:
cmd = [sys.executable, "-m", "clan_app"]
print(f"Running: {cmd}")
rapp = Popen(
cmd, text=True, stdout=sys.stdout, stderr=sys.stderr, start_new_session=True
cmd,
text=True,
stdout=sys.stdout,
stderr=sys.stderr,
start_new_session=True,
)
yield GtkProc(rapp)
# Cleanup: Terminate your application

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M9.223 38.777h8.444V43H5V30.333h4.223zM43 43h-4.223v-8.444h-8.444V43h-4.222V21.889H43zM30.333 30.333h8.444v-4.222h-8.444zM17.667 9.223H9.223v4.221h8.444v4.223H9.223v4.222h8.444v4.222H5V5h12.667zm4.222 12.666h-4.222v-4.222h4.222zM43 17.667h-4.223V9.223h-8.444V5H43zm-21.111-4.223h-4.222V9.223h4.222z"/></svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M27 38H6V17H10V13H13.5V9H37.5V13H41V24H27V27H34V31H30.5V34.5H27V38ZM16.5 20.5H20V17H16.5V20.5Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M27 38H6V17h4v-4h3.5V9h24v4H41v11H27v3h7v4h-3.5v3.5H27zM16.5 20.5H20V17h-3.5z"/></svg>

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M46 46H2V2h44zM16.667 33.777h4.889V28.89h-4.889zm-4.89-4.888h4.89V24h-4.89zm9.779 0h4.888V24h-4.888zM26.444 24h4.889v-4.889h-4.889zm4.889-9.777v4.888h4.89v-4.888z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor">
<path d="M36.888 11H41.3v4.413h-4.412zm-4.413 8.825v-4.412h4.413v4.412zm-4.413 4.413v-4.413h4.413v4.413zM23.65 28.65h4.413v-4.412H23.65zm-4.412 4.413h4.412V28.65h-4.412zm-4.413 0v4.412h4.413v-4.413zm-4.412-4.413h4.412v4.413h-4.412zm0 0H6v-4.412h4.413z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M36.888 11H41.3v4.413h-4.412zm-4.413 8.825v-4.412h4.413v4.412zm-4.413 4.413v-4.413h4.413v4.413zM23.65 28.65h4.413v-4.412H23.65zm-4.412 4.413h4.412V28.65h-4.412zm-4.413 0v4.412h4.413v-4.413zm-4.412-4.413h4.412v4.413h-4.412zm0 0H6v-4.412h4.413z"/></svg>

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -1,10 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="89" fill="currentColor">
<g clip-path="url(#a)">
<path d="M57.709 20.105H68.62c1.157 0 2.099-.94 2.099-2.095V9.632a2.1 2.1 0 0 0-2.099-2.095h-3.439c-1.111 0-2.014-.9-2.014-2.01V2.095A2.1 2.1 0 0 0 61.07 0H30.02a2.1 2.1 0 0 0-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01H22.47c-1.157 0-2.098.94-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01h-3.439c-1.157 0-2.099.94-2.099 2.094 0 0-.503-1.272-.503 22.493 0 21.247.503 19.38.503 19.38 0 1.156.942 2.096 2.1 2.096h3.438c1.111 0 2.014.9 2.014 2.01v3.517c0 1.109.902 2.01 2.013 2.01h3.524c1.111 0 2.014.9 2.014 2.01v3.432a2.1 2.1 0 0 0 2.098 2.094h30.211c1.157 0 2.099-.94 2.099-2.094v-3.433c0-1.11.902-2.01 2.013-2.01h5.557c1.158 0 2.099-.94 2.099-2.094v-9.984a2.1 2.1 0 0 0-2.099-2.095h-13.03c-1.157 0-2.098.94-2.098 2.095v5.044c0 1.11-.902 2.01-2.014 2.01H37.488c-1.111 0-2.013-.9-2.013-2.01v-5.11a2.1 2.1 0 0 0-2.099-2.094h-5.119c-1.111 0-1.739.163-2.014-2.01-.085-.698-.13-1.553-.196-2.695-.163-2.878-.307-1.723-.307-10.369 0-12.085.314-15.563.503-17.24.19-1.677.903-2.01 2.014-2.01h5.12c1.156 0 2.098-.94 2.098-2.094v-3.433c0-1.109.902-2.01 2.013-2.01h16.116c1.111 0 2.014.901 2.014 2.01v3.433c0 1.155.94 2.094 2.098 2.094zM18.626 73.757h-2.478a.87.87 0 0 1-.87-.868v-2.473c0-.96-.777-1.743-1.745-1.743H6.838c-.96 0-1.745.777-1.745 1.743v2.473a.87.87 0 0 1-.87.868H1.746c-.961 0-1.746.776-1.746 1.742v6.682c0 .96.778 1.742 1.746 1.742h2.477c.484 0 .87.392.87.868v2.473c0 .96.778 1.743 1.745 1.743h6.695c.961 0 1.746-.777 1.746-1.743v-2.473c0-.483.392-.868.87-.868h2.477c.961 0 1.746-.776 1.746-1.742v-6.682c0-.96-.778-1.742-1.746-1.742"/>
</g>
<defs>
<clipPath id="a">
<path d="M0 0h72v89H0z"/>
</clipPath>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="89" fill="currentColor"><g clip-path="url(#a)"><path d="M57.709 20.105H68.62c1.157 0 2.099-.94 2.099-2.095V9.632a2.1 2.1 0 0 0-2.099-2.095h-3.439c-1.111 0-2.014-.9-2.014-2.01V2.095A2.1 2.1 0 0 0 61.07 0H30.02a2.1 2.1 0 0 0-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01H22.47c-1.157 0-2.098.94-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01h-3.439c-1.157 0-2.099.94-2.099 2.094 0 0-.503-1.272-.503 22.493 0 21.247.503 19.38.503 19.38 0 1.156.942 2.096 2.1 2.096h3.438c1.111 0 2.014.9 2.014 2.01v3.517c0 1.109.902 2.01 2.013 2.01h3.524c1.111 0 2.014.9 2.014 2.01v3.432a2.1 2.1 0 0 0 2.098 2.094h30.211c1.157 0 2.099-.94 2.099-2.094v-3.433c0-1.11.902-2.01 2.013-2.01h5.557c1.158 0 2.099-.94 2.099-2.094v-9.984a2.1 2.1 0 0 0-2.099-2.095h-13.03c-1.157 0-2.098.94-2.098 2.095v5.044c0 1.11-.902 2.01-2.014 2.01H37.488c-1.111 0-2.013-.9-2.013-2.01v-5.11a2.1 2.1 0 0 0-2.099-2.094h-5.119c-1.111 0-1.739.163-2.014-2.01-.085-.698-.13-1.553-.196-2.695-.163-2.878-.307-1.723-.307-10.369 0-12.085.314-15.563.503-17.24.19-1.677.903-2.01 2.014-2.01h5.12c1.156 0 2.098-.94 2.098-2.094v-3.433c0-1.109.902-2.01 2.013-2.01h16.116c1.111 0 2.014.901 2.014 2.01v3.433c0 1.155.94 2.094 2.098 2.094zM18.626 73.757h-2.478a.87.87 0 0 1-.87-.868v-2.473c0-.96-.777-1.743-1.745-1.743H6.838c-.96 0-1.745.777-1.745 1.743v2.473a.87.87 0 0 1-.87.868H1.746c-.961 0-1.746.776-1.746 1.742v6.682c0 .96.778 1.742 1.746 1.742h2.477c.484 0 .87.392.87.868v2.473c0 .96.778 1.743 1.745 1.743h6.695c.961 0 1.746-.777 1.746-1.743v-2.473c0-.483.392-.868.87-.868h2.477c.961 0 1.746-.776 1.746-1.742v-6.682c0-.96-.778-1.742-1.746-1.742"/></g><defs><clipPath id="a"><path d="M0 0h72v89H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="223" height="89" fill="currentColor"><g clip-path="url(#a)"><path d="M55.503 18.696h10.104a1.946 1.946 0 0 0 1.943-1.948v-7.79c0-1.075-.87-1.947-1.943-1.947h-3.186a1.863 1.863 0 0 1-1.866-1.87V1.947C60.555.872 59.685 0 58.612 0h-27.98a1.946 1.946 0 0 0-1.944 1.947v3.194c0 1.036-.832 1.87-1.865 1.87h-3.187a1.946 1.946 0 0 0-1.943 1.947v3.194c0 1.036-.832 1.87-1.866 1.87h-3.186a1.946 1.946 0 0 0-1.943 1.947s-.467 1.153-.467 23.253c0 19.763.467 21.913.467 21.913 0 1.075.87 1.948 1.943 1.948h3.186c1.034 0 1.866.833 1.866 1.87v3.271c0 1.036.831 1.87 1.865 1.87h3.265c1.033 0 1.865.833 1.865 1.87v3.193c0 1.075.87 1.948 1.943 1.948h27.981a1.946 1.946 0 0 0 1.943-1.948v-3.194c0-1.036.832-1.87 1.866-1.87h5.145a1.946 1.946 0 0 0 1.943-1.947v-9.285c0-1.075-.87-1.948-1.943-1.948H55.503a1.946 1.946 0 0 0-1.943 1.948v4.69c0 1.035-.832 1.869-1.866 1.869H37.55a1.863 1.863 0 0 1-1.866-1.87v-4.752c0-1.075-.87-1.947-1.943-1.947H29c-1.034 0-1.609.148-1.865-1.87-.078-.646-.125-1.44-.18-2.508-.147-2.68-.287-5.5-.287-13.539 0-11.24.288-16.81.466-18.369.18-1.558.832-1.87 1.866-1.87h4.741a1.946 1.946 0 0 0 1.943-1.947v-3.193c0-1.037.832-1.87 1.866-1.87h14.145c1.034 0 1.866.833 1.866 1.87v3.193c0 1.075.87 1.948 1.943 1.948M20.247 74.822h-2.293a.814.814 0 0 1-.808-.81v-2.298c0-.896-.723-1.62-1.617-1.62H9.327c-.894 0-1.617.724-1.617 1.62v2.298c0 .444-.365.81-.808.81H4.609c-.894 0-1.617.725-1.617 1.62v6.217c0 .896.723 1.62 1.617 1.62h2.293c.443 0 .808.366.808.81v2.299c0 .895.723 1.62 1.617 1.62h6.202c.894 0 1.617-.725 1.617-1.62v-2.299c0-.444.365-.81.808-.81h2.293c.894 0 1.617-.724 1.617-1.62v-6.216c0-.896-.723-1.62-1.617-1.62M221.135 35.04h-1.71a1.863 1.863 0 0 1-1.866-1.87v-3.272c0-1.036-.831-1.87-1.865-1.87h-3.265a1.863 1.863 0 0 1-1.865-1.87v-3.271c0-1.036-.832-1.87-1.865-1.87h-20.971a1.863 1.863 0 0 0-1.865 1.87v3.965c0 .514-.42.935-.933.935h-3.559c-.513 0-.84-.32-.933-.935l-.622-3.918c-.148-1.099-.676-1.777-1.788-1.777l-3.653-.14h-2.052a3.736 3.736 0 0 0-3.73 3.74V61.68a3.736 3.736 0 0 1-3.731 3.739h-8.394a1.863 1.863 0 0 1-1.866-1.87V36.714c0-11.825-7.461-18.813-22.556-18.813-13.718 0-20.325 5.04-21.203 14.443-.109 1.153.552 1.815 1.702 1.815l7.757.569c1.143.1 1.594-.554 1.811-1.652.77-3.74 4.174-5.827 9.933-5.827 7.081 0 10.042 3.358 10.042 9.076v3.014c0 1.036-.831 1.87-1.865 1.87l-.342-.024h-9.715c-15.421 0-22.984 5.983-22.984 17.956 0 3.802.778 7.058 2.254 9.738h-.59c-1.765-1.27-2.457-2.236-3.055-2.93-.256-.295-.653-.537-1.345-.537h-1.717l-5.993.008h-3.264a3.736 3.736 0 0 1-3.731-3.74V1.769C89.74.654 89.072 0 87.969 0H79.55c-1.034 0-1.865.732-1.865 1.768l-.024 54.304v13.554c0 4.13 3.343 7.479 7.462 7.479h50.84c8.448-.429 8.604-3.42 9.436-4.542.645 3.56 1.865 4.347 4.71 4.518 8.137.117 18.343.032 18.49.024h4.975c4.119 0 6.684-3.35 6.684-7.479l.777-27.264c0-1.036.832-1.87 1.866-1.87h2.021a1.56 1.56 0 0 0 1.554-1.558v-3.583c0-1.036.832-1.87 1.866-1.87h11.868a3.37 3.37 0 0 1 3.366 3.373v3.249c0 1.075.87 1.947 1.943 1.947h4.119c.513 0 .933.42.933.935v32.25c0 1.036.831 1.87 1.865 1.87h6.84a3.736 3.736 0 0 0 3.731-3.74V36.91c0-1.036-.832-1.87-1.866-1.87zM142.64 54.225c0 8.927-6.132 14.715-15.335 14.715-6.606 0-9.793-2.953-9.793-8.748 0-6.442 3.832-9.636 11.62-9.636h13.508v3.669"/></g><defs><clipPath id="a"><path d="M0 0h223v89H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M35.667 7.667h4.666v4.666H45v23.334h-4.667v4.666h-4.666V45H12.333v-4.667H7.667v-4.666H3V12.333h4.667V7.667h4.666V3h23.334zM15 29.4V33h3.6v-3.6zm14.4 0V33H33v-3.6zm-10.8-3.6v3.6h3.6v-3.6zm7.2 0v3.6h3.6v-3.6zm-3.6-3.6v3.6h3.6v-3.6zm-3.6-3.6v3.6h3.6v-3.6zm7.2 0v3.6h3.6v-3.6zM15 15v3.6h3.6V15zm14.4 0v3.6H33V15z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M15.6 9h4.2v4.286h-4.2zM11.4 13.286h4.2v4.285h-4.2zM7.2 17.571h4.2v4.286H7.2zM3 21.857h4.2v4.286H3zM7.2 26.143h4.2v4.286H7.2zM11.4 30.429h4.2v4.285h-4.2zM15.6 34.714h4.2V39h-4.2zM32.4 9h-4.2v4.286h4.2zM36.6 13.286h-4.2v4.285h4.2zM40.8 17.571h-4.2v4.286h4.2zM45 21.857h-4.2v4.286H45zM40.8 26.143h-4.2v4.286h4.2z"/><path d="M36.6 30.429h-4.2v4.285h4.2zM32.4 34.714h-4.2V39h4.2z"/></svg>

After

Width:  |  Height:  |  Size: 476 B

View File

@@ -1,25 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="42" viewBox="0 0 35 42" fill="currentColor">
<rect y="6" width="6" height="6"/>
<rect x="6" y="6" width="6" height="6"/>
<rect x="12" y="6" width="6" height="6"/>
<rect x="6" y="12" width="6" height="6"/>
<rect x="18" y="18" width="6" height="6"/>
<rect x="18" y="12" width="6" height="6"/>
<rect x="12" y="24" width="6" height="6"/>
<rect x="12" y="18" width="6" height="6"/>
<rect x="12" y="12" width="6" height="6"/>
<rect width="6" height="6"/>
<rect x="6" width="6" height="6"/>
<rect x="24" y="18" width="6" height="6"/>
<rect y="12" width="6" height="6"/>
<rect x="6" y="18" width="6" height="6"/>
<rect y="18" width="6" height="6"/>
<rect y="24" width="6" height="6"/>
<rect y="30" width="6" height="6"/>
<rect y="36" width="6" height="6"/>
<rect x="6" y="24" width="6" height="6"/>
<rect x="18" y="24" width="6" height="6"/>
<rect x="24" y="24" width="6" height="6"/>
<rect x="29" y="24" width="6" height="6"/>
<rect x="6" y="30" width="6" height="6"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M8 9h6v6H8zM14 9h6v6h-6zM20 9h6v6h-6zM14 15h6v6h-6zM26 21h6v6h-6zM26 15h6v6h-6zM20 27h6v6h-6zM20 21h6v6h-6zM20 15h6v6h-6zM8 3h6v6H8zM14 3h6v6h-6zM32 21h6v6h-6zM8 15h6v6H8zM14 21h6v6h-6zM8 21h6v6H8zM8 27h6v6H8zM8 33h6v6H8zM8 39h6v6H8zM14 27h6v6h-6zM26 27h6v6h-6zM32 27h6v6h-6z"/><path d="M37 27h6v6h-6zM14 33h6v6h-6z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M8.7 9h11.1v5.625H42v3.75H19.8V24H8.7v-5.625H5v-3.75h3.7zm3.7 3.75v7.5h3.7v-7.5zM27.2 24h11.1v5.625H42v3.75h-3.7V39H27.2v-5.625H5v-3.75h22.2zm3.7 3.75v7.5h3.7v-7.5z" clip-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M11.7 9h8.1v5.625H42v3.75H19.8V24h-8.1v-5.625H5v-3.75h6.7zm15.5 15h8.1v5.625H42v3.75h-6.7V39h-8.1v-5.625H5v-3.75h22.2z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M34.25 9.3H45v34.4H2.001v-4.3H2V13.6h.001V9.3H12.75V5h21.5zM19.201 30.8v4.3h8.6v-4.3zm-4.3-4.3v4.3h4.3v-4.3zm12.9 0v4.3h4.3v-4.3zm-12.9-8.6v4.3h4.3v-4.3zm12.9 0v4.3h4.3v-4.3z"/></svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M20.2002 12.7998H23V15.5996H25.8008V14.2002H28.6006V10H34.2002V12.7998H37V15.5996H39.8008V24H37V26.7998H34.2002V29.5996H31.4004V32.4004H28.6006V35.2002H25.8008V38H23V35.2002H20.2002V32.4004H17.4004V29.5996H14.6006V26.7998H11.8008V24H9V15.5996H11.8008V12.7998H14.6006V10H20.2002V12.7998Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M20.2 12.8H23v2.8h2.8v-1.4h2.8V10h5.6v2.8H37v2.8h2.8V24H37v2.8h-2.8v2.8h-2.8v2.8h-2.8v2.8h-2.8V38H23v-2.8h-2.8v-2.8h-2.8v-2.8h-2.8v-2.8h-2.8V24H9v-8.4h2.8v-2.8h2.8V10h5.6z"/></svg>

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M38.666 5V39.667H38.667V5H43V44H4V5H38.666ZM12.666 35.334H16.999V31H12.666V35.334ZM29.999 35.334H34.333V31H29.999V35.334ZM21.333 26.667H25.666V22.334H21.333V26.667ZM12.666 18H16.999V13.667H12.666V18ZM29.999 18H34.333V13.667H29.999V18Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M38.666 5v34.667h.001V5H43v39H4V5zm-26 30.334h4.333V31h-4.333zm17.333 0h4.334V31h-4.334zm-8.666-8.667h4.333v-4.333h-4.333zM12.666 18h4.333v-4.333h-4.333zm17.333 0h4.334v-4.333h-4.334z"/></svg>

Before

Width:  |  Height:  |  Size: 361 B

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M7.667 45H3v-4.667h4.667zM17 45h-4.667v-4.667H17zm9.333 0h-4.666v-4.667h4.666zm9.334 0H31v-4.667h4.667zM45 45h-4.667v-4.667H45zM7.667 35.667H3V31h4.667zm37.333 0h-4.667V31H45zM7.667 26.333H3v-4.666h4.667zm37.333 0h-4.667V7.667H21.667V3H45zM7.667 17H3v-4.667h4.667zm0-9.333H3V3h4.667zm9.333 0h-4.667V3H17z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M38 42H10V38H6V10H10V6H38V10H42V38H38V42ZM18 32H30V28H18V32ZM14 28H18V24H14V28ZM30 28H34V24H30V28ZM16 20H20V16H16V20ZM28 20H32V16H28V20Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M38 42H10v-4H6V10h4V6h28v4h4v28h-4zM18 32h12v-4H18zm-4-4h4v-4h-4zm16 0h4v-4h-4zm-14-8h4v-4h-4zm12 0h4v-4h-4z"/></svg>

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M42 42H14V38H38V14H42V42ZM34 6V34H6V6H34ZM18 18H14V22H18V26H22V22H26V18H22V14H18V18Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M42 42H14v-4h24V14h4zM34 6v28H6V6zM18 18h-4v4h4v4h4v-4h4v-4h-4v-4h-4z"/></svg>

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 170 B

View File

@@ -1,13 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="27" viewBox="0 0 38 27" fill="currentColor">
<rect x="4.46155" y="4.15381" width="4.15385" height="4.15385"/>
<rect x="29.3846" y="4.15381" width="4.15385" height="4.15385"/>
<rect x="8.61539" width="4.15385" height="4.15385"/>
<rect x="33.5385" width="4.15385" height="4.15385"/>
<rect x="0.307678" width="4.15385" height="4.15385"/>
<rect x="25.2308" width="4.15385" height="4.15385"/>
<rect x="0.307678" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="25.2308" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="8.61539" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="33.5385" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="4" y="23" width="30" height="4"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.247 38H8.753v-4.148h30.494zM9.223 22.923H5v-4.308h4.223zm8.444 0h-4.223v-4.308h4.223zm16.889 0h-4.223v-4.308h4.223zm8.444 0h-4.223v-4.308H43zm-29.556-4.308H9.223v-4.307h4.221zm25.333 0h-4.221v-4.307h4.221zM9.223 14.308H5V10h4.223zm8.444 0h-4.223V10h4.223zm16.889 0h-4.223V10h4.223zm8.444 0h-4.223V10H43z"/></svg>

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M39.2002 39.2002H43V43H39.2002V39.2021H35.3994V35.4014H39.2002V39.2002ZM27.7998 8.80078H31.5996V31.6016H27.7998V35.4004H12.6006V12.6016H20.2002V8.80078H12.6006V5H27.7998V8.80078ZM35.4004 35.4004H31.6006V31.6006H35.4004V35.4004ZM12.5996 12.5996H8.7998V20.2002H12.5996V31.6016H8.7998V27.8008H5V12.5996H8.7998V8.80078H12.5996V12.5996ZM35.4004 27.8008H31.6006V12.5996H35.4004V27.8008Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.2 39.2H43V43h-3.8v-3.798h-3.8v-3.8h3.8zM27.8 8.8h3.8v22.802h-3.8V35.4H12.6V12.602h7.6V8.8h-7.6V5h15.2zm7.6 26.6h-3.8v-3.8h3.8zM12.6 12.6H8.8v7.6h3.8v11.402H8.8V27.8H5V12.6h3.8V8.8h3.8zm22.8 15.2h-3.8V12.6h3.8z"/></svg>

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M43 43H5V20.2h38zm-3.8-26.6H8.8v-3.8h30.4zm-3.8-7.6H12.6V5h22.8z"/></svg>

After

Width:  |  Height:  |  Size: 165 B

View File

@@ -1,8 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="23" viewBox="0 0 36 23" fill="currentColor">
<rect x="27" width="22.5" height="4.5" transform="rotate(90 27 0)"/>
<rect x="31.5" y="4.5" width="13.5" height="4.5" transform="rotate(90 31.5 4.5)"/>
<rect x="36" y="9" width="4.5" height="4.5" transform="rotate(90 36 9)"/>
<rect width="22.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 9 0)"/>
<rect width="13.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 4.5 4.5)"/>
<rect width="4.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 0 9)"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M33 13v22.5h-4.5V13zM37.5 17.5V31H33V17.5zM42 22v4.5h-4.5V22zM15 13v22.5h4.5V13zM10.5 17.5V31H15V17.5zM6 22v4.5h4.5V22z"/></svg>

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 220 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M39.4004 31.2002H35.7998V34.7998H32.2002V38.3994H28.5996V42H25V38.3994H21.4004V34.7998H17.7998V31.2002H14.2002V27.6006H39.4004V31.2002ZM28.5996 13.2002H32.2002V16.7998H35.7998V20.3994H39.4004V24H43V27.5996H10.5996V24H7V9.60059H28.5996V13.2002ZM14.2002 13.2002V16.7998H17.7998V13.2002H14.2002ZM25 9.59961H7V6H25V9.59961Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.4 31.2h-3.6v3.6h-3.6v3.6h-3.6V42H25v-3.6h-3.6v-3.6h-3.6v-3.6h-3.6v-3.6h25.2zm-10.8-18h3.6v3.6h3.6v3.6h3.6V24H43v3.6H10.6V24H7V9.6h21.6zm-14.4 0v3.6h3.6v-3.6zM25 9.6H7V6h18z"/></svg>

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8 0H28.4V9.6H18.8V0ZM11.6 12H35.6V16.8H30.8V33.6V48H26V33.6H21.2V48H16.4V33.6V16.8H11.6V12ZM6.8 7.2V12H11.6V7.2H6.8ZM6.8 7.2L2 7.2V2.4H6.8V7.2ZM40.4 7.2V12H35.6V7.2H40.4ZM40.4 7.2L40.4 2.4H45.2V7.2L40.4 7.2Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M18.8 0h9.6v9.6h-9.6zm-7.2 12h24v4.8h-4.8V48H26V33.6h-4.8V48h-4.8V16.8h-4.8zM6.8 7.2V12h4.8V7.2zm0 0H2V2.4h4.8zm33.6 0V12h-4.8V7.2zm0 0V2.4h4.8v4.8z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M42 6H5v37h37zm-20.555 8.222h4.111v12.334h-4.111zm0 16.445h4.111v4.11h-4.111z" clip-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M26 6h4v4h4v4h4v4h4v4h4v4h-4v4h-4v4h-4v4h-4v4h-4v4h-4v-4h-4v-4h-4v-4h-4v-4H6v-4H2v-4h4v-4h4v-4h4v-4h4V6h4V2h4zm-4 24v4h4v-4zm0-16v12h4V14z"/></svg>

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -15,9 +15,11 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@tanstack/solid-query": "^5.85.5",
"@tanstack/solid-query-devtools": "^5.85.5",
"@tanstack/solid-query-persist-client": "^5.85.5",
"@tanstack/solid-virtual": "^3.13.12",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",
@@ -2487,12 +2489,12 @@
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.81.2.tgz",
"integrity": "sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A==",
"version": "5.83.1",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz",
"integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.18.1"
"@typescript-eslint/utils": "^8.37.0"
},
"funding": {
"type": "github",
@@ -2502,10 +2504,169 @@
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.40.0",
"@typescript-eslint/types": "^8.40.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.40.0",
"@typescript-eslint/tsconfig-utils": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.40.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
"integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2513,22 +2674,35 @@
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"version": "5.84.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz",
"integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.83.0.tgz",
"integrity": "sha512-RF8Tv9+6+Kmzj+EafbTzvzzPq+J5SzHtc1Tz3D2MZ/EvlZTH+GL5q4HNnWK3emg7CB6WzyGnTuERmmWJaZs8/w==",
"node_modules/@tanstack/query-persist-client-core": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.5.tgz",
"integrity": "sha512-2JQiyiTVaaUu8pwPqOp6tjNa64ZN+0T9eZ3lfksV4le1VuG99fTcAYmZFIydvzwWlSM7GEF/1kpl5bwW2Y1qfQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.85.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.85.5.tgz",
"integrity": "sha512-0o0Ibk9wqydm4JatbIjmvDu1+MofeZ1bU9BKwAbpt7HYjrLVCeddpW6zGmp41nN7t/mHJyR+ctW9oiNumCkEfg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.85.5"
},
"funding": {
"type": "github",
@@ -2539,22 +2713,65 @@
}
},
"node_modules/@tanstack/solid-query-devtools": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.83.0.tgz",
"integrity": "sha512-Z0wQlAWXz/U2bJ/paMRBTDhMoPnB9Te6GmA21sXnI+nDnAAPZRcPxFBiCgYJS3eFsvbkdRGJwoUSQrdIgy0shg==",
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.85.5.tgz",
"integrity": "sha512-9rC22wILlV9Lcsi4xKPmzRkNio1NOxNT36diIS+HjpOmhsEP/aI8XkNKQa/KPhhaSN2naYaTCJamh7eBAQ0Ymg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
"@tanstack/query-devtools": "5.84.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.83.0",
"@tanstack/solid-query": "^5.85.5",
"solid-js": "^1.6.0"
}
},
"node_modules/@tanstack/solid-query-persist-client": {
"version": "5.85.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-persist-client/-/solid-query-persist-client-5.85.5.tgz",
"integrity": "sha512-2aG7UnLZlfE3R4XKqYuIeXVKjJOghjsjq4EU2Ifp915FTBZcZo61sEw1zRqRlrDjEFYAs4kJUZwqViDSJYyX2g==",
"license": "MIT",
"dependencies": {
"@tanstack/query-persist-client-core": "5.85.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.85.5",
"solid-js": "^1.6.0"
}
},
"node_modules/@tanstack/solid-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.13.12.tgz",
"integrity": "sha512-0dS8GkBTmbuM9cUR6Jni0a45eJbd32CAEbZj8HrZMWIj3lu974NpGz5ywcomOGJ9GdeHuDaRzlwtonBbKV1ihQ==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"solid-js": "^1.3.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -2894,6 +3111,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.36.0",
@@ -2915,6 +3133,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
@@ -2932,6 +3151,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2972,6 +3192,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2985,6 +3206,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.36.0",
@@ -3013,6 +3235,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -3025,6 +3248,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
@@ -3048,6 +3272,7 @@
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
@@ -3065,6 +3290,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -72,9 +72,11 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@tanstack/solid-query": "^5.85.5",
"@tanstack/solid-query-devtools": "^5.85.5",
"@tanstack/solid-query-persist-client": "^5.85.5",
"@tanstack/solid-virtual": "^3.13.12",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",

View File

@@ -5,3 +5,14 @@
.horizontal_button {
@apply grow max-w-[18rem];
}
/* Vendored from tooltip */
.tooltipContent {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
max-width: min(calc(100vw - 16px), 380px);
&.inverted {
@apply bg-def-2;
}
}

View File

@@ -87,7 +87,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
{value() && (
<Tooltip placement="top">
<Tooltip.Portal>
<Tooltip.Content class="tooltip-content">
<Tooltip.Content class={styles.tooltipContent}>
<Typography
hierarchy="body"
size="xs"

View File

@@ -55,23 +55,14 @@ export const Label = (props: LabelProps) => {
<Tooltip
placement="top"
inverted={props.inverted}
trigger={
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
}
description={props.tooltip}
>
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{props.tooltip}
</Typography>
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
</Tooltip>
)}
</props.labelComponent>

View File

@@ -27,7 +27,7 @@ div.form-field.machine-tags {
@apply bg-def-acc-1 outline-def-acc-2;
}
&:focus-visible {
&:not(:read-only):focus-visible {
@apply bg-def-1 outline-def-acc-3;
box-shadow:
@@ -106,7 +106,7 @@ div.form-field.machine-tags {
@apply bg-inv-acc-2 outline-inv-acc-2;
}
&:focus-visible {
&:not(:read-only):focus-visible {
@apply bg-inv-acc-4;
box-shadow:
0 0 0 0.125rem theme(colors.bg.inv.1),

View File

@@ -17,7 +17,7 @@ div.form-field {
@apply bg-def-acc-1 outline-def-acc-2;
}
&:focus-visible {
&:not(:read-only):focus-visible {
@apply bg-def-1 outline-def-acc-3;
box-shadow:
@@ -119,7 +119,7 @@ div.form-field {
@apply bg-inv-acc-2 outline-inv-acc-2;
}
&:focus-visible {
&:not(:read-only):focus-visible {
@apply bg-inv-acc-4;
box-shadow:
0 0 0 0.125rem theme(colors.bg.inv.1),

View File

@@ -1,5 +1,8 @@
import cx from "classnames";
import { Component, JSX, splitProps } from "solid-js";
import Address from "@/icons/address.svg";
import AI from "@/icons/ai.svg";
import ArrowBottom from "@/icons/arrow-bottom.svg";
import ArrowLeft from "@/icons/arrow-left.svg";
import ArrowRight from "@/icons/arrow-right.svg";
@@ -10,9 +13,12 @@ import CaretLeft from "@/icons/caret-left.svg";
import CaretRight from "@/icons/caret-right.svg";
import CaretUp from "@/icons/caret-up.svg";
import Checkmark from "@/icons/checkmark.svg";
import CheckSolid from "@/icons/check-solid.svg";
import ClanIcon from "@/icons/clan-icon.svg";
import Cursor from "@/icons/cursor.svg";
import Close from "@/icons/close.svg";
import CloseCircle from "@/icons/close-circle.svg";
import Code from "@/icons/code.svg";
import Cursor from "@/icons/cursor.svg";
import Download from "@/icons/download.svg";
import Edit from "@/icons/edit.svg";
import Expand from "@/icons/expand.svg";
@@ -21,35 +27,39 @@ import EyeOpen from "@/icons/eye-open.svg";
import Filter from "@/icons/filter.svg";
import Flash from "@/icons/flash.svg";
import Folder from "@/icons/folder.svg";
import General from "@/icons/general.svg";
import Grid from "@/icons/grid.svg";
import Heart from "@/icons/heart.svg";
import Info from "@/icons/info.svg";
import List from "@/icons/list.svg";
import Load from "@/icons/load.svg";
import Machine from "@/icons/machine.svg";
import Minimize from "@/icons/minimize.svg";
import Modules from "@/icons/modules.svg";
import More from "@/icons/more.svg";
import NewMachine from "@/icons/new-machine.svg";
import Offline from "@/icons/offline.svg";
import Paperclip from "@/icons/paperclip.svg";
import Plus from "@/icons/plus.svg";
import Reload from "@/icons/reload.svg";
import Report from "@/icons/report.svg";
import Search from "@/icons/search.svg";
import Settings from "@/icons/settings.svg";
import Trash from "@/icons/trash.svg";
import Update from "@/icons/update.svg";
import WarningFilled from "@/icons/warning-filled.svg";
import Modules from "@/icons/modules.svg";
import NewMachine from "@/icons/new-machine.svg";
import AI from "@/icons/ai.svg";
import User from "@/icons/user.svg";
import Heart from "@/icons/heart.svg";
import SearchFilled from "@/icons/search-filled.svg";
import Offline from "@/icons/offline.svg";
import Services from "@/icons/services.svg";
import Settings from "@/icons/settings.svg";
import Switch from "@/icons/switch.svg";
import Tag from "@/icons/tag.svg";
import Machine from "@/icons/machine.svg";
import Trash from "@/icons/trash.svg";
import Update from "@/icons/update.svg";
import User from "@/icons/user.svg";
import WarningFilled from "@/icons/warning-filled.svg";
import { Dynamic } from "solid-js/web";
import { Color, fgClass } from "../colors";
const icons = {
Address,
AI,
ArrowBottom,
ArrowLeft,
@@ -61,8 +71,11 @@ const icons = {
CaretRight,
CaretUp,
Checkmark,
CheckSolid,
ClanIcon,
Close,
CloseCircle,
Code,
Cursor,
Download,
Edit,
@@ -72,12 +85,14 @@ const icons = {
Filter,
Flash,
Folder,
General,
Grid,
Heart,
Info,
List,
Load,
Machine,
Minimize,
Modules,
More,
NewMachine,
@@ -88,6 +103,7 @@ const icons = {
Report,
Search,
SearchFilled,
Services,
Settings,
Switch,
Tag,
@@ -101,8 +117,6 @@ export type IconVariant = keyof typeof icons;
const viewBoxes: Partial<Record<IconVariant, string>> = {
ClanIcon: "0 0 72 89",
Offline: "0 0 38 27",
Cursor: "0 0 35 42",
};
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {

View File

@@ -0,0 +1,15 @@
.modal {
@apply w-screen max-w-2xl h-fit flex flex-col;
.content {
@apply flex flex-col gap-2;
}
.header {
@apply flex items-center justify-between;
}
.clans {
@apply flex flex-col gap-2;
}
}

View File

@@ -0,0 +1,95 @@
import { Modal } from "../../components/Modal/Modal";
import cx from "classnames";
import styles from "./ListClansModal.module.css";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
import { navigateToClan, navigateToOnboarding } from "@/src/hooks/clan";
import { useNavigate } from "@solidjs/router";
import { For, Show } from "solid-js";
import { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
import { useClanListQuery } from "@/src/hooks/queries";
import { Alert } from "@/src/components/Alert/Alert";
import { NavSection } from "../NavSection/NavSection";
export interface ListClansModalProps {
onClose?: () => void;
error?: {
title: string;
description: string;
};
}
export const ListClansModal = (props: ListClansModalProps) => {
const navigate = useNavigate();
const query = useClanListQuery(clanURIs());
// we only want clans we could interrogate successfully
// todo how to surface the ones that failed to users?
const clanList = () => query.filter((it) => it.isSuccess);
const selectClan = (uri: string) => () => {
if (uri == activeClanURI()) {
navigateToClan(navigate, uri);
} else {
setActiveClanURI(uri);
}
};
return (
<Modal
title="Select Clan"
open
onClose={props.onClose}
class={cx(styles.modal)}
>
<div class={cx(styles.content)}>
<Show when={props.error}>
<Alert
type="error"
title={props.error?.title || ""}
description={props.error?.description}
/>
</Show>
<div class={cx(styles.header)}>
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
transform="uppercase"
>
Your Clans
</Typography>
<Button
hierarchy="secondary"
ghost
size="s"
startIcon="Plus"
onClick={() => {
props.onClose?.();
navigateToOnboarding(navigate, true);
}}
>
Add Clan
</Button>
</div>
<ul class={cx(styles.clans)}>
<For each={clanList()}>
{(clan) => (
<li>
<NavSection
label={clan.data.name}
description={clan.data.description ?? undefined}
onClick={selectClan(clan.data.uri)}
/>
</li>
)}
</For>
</ul>
</div>
</Modal>
);
};

View File

@@ -37,8 +37,8 @@
}
.backdrop {
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
-webkit-backdrop-filter: blur(4px);
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center z-50;
@apply bg-inv-4 opacity-70;
}
.contentWrapper {

View File

@@ -13,9 +13,9 @@ import Icon from "../Icon/Icon";
import cx from "classnames";
import { Dynamic } from "solid-js/web";
export type ModalContextType = {
export interface ModalContextType {
portalRef: HTMLDivElement;
};
}
const ModalContext = createContext<unknown>();
@@ -30,7 +30,7 @@ export const useModalContext = () => {
export interface ModalProps {
id?: string;
title: string;
onClose: () => void;
onClose?: () => void;
children: JSX.Element;
mount?: Node;
class?: string;
@@ -57,13 +57,11 @@ export const Modal = (props: ModalProps) => {
>
{props.title}
</Typography>
<KDialog.CloseButton
onClick={() => {
props.onClose();
}}
>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
<Show when={props.onClose}>
<KDialog.CloseButton onClick={props.onClose}>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</Show>
</div>
<Show when={props.metaHeader}>
{(metaHeader) => (

View File

@@ -0,0 +1,16 @@
.navSection {
@apply w-full flex gap-1.5 items-center justify-between;
@apply rounded-md px-4 py-5 bg-def-2;
.meta {
@apply flex flex-col gap-1;
}
&:hover {
@apply bg-def-3 cursor-pointer;
}
&:active {
@apply bg-def-4;
}
}

View File

@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
NavSection,
NavSectionProps,
} from "@/src/components/NavSection/NavSection";
const meta: Meta<NavSectionProps> = {
title: "Components/NavSection",
component: NavSection,
decorators: [
(Story: StoryObj) => (
<div class="w-96">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<NavSectionProps>;
export const Default: Story = {
args: {
label: "My Clan",
},
};
export const WithDescription: Story = {
args: {
...Default.args,
description:
"This is my Clan. There are many Clans like it, but this one is mine",
},
};

View File

@@ -0,0 +1,35 @@
import cx from "classnames";
import styles from "./NavSection.module.css";
import { Button } from "@kobalte/core/button";
import Icon from "../Icon/Icon";
import { Typography } from "../Typography/Typography";
import { Show } from "solid-js";
export interface NavSectionProps {
label: string;
description?: string;
onClick: () => void;
}
export const NavSection = (props: NavSectionProps) => {
return (
<Button class={cx(styles.navSection)} onClick={props.onClick}>
<div class={cx(styles.meta)}>
<Typography hierarchy="label" size="default" weight="bold">
{props.label}
</Typography>
<Show when={props.description}>
<Typography
hierarchy="body"
size="s"
weight="normal"
color="secondary"
>
{props.description}
</Typography>
</Show>
</div>
<Icon icon="CaretRight" />
</Button>
);
};

View File

@@ -0,0 +1,102 @@
.searchInput {
@apply w-full bg-inv-4 fg-inv-1;
font-size: 0.875rem;
font-weight: 500;
font-family: "Archivo", sans-serif;
line-height: 132%;
&::placeholder {
@apply fg-def-4;
}
&:focus,
&:focus-visible {
@apply outline-none;
}
&[data-invalid] {
@apply outline-semantic-error-4;
}
&[data-disabled] {
@apply outline-def-2 fg-def-4 cursor-not-allowed;
}
&[data-readonly] {
@apply outline-none border-none bg-inherit p-0 cursor-auto resize-none;
}
}
.searchHeader {
@apply bg-inv-3 flex gap-2 items-center p-2 rounded-md z-50;
@apply px-3 pt-3 pb-2;
}
.inputContainer {
@apply flex items-center gap-2 bg-inv-4 rounded-md px-1 w-full;
:has :focus-visible {
@apply bg-def-1;
}
}
.searchItem {
@apply flex py-1 px-2 pr-4 gap-2 justify-between items-center rounded-md;
& [role="option"] {
@apply flex flex-col w-full;
}
/* Icon */
& [role="complementary"] {
@apply size-8 flex items-center justify-center bg-white rounded-md;
}
&[data-highlighted],
&:focus,
&:focus-visible,
&:hover {
@apply bg-inv-acc-2;
}
&:active {
@apply bg-inv-acc-3;
}
}
.searchContainer {
@apply bg-gradient-to-b from-bg-inv-3 to-bg-inv-4;
@apply h-[14.5rem] rounded-lg;
border: 1px solid #2b4647;
background:
linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.18) 100%),
linear-gradient(
180deg,
var(--clr-bg-inv-3, rgba(43, 70, 71, 0.79)) 0%,
var(--clr-bg-inv-4, rgba(32, 54, 55, 0.79)) 100%
);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.searchContent {
@apply px-3;
height: calc(14.5rem - 4rem);
}
@keyframes contentHide {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-8px);
}
}

View File

@@ -0,0 +1,75 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Search, SearchProps, Module } from "./Search";
const meta = {
title: "Components/Search",
component: Search,
} satisfies Meta<SearchProps>;
export default meta;
type Story = StoryObj<SearchProps>;
// To test the virtualizer, we can generate a list of modules
function generateModules(count: number): Module[] {
const greek = [
"alpha",
"beta",
"gamma",
"delta",
"epsilon",
"zeta",
"eta",
"theta",
"iota",
"kappa",
"lambda",
"mu",
"nu",
"xi",
"omicron",
"pi",
"rho",
"sigma",
"tau",
"upsilon",
"phi",
"chi",
"psi",
"omega",
];
const modules: Module[] = [];
for (let i = 0; i < count; i++) {
modules.push({
value: `lolcat/module-${i + 1}`,
name: `Module ${i + 1}`,
description: `${greek[i % greek.length]}#${i + 1}`,
input: "lolcat",
});
}
return modules;
}
export const Default: Story = {
args: {
// Test with lots of modules
options: generateModules(1000),
},
render: (args: SearchProps) => {
return (
<div class="absolute bottom-1/3 w-3/4 px-3">
<Search
{...args}
onChange={(module) => {
// Go to the module configuration
console.log("Selected module:", module);
}}
/>
</div>
);
},
};

View File

@@ -0,0 +1,210 @@
import Icon from "../Icon/Icon";
import { Button } from "../Button/Button";
import styles from "./Search.module.css";
import { Combobox } from "@kobalte/core/combobox";
import { createMemo, createSignal, For } from "solid-js";
import { Typography } from "../Typography/Typography";
import { createVirtualizer } from "@tanstack/solid-virtual";
import { CollectionNode } from "@kobalte/core/*";
export interface Module {
value: string;
name: string;
input: string;
description: string;
}
export interface SearchProps {
onChange: (value: Module | null) => void;
options: Module[];
}
export function Search(props: SearchProps) {
// Controlled input value, to allow resetting the input itself
const [value, setValue] = createSignal<Module | null>(null);
const [inputValue, setInputValue] = createSignal<string>("");
let inputEl: HTMLInputElement;
let listboxRef: HTMLUListElement;
// const [currentItems, setItems] = createSignal(ALL_MODULES_OPTIONS_FLAT);
const [comboboxItems, setComboboxItems] = createSignal<
CollectionNode<Module>[]
>(
props.options.map((item) => ({
rawValue: item,
})) as CollectionNode<Module>[],
);
// Create a reactive virtualizer that updates when items change
const virtualizer = createMemo(() => {
const items = comboboxItems();
const newVirtualizer = createVirtualizer({
count: items.length,
getScrollElement: () => listboxRef,
getItemKey: (index) => {
const item = items[index];
return item?.rawValue?.value || `item-${index}`;
},
estimateSize: () => 42,
gap: 6,
overscan: 5,
});
return newVirtualizer;
});
return (
<Combobox<Module>
value={value()}
onChange={(value) => {
setValue(value);
setInputValue(value ? value.name : "");
props.onChange(value);
}}
class={styles.searchContainer}
placement="bottom-start"
options={props.options}
optionValue="value"
optionTextValue="name"
optionLabel="name"
placeholder="Search a service"
sameWidth={true}
open={true}
gutter={7}
modal={false}
flip={false}
virtualized={true}
allowsEmptyCollection={true}
closeOnSelection={false}
triggerMode="manual"
noResetInputOnBlur={true}
>
<Combobox.Control<Module> class={styles.searchHeader}>
{(state) => (
<div class={styles.inputContainer}>
<Icon icon="Search" color="quaternary" />
<Combobox.Input
ref={(el) => {
inputEl = el;
}}
class={styles.searchInput}
placeholder={"Search a service"}
value={inputValue()}
onChange={(e) => {
setInputValue(e.currentTarget.value);
}}
/>
<Button
type="reset"
hierarchy="primary"
size="s"
ghost
icon="CloseCircle"
onClick={() => {
state.clear();
setInputValue("");
// Dispatch an input event to notify combobox listeners
inputEl.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true }),
);
}}
/>
</div>
)}
</Combobox.Control>
<Combobox.Portal>
<Combobox.Content class={styles.searchContent} tabIndex={-1}>
<Combobox.Listbox<Module>
ref={(el) => {
listboxRef = el;
}}
style={{
height: "100%",
width: "100%",
overflow: "auto",
"overflow-y": "auto",
}}
scrollToItem={(key) => {
const idx = comboboxItems().findIndex(
(option) => option.rawValue.value === key,
);
virtualizer().scrollToIndex(idx);
}}
>
{(items) => {
// Update the virtualizer with the filtered items
const arr = Array.from(items());
setComboboxItems(arr);
return (
<div
style={{
height: `${virtualizer().getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
<For each={virtualizer().getVirtualItems()}>
{(virtualRow) => {
const item: CollectionNode<Module> | undefined =
items().getItem(virtualRow.key as string);
if (!item) {
console.warn("Item not found for key:", virtualRow.key);
return null;
}
return (
<Combobox.Item
item={item}
class={styles.searchItem}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div role="complementary">
<Icon icon="Code" />
</div>
<div role="option">
<Combobox.ItemLabel class="flex">
<Typography
hierarchy="body"
size="s"
weight="medium"
inverted
>
{item.rawValue.name}
</Typography>
</Combobox.ItemLabel>
<Typography
hierarchy="body"
size="xxs"
weight="normal"
color="quaternary"
inverted
class="flex justify-between"
>
<span>{item.rawValue.description}</span>
<span>by {item.rawValue.input}</span>
</Typography>
</div>
</Combobox.Item>
);
}}
</For>
</div>
);
}}
</Combobox.Listbox>
</Combobox.Content>
</Combobox.Portal>
</Combobox>
);
}

View File

@@ -11,6 +11,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { addClanURI, resetStore } from "@/src/stores/clan";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
import { encodeBase64 } from "@/src/hooks/clan";
import { ApiClientProvider } from "@/src/hooks/ApiClient";
import {
ApiCall,
OperationArgs,
OperationNames,
OperationResponse,
} from "@/src/hooks/api";
const defaultClanURI = "/home/brian/clans/my-clan";
@@ -24,10 +31,16 @@ const queryData = {
europa: {
name: "Europa",
machineClass: "nixos",
state: {
status: "online",
},
},
ganymede: {
name: "Ganymede",
machineClass: "nixos",
state: {
status: "out_of_sync",
},
},
},
},
@@ -40,10 +53,16 @@ const queryData = {
callisto: {
name: "Callisto",
machineClass: "nixos",
state: {
status: "not_installed",
},
},
amalthea: {
name: "Amalthea",
machineClass: "nixos",
state: {
status: "offline",
},
},
},
},
@@ -56,10 +75,16 @@ const queryData = {
thebe: {
name: "Thebe",
machineClass: "nixos",
state: {
status: "online",
},
},
sponde: {
name: "Sponde",
machineClass: "nixos",
state: {
status: "online",
},
},
},
},
@@ -123,6 +148,18 @@ export default meta;
type Story = StoryObj<RouteSectionProps>;
const mockFetcher = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
) =>
({
uuid: "mock",
result: Promise.reject<OperationResponse<K>>("not implemented"),
cancel: async () => {
throw new Error("not implemented");
},
}) satisfies ApiCall<K>;
export const Default: Story = {
args: {},
decorators: [
@@ -141,16 +178,28 @@ export const Default: Story = {
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
const machines = clan.machines || {};
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
clan.machines || {},
machines,
);
Object.entries(machines).forEach(([name, machine]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machine", name, "state"],
machine.state,
);
});
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
<ApiClientProvider client={{ fetch: mockFetcher }}>
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
</ApiClientProvider>
);
},
],

View File

@@ -3,11 +3,12 @@ import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/Typography/Typography";
import { For } from "solid-js";
import { For, useContext } from "solid-js";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
import { useMachinesQuery, useMachineStateQuery } from "@/src/hooks/queries";
import { useMachineStateQuery } from "@/src/hooks/queries";
import { SidebarProps } from "./Sidebar";
import { ClanContext } from "@/src/routes/Clan/Clan";
interface MachineProps {
clanURI: string;
@@ -56,7 +57,11 @@ const MachineRoute = (props: MachineProps) => {
export const SidebarBody = (props: SidebarProps) => {
const clanURI = useClanURI();
const machineList = useMachinesQuery(clanURI);
const ctx = useContext(ClanContext);
if (!ctx) {
throw new Error("ClanContext not found");
}
const sectionLabels = (props.staticSections || []).map(
(section) => section.title,
@@ -96,7 +101,7 @@ export const SidebarBody = (props: SidebarProps) => {
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={Object.entries(machineList.data || {})}>
<For each={Object.entries(ctx.machinesQuery.data || {})}>
{([id, machine]) => (
<MachineRoute
clanURI={clanURI}

View File

@@ -39,7 +39,7 @@ div.sidebar-header {
}
.sidebar-dropdown-content {
@apply flex flex-col w-full px-2 py-1.5 z-10;
@apply flex flex-col w-full px-2 py-1.5 z-10 gap-3;
@apply bg-def-1 rounded-bl-md rounded-br-md;
@apply border border-def-2;
@@ -58,6 +58,7 @@ div.sidebar-header {
@apply px-1;
.dropdown-group-label {
@apply flex items-baseline justify-between w-full;
}
.dropdown-group-items {

View File

@@ -3,10 +3,15 @@ import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For, Suspense } from "solid-js";
import { useClanListQuery } from "@/src/hooks/queries";
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
import { clanURIs } from "@/src/stores/clan";
import { createSignal, For, Suspense, useContext } from "solid-js";
import {
navigateToClan,
navigateToOnboarding,
useClanURI,
} from "@/src/hooks/clan";
import { setActiveClanURI } from "@/src/stores/clan";
import { Button } from "../Button/Button";
import { ClanContext } from "@/src/routes/Clan/Clan";
export const SidebarHeader = () => {
const navigate = useNavigate();
@@ -14,10 +19,23 @@ export const SidebarHeader = () => {
const [open, setOpen] = createSignal(false);
// get information about the current active clan
const clanURI = useClanURI();
const allClans = useClanListQuery(clanURIs());
const ctx = useContext(ClanContext);
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
if (!ctx) {
throw new Error("SidebarContext not found");
}
const clanURI = useClanURI();
const clanChar = () =>
ctx?.activeClanQuery?.data?.name.charAt(0).toUpperCase();
const clanName = () => ctx?.activeClanQuery?.data?.name;
const clanList = () =>
ctx.allClansQueries
.filter((it) => it.isSuccess)
.map((it) => it.data!)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div class="sidebar-header">
@@ -32,7 +50,7 @@ export const SidebarHeader = () => {
weight="bold"
inverted={true}
>
{activeClan()?.data?.name.charAt(0).toUpperCase()}
{clanChar()}
</Typography>
</div>
<Typography
@@ -41,7 +59,7 @@ export const SidebarHeader = () => {
weight="bold"
inverted={!open()}
>
{activeClan()?.data?.name}
{clanName()}
</Typography>
</div>
<DropdownMenu.Icon>
@@ -71,26 +89,36 @@ export const SidebarHeader = () => {
family="mono"
size="xs"
color="tertiary"
transform="uppercase"
>
YOUR CLANS
Your Clans
</Typography>
<Button
hierarchy="secondary"
ghost
size="xs"
startIcon="Plus"
onClick={() => navigateToOnboarding(navigate, true)}
>
Add
</Button>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={allClans}>
<For each={clanList()}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() =>
navigateToClan(navigate, clan.data!.uri)
}
onSelect={() => {
setActiveClanURI(clan.uri);
}}
>
<Typography
hierarchy="label"
size="xs"
weight="medium"
>
{clan.data?.name}
{clan.name}
</Typography>
</DropdownMenu.Item>
</Suspense>

View File

@@ -5,7 +5,7 @@ div.sidebar-section {
@apply flex items-center justify-between px-1.5;
& > div.controls {
@apply flex justify-end gap-2;
@apply flex items-center justify-end;
}
}

View File

@@ -12,8 +12,7 @@ import {
import { OperationNames, SuccessData } from "@/src/hooks/api";
import { GenericSchema, GenericSchemaAsync } from "valibot";
import { Typography } from "@/src/components/Typography/Typography";
import { Button as KButton } from "@kobalte/core/button";
import Icon from "@/src/components/Icon/Icon";
import { Button } from "@/src/components/Button/Button";
import "./SidebarSection.css";
import { Loader } from "../../components/Loader/Loader";
@@ -83,24 +82,24 @@ export function SidebarSectionForm<
</Typography>
<div class="controls h-4">
{editing() && !formStore.submitting && (
<KButton type="submit">
<Icon
icon="Checkmark"
color="tertiary"
size="0.75rem"
inverted
/>
</KButton>
<Button
hierarchy="primary"
size="xs"
startIcon="Checkmark"
ghost
type="submit"
>
Save
</Button>
)}
{editing() && formStore.submitting && <Loader />}
<KButton onClick={editOrClose}>
<Icon
icon={editing() ? "Close" : "Edit"}
color="tertiary"
size="0.75rem"
inverted
/>
</KButton>
<Button
hierarchy="primary"
ghost
size="xs"
icon={editing() ? "Close" : "Edit"}
onClick={editOrClose}
/>
</div>
</div>
<div class="content">

View File

@@ -2,8 +2,6 @@ import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
import { Divider } from "@/src/components/Divider/Divider";
import { ToolbarButton } from "./ToolbarButton";
import { Tooltip } from "../Tooltip/Tooltip";
import { Typography } from "../Typography/Typography";
const meta: Meta<ToolbarProps> = {
title: "Components/Toolbar",
@@ -18,11 +16,24 @@ export const Default: Story = {
args: {
children: (
<>
<ToolbarButton name="select" icon="Cursor" />
<ToolbarButton name="new-machine" icon="NewMachine" />
<ToolbarButton
name="select"
icon="Cursor"
description="Select my thing"
/>
<ToolbarButton
name="new-machine"
icon="NewMachine"
description="Select this thing"
/>
<Divider orientation="vertical" />
<ToolbarButton name="modules" icon="Modules" selected={true} />
<ToolbarButton name="ai" icon="AI" />
<ToolbarButton
name="modules"
icon="Modules"
selected={true}
description="Add service"
/>
<ToolbarButton name="ai" icon="AI" description="Call your AI Manager" />
</>
),
},
@@ -40,49 +51,22 @@ export const WithTooltip: Story = {
args: {
children: (
<>
<Tooltip
trigger={<ToolbarButton name="select" icon="Cursor" />}
placement="top"
>
<div class="mb-1 p-1 text-fg-inv-1">
<Typography hierarchy="label" size="s" color="inherit">
Select an object
</Typography>
</div>
</Tooltip>
<Divider orientation="vertical" />
<Tooltip
trigger={<ToolbarButton name="new-machine" icon="NewMachine" />}
placement="top"
>
<div class="mb-1 p-1 text-fg-inv-1">
<Typography hierarchy="label" size="s" color="inherit">
Create a new machine
</Typography>
</div>
</Tooltip>
<Tooltip
trigger={
<ToolbarButton name="modules" icon="Modules" selected={true} />
}
placement="top"
>
<div class="mb-1 p-1 text-fg-inv-1">
<Typography hierarchy="label" size="s" color="inherit">
Manage Services
</Typography>
</div>
</Tooltip>
<Tooltip
trigger={<ToolbarButton name="ai" icon="AI" />}
placement="top"
>
<div class="mb-1 p-1 text-fg-inv-1">
<Typography hierarchy="label" size="s" color="inherit">
Chat with AI
</Typography>
</div>
</Tooltip>
<ToolbarButton name="select" icon="Cursor" description="Select" />
<ToolbarButton
name="new-machine"
icon="NewMachine"
description="Select"
/>
<ToolbarButton
name="modules"
icon="Modules"
selected={true}
description="Select"
/>
<ToolbarButton name="ai" icon="AI" description="Select" />
</>
),
},

View File

@@ -3,22 +3,26 @@ import cx from "classnames";
import { Button } from "@kobalte/core/button";
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
import type { JSX } from "solid-js";
import { Tooltip } from "../Tooltip/Tooltip";
export interface ToolbarButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
icon: IconVariant;
description: JSX.Element;
selected?: boolean;
}
export const ToolbarButton = (props: ToolbarButtonProps) => {
return (
<Button
class={cx(styles.toolbar_button, {
[styles["selected"]]: props.selected,
})}
{...props}
>
<Icon icon={props.icon} inverted={!props.selected} />
</Button>
<Tooltip description={props.description} gutter={10} placement="top">
<Button
class={cx(styles.toolbar_button, {
[styles["selected"]]: props.selected,
})}
{...props}
>
<Icon icon={props.icon} inverted={!props.selected} />
</Button>
</Tooltip>
);
};

View File

@@ -1,4 +1,4 @@
div.tooltip-content {
.tooltipContent {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
max-width: min(calc(100vw - 16px), 380px);

View File

@@ -1,31 +1,40 @@
import "./Tooltip.css";
import {
Tooltip as KTooltip,
TooltipRootProps as KTooltipRootProps,
} from "@kobalte/core/tooltip";
import cx from "classnames";
import { JSX } from "solid-js";
import styles from "./Tooltip.module.css";
import { Typography } from "../Typography/Typography";
export interface TooltipProps extends KTooltipRootProps {
inverted?: boolean;
trigger: JSX.Element;
children: JSX.Element;
description: JSX.Element;
animation?: "bounce";
}
export const Tooltip = (props: TooltipProps) => {
return (
<KTooltip {...props}>
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
<KTooltip.Trigger>{props.children}</KTooltip.Trigger>
<KTooltip.Portal>
<KTooltip.Content
class={cx("tooltip-content", {
inverted: props.inverted,
class={cx(styles.tooltipContent, {
[styles.inverted]: props.inverted,
"animate-bounce": props.animation == "bounce",
})}
>
{props.placement == "bottom" && <KTooltip.Arrow />}
{props.children}
<Typography
hierarchy="body"
size="s"
weight="medium"
color="primary"
inverted={!props.inverted}
>
{props.description}
</Typography>
{props.placement == "top" && <KTooltip.Arrow />}
</KTooltip.Content>
</KTooltip.Portal>

View File

@@ -36,6 +36,9 @@ export const navigateToClan = (navigate: Navigator, clanURI: string) => {
navigate(path);
};
export const navigateToOnboarding = (navigate: Navigator, addClan: boolean) =>
navigate(`/${addClan ? "?addClan=true" : ""}`);
export const navigateToMachine = (
navigate: Navigator,
clanURI: string,

Some files were not shown because too many files have changed in this diff Show More