Compare commits

..

171 Commits

Author SHA1 Message Date
Johannes Kirschbauer
b8749f29bb docs: sort reference sidebar 2025-07-24 16:59:45 +02:00
Johannes Kirschbauer
b6a04e4f12 docs: restore index page 2025-07-24 16:54:49 +02:00
Johannes Kirschbauer
caaf9dc4f3 docs: unify documentation
Strictly enforce diataxis
Use resource driven approach
Can extend later to add 'developer' link index page
2025-07-24 16:51:57 +02:00
Johannes Kirschbauer
59105bd1da docs/options: expose all clan options in NüschtOS search 2025-07-24 09:42:21 +02:00
pinpox
0ffad32657 Merge pull request 'Add general intro doc text' (#4470) from add-banner-doctext into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4470
2025-07-23 16:36:03 +00:00
pinpox
50803c2e25 Add general intro doc text 2025-07-23 18:32:28 +02:00
hsjobeki
ebdd3e8413 Merge pull request 'ui/cubes: reactive wiring, use orthographic camera' (#4468) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4468
2025-07-23 14:09:42 +00:00
hsjobeki
ffe58fc189 Merge pull request 'feat(ui): move toolbar lower down' (#4467) from ui/refine-toolbar-position into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4467
2025-07-23 14:07:02 +00:00
Johannes Kirschbauer
7065464227 ui/cubes: reactive updates, use orthographic 2025-07-23 16:05:51 +02:00
Johannes Kirschbauer
5f567e2473 hooks: add maybeMachine hook 2025-07-23 16:04:55 +02:00
Johannes Kirschbauer
46ffcdf182 ui/css: format extra css 2025-07-23 16:04:41 +02:00
Johannes Kirschbauer
9afeec5683 ui: remove left over process-compose-2d.yml 2025-07-23 16:04:16 +02:00
Luis Hebendanz
329047e865 Merge pull request 'Move developer guides to the "Developer" section' (#4462) from Qubasa/clan-core:dev_docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4462
2025-07-23 12:10:45 +00:00
Qubasa
5c7e6b3830 docs: Move developer guides into the Developer section
nix fmt

address davhau review
2025-07-23 18:31:19 +07:00
hsjobeki
1e51439414 Merge pull request 'pytest: add simple clan_flake function' (#4453) from api-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4453
2025-07-23 10:27:05 +00:00
Brian McGee
a472f7f696 feat(ui): move toolbar lower down 2025-07-23 11:23:31 +01:00
Johannes Kirschbauer
29c764773f pytest: clan_flake allow usage of plain dicts 2025-07-23 12:15:54 +02:00
brianmcgee
af056f2355 Merge pull request 'feat(ui): set a fixed width for welcome screen' (#4464) from ui/max-width-onboarding into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4464
2025-07-23 09:32:12 +00:00
Brian McGee
6803f3c6f5 feat(ui): set a fixed width for welcome screen 2025-07-23 10:27:33 +01:00
brianmcgee
6b9ce0da66 Merge pull request 'feat(ui): add sidebar and flesh out app routes' (#4463) from ui/add-sidebar-nav into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4463
2025-07-23 09:26:43 +00:00
Brian McGee
38d62af1ba feat(ui): add sidebar and flesh out app routes 2025-07-23 10:16:00 +01:00
Luis Hebendanz
c880ab7cc1 Merge pull request 'feat(docs): enhance styling for typeset' (#4461) from badele/fork-clan-core:docs/update-style into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4461
2025-07-23 08:50:44 +00:00
Bruno Adelé
613a1fb553 feat(docs): enhance styling for typeset 2025-07-23 10:08:52 +02:00
Kenji Berthold
14f255c2d5 Merge pull request 'pkgs/cli: Fix fstring interplolation' (#4459) from kenji/ke-fix-typo into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4459
2025-07-23 07:55:04 +00:00
hsjobeki
eaa5a9a204 Merge pull request 'ui/scene: add timeout for splashscreen' (#4460) from ui-scene-2 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4460
2025-07-23 07:55:01 +00:00
Johannes Kirschbauer
34ccbcc13d ui/scene: add timeout for splashscreen 2025-07-23 09:50:59 +02:00
a-kenji
f58a120db1 pkgs/cli: Fix fstring interplolation
Closes: #4458
2025-07-23 09:43:51 +02:00
Qubasa
5b59cfbc34 docs: Remove emojies from getting started 2025-07-23 13:57:51 +07:00
DavHau
cc69892e3b create clan: better info about existing sop keys
When creating a new clan, the key selection now looks like this:
```
Found existing admin keys on this machine:
1: type: AGE
   pubkey: age1xyz...
   source: /home/grmpf/.config/sops/age/keys.txt
2: type: PGP
   pubkey: abc...
   source: SOPS_PGP_FP
Select keys to use (comma-separated list of numbers, or leave empty to select all):
```

This is achieved by adding a `source` attribute to `SopsKey`.
2025-07-23 13:22:19 +07:00
DavHau
c94330ee9c clan create: fix failure when path was single word
This should better be fixed with types. It should be possible to initialize a flake from a Path, making it very clear that a path `foo` is meant and not a remote flake called `foo`
2025-07-23 12:33:57 +07:00
Jörg Thalheim
377056e80c clan flakes create: initialize keys automatically (#4435)
fixes https://git.clan.lol/clan/clan-core/issues/2665
fixes https://git.clan.lol/clan/clan-core/issues/4407

Co-authored-by: DavHau <d.hauer.it@gmail.com>
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4435
Co-authored-by: Jörg Thalheim <joerg@thalheim.io>
Co-committed-by: Jörg Thalheim <joerg@thalheim.io>
2025-07-23 04:44:55 +00:00
Johannes Kirschbauer
1dbaff7b61 pytest: add simple clan_flake function
Takes a clan nix expression as a string
Is empty by default and does nothing
Expensive lockfile patching is done once per session
2025-07-22 23:24:40 +02:00
clan-bot
bf416f1b5f Merge pull request 'Update disko' (#4452) from update-disko into main 2025-07-22 20:12:35 +00:00
gitea-actions[bot]
d83bcf638f Update disko 2025-07-22 20:00:49 +00:00
Kenji Berthold
acfe3b0a04 Merge pull request 'pkgs/clan: Fix common command flags registering' (#4451) from kenji/ke-completions-add-flake into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4451
2025-07-22 18:10:15 +00:00
a-kenji
04f36a4cb1 pkgs/clan: Fix common command flags registering
Fix common command flags registering.
Register the common command flags before triggering autocomplete,
that way we can use the flags in the autocompletions themselves.
2025-07-22 19:56:07 +02:00
hsjobeki
41a0138c16 Merge pull request 'clan/create: api fixes and unit tests' (#4449) from api-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4449
2025-07-22 17:28:37 +00:00
Johannes Kirschbauer
f1be729206 create/clan: unit tests init 2025-07-22 19:17:41 +02:00
a-kenji
cacd853374 pkgs/cli: Support the flake argument for clan shell completions 2025-07-22 19:06:42 +02:00
brianmcgee
07caa6890f Merge pull request 'chore(ui): finish simplifying clan query params' (#4450) from ui/simplify-clan-params into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4450
2025-07-22 16:49:29 +00:00
Brian McGee
9706285474 chore(ui): finish simplifying clan query params 2025-07-22 17:45:23 +01:00
Kenji Berthold
1510b4014b Merge pull request 'pkgs/cli: Autocomplete various vars subcommands' (#4447) from kenji/ke-complete-vars into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4447
2025-07-22 16:27:32 +00:00
lassulus
d5e0f7e505 Merge pull request 'fix: handle arbitrary store paths references in flake cache' (#4441) from fix-flake-caching into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4441
2025-07-22 16:18:15 +00:00
Johannes Kirschbauer
b9e5cf1220 clan/create: use post_processing hook 2025-07-22 18:14:56 +02:00
Johannes Kirschbauer
f4eb59c373 create/clan: add validation to create arguments 2025-07-22 18:13:56 +02:00
brianmcgee
09b92084c8 Merge pull request 'ui/simplify-clan-params' (#4448) from ui/simplify-clan-params into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4448
2025-07-22 16:13:37 +00:00
Johannes Kirschbauer
06257d044a test_create: fix duplicated variables 2025-07-22 18:12:51 +02:00
Johannes Kirschbauer
34ca7a4a7b create/clan: add abstraction for initial meta
Directly passing through persisted data is bad
2025-07-22 18:12:17 +02:00
brianmcgee
ce70be5ca3 Merge pull request 'Add tanstack devtools to UI' (#4446) from feat/tanstack-devstools into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4446
2025-07-22 16:11:27 +00:00
Brian McGee
dd3051d62b chore(ui): simplify clan uri params
Now that everything lives under `/clans/:clanURI` we don't need to handle the case where the param might be null.
2025-07-22 17:08:12 +01:00
Brian McGee
5f290fed7f chore(ui): remove solid-devtools
It's a chrome extension which is pointless inside of webview which is webkit.
2025-07-22 17:07:44 +01:00
Johannes Kirschbauer
a34ec8ed22 templates: add post_process hook for tests or other extensions 2025-07-22 18:07:26 +02:00
Johannes Kirschbauer
4597b207e7 pytest: fixtures offline_flake_hook init 2025-07-22 18:06:47 +02:00
Johannes Kirschbauer
9257cb02ee validator: hostname init 2025-07-22 18:06:27 +02:00
a-kenji
cd8a1d9a32 pkgs/cli: Autocomplete various vars subcommands
Add autocomplete for `vars` for the following subcommands:

```
clan vars get [machine] [var_id]
clan vars set [machine] [var_id]
```
2025-07-22 18:03:55 +02:00
Brian McGee
ee9ae21bd2 feat(ui): add tanstack devtools for debugging queries 2025-07-22 16:33:53 +01:00
Jörg Thalheim
bd1451ce18 fix: handle arbitrary store paths references in flake cache
Previously, paths like /nix/store/hash-file.nix:123 were incorrectly
treated as pure store paths and wrapped in {"outPath": ...}, breaking
the cache. This fix:

- Adds helper functions to properly detect and handle store references
- Distinguishes between pure store paths and paths with metadata (line numbers)
- Supports multiple store references in a single string
- Handles custom NIX_STORE_DIR correctly
- Ensures existence checks work for all store references

Also fixes test_cache_gc to delete NIX_REMOTE for proper local store testing.
2025-07-22 17:13:04 +02:00
pinpox
a94cc4b7f7 Merge pull request 'Add wait_for_file testing helper' (#4442) from add-wait-for-file into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4442
2025-07-22 14:38:07 +00:00
pinpox
cf2ccd7e14 Add wait_for_file testing helper 2025-07-22 16:27:20 +02:00
hsjobeki
69ab00b34b Merge pull request 'store: move merge_objects into persistence helpers' (#4440) from api-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4440
2025-07-22 13:12:04 +00:00
Johannes Kirschbauer
0043870882 merge_objects: add unit tests 2025-07-22 15:01:00 +02:00
Johannes Kirschbauer
0ea42ae541 store: move merge_objects into persistence helpers 2025-07-22 15:01:00 +02:00
Kenji Berthold
ad50cfbcbb Merge pull request 'docs: Fix typo' (#4439) from kenji/ke-typo-getting-started into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4439
2025-07-22 11:30:58 +00:00
Kenji Berthold
cf65ae81cf Merge pull request 'pkgs/cli: Add disko template completion to clan templates apply disk' (#4438) from kenji/ke-complete-disko into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4438
2025-07-22 11:26:58 +00:00
a-kenji
19ca7d9a77 docs: Fix typo 2025-07-22 13:26:06 +02:00
Kenji Berthold
0b2ee45526 Merge pull request 'pkgs/cli: Add completions to clan flakes create --template [TEMPLATE]' (#4437) from kenji/ke-complete-template into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4437
2025-07-22 11:25:04 +00:00
a-kenji
28e39ada84 pkgs/cli: Add disko template completion to clan templates apply disk 2025-07-22 13:04:45 +02:00
a-kenji
fb52b955cc pkgs/cli: Add completions to clan flakes create --template [TEMPLATE]
Add completions to `clan flakes create --template [TEMPLATE]`
2025-07-22 13:01:45 +02:00
pinpox
77f75b916d Merge pull request 'Fix store symlinks in container test' (#4436) from fix-container-symlinks into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4436
2025-07-22 10:45:02 +00:00
pinpox
97022ba873 Fix store symlinks in container test 2025-07-22 12:28:11 +02:00
Luis Hebendanz
aee71b3fd6 Merge pull request 'pkgs/cli: Validate flake for network subcommands' (#4433) from kenji/ke-networks-validate-flake into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4433
2025-07-22 05:27:45 +00:00
Luis Hebendanz
76535852e4 Merge pull request 'pkgs/cli: Add machine to output while uploading sources' (#4429) from kenji/ke-update-add-machine into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4429
2025-07-22 05:27:06 +00:00
Kenji Berthold
a694e8d122 Merge pull request 'pkgs/cli: Fix typo in networking help' (#4431) from kenji/ke-network-fix-typo into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4431
2025-07-21 20:38:54 +00:00
Kenji Berthold
93fee8263f Merge pull request 'pkgs/cli: Fix typo in networking list help' (#4432) from kenji/ke-networks-fix-typo into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4432
2025-07-21 20:23:55 +00:00
clan-bot
28859641eb Merge pull request 'Update flake-parts' (#4430) from update-flake-parts into main 2025-07-21 20:18:37 +00:00
a-kenji
3a2be243c0 pkgs/cli: Add machine to output while uploading sources
Add the `machine` to it's output while uploading sources.
2025-07-21 22:15:43 +02:00
a-kenji
9fdf41813a pkgs/cli: Validate flake for network subcommands
When running for example `clan networks list` we now get a reasonable
error message, instead of:
```
Traceback (most recent call last):
  File "/nix/store/8ygq8bfxqydk2917mmg32wy9wb0qzzzd-clan-cli/bin/.clan-wrapped", line 9, in <module>
    sys.exit(main())
             ~~~~^^
  File "/nix/store/8ygq8bfxqydk2917mmg32wy9wb0qzzzd-clan-cli/lib/python3.13/site-packages/clan_cli/cli.py", line 516, in main
    args.func(args)
    ~~~~~~~~~^^^^^^
  File "/nix/store/8ygq8bfxqydk2917mmg32wy9wb0qzzzd-clan-cli/lib/python3.13/site-packages/clan_cli/network/list.py", line 12, in list_command
    networks = networks_from_flake(flake)
  File "/nix/store/8ygq8bfxqydk2917mmg32wy9wb0qzzzd-clan-cli/lib/python3.13/site-packages/clan_lib/network/network.py", line 106, in networks_from_flake
    networks_ = flake.select("clan.exports.instances.*.networking")
                ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'select'
```
2025-07-21 22:13:48 +02:00
a-kenji
04f3a9480f pkgs/cli: Fix typo in networking list help 2025-07-21 22:07:29 +02:00
a-kenji
f7762b3119 pkgs/cli: Fix typo in networking help 2025-07-21 22:06:03 +02:00
gitea-actions[bot]
634e4116cf Update flake-parts 2025-07-21 20:00:52 +00:00
Kenji Berthold
015c09b0e5 Merge pull request 'docs: Fix typos in getting-started guide' (#4428) from kenji/ke-getting-started-fix-typo into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4428
2025-07-21 18:54:37 +00:00
Kenji Berthold
6e0a43c777 Merge pull request 'clanServices/zerotier: Make moon configuration optional' (#4427) from kenji/ke-zerotier-make-moon-optional into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4427
2025-07-21 18:54:30 +00:00
a-kenji
7fc527b649 docs: Fix typos in getting-started guide 2025-07-21 20:47:41 +02:00
a-kenji
2f0ba0782a clanServices/zerotier: Make moon configuration optional
Make moon configuration optional. Before the `attrNames` evaluated the
attributes eagerly, which in practice meant that you had to set a moon,
if there was a controller configured, which is not on purpose.
2025-07-21 20:43:25 +02:00
hsjobeki
bc3b6c792f Merge pull request 'services: fix extraModules as path' (#4422) from fix-extra-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4422
2025-07-21 17:56:58 +00:00
Johannes Kirschbauer
b5a3d617fd services: fix extraModules as path 2025-07-21 19:51:16 +02:00
Mic92
579492f071 Merge pull request 'migration guide: fix moon example' (#4423) from docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4423
2025-07-21 17:11:14 +00:00
Jörg Thalheim
0ed02da28f migration guide: fix moon example 2025-07-21 19:07:47 +02:00
Mic92
4abfbb05a2 Merge pull request 'extend migration guide' (#4421) from docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4421
2025-07-21 16:23:58 +00:00
Jörg Thalheim
6126cccbcc extend migration guide 2025-07-21 18:10:58 +02:00
brianmcgee
9e77d16e6d Merge pull request 'fix(ui): alignment issues with forms' (#4418) from ui/minor-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4418
2025-07-21 12:13:36 +00:00
Brian McGee
53752d4a69 fix(ui): alignment issues with forms 2025-07-21 13:09:53 +01:00
DavHau
38955f763f clan default template: add inputs to specialArgs 2025-07-21 18:39:51 +07:00
brianmcgee
bd97896899 Merge pull request 'fix(ui): remove extra margin in modal title' (#4415) from ui/minor-fixes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4415
2025-07-21 10:22:15 +00:00
Brian McGee
d6efeb3295 fix(ui): remove extra margin in modal title 2025-07-21 11:18:22 +01:00
Luis Hebendanz
e3247d9c36 Merge pull request 'Fix multiple bugs in 'clan networking' command' (#4389) from Qubasa/clan-core:deploy_network into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4389
2025-07-21 07:35:54 +00:00
Qubasa
4055508588 clan-lib: Add object_name to ClassSource and don't override __repr__ from NetworkTechnologyBase instead overwrite it in ClassSource 2025-07-21 14:25:01 +07:00
Qubasa
ff65dfc883 clanServices: change tor service to have "client" and "server" roles instead of just "default"
also improve error message when user forgot to update machine in clan
networking command
2025-07-21 14:25:01 +07:00
Qubasa
1f5ef04a61 clan-lib: Fix network.py missing vars generation and use import_with_source for better trace ability 2025-07-21 12:40:49 +07:00
Qubasa
89f0e90910 clan-lib: Init import_utils to add debug information to dynamically imported modules 2025-07-21 12:40:49 +07:00
Qubasa
137aa71529 clan-lib: Fix is_running of tor.py 2025-07-21 12:40:49 +07:00
Qubasa
4b5273fbc1 clanServices: Fix tor service not exposing SOCKS port 2025-07-21 12:40:49 +07:00
clan-bot
aed48be645 Merge pull request 'Update data-mesher' (#4414) from update-data-mesher into main 2025-07-21 05:16:44 +00:00
gitea-actions[bot]
5fdc9823d1 Update data-mesher 2025-07-21 05:00:49 +00:00
clan-bot
f6284a7ac2 Merge pull request 'Update treefmt-nix' (#4405) from update-treefmt-nix into main 2025-07-20 15:15:54 +00:00
gitea-actions[bot]
72473746ff Update treefmt-nix 2025-07-20 15:01:26 +00:00
hsjobeki
4b36b3e07c Merge pull request 'ui/scene: mock create machine modal for testing' (#4404) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4404
2025-07-19 16:23:56 +00:00
Johannes Kirschbauer
5a63eeed4e ui/scene: mock create machine modal for testing 2025-07-19 18:19:37 +02:00
Johannes Kirschbauer
ac96d67f09 components/modal: fix missing onClose call 2025-07-19 18:19:19 +02:00
Johannes Kirschbauer
d01342aa79 components/modal: add missing properties {mount, class} 2025-07-19 18:18:56 +02:00
Johannes Kirschbauer
2d404254da ui/scene: fix initBase visibility 2025-07-19 18:18:05 +02:00
Johannes Kirschbauer
71b69c1010 ui/scene: add promise based create machine callback" 2025-07-19 18:17:38 +02:00
Johannes Kirschbauer
f155c68efe ui/scene: fix animateToPosition 2025-07-19 18:16:53 +02:00
hsjobeki
e57741b60c Merge pull request 'ui/scene: clean up initBase' (#4403) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4403
2025-07-19 12:51:04 +00:00
Johannes Kirschbauer
c9cacfcf62 ui/scene: fix typing checks 2025-07-19 14:47:23 +02:00
Johannes Kirschbauer
2d937b80b1 ui/scene: clean up initBase 2025-07-19 14:40:32 +02:00
clan-bot
e8b91e63bc Merge pull request 'Update treefmt-nix' (#4402) from update-treefmt-nix into main 2025-07-19 10:17:05 +00:00
gitea-actions[bot]
a9d6fa7712 Update treefmt-nix 2025-07-19 10:01:30 +00:00
hsjobeki
65a23983c2 Merge pull request 'ui/scene: add loading splash screen' (#4400) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4400
2025-07-18 17:42:15 +00:00
Johannes Kirschbauer
c181400267 ui/scene: add loading splash screen 2025-07-18 19:37:06 +02:00
hsjobeki
e8ff0d1ad4 Merge pull request 'ui/render: optimize rendering, requestRenderIfNotRequested' (#4398) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4398
2025-07-18 17:36:44 +00:00
Johannes Kirschbauer
f9f8a947e2 ui/splash: add scene splash screen 2025-07-18 19:36:02 +02:00
Johannes Kirschbauer
c5b0154af7 ui/logos: add darknet-builder logo 2025-07-18 19:35:11 +02:00
brianmcgee
864742f05f Merge pull request 'feat(ui): add creating cube animation' (#4399) from ui/creating-animation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4399
2025-07-18 16:39:08 +00:00
Brian McGee
38b043f625 feat(ui): add creating cube animation 2025-07-18 17:31:30 +01:00
Johannes Kirschbauer
174e66ef95 ui/render: optimize rendering, requestRenderIfNotRequested 2025-07-18 18:15:30 +02:00
hsjobeki
315049de20 Merge pull request 'ui/controls: replace manual listeners with mapControl' (#4397) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4397
2025-07-18 15:49:36 +00:00
Johannes Kirschbauer
2e577dbd1e ui/controls: replace manual listeners with mapControl 2025-07-18 17:45:53 +02:00
Mic92
a9b457e063 Merge pull request 'clanServices/wifi: handle multiple instances' (#4260) from nim65s/clan-core:multi-wifi into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4260
2025-07-18 15:19:24 +00:00
hsjobeki
4281770ec7 Merge pull request 'ui/scene: hook up api' (#4388) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4388
2025-07-18 15:15:41 +00:00
Johannes Kirschbauer
1bd950fa39 ui/scene: remove all unneded complexity to reduce complexity and improve performance 2025-07-18 17:12:09 +02:00
Johannes Kirschbauer
e37b61240b ui/routing: move scene down clans/:id" 2025-07-18 17:11:32 +02:00
Johannes Kirschbauer
23d2975bb5 ui/store: add methods for sceneData 2025-07-18 17:11:04 +02:00
Johannes Kirschbauer
d441d4c1c1 ui/hooks: add overloaded useClanUri 2025-07-18 17:10:39 +02:00
Mic92
840cb7e2cb Merge pull request 'nginx: drop recommendedZstdSettings' (#4396) from zstd into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4396
2025-07-18 14:23:52 +00:00
Jörg Thalheim
cf232e1002 nginx: drop recommendedZstdSettings
nixpkgs no longer recommends it.
2025-07-18 16:17:36 +02:00
Mic92
7414dc6e7e Merge pull request 'clan-app: fix x86_64-darwin build' (#4395) from darwin-build into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4395
2025-07-18 14:10:26 +00:00
Jörg Thalheim
d97f997349 clan-app: fix x86_64-darwin build 2025-07-18 16:06:12 +02:00
pinpox
0621ae1ca6 Merge pull request 'fix workfow' (#4393) from fix-clan-core-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4393
2025-07-18 13:37:56 +00:00
pinpox
992048e1b2 Fix update-clan-core-for-checks script
create-pr needs to use /bin/sh to work. This PR makes the script posix
compliant, replacing any bash specific features with plain sh
alternatives
2025-07-18 15:33:36 +02:00
Mic92
261cad7674 Merge pull request 'build x86_64-darwin on main every few hours' (#4392) from darwin-ci into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4392
2025-07-18 12:43:17 +00:00
Jörg Thalheim
a012e4b1af build x86_64-darwin on main every few hours 2025-07-18 14:39:07 +02:00
Guilhem Saurel
158b98ee05 clanServices/wifi: fix for multiple instances
Without this, `nix build .#checks.x86_64-linux.wifi` fails with:
```
error: The option `nodes.first.systemd.services.NetworkManager-setup-secrets.serviceConfig.ExecStart' has conflicting definition values:
- In `/nix/store/x0…45-source/clanServices/wifi/default.nix, via option mappedServices."self-@clan/wifi".roles.default.perInstance, via option nixosModule': <derivation wifi-secrets>
- In `/nix/store/x0…45-source/clanServices/wifi/default.nix, via option mappedServices."self-@clan/wifi".roles.default.perInstance, via option nixosModule': <derivation wifi-secrets>
Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.
```
2025-07-17 23:30:50 +02:00
Guilhem Saurel
14d367e50f clanServices/wifi: update test with a second instance 2025-07-17 23:30:47 +02:00
lassulus
48c575699e Merge pull request 'network module + CLI' (#4344) from networking into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4344
2025-07-17 13:36:53 +00:00
lassulus
60768cc537 Add networking module
This adds a (for now hidden) clan network command that exposes list,
ping, overview subcommands to get informations about configured
networks.
ClanServices can now use the exports to define network specific
information.

This is not the complete feature yet, as we are lacking more tests and
documentation, but merging this now makes it easier to iterate.
2025-07-17 15:23:08 +02:00
Johannes Kirschbauer
c26dff282b ui/queries: init queries folder 2025-07-17 13:49:16 +02:00
hsjobeki
5022f6f26c Merge pull request 'ui/clan: rework routing concept' (#4385) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4385
2025-07-17 11:39:33 +00:00
Johannes Kirschbauer
94b93074bc ui/query: add correct resource path 2025-07-17 13:35:50 +02:00
Johannes Kirschbauer
d962033236 ui/clan: rework routing concept 2025-07-17 10:54:48 +02:00
Johannes Kirschbauer
a548851245 ui/hooks: useMaybeClanUri init hook
Needed for pre-rendering the cube scene with clanURI = null
When it later receives a value scene will get populated without completely re-rendering
2025-07-17 10:51:32 +02:00
Johannes Kirschbauer
b32e61bb6d ui/app: wrap with query client povider to make api cached calls 2025-07-17 10:49:47 +02:00
Johannes Kirschbauer
e731322af3 ui/store: infer type from return arg 2025-07-17 10:49:12 +02:00
hsjobeki
fd21c6b4ee Merge pull request 'buildClan: Add deprecation warning' (#4384) from Qubasa/clan-core:migrate_away_buildClan into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4384
Reviewed-by: hsjobeki <hsjobeki@gmail.com>
2025-07-17 08:44:04 +00:00
Qubasa
5a86862f47 buildClan: Add deprecation warning 2025-07-17 15:32:12 +07:00
Michael Hoang
1d1a2563c3 Merge pull request 'flake: remove unnecessary follows for data-mesher' (#4383) from push-yzqmtrtrkkzt into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4383
2025-07-17 07:37:20 +00:00
Michael Hoang
4bc57980ff flake: remove unnecessary follows for data-mesher 2025-07-17 17:30:36 +10:00
Luis Hebendanz
3afd0c0971 Merge pull request 'inventory: Add missing default value for exports.instances and exports.machines' (#4382) from Qubasa/clan-core:fix_inv_missing_default into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4382
2025-07-17 06:17:37 +00:00
Qubasa
e6a6cb27ec inventory: Add missing default value for exports.instances and exports.machines 2025-07-17 13:10:30 +07:00
clan-bot
dcd78c5d84 Merge pull request 'Update disko' (#4381) from update-disko into main 2025-07-17 05:16:49 +00:00
gitea-actions[bot]
2a1ad66292 Update disko 2025-07-17 05:00:49 +00:00
brianmcgee
5d0d4404b8 Merge pull request 'chore: add a check for background.jpg' (#4380) from chore/stupid-jpg-check into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4380
2025-07-16 16:15:38 +00:00
Brian McGee
7b369c77b5 chore: add a check for background.jpg 2025-07-16 18:11:40 +02:00
hsjobeki
06b70a982b Merge pull request 'UI/cubes: extend cubes scene' (#4375) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4375
2025-07-16 15:20:27 +00:00
Johannes Kirschbauer
c9b1b0fb94 ui/cubes: align with design 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
66bdbb0959 ui/cubes: init story 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
752f030d03 ui/storybook: add all stories 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
8c7e93c92e UI/cubes: group logic to add more meshed 2025-07-16 17:12:09 +02:00
Johannes Kirschbauer
579885a6e2 cubes: scene extend 2025-07-16 17:12:09 +02:00
brianmcgee
45f7ebc0c9 Merge pull request 'feat: onboarding workflow' (#4379) from ui/onboarding-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4379
2025-07-16 15:10:04 +00:00
Brian McGee
997d675f8c feat: onboarding workflow 2025-07-16 17:04:34 +02:00
159 changed files with 5988 additions and 2289 deletions

View File

@@ -0,0 +1,20 @@
name: Build Clan App (Darwin)
on:
schedule:
# Run every 4 hours
- cron: "0 */4 * * *"
workflow_dispatch:
push:
branches:
- main
jobs:
build-clan-app-darwin:
runs-on: nix
steps:
- uses: actions/checkout@v4
- name: Build clan-app for x86_64-darwin
run: |
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
#!/bin/sh
# Shared script for creating pull requests in Gitea workflows
set -euo pipefail
set -eu
# Required environment variables:
# - CI_BOT_TOKEN: Gitea bot token for authentication
@@ -8,22 +9,22 @@ set -euo pipefail
# - PR_TITLE: Title of the pull request
# - PR_BODY: Body/description of the pull request
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
if [ -z "${CI_BOT_TOKEN:-}" ]; then
echo "Error: CI_BOT_TOKEN is not set" >&2
exit 1
fi
if [[ -z "${PR_BRANCH:-}" ]]; then
if [ -z "${PR_BRANCH:-}" ]; then
echo "Error: PR_BRANCH is not set" >&2
exit 1
fi
if [[ -z "${PR_TITLE:-}" ]]; then
if [ -z "${PR_TITLE:-}" ]; then
echo "Error: PR_TITLE is not set" >&2
exit 1
fi
if [[ -z "${PR_BODY:-}" ]]; then
if [ -z "${PR_BODY:-}" ]; then
echo "Error: PR_BODY is not set" >&2
exit 1
fi
@@ -43,9 +44,12 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
}" \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
pr_number=$(echo "$resp" | jq -r '.number')
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
echo "Error parsing response from pull request creation" >&2
exit 1
fi
if [[ "$pr_number" == "null" ]]; then
if [ "$pr_number" = "null" ]; then
echo "Error creating pull request:" >&2
echo "$resp" | jq . >&2
exit 1
@@ -64,12 +68,15 @@ while true; do
"delete_branch_after_merge": true
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
msg=$(echo "$resp" | jq -r '.message')
if [[ "$msg" != "Please try again later" ]]; then
if ! msg=$(echo "$resp" | jq -r '.message'); then
echo "Error parsing merge response" >&2
exit 1
fi
if [ "$msg" != "Please try again later" ]; then
break
fi
echo "Retrying in 2 seconds..."
sleep 2
done
echo "Pull request #$pr_number merge initiated"
echo "Pull request #$pr_number merge initiated"

View File

@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/concepts/generators/)<!-- [secrets.md](docs/site/concepts/generators.md) -->.
### Contributing to Clan

View File

@@ -4,7 +4,7 @@ description = "Statically configure borgbackup with sane defaults."
!!! Danger "Deprecated"
Use [borgbackup](borgbackup.md) instead.
Don't use borgbackup-static through [inventory](../../guides/inventory.md).
Don't use borgbackup-static through [inventory](../../concepts/inventory.md).
This module implements the `borgbackup` backend and implements sane defaults
for backup management through `borgbackup` for members of the clan.

View File

@@ -38,7 +38,6 @@
recommendedOptimisation = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
recommendedZstdSettings = lib.mkDefault true;
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
# instead of going to the journal!

View File

@@ -12,7 +12,7 @@ After the system was installed/deployed the following command can be used to dis
clan vars get [machine_name] root-password/root-password
```
See also: [Vars](../../guides/vars-backend.md)
See also: [Vars](../../concepts/generators.md)
To regenerate the password run:
```

View File

@@ -16,7 +16,7 @@ After the system was installed/deployed the following command can be used to dis
clan vars get [machine_name] root-password/root-password
```
See also: [Vars](../../guides/vars-backend.md)
See also: [Vars](../../concepts/generators.md)
To regenerate the password run:
```

View File

@@ -0,0 +1,47 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/internet";
manifest.description = "direct access (or via ssh jumphost) to machines";
manifest.categories = [
"System"
"Network"
];
roles.default = {
interface =
{ lib, ... }:
{
options = {
host = lib.mkOption {
type = lib.types.str;
description = ''
ip address or hostname (domain) of the machine
'';
};
jumphosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
optional list of jumphosts to use to connect to the machine
'';
};
};
};
perInstance =
{
roles,
lib,
settings,
...
}:
{
exports.networking = {
# TODO add user space network support to clan-cli
peers = lib.mapAttrs (_name: machine: {
host.plain = machine.settings.host;
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
}) roles.default.machines;
};
};
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
internet = module;
};
}

View File

@@ -0,0 +1,110 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/tor";
manifest.description = "Onion routing, use Hidden services to connect your machines";
manifest.categories = [
"System"
"Network"
];
roles.client = {
perInstance =
{
...
}:
{
nixosModule =
{
...
}:
{
config = {
services.tor = {
enable = true;
torsocks.enable = true;
client.enable = true;
};
};
};
};
};
roles.server = {
# interface =
# { lib, ... }:
# {
# options = {
# OciSettings = lib.mkOption {
# type = lib.types.raw;
# default = null;
# description = "NixOS settings for virtualisation.oci-container.<name>.settings";
# };
# buildContainer = lib.mkOption {
# type = lib.types.nullOr lib.types.str;
# default = null;
# };
# };
# };
perInstance =
{
instanceName,
roles,
lib,
...
}:
{
exports.networking = {
priority = lib.mkDefault 10;
# TODO add user space network support to clan-cli
module = "clan_lib.network.tor";
peers = lib.mapAttrs (name: machine: {
host.var = {
machine = name;
generator = "tor_${instanceName}";
file = "hostname";
};
}) roles.server.machines;
};
nixosModule =
{
pkgs,
config,
...
}:
{
config = {
services.tor = {
enable = true;
relay.onionServices."clan_${instanceName}" = {
version = 3;
# TODO get ports from instance machine config
map = [
{
port = 22;
target.port = 22;
}
];
secretKey = config.clan.core.vars.generators."tor_${instanceName}".files.hs_ed25519_secret_key.path;
};
};
clan.core.vars.generators."tor_${instanceName}" = {
files.hs_ed25519_secret_key = { };
files.hostname = { };
runtimeInputs = with pkgs; [
coreutils
tor
];
script = ''
mkdir -p data
echo -e "DataDirectory ./data\nSocksPort 0\nHiddenServiceDir ./hs\nHiddenServicePort 80 127.0.0.1:80" > torrc
timeout 2 tor -f torrc || :
mv hs/hs_ed25519_secret_key $out/hs_ed25519_secret_key
mv hs/hostname $out/hostname
'';
};
};
};
};
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
tor = module;
};
}

View File

@@ -39,7 +39,7 @@ in
};
perInstance =
{ settings, ... }:
{ instanceName, settings, ... }:
{
nixosModule =
{ pkgs, config, ... }:
@@ -86,7 +86,7 @@ in
# service to generate the environment file containing all secrets, as
# expected by the nixos NetworkManager-ensure-profile service
systemd.services.NetworkManager-setup-secrets = {
systemd.services."NetworkManager-setup-secrets-${instanceName}" = {
description = "Generate wifi secrets for NetworkManager";
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
partOf = [ "NetworkManager-ensure-profiles.service" ];

View File

@@ -7,8 +7,16 @@
inventory = {
machines.test = { };
machines.second = { };
instances = {
wg-test-all = {
module.name = "@clan/wifi";
module.input = "self";
roles.default.tags.all = { };
roles.default.settings.networks.all = { };
};
wg-test-one = {
module.name = "@clan/wifi";
module.input = "self";

View File

@@ -134,9 +134,9 @@
systemd.services.zerotier-inventory-autoaccept =
let
machines = uniqueStrings (
(lib.attrNames roles.moon.machines)
++ (lib.attrNames roles.controller.machines)
++ (lib.attrNames roles.peer.machines)
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))
);
networkIps = builtins.foldl' (
ips: name:

View File

@@ -32,6 +32,33 @@ let
};
};
}).config;
testFlakeNoMoon =
(clanLib.clan {
self = { };
directory = ./vm;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.sara = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.bam = {
nixpkgs.hostPlatform = "x86_64-linux";
};
modules.zerotier = module;
inventory.instances = {
zerotier = {
module.name = "zerotier";
module.input = "self";
roles.peer.tags.all = { };
roles.controller.machines.bam = { };
};
};
}).config;
in
{
test_peers = {
@@ -73,4 +100,30 @@ in
networkName = "zerotier";
};
};
test_peers_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.jon.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = false;
networkName = "zerotier";
};
};
test_controller_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.bam.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = true;
networkName = "zerotier";
};
};
}

View File

@@ -48,61 +48,104 @@ nav:
- Home: index.md
- Guides:
- Getting Started:
- 🚀 Creating Your First Clan: guides/getting-started/index.md
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
- ⚙️ Add Machines: guides/getting-started/add-machines.md
- ⚙️ Add User: guides/getting-started/add-user.md
- ⚙️ Add Services: guides/getting-started/add-services.md
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
- 🚢 Deploy Machine: guides/getting-started/deploy.md
- 🧪 Continuous Integration: guides/getting-started/check.md
- clanServices: guides/clanServices.md
- Disk Encryption: guides/disk-encryption.md
- Mesh VPN: guides/mesh-vpn.md
- Creating Your First Clan: guides/getting-started/index.md
- Create USB Installer: guides/getting-started/installer.md
- Add Machines: guides/getting-started/add-machines.md
- Add User: guides/getting-started/add-user.md
- Add Services: guides/getting-started/add-services.md
- Deploy Machine: guides/getting-started/deploy.md
- Continuous Integration: guides/getting-started/check.md
- Using Services: guides/clanServices.md
- Backup & Restore: guides/backups.md
- Vars Backend: guides/vars-backend.md
- Facts Backend: guides/secrets.md
- Adding more machines: guides/more-machines.md
- Disk Encryption: guides/disk-encryption.md
- Age Plugins: guides/age-plugins.md
- Secrets management: guides/secrets.md
- Target Host: guides/target-host.md
- Inventory:
- Inventory: guides/inventory.md
- Zerotier VPN: guides/mesh-vpn.md
- Secure Boot: guides/secure-boot.md
- Flake-parts: guides/flake-parts.md
- Authoring:
- clanService: guides/authoring/clanServices/index.md
- Disk Template: guides/authoring/templates/disk/disko-templates.md
- clanModule: guides/authoring/clanModules/index.md
- macOS: guides/macos.md
- Contributing:
- Contribute: guides/contributing/CONTRIBUTING.md
- Contributing: guides/contributing/CONTRIBUTING.md
- Debugging: guides/contributing/debugging.md
- Testing: guides/contributing/testing.md
- Writing a Service Module: guides/services/community.md
- 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
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
- Disk id: guides/migrations/disk-id.md
- macOS: guides/macos.md
- Concepts:
- Inventory: concepts/inventory.md
- Generators: concepts/generators.md
- Autoincludes: concepts/autoincludes.md
- Reference:
- Overview: reference/index.md
- Clan Options: options.md
- Services:
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- Interface for making Services: reference/clanServices/clan-service-author-interface.md
- Modules:
- Overview:
- reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- API: reference/clanServices/clan-service-author-interface.md
- CLI:
- Overview: reference/cli/index.md
- reference/cli/backups.md
- reference/cli/facts.md
- reference/cli/flakes.md
- reference/cli/flash.md
- reference/cli/machines.md
- reference/cli/select.md
- reference/cli/secrets.md
- reference/cli/show.md
- reference/cli/ssh.md
- reference/cli/state.md
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- clan.core (NixOS Options):
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Decisions:
- Architecture Decisions: decisions/README.md
- 01-clanModules: decisions/01-ClanModules.md
- 02-clan-api: decisions/02-clan-api.md
- 03-adr-numbering-process: decisions/03-adr-numbering-process.md
- 04-fetching-nix-from-python: decisions/04-fetching-nix-from-python.md
- 05-deployment-parameters: decisions/05-deployment-parameters.md
- Template: decisions/_template.md
- Glossary: reference/glossary.md
- Developer-api: api.md
- Modules (deprecated):
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
@@ -145,50 +188,6 @@ nav:
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- CLI:
- Overview: reference/cli/index.md
- reference/cli/backups.md
- reference/cli/facts.md
- reference/cli/flakes.md
- reference/cli/flash.md
- reference/cli/machines.md
- reference/cli/select.md
- reference/cli/secrets.md
- reference/cli/show.md
- reference/cli/ssh.md
- reference/cli/state.md
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- NixOS Modules:
- clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Nix API:
- clan: reference/nix-api/clan.md
- Inventory: reference/nix-api/inventory.md
- Glossary: reference/glossary.md
- Decisions:
- Architecture Decisions: decisions/README.md
- 01-clanModules: decisions/01-ClanModules.md
- 02-clan-api: decisions/02-clan-api.md
- 03-adr-numbering-process: decisions/03-adr-numbering-process.md
- 04-fetching-nix-from-python: decisions/04-fetching-nix-from-python.md
- 05-deployment-parameters: decisions/05-deployment-parameters.md
- Template: decisions/_template.md
- Options: options.md
- Developer:
- Introduction: intern/index.md
- API: intern/api.md
docs_dir: site
site_dir: out
@@ -246,3 +245,6 @@ plugins:
- search
- macros
- redoc-tag
- redirects:
redirect_maps:
guides/getting-started/secrets.md: concepts/generators.md

View File

@@ -40,6 +40,7 @@ pkgs.stdenv.mkDerivation {
mkdocs-material
mkdocs-macros
mkdocs-redoc-tag
mkdocs-redirects
]);
configurePhase = ''
pushd docs

View File

@@ -114,9 +114,6 @@
in
{
options = {
_ = mkOption {
type = types.raw;
};
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
@@ -149,20 +146,29 @@
};
};
mkScope = name: modules: {
inherit name;
modules = [
{
_module.args = { inherit clanLib; };
_file = "docs mkScope";
}
{ noInstanceOptions = true; }
../../../lib/modules/inventoryClass/interface.nix
] ++ mapAttrsToList fakeInstanceOptions modules;
urlPrefix = "https://github.com/nix-community/dream2nix/blob/main/";
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
] ++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
in
{
# Uncomment for debugging
# legacyPackages.docModules = lib.evalModules {
# modules = docModules;
# };
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
docs-options =
(privateInputs.nuschtos or inputs.nuschtos)
@@ -171,7 +177,13 @@
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
scopes = [
{
name = "Clan";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
};
};
};

View File

@@ -193,7 +193,7 @@ def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
def module_nix_usage(module_name: str) -> str:
return f"""## Usage via Nix
**This module can be also imported directly in your nixos configuration. Although it is recommended to use the [inventory](../../reference/nix-api/inventory.md) interface if available.**
**This module can be also imported directly in your nixos configuration. Although it is recommended to use the [inventory](../../concepts/inventory.md) interface if available.**
Some modules are considered 'low-level' or 'expert modules' and are not available via the inventory interface.
@@ -373,7 +373,7 @@ This module can be used via predefined roles
"""
Every role has its own configuration options, which are each listed below.
For more information, see the [inventory guide](../../guides/inventory.md).
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.
@@ -402,7 +402,7 @@ certain option types restricted to enable configuration through a graphical
interface.
!!! note "🔹"
Modules with this indicator support the [inventory](../../guides/inventory.md) feature.
Modules with this indicator support the [inventory](../../concepts/inventory.md) feature.
"""
@@ -465,6 +465,10 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
for module_name, module_info in service_links.items():
# Skip specific modules that are not ready for documentation
if module_name in ["internet", "tor"]:
continue
output = f"# {module_name}\n\n"
# output += f"`clan.modules.{module_name}`\n"
output += f"*{module_info['manifest']['description']}*\n"
@@ -675,86 +679,6 @@ def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
return f"{to_md_li(module_name, frontmatter)}\n\n"
def produce_build_clan_docs() -> None:
if not BUILD_CLAN_PATH:
msg = f"Environment variables are not set correctly: BUILD_CLAN_PATH={BUILD_CLAN_PATH}. Expected a path to the optionsJSON"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
output = """# Clan
This provides an overview of the available arguments of the `clan` interface.
Each attribute is documented below
- **clan-core.lib.clan**: A function that takes an attribute set.
??? example "clan Example"
```nix
clan {
self = self;
machines = {
jon = { };
sara = { };
};
};
```
- **clan with flake-parts**: Import the FlakeModule
After importing the FlakeModule you can define your `clan` as a flake attribute
All attribute can be defined via `clan.*`
Further information see: [flake-parts](../../guides/flake-parts.md) guide.
??? example "flake-parts Example"
```nix
flake-parts.lib.mkFlake { inherit inputs; } ({
systems = [];
imports = [
clan-core.flakeModules.default
];
clan = {
machines = {
jon = { };
sara = { };
};
};
});
```
"""
with Path(BUILD_CLAN_PATH).open() as f:
options: dict[str, dict[str, Any]] = json.load(f)
split = split_options_by_root(options)
for option_name, options in split.items():
# Skip underscore options
if option_name.startswith("_"):
continue
# Skip inventory sub options
# Inventory model has its own chapter
if option_name.startswith("inventory."):
continue
print(f"[build_clan_docs] Rendering option of {option_name}...")
root = options_to_tree(options)
for option in root.suboptions:
output += options_docs_from_tree(option, init_level=2)
outfile = Path(OUT) / "nix-api/clan.md"
outfile.parent.mkdir(parents=True, exist_ok=True)
with Path.open(outfile, "w") as of:
of.write(output)
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.
@@ -801,7 +725,7 @@ Typically needed by module authors to define roles, behavior and metadata for di
!!! Note
This is not a user-facing documentation, but rather meant as a reference for *module authors*
See: [clanService Authoring Guide](../../guides/authoring/clanServices/index.md)
See: [clanService Authoring Guide](../../guides/services/community.md)
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model
@@ -830,48 +754,6 @@ class Option:
suboptions: list["Option"] = field(default_factory=list)
def produce_inventory_docs() -> None:
if not BUILD_CLAN_PATH:
msg = f"Environment variables are not set correctly: BUILD_CLAN_PATH={BUILD_CLAN_PATH}. Expected a path to the optionsJSON"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
output = """# Inventory
This provides an overview of the available attributes of the `inventory` model.
It can be set via the `inventory` attribute of the [`clan`](./clan.md#inventory) function, or via the [`clan.inventory`](./clan.md#inventory) attribute of flake-parts.
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model
with Path(BUILD_CLAN_PATH).open() as f:
options: dict[str, dict[str, Any]] = json.load(f)
clan_root_option = options_to_tree(options)
# Find the inventory options
inventory_opt: None | Option = None
for opt in clan_root_option.suboptions:
if opt.name == "inventory":
inventory_opt = opt
break
if not inventory_opt:
print("No inventory options found.")
exit(1)
# Render the inventory options
# This for loop excludes the root node
for option in inventory_opt.suboptions:
output += options_docs_from_tree(option, init_level=2)
outfile = Path(OUT) / "nix-api/inventory.md"
outfile.parent.mkdir(parents=True, exist_ok=True)
with Path.open(outfile, "w") as of:
of.write(output)
def option_short_name(option_name: str) -> str:
parts = option_name.split(".")
short_name = ""
@@ -980,9 +862,6 @@ def options_docs_from_tree(
if __name__ == "__main__": #
produce_clan_core_docs()
produce_build_clan_docs()
produce_inventory_docs()
produce_clan_service_author_docs()
produce_clan_modules_docs()

View File

@@ -0,0 +1,15 @@
Clan automatically imports the following files from a directory and registers them.
## Machine registration
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
!!! info "Automatically loaded files"
The following files are loaded automatically for each 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).

View File

@@ -1,7 +1,4 @@
!!! Note
Vars is the new secret backend that will soon replace the Facts backend
# Generators
Defining a linux user's password via the nixos configuration previously required running `mkpasswd ...` and then copying the hash back into the nix configuration.
@@ -11,7 +8,7 @@ For a more general explanation of what clan vars are and how it works, see the i
This guide assumes
- Clan is set up already (see [Getting Started](../guides/getting-started/index.md))
- a machine has been added to the clan (see [Adding Machines](./more-machines.md))
- a machine has been added to the clan (see [Adding Machines](../guides/getting-started/add-machines.md))
This section will walk you through the following steps:
@@ -23,7 +20,7 @@ This section will walk you through the following steps:
6. share the root password between machines
7. change the password
## Declare the generator
## Declare a generator
In this example, a `vars` `generator` is used to:

View File

@@ -9,8 +9,6 @@ The inventory logic will automatically derive the modules and configurations to
The following tutorial will walk through setting up a Backup service where the terms `Service` and `Role` will become more clear.
See also: [Inventory API Documentation](../reference/nix-api/inventory.md)
!!! example "Experimental status"
The inventory implementation is not considered stable yet.
We are actively soliciting feedback from users.
@@ -19,7 +17,7 @@ See also: [Inventory API Documentation](../reference/nix-api/inventory.md)
## Prerequisites
- [x] [Add multiple machines](./more-machines.md) to your Clan.
- [x] [Add some machines](../guides/getting-started/add-machines.md) to your Clan.
## Services

View File

@@ -0,0 +1,59 @@
## Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -1,229 +0,0 @@
# Authoring a clanModule
!!! Danger "Will get deprecated soon"
Please consider twice creating new modules in this format
[`clan.service` module](../clanServices/index.md) will be the new standard soon.
This site will guide you through authoring your first module. Explaining which conventions must be followed, such that others will have an enjoyable experience and the module can be used with minimal effort.
!!! Tip
External ClanModules can be ad-hoc loaded via [`clan.inventory.modules`](../../../reference/nix-api/inventory.md#inventory.modules)
## Bootstrapping the `clanModule`
A ClanModule is a specific subset of a [NixOS Module](https://nix.dev/tutorials/module-system/index.html), but it has some constraints and might be used via the [Inventory](../../../guides/inventory.md) interface.
In fact a `ClanModule` can be thought of as a layer of abstraction on-top of NixOS and/or other ClanModules. It may configure sane defaults and provide an ergonomic interface that is easy to use and can also be used via a UI that is under development currently.
Because ClanModules should be configurable via `json`/`API` all of its interface (`options`) must be serializable.
!!! Tip
ClanModules interface can be checked by running the json schema converter as follows.
`nix build .#legacyPackages.x86_64-linux.schemas.inventory`
If the build succeeds the module is compatible.
## Directory structure
Each module SHOULD be a directory of the following format:
```sh
# Example: borgbackup
clanModules/borgbackup
├── README.md
└── roles
├── client.nix
└── server.nix
```
!!! Tip
`README.md` is always required. See section [Readme](#readme) for further details.
The `roles` folder is strictly required for `features = [ "inventory" ]`.
## Registering the module
=== "User module"
If the module should be ad-hoc loaded.
It can be made available in any project via the [`clan.inventory.modules`](../../../reference/nix-api/inventory.md#inventory.modules) attribute.
```nix title="flake.nix"
# ...
# Sometimes this attribute set is defined in clan.nix
clan-core.lib.clan {
# 1. Add the module to the available clanModules with inventory support
inventory.modules = {
custom-module = ./modules/my_module;
};
# 2. Use the module in the inventory
inventory.services = {
custom-module.instance_1 = {
roles.default.machines = [ "machineA" ];
};
};
};
```
=== "Upstream module"
If the module will be contributed to [`clan-core`](https://git.clan.lol/clan-core)
The clanModule must be registered within the `clanModules` attribute in `clan-core`
```nix title="clanModules/flake-module.nix"
--8<-- "clanModules/flake-module.nix:0:5"
# Register our new module here
# ...
```
## Readme
The `README.md` is a required file for all modules. It MUST contain frontmatter in [`toml`](https://toml.io) format.
```markdown
---
description = "Module A"
---
This is the example module that does xyz.
```
See the [Full Frontmatter reference](../../../reference/clanModules/frontmatter/index.md) further details and all supported attributes.
## Roles
If the module declares to implement `features = [ "inventory" ]` then it MUST contain a roles directory.
Each `.nix` file in the `roles` directory is added as a role to the inventory service.
Other files can also be placed alongside the `.nix` files
```sh
└── roles
├── client.nix
└── server.nix
```
Adds the roles: `client` and `server`
??? Tip "Good to know"
Sometimes a `ClanModule` should be usable via both clan's `inventory` concept but also natively as a NixOS module.
> In the long term, we want most modules to implement support for the inventory,
> but we are also aware that there are certain low-level modules that always serve as a backend for other higher-level `clanModules` with inventory support.
> These modules may not want to implement inventory interfaces as they are always used directly by other modules.
This can be achieved by placing an additional `default.nix` into the root of the ClanModules directory as shown:
```sh
# ModuleA
├── README.md
├── default.nix
└── roles
└── default.nix
```
```nix title="default.nix"
{...}:{
imports = [ ./roles/default.nix ];
}
```
By utilizing this pattern the module (`moduleA`) can then be imported into any regular NixOS module via:
```nix
{...}:{
imports = [ clanModules.moduleA ];
}
```
## Adding configuration options
While we recommend to keep the interface as minimal as possible and deriving all required information from the `roles` model it might sometimes be required or convenient to expose customization options beyond `roles`.
The following shows how to add options to your module.
**It is important to understand that every module has its own namespace where it should declare options**
**`clan.{moduleName}`**
???+ Example
The following example shows how to register options in the module interface
and how it can be set via the inventory
```nix title="/default.nix"
custom-module = ./modules/custom-module;
```
Since the module is called `custom-module` all of its exposed options should be added to `options.clan.custom-module.*...*`
```nix title="custom-module/roles/default.nix"
{
options = {
clan.custom-module.foo = mkOption {
type = types.str;
default = "bar";
};
};
}
```
If the module is [registered](#registering-the-module).
Configuration can be set as follows.
```nix title="flake.nix"
# Sometimes this attribute set is defined in clan.nix
clan-core.lib.clan {
inventory.services = {
custom-module.instance_1 = {
roles.default.machines = [ "machineA" ];
roles.default.config = {
# All configuration here is scoped to `clan.custom-module`
foo = "foobar";
};
};
};
}
```
## Organizing the ClanModule
Each `{role}.nix` is included into the machine if the machine is declared to have the role.
For example
```nix
roles.client.machines = ["MachineA"];
```
Then `roles/client.nix` will be added to the machine `MachineA`.
This behavior makes it possible to split the interface and common code paths when using multiple roles.
In the concrete example of `borgbackup` this allows a `server` to declare a different interface than the corresponding `client`.
The client offers configuration option, to exclude certain local directories from being backed up:
```nix title="roles/client.nix"
# Example client interface
options.clan.borgbackup.exclude = ...
```
The server doesn't offer any configuration option. Because everything is set-up automatically.
```nix title="roles/server.nix"
# Example server interface
options.clan.borgbackup = {};
```
Assuming that there is a common code path or a common interface between `server` and `client` this can be structured as:
```nix title="roles/server.nix, roles/client.nix"
{...}: {
# ...
imports = [ ../common.nix ];
}
```

View File

@@ -138,7 +138,7 @@ You can use services exposed by Clans core module library, `clan-core`.
You can also author your own `clanService` modules.
🔗 Learn how to write your own service: [Authoring a clanService](../guides/authoring/clanServices/index.md)
🔗 Learn how to write your own service: [Authoring a service](../guides/services/community.md)
You might expose your service module from your flake — this makes it easy for other people to also use your module in their clan.
@@ -154,6 +154,6 @@ You might expose your service module from your flake — this makes it easy for
## Whats Next?
* [Author your own clanService →](../guides/authoring/clanServices/index.md)
* [Author your own clanService →](../guides/services/community.md)
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.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](../reference/nix-api/clan.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.md) available within `mkFlake`.
```nix
{

View File

@@ -6,7 +6,7 @@ Machines can be added using the following methods
- Editing machines/`machine_name`/configuration.nix (automatically included if it exists)
- `clan machines create` (imperative)
See the complete [list](../../guides/more-machines.md#automatic-registration) of auto-loaded files.
See the complete [list](../../concepts/autoincludes.md) of auto-loaded files.
## Create a machine

View File

@@ -41,7 +41,7 @@ To learn more: [Guide about clanService](../clanServices.md)
```
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
Or read [authoring/clanServices](../../guides/services/community.md) if you want to bring your own
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.

View File

@@ -57,7 +57,7 @@ For more information see [clanService/users](../../reference/clanServices/users.
Some people like to define a `users` folder in their repository root.
That allows to bind all user specific logic to a single place (`default.nix`)
Which can be imported into individual machines to make the user avilable on that machine.
Which can be imported into individual machines to make the user available on that machine.
```bash
.
@@ -107,7 +107,7 @@ We can use this property of clan services to bind a nixosModule to the user, whi
}
```
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
1. Type `path` or `string`: Must point to a separate file. Inlining a module is not possible
!!! Note "This is inspiration"
Our community might come up with better solutions soon.

View File

@@ -8,7 +8,6 @@ Now that you have created a machines, added some services and setup secrets. Thi
- [x] RAM > 2GB
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
## Physical Hardware
@@ -18,7 +17,7 @@ Steps:
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
- Note down a reachable ip address (*ipv4*, *ipv6* or *tor*)
---
@@ -169,7 +168,7 @@ Re-run the command with the correct disk:
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
```
Should now be succesfull
Should now be successful
```shellSession
Applied disk template 'single-disk' to machine 'jon'

View File

@@ -59,7 +59,7 @@ Enter a *name*, confirm with *enter*. A directory with that name will be created
## Explore the Project Structure
Take a lookg at all project files:
Take a look at all project files:
```bash
cd my-clan
@@ -125,11 +125,10 @@ To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix
You can continue with **any** of the following steps at your own pace:
- [x] [Install Nix & Clan CLI](./index.md)
- [x] [Initialize Clan](./index.md#initialize-your-project)
- [x] [Initialize Clan](./index.md#add-clan-cli-to-your-shell)
- [ ] [Create USB Installer (optional)](./installer.md)
- [ ] [Add Machines](./add-machines.md)
- [ ] [Add a User](./add-user.md)
- [ ] [Add Services](./add-services.md)
- [ ] [Configure Secrets](./secrets.md)
- [ ] [Deploy](./deploy.md) - Requires configured secrets
- [ ] [Setup CI (optional)](./check.md)

View File

@@ -1,179 +0,0 @@
Setting up secrets is **Required** for any *machine deployments* or *vm runs* - You need to complete the steps: [Create Admin Keypair](#create-your-admin-keypair) and [Add Your Public Key(s)](#add-your-public-keys)
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
This guide will walk you through:
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
### Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
### Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Further: Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -7,7 +7,7 @@ This guide explains how to manage macOS machines using Clan.
Currently, Clan supports the following features for macOS:
- `clan machines update` for existing [nix-darwin](https://github.com/nix-darwin/nix-darwin) installations
- Support for [vars](../guides/vars-backend.md)
- Support for [vars](../concepts/generators.md)
## Add Your Machine to Your Clan Flake

View File

@@ -1,7 +1,7 @@
# Migrating from using `clanModules` to `clanServices`
**Audience**: This is a guide for **people using `clanModules`**.
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../authoring/clanServices/index.md)
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../../guides/services/community.md)
## What's Changing?
@@ -35,6 +35,37 @@ services = {
};
```
### Complex Example: Multi-service Setup
```nix
# Old format
services = {
borgbackup.production = {
roles.server.machines = [ "backup-server" ];
roles.server.config = {
directory = "/var/backup/borg";
};
roles.client.tags = [ "backup" ];
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
};
zerotier.company-network = {
roles.controller.machines = [ "network-controller" ];
roles.moon.machines = [ "moon-1" "moon-2" ];
roles.peer.tags = [ "nixos" ];
};
sshd.internal = {
roles.server.tags = [ "nixos" ];
roles.client.tags = [ "nixos" ];
config.certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
```
---
## ✅ After: New `instances` Definition with `clanServices`
@@ -70,6 +101,56 @@ instances = {
};
```
### Complex Example Migrated
```nix
# New format
instances = {
borgbackup-production = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.server.machines."backup-server" = { };
roles.server.settings = {
directory = "/var/backup/borg";
};
roles.client.tags.backup = { };
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
};
zerotier-company-network = {
module = {
name = "zerotier";
input = "clan-core";
};
roles.controller.machines."network-controller" = { };
roles.moon.machines."moon-1".settings = {
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
};
roles.moon.machines."moon-2".settings = {
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
};
roles.peer.tags.nixos = { };
};
sshd-internal = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.nixos = { };
roles.client.tags.nixos = { };
roles.client.settings = {
certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
};
```
---
## Steps to Migrate
@@ -131,6 +212,33 @@ roles.default.machines."test-inventory-machine".settings = {
};
```
### Important Type Changes
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
```nix
# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];
# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };
```
### Handling Multiple Machines/Tags
When you need to assign multiple machines or tags to a role:
```nix
# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];
# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };
```
---
!!! Warning
@@ -138,8 +246,89 @@ roles.default.machines."test-inventory-machine".settings = {
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
## Troubleshooting Common Migration Errors
### Error: "not of type `attribute set of (submodule)`"
This error occurs when using lists instead of attribute sets for tags or machines:
```
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
```
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
### Error: "unsupported attribute `module`"
This error indicates the module structure is incorrect:
```
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
```
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
### Error: "attribute 'pkgs' missing"
This suggests the instance configuration is trying to use imports incorrectly:
```
error: attribute 'pkgs' missing
```
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
### Removed Features
The following features from the old `services` format are no longer supported in `instances`:
- Top-level `config` attribute (use `roles.<role>.settings` instead)
- Direct module imports (use the `module` declaration instead)
### extraModules Support
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
**Old format (string paths relative to clan root):**
```nix
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
```
**New format (NixOS modules):**
```nix
# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];
# Or inline module definition
roles.client.extraModules = [
{ config, ... }: {
# Your module configuration here
}
];
```
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
**Alternative: Using @clan/importer**
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
```nix
instances = {
my-importer = {
module.name = "@clan/importer";
module.input = "clan-core";
roles.default.tags.my-tag = { };
roles.default.extraModules = [ self.nixosModules.myModule ];
};
};
```
## Further reference
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
* [Inventory Concept](../../concepts/inventory.md)
* [Authoring a 'clan.service' module](../../guides/services/community.md)
* [ClanServices](../clanServices.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)

View File

@@ -3,7 +3,7 @@
For a high level overview about `vars` see our [blog post](https://clan.lol/blog/vars/).
This guide will help you migrate your modules that still use our [`facts`](../../guides/secrets.md) backend
to the [`vars`](../../guides/vars-backend.md) backend.
to the [`vars`](../../concepts/generators.md) backend.
The `vars` [module](../../reference/clan.core/vars.md) and the clan [command](../../reference/cli/vars.md) work in tandem, they should ideally be kept in sync.

View File

@@ -1,50 +0,0 @@
Clan has two general methods of adding machines:
- **Automatic**: Detects every folder in the `machines` folder.
- **Declarative**: Explicit declarations in Nix.
## Automatic registration
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
!!! info "Automatically loaded files"
The following files are loaded automatically for each 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).
## Manual declaration
Machines can be added via [`clan.inventory.machines`](../guides/inventory.md) or in `clan.machines`, which allows for defining NixOS options.
=== "**Individual Machine Configuration**"
```{.nix}
clan-core.lib.clan {
machines = {
"jon" = {
# Any valid nixos config
};
};
}
```
=== "**Inventory Configuration**"
```{.nix}
clan-core.lib.clan {
inventory = {
machines = {
"jon" = {
# Inventory can set tags and other metadata
tags = [ "zone1" ];
deploy.targetHost = "root@jon";
};
};
};
}
```

View File

@@ -1,25 +1,141 @@
If you want to know more about how to save and share passwords in your clan read further!
This article provides an overview over the underlying secrets system which is used by [Vars](../concepts/generators.md).
Under most circumstances you should use [Vars](../concepts/generators.md) directly instead.
### Adding a Secret
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
Manually interacting with secrets via `clan secrets [set|remove]`, etc may break the integrity of your `Vars` state.
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
## Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
## Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Adding a Secret
```shellSession
clan secrets set mysecret
Paste your secret:
```
### Retrieving a Stored Secret
## Retrieving a Stored Secret
```bash
clan secrets get mysecret
```
### List all Secrets
## List all Secrets
```bash
clan secrets list
```
### NixOS integration
## NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
@@ -37,7 +153,7 @@ In your nixos configuration you can get a path to secrets like this `config.sops
}
```
### Assigning Access
## Assigning Access
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.

View File

@@ -1,16 +1,16 @@
# Authoring a 'clan.service' module
!!! Tip
This is the successor format to the older [clanModules](../clanModules/index.md)
This is the successor format to the older [clanModules](../../reference/clanModules/index.md)
While some features might still be missing we recommend to adapt this format early and give feedback.
## Service Module Specification
This section explains how to author a clan service module.
We discussed the initial architecture in [01-clan-service-modules](../../../decisions/01-ClanModules.md) and decided to rework the format.
We discussed the initial architecture in [01-clan-service-modules](../../decisions/01-ClanModules.md) and decided to rework the format.
For the full specification and current state see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
For the full specification and current state see: **[Service Author Reference](../../reference/clanServices/clan-service-author-interface.md)**
### A Minimal module
@@ -52,7 +52,7 @@ The imported module file must fulfill at least the following requirements:
}
```
For more attributes see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
For more attributes see: **[Service Author Reference](../../reference/clanServices/clan-service-author-interface.md)**
### Adding functionality to the module
@@ -266,6 +266,6 @@ The benefit of this approach is that downstream users can override the value of
## Further
- [Reference Documentation for Service Authors](../../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)
- [Reference Documentation for Service Authors](../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../guides/migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../decisions/01-ClanModules.md)

View File

@@ -4,7 +4,21 @@ hide:
- toc
---
# :material-home: Welcome to **Clan**'s documentation
# :material-home: What is Clan?
[Clan](https://clan.lol/) is a peer-to-peer computer management framework that
empowers you to reclaim control over your digital computing experience. Built on
NixOS, Clan provides a unified interface for managing networks of machines with
automated [secret management](./guides/secrets.md), secure [mesh VPN
connectivity](./guides/mesh-vpn.md), and customizable installation images. Whether
you're running a homelab or building decentralized computing infrastructure,
Clan simplifies configuration management while restoring your independence from
closed computing ecosystems.
At the heart of Clan are [Clan Services](./reference/clanServices/index.md) - the core
concept that enables you to add functionality across multiple machines in your
network. While Clan ships with essential core services, you can [create custom
services](./guides/clanServices.md) tailored to your specific needs.
[Getting Started](./guides/getting-started/index.md){ .md-button }
@@ -14,47 +28,49 @@ hide:
<div class="grid cards" markdown>
- [Adding more machines](./guides/more-machines.md)
- [Create a Machine](./guides/getting-started/add-machines.md)
---
Learn how Clan automatically includes machines and Nix files.
How to create your first machine
- [Vars Backend](./guides/vars-backend.md)
- [macOS](./guides/macos.md)
---
Learn how to manage secrets with vars.
- [Inventory](./guides/inventory.md)
---
Clan's declaration format for running **services** on one or multiple **machines**.
- [Flake-parts](./guides/flake-parts.md)
---
Use Clan with [https://flake.parts/]()
How to manage macOS machines with nix-darwin
- [Contribute](./guides/contributing/CONTRIBUTING.md)
---
Discover how to set up a development environment to contribute to Clan!
How to set up a development environment
- [macOS machines](./guides/macos.md)
</div>
## Concepts
Explore the foundational ideas.
<div class="grid cards" markdown>
- [Generators](./concepts/generators.md)
---
Manage macOS machines with nix-darwin
Learn about Generators
- [Inventory](./concepts/inventory.md)
---
Learn about Inventory
</div>
## API Reference
**Reference API Documentation**
Technical reference for Clan's CLI and Nix modules
<div class="grid cards" markdown>
@@ -62,29 +78,22 @@ hide:
---
The `clan` CLI command
Command-line interface.
Full reference for the `clan` CLI tool.
- [Service Modules](./reference/clanServices/index.md)
---
An overview of available service modules
Overview of built-in service modules that provide composable functionality across machines.
- [Core](./reference/clan.core/index.md)
- [Core NixOS-module](./reference/clan.core/index.md)
---
The clan core nix module.
This is imported when using clan and is the basis of the extra functionality
that can be provided.
- [(Legacy) Modules](./reference/clanModules/index.md)
---
An overview of available clanModules
!!! Example "These will be deprecated soon"
The foundation of Clan's functionality
Reference for the `clan-core` NixOS module — automatically part of any machine to enable Clan's core features.
</div>

View File

@@ -1,25 +0,0 @@
# Developer Documentation
!!! Danger
This documentation is **not** intended for external users. It may contain low-level details and internal-only interfaces.*
Welcome to the internal developer documentation.
This section is intended for contributors, engineers, and internal stakeholders working directly with our system, tooling, and APIs. It provides a technical overview of core components, internal APIs, conventions, and patterns that support the platform.
Our goal is to make the internal workings of the system **transparent, discoverable, and consistent** — helping you contribute confidently, troubleshoot effectively, and build faster.
## What's Here?
!!! note "docs migration ongoing"
- [ ] **API Reference**: 🚧🚧🚧 Detailed documentation of internal API functions, inputs, and expected outputs. 🚧🚧🚧
- [ ] **System Concepts**: Architectural overviews and domain-specific guides.
- [ ] **Development Guides**: How to test, extend, or integrate with key components.
- [ ] **Design Notes**: Rationales behind major design decisions or patterns.
## Who is This For?
* Developers contributing to the platform
* Engineers debugging or extending internal systems
* Anyone needing to understand **how** and **why** things work under the hood

View File

@@ -4,10 +4,10 @@ This section of the site provides an overview of available options and commands
---
- [Clan Configuration Option](../options.md) - for defining a Clan
- Learn how to use the [Clan CLI](./cli/index.md)
- Explore available services and application [modules](./clanModules/index.md)
- Discover [configuration options](./clan.core/index.md) that manage essential features
- Find descriptions of the [Nix interfaces](./nix-api/clan.md) for defining a Clan
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.
---

View File

@@ -2,6 +2,7 @@
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format("truetype");
@@ -20,3 +21,9 @@
.md-nav__item.md-nav__item--section > label > span {
color: var(--md-typeset-a-color);
}
.md-typeset h4 {
margin: 3em 0 0.5em;
font-weight: bold;
color: #7ebae4;
}

26
flake.lock generated
View File

@@ -13,11 +13,11 @@
]
},
"locked": {
"lastModified": 1752589312,
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
"lastModified": 1753067306,
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
},
"original": {
"type": "tarball",
@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1752541678,
"narHash": "sha256-dyhGzkld6jPqnT/UfGV2oqe7tYn7hppAqFvF3GZTyXY=",
"lastModified": 1753140376,
"narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=",
"owner": "nix-community",
"repo": "disko",
"rev": "2bf3421f7fed5c84d9392b62dcb9d76ef09796a7",
"rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"type": "github"
},
"original": {
@@ -181,11 +181,11 @@
]
},
"locked": {
"lastModified": 1752055615,
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
"lastModified": 1753006367,
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
"type": "github"
},
"original": {

View File

@@ -30,7 +30,6 @@
inputs = {
flake-parts.follows = "flake-parts";
nixpkgs.follows = "nixpkgs";
systems.follows = "systems";
treefmt-nix.follows = "treefmt-nix";
};
};

View File

@@ -78,7 +78,87 @@ in
internal = true;
visible = false;
type = types.deferredModule;
default = { };
default = {
options.networking = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
priority = lib.mkOption {
type = lib.types.int;
default = 1000;
description = ''
priority with which this network should be tried.
higher priority means it gets used earlier in the chain
'';
};
module = lib.mkOption {
# type = lib.types.enum [
# "clan_lib.network.direct"
# "clan_lib.network.tor"
# ];
type = lib.types.str;
default = "clan_lib.network.direct";
description = ''
the technology this network uses to connect to the target
This is used for userspace networking with socks proxies.
'';
};
# should we call this machines? hosts?
peers = lib.mkOption {
# <name>
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
SSHOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
host = lib.mkOption {
description = '''';
type = lib.types.attrTag {
plain = lib.mkOption {
type = lib.types.str;
description = ''
a plain value, which can be read directly from the config
'';
};
var = lib.mkOption {
type = lib.types.submodule {
options = {
machine = lib.mkOption {
type = lib.types.str;
example = "jon";
};
generator = lib.mkOption {
type = lib.types.str;
example = "tor-ssh";
};
file = lib.mkOption {
type = lib.types.str;
example = "hostname";
};
};
};
};
};
};
};
}
)
);
};
};
}
);
};
};
description = ''
A module that is used to define the module of flake level exports -
@@ -149,8 +229,8 @@ in
};
inventory = lib.mkOption {
type = types.submodule {
imports = [
type = types.submoduleWith {
modules = [
{
_module.args = { inherit clanLib; };
_file = "clan interface";
@@ -275,8 +355,6 @@ in
templates = lib.mkOption { type = lib.types.raw; };
machines = lib.mkOption { type = lib.types.raw; };
clan-cli = lib.mkOption { type = lib.types.raw; };
};
};
};

View File

@@ -247,7 +247,7 @@ in
{
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs;
inherit flakeInputs directory;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};
@@ -273,9 +273,6 @@ in
# machine specifics
machines = configsPerSystem;
# export clan-cli in clanInternals to tie the CLI to the flake
clan-cli = builtins.mapAttrs (_sys: pkgs: pkgs.clan-cli) clan-core.packages;
};
};
}

View File

@@ -7,8 +7,29 @@
}:
rec {
buildClan =
# TODO: Once all templates and docs are migrated add: lib.warn "'buildClan' is deprecated. Use 'clan-core.lib.clan' instead"
module: (clan module).config;
module:
lib.warn ''
==================== DEPRECATION NOTICE ====================
Please migrate
from: 'clan = inputs.<clan-core>.lib.buildClan'
to : 'clan = inputs.<clan-core>.lib.clan'
in your flake.nix.
Please also migrate
from: 'inherit (clan) nixosConfigurations clanInternals; '
to : "
inherit (clan.config) nixosConfigurations clanInternals;
clan = clan.config;
"
in your flake.nix.
Reason:
- Improves consistency between flake-parts and non-flake-parts users.
- It also allows us to use the top level attribute 'clan' to expose
attributes that can be used for cross-clan functionality.
============================================================
'' (clan module).config;
clan =
{

View File

@@ -1,4 +1,8 @@
# Wraps all services in one fixed point module
{
# TODO: consume directly from clan.config
directory,
}:
{
lib,
config,
@@ -29,6 +33,8 @@ in
{
_module.args._ctx = [ name ];
_module.args.exports' = config.exports;
_module.args.directory = directory;
}
)
./service-module.nix
@@ -48,6 +54,7 @@ in
{
options = {
instances = lib.mkOption {
default = { };
# instances.<instanceName>...
type = types.attrsOf (submoduleWith {
modules = [
@@ -57,6 +64,7 @@ in
};
# instances.<machineName>...
machines = lib.mkOption {
default = { };
type = types.attrsOf (submoduleWith {
modules = [
config.exportsModule
@@ -69,8 +77,5 @@ in
};
default = { };
};
debug = mkOption {
default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
};
};
}

View File

@@ -24,6 +24,7 @@ in
flakeInputs,
# The clan inventory
inventory,
directory,
clanCoreModules,
prefix ? [ ],
exportsModule,
@@ -128,7 +129,7 @@ in
_ctx = prefix;
};
modules = [
./all-services-wrapper.nix
(import ./all-services-wrapper.nix { inherit directory; })
] ++ modules;
};

View File

@@ -2,6 +2,7 @@
lib,
config,
_ctx,
directory,
...
}:
let
@@ -212,7 +213,7 @@ in
options.extraModules = lib.mkOption {
default = [ ];
type = types.listOf (types.deferredModule);
type = types.listOf (types.either types.deferredModule types.str);
};
})
];
@@ -755,10 +756,14 @@ in
instanceRes
// {
nixosModule = {
imports = [
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
] ++ instanceCfg.roles.${roleName}.extraModules;
imports =
[
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
]
++ (map (
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
) instanceCfg.roles.${roleName}.extraModules);
};
}

View File

@@ -45,6 +45,7 @@ let
};
in
clanLib.inventory.mapInstances {
directory = ./.;
clanCoreModules = { };
flakeInputs = flakeInputsFixture;
inherit inventory;
@@ -52,6 +53,7 @@ let
};
in
{
extraModules = import ./extraModules.nix { inherit clanLib; };
exports = import ./exports.nix { inherit lib clanLib; };
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
test_simple =

View File

@@ -0,0 +1,33 @@
{ clanLib }:
let
clan = clanLib.clan {
self = { };
directory = ./.;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
# A module that adds exports perMachine
modules.A = {
manifest.name = "A";
roles.peer = { };
};
inventory = {
instances.A = {
module.input = "self";
roles.peer.tags.all = { };
roles.peer.extraModules = [ ./oneOption.nix ];
};
};
};
in
{
test_1 = {
inherit clan;
expr = clan.config.nixosConfigurations.jon.config.testDebug;
expected = 42;
};
}

View File

@@ -0,0 +1,6 @@
{ lib, ... }:
{
options.testDebug = lib.mkOption {
default = 42;
};
}

View File

@@ -142,7 +142,7 @@ in
- The module MUST have at least `features = [ "inventory" ]` in the frontmatter section.
- The module MUST have a subfolder `roles` with at least one `{roleName}.nix` file.
For further information see: [Module Authoring Guide](../../guides/authoring/clanServices/index.md).
For further information see: [Module Authoring Guide](../../guides/services/community.md).
???+ example
```nix
@@ -179,8 +179,7 @@ in
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/guides/clanServices/
And: https://docs.clan.lol/guides/authoring/clanServices/
See: https://docs.clan.lol/guides/services/community/
'' moduleSet;
};

View File

@@ -313,6 +313,18 @@ class Machine:
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.
"""
def check_file(_last_try: bool) -> bool:
result = self.execute(f"test -e {filename}")
return result.returncode == 0
with self.nested(f"waiting for file '{filename}'"):
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.
@@ -407,6 +419,14 @@ def setup_filesystems(container: ContainerInfo) -> None:
Path("/etc/os-release").touch()
Path("/etc/machine-id").write_text("a5ea3f98dedc0278b6f3cc8c37eeaeac")
container.nix_store_dir.mkdir(parents=True)
# Recreate symlinks
for file in Path("/nix/store").iterdir():
if file.is_symlink():
target = file.readlink()
sym = container.nix_store_dir / file.name
os.symlink(target, sym)
# Read /proc/mounts and replicate every bind mount
with Path("/proc/self/mounts").open() as f:
for line in f:

View File

@@ -31,6 +31,7 @@
The deployment data is now accessed directly from the configuration
instead of being written to a separate JSON file.
'';
defaultText = "error: deployment.json file generation has been removed in favor of direct selectors.";
};
deployment.buildHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
@@ -54,10 +55,10 @@
deployment.nixosMobileWorkaround = lib.mkOption {
type = lib.types.bool;
description = ''
if true, the deployment will first do a nixos-rebuild switch
if true, the deployment will first do a nixos-rebuild switch
to register the boot profile the command will fail applying it to the running system
which is why afterwards we execute a nixos-rebuild test to apply
the new config without having to reboot.
which is why afterwards we execute a nixos-rebuild test to apply
the new config without having to reboot.
This is a nixos-mobile deployment bug and will be removed in the future
'';
default = false;

View File

@@ -29,10 +29,7 @@ def _get_lib_names() -> list[str]:
msg = f"Unsupported architecture: {machine}"
raise RuntimeError(msg)
if system == "darwin":
if machine == "arm64":
return ["libwebview.dylib"]
msg = "Not supported"
raise RuntimeError(msg)
return ["libwebview.dylib"]
# linux
return ["libwebview.so"]

View File

@@ -1,39 +0,0 @@
version: "0.5"
processes:
# App Dev
clan-app-ui:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm install
vite
ready_log_line: "VITE"
clan-app:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app
./bin/clan-app --debug --content-uri http://localhost:3000
depends_on:
clan-app-ui:
condition: "process_log_ready"
is_foreground: true
ready_log_line: "Debug mode enabled"
# Storybook Dev
storybook:
namespace: "storybook"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm run storybook-dev -- --ci
ready_log_line: "started"
luakit:
namespace: "storybook"
command: "luakit http://localhost:6006"
depends_on:
storybook:
condition: "process_log_ready"

View File

@@ -21,6 +21,12 @@ buildNpmPackage (_finalAttrs: {
mkdir -p api
cp -r ${clan-ts-api}/* api
cp -r ${fonts} ".fonts"
# only needed for the next couple weeks to make sure this file doesn't make it back into the git history
if [[ -f "${./ui}/src/routes/Onboarding/background.jpg" ]]; then
echo "background.jpg found, exiting"
exit 1
fi
'';
# todo figure out why this fails only inside of Nix

View File

@@ -3,7 +3,7 @@ import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
const config: StorybookConfig = {
framework: "@kachurun/storybook-solid-vite",
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
addons: [
"@storybook/addon-links",
"@storybook/addon-docs",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -17,6 +17,7 @@
"@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",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",
@@ -53,7 +54,6 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -360,22 +360,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
@@ -1552,13 +1536,6 @@
"node": ">= 8"
}
},
"node_modules/@nothing-but/utils": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@nothing-but/utils/-/utils-0.17.0.tgz",
"integrity": "sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.5.0.tgz",
@@ -1813,64 +1790,6 @@
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@solid-devtools/debugger": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.1.tgz",
"integrity": "sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-devtools/shared": "^0.20.0",
"@solid-primitives/bounds": "^0.1.1",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/keyboard": "^1.3.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-devtools/shared": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@solid-devtools/shared/-/shared-0.20.0.tgz",
"integrity": "sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/media": "^2.3.1",
"@solid-primitives/refs": "^1.1.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/styles": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-primitives/bounds": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/bounds/-/bounds-0.1.3.tgz",
"integrity": "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/resize-observer": "^2.1.3",
"@solid-primitives/static-store": "^0.1.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/event-listener": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz",
@@ -1883,21 +1802,6 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyboard": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.3.tgz",
"integrity": "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyed": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.2.tgz",
@@ -1985,16 +1889,6 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/scheduled": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/scheduled/-/scheduled-1.5.2.tgz",
"integrity": "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/static-store": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz",
@@ -2028,20 +1922,6 @@
}
}
},
"node_modules/@solid-primitives/styles": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.1.2.tgz",
"integrity": "sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/trigger": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.2.2.tgz",
@@ -2281,9 +2161,19 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
"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==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"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==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2291,12 +2181,12 @@
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.81.5.tgz",
"integrity": "sha512-VqVXaxiJIsKA6B45uApF+RUD3g8Roj/vdAuGpHMjR+RyHqlyQ+hOwgmALkzlbkbIaWCQi8CJOvrbU6WOBuMOxA==",
"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==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.81.5"
"@tanstack/query-core": "5.83.0"
},
"funding": {
"type": "github",
@@ -2306,6 +2196,23 @@
"solid-js": "^1.6.0"
}
},
"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==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.83.0",
"solid-js": "^1.6.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -6996,29 +6903,6 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/solid-devtools": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/solid-devtools/-/solid-devtools-0.34.3.tgz",
"integrity": "sha512-ZQua959n+Zu3sLbm9g0IRjYUb1YYlYbu83PWLRoKbSsq0a3ItQNhnS2OBU7rQNmOKZiMexNo9Z3izas9BcOKDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.6",
"@solid-devtools/debugger": "^0.28.1",
"@solid-devtools/shared": "^0.20.0"
},
"peerDependencies": {
"solid-js": "^1.9.0",
"vite": "^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/solid-js": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz",

View File

@@ -52,7 +52,6 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -73,6 +72,7 @@
"@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",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",

View File

@@ -138,6 +138,10 @@
transition: all 0.5s ease;
}
}
& > span.typography {
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
}
}
/* button group */

View File

@@ -9,6 +9,7 @@ import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/Form/Checkbox";
import { FieldProps } from "./Field";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
const FieldsetExamples = (props: FieldsetProps) => (
<div class="flex flex-col gap-8">
@@ -26,7 +27,7 @@ const meta = {
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[1024px]": context.args.orientation == "horizontal",
"w-[512px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
@@ -63,6 +64,11 @@ export const Default: Story = {
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<HostFileInput
{...props}
label="Profile pic"
onSelectFile={async () => "/home/foo/bar/baz/fizz/buzz/bla/bizz"}
/>
<Checkbox {...props} label="Accept Terms" required={true} />
</>
),

View File

@@ -1,5 +1,11 @@
div.form-field.host-file {
button {
@apply w-1/2;
@apply w-fit;
}
&.horizontal {
button {
@apply grow max-w-[18rem];
}
}
}

View File

@@ -58,7 +58,7 @@ export type Story = StoryObj<typeof meta>;
export const Bare: Story = {
args: {
onSelectFile: async () => {
return "/home/bob/clans/my-clan";
return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz";
},
input: {
placeholder: "e.g. 11/06/89",

View File

@@ -12,6 +12,8 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
import { FieldProps } from "./Field";
import { Orienter } from "./Orienter";
import { createSignal } from "solid-js";
import { Tooltip } from "@kobalte/core/tooltip";
import { Typography } from "@/src/components/Typography/Typography";
export type HostFileInputProps = FieldProps &
TextFieldRootProps & {
@@ -20,10 +22,21 @@ export type HostFileInputProps = FieldProps &
};
export const HostFileInput = (props: HostFileInputProps) => {
const [value, setValue] = createSignal<string | undefined>(undefined);
const [value, setValue] = createSignal<string>(props.value || "");
let actualInputElement: HTMLInputElement | undefined;
const selectFile = async () => {
setValue(await props.onSelectFile());
try {
console.log("selecting file", props.onSelectFile);
setValue(await props.onSelectFile());
actualInputElement?.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true }),
);
} catch (error) {
console.log("Error selecting file", error);
// todo work out how to display the error
}
};
return (
@@ -33,26 +46,65 @@ export const HostFileInput = (props: HostFileInputProps) => {
ghost: props.ghost,
})}
{...props}
value={value()}
onChange={setValue}
>
<Orienter orientation={props.orientation} align={"start"}>
<Orienter
orientation={props.orientation}
align={props.orientation == "horizontal" ? "center" : "start"}
>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}
{...props}
/>
<TextField.Input {...props.input} hidden={true} />
<TextField.Input
{...props.input}
hidden={true}
value={value()}
ref={(el: HTMLInputElement) => {
actualInputElement = el; // Capture for local use
}}
/>
<Button
hierarchy="secondary"
size={props.size}
startIcon="Folder"
onClick={selectFile}
>
{value() ? value() : "No Selection"}
</Button>
{!value() && (
<Button
hierarchy="secondary"
size={props.size}
startIcon="Folder"
onClick={selectFile}
disabled={props.disabled || props.readOnly}
>
No Selection
</Button>
)}
{value() && (
<Tooltip placement="top">
<Tooltip.Portal>
<Tooltip.Content class="tooltip-content">
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{value()}
</Typography>
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
<Tooltip.Trigger
as={Button}
hierarchy="secondary"
size={props.size}
startIcon="Folder"
onClick={selectFile}
disabled={props.disabled || props.readOnly}
>
{value()}
</Tooltip.Trigger>
</Tooltip>
)}
</Orienter>
</TextField>
);

View File

@@ -22,40 +22,3 @@ div.form-label {
}
}
}
div.tooltip-content {
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
max-width: min(calc(100vw - 16px), 380px);
transform-origin: var(--kb-tooltip-content-transform-origin);
animation: tooltipHide 250ms ease-in forwards;
&[data-expanded] {
animation: tooltipShow 250ms ease-out;
}
&.inverted {
@apply bg-def-2;
}
}
@keyframes tooltipShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes tooltipHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -1,12 +1,11 @@
import { Show } from "solid-js";
import { Typography } from "@/src/components/Typography/Typography";
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import Icon from "@/src/components/Icon/Icon";
import { TextField } from "@kobalte/core/text-field";
import { Checkbox } from "@kobalte/core/checkbox";
import { Combobox } from "@kobalte/core/combobox";
import "./Label.css";
import cx from "classnames";
export type Size = "default" | "s";
@@ -49,31 +48,27 @@ export const Label = (props: LabelProps) => {
{props.label}
</Typography>
{props.tooltip && (
<KTooltip placement="top">
<KTooltip.Trigger>
<Tooltip
placement="top"
inverted={props.inverted}
trigger={
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
<KTooltip.Portal>
<KTooltip.Content
class={cx("tooltip-content", { inverted: props.inverted })}
>
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{props.tooltip}
</Typography>
<KTooltip.Arrow />
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip.Trigger>
</KTooltip>
}
>
<Typography
hierarchy="body"
size="xs"
weight="medium"
inverted={!props.inverted}
>
{props.tooltip}
</Typography>
</Tooltip>
)}
</props.labelComponent>
{props.description && (

View File

@@ -5,7 +5,7 @@ div.orienter {
}
&.horizontal {
@apply flex-row justify-start;
@apply flex-row justify-between gap-0;
& > div.form-label {
@apply w-1/2 shrink;

View File

@@ -1,5 +1,5 @@
div.modal-content {
@apply max-w-[512px];
@apply min-w-[320px] max-w-[512px];
@apply rounded-md;
/* todo replace with a theme() color */
@@ -12,7 +12,7 @@ div.modal-content {
@apply border border-def-2 rounded-tl-md rounded-tr-md;
@apply border-b-def-3;
& > .title {
& > .modal-title {
@apply mx-auto;
}
}

View File

@@ -3,6 +3,7 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
import "./Modal.css";
import { Typography } from "../Typography/Typography";
import Icon from "../Icon/Icon";
import cx from "classnames";
export interface ModalContext {
close(): void;
@@ -13,6 +14,8 @@ export interface ModalProps {
title: string;
onClose: () => void;
children: (ctx: ModalContext) => JSX.Element;
mount?: Node;
class?: string;
}
export const Modal = (props: ModalProps) => {
@@ -20,18 +23,33 @@ export const Modal = (props: ModalProps) => {
return (
<KDialog id={props.id} open={open()} modal={true}>
<KDialog.Portal>
<KDialog.Content class="modal-content">
<KDialog.Portal mount={props.mount}>
<KDialog.Content class={cx("modal-content", props.class)}>
<div class="header">
<Typography class="title" hierarchy="label" family="mono" size="xs">
<Typography
class="modal-title"
hierarchy="label"
family="mono"
size="xs"
>
{props.title}
</Typography>
<KDialog.CloseButton onClick={() => setOpen(false)}>
<KDialog.CloseButton
onClick={() => {
setOpen(false);
props.onClose();
}}
>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</div>
<div class="body">
{props.children({ close: () => setOpen(false) })}
{props.children({
close: () => {
setOpen(false);
props.onClose();
},
})}
</div>
</KDialog.Content>
</KDialog.Portal>

View File

@@ -1,5 +1,5 @@
div.sidebar {
@apply h-full w-auto max-w-60 border-none;
@apply w-60 border-none;
& > div.header {
}

View File

@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { Suspense } from "solid-js";
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";
const defaultClanURI = "/home/brian/clans/my-clan";
const queryData = {
"/home/brian/clans/my-clan": {
details: {
name: "Brian's Clan",
uri: "/home/brian/clans/my-clan",
},
machines: {
europa: {
name: "Europa",
machineClass: "nixos",
},
ganymede: {
name: "Ganymede",
machineClass: "nixos",
},
},
},
"/home/brian/clans/davhau": {
details: {
name: "Dave's Clan",
uri: "/home/brian/clans/davhau",
},
machines: {
callisto: {
name: "Callisto",
machineClass: "nixos",
},
amalthea: {
name: "Amalthea",
machineClass: "nixos",
},
},
},
"/home/brian/clans/mic92": {
details: {
name: "Mic92's Clan",
uri: "/home/brian/clans/mic92",
},
machines: {
thebe: {
name: "Thebe",
machineClass: "nixos",
},
sponde: {
name: "Sponde",
machineClass: "nixos",
},
},
},
};
const staticSections = [
{
title: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
];
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar",
component: Sidebar,
render: () => {
// set history to point to our test clan
const history = createMemoryHistory();
history.set({ value: `/clans/${encodeBase64(defaultClanURI)}` });
// reset local storage and then add each clan
resetStore();
Object.keys(queryData).forEach((uri) => addClanURI(uri));
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => <Suspense>{props.children}</Suspense>}
>
<Route
path="/clans/:clanURI"
component={() => <Sidebar staticSections={staticSections} />}
>
<Route path="/" />
<Route
path="/machines/:machineID"
component={() => <h1>Machine</h1>}
/>
</Route>
</MemoryRouter>
<SolidQueryDevtools initialIsOpen={true} />
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
decorators: [
(Story: StoryObj) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
Object.entries(queryData).forEach(([clanURI, clan]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
clan.machines || {},
);
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
};

View File

@@ -0,0 +1,28 @@
import "./Sidebar.css";
import { SidebarHeader } from "@/src/components/Sidebar/SidebarHeader";
import { SidebarBody } from "@/src/components/Sidebar/SidebarBody";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
title: string;
links: LinkProps[];
}
export interface SidebarProps {
staticSections?: SectionProps[];
}
export const Sidebar = (props: SidebarProps) => {
return (
<>
<div class="sidebar">
<SidebarHeader />
<SidebarBody {...props} />
</div>
</>
);
};

View File

@@ -1,46 +1,60 @@
import "./SidebarNavBody.css";
import "./SidebarBody.css";
import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/Typography/Typography";
import {
MachineProps,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { For } from "solid-js";
import { For, Suspense } from "solid-js";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
import { useMachinesQuery } from "@/src/queries/queries";
import { SidebarProps } from "./Sidebar";
interface MachineProps {
clanURI: string;
machineID: string;
name: string;
status: MachineStatus;
serviceCount: number;
}
const MachineRoute = (props: MachineProps) => (
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.label}
</Typography>
<MachineStatus status={props.status} />
<A href={buildMachinePath(props.clanURI, props.machineID)}>
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.name}
</Typography>
<MachineStatus status={props.status} />
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
</A>
);
export const SidebarNavBody = (props: SidebarNavProps) => {
const sectionLabels = props.extraSections.map((section) => section.label);
export const SidebarBody = (props: SidebarProps) => {
const clanURI = useClanURI();
const machineList = useMachinesQuery(clanURI);
const sectionLabels = (props.staticSections || []).map(
(section) => section.title,
);
// controls which sections are open by default
// we want them all to be open by default
@@ -75,21 +89,27 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={props.clanDetail.machines}>
{(machine) => (
<A href={machine.path}>
<MachineRoute {...machine} />
</A>
)}
</For>
</nav>
<Suspense fallback={"Loading..."}>
<nav>
<For each={Object.entries(machineList.data || {})}>
{([id, machine]) => (
<MachineRoute
clanURI={clanURI}
machineID={id}
name={machine.name || id}
status="Not Installed"
serviceCount={0}
/>
)}
</For>
</nav>
</Suspense>
</Accordion.Content>
</Accordion.Item>
<For each={props.extraSections}>
<For each={props.staticSections}>
{(section) => (
<Accordion.Item class="item" value={section.label}>
<Accordion.Item class="item" value={section.title}>
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Typography
@@ -100,7 +120,7 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
inverted={true}
color="tertiary"
>
{section.label}
{section.title}
</Typography>
<Icon
icon="CaretDown"

View File

@@ -15,10 +15,11 @@ div.sidebar-header {
transition: all 250ms ease-in-out;
div.title {
div.clan-label {
@apply flex items-center gap-2 justify-start;
& > .clan-icon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7;
}
}

View File

@@ -0,0 +1,107 @@
import "./SidebarHeader.css";
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 { useAllClanDetailsQuery } from "@/src/queries/queries";
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
import { clanURIs } from "@/src/stores/clan";
export const SidebarHeader = () => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
// get information about the current active clan
const clanURI = useClanURI();
const allClans = useAllClanDetailsQuery(clanURIs());
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
return (
<div class="sidebar-header">
<Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="clan-label">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{activeClan()?.data?.name.charAt(0).toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{activeClan()?.data?.name}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigateToClan(navigate, clanURI)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={allClans}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() =>
navigateToClan(navigate, clan.data!.uri)
}
>
<Typography
hierarchy="label"
size="xs"
weight="medium"
>
{clan.data?.name}
</Typography>
</DropdownMenu.Item>
</Suspense>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Suspense>
</div>
);
};

View File

@@ -1,109 +0,0 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import {
SidebarNav,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { Suspense } from "solid-js";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const sidebarNavProps: SidebarNavProps = {
clanLinks: [
{ label: "Brian's Clan", path: "/clan/1" },
{ label: "Dave's Clan", path: "/clan/2" },
{ label: "Mic92's Clan", path: "/clan/3" },
],
clanDetail: {
label: "Brian's Clan",
settingsPath: "/clan/1/settings",
machines: [
{
label: "Backup & Home",
path: "/clan/1/machine/backup",
serviceCount: 3,
status: "Online",
},
{
label: "Raspberry Pi",
path: "/clan/1/machine/pi",
serviceCount: 1,
status: "Offline",
},
{
label: "Mom's Laptop",
path: "/clan/1/machine/moms-laptop",
serviceCount: 2,
status: "Installed",
},
{
label: "Dad's Laptop",
path: "/clan/1/machine/dads-laptop",
serviceCount: 4,
status: "Not Installed",
},
],
},
extraSections: [
{
label: "Tools",
links: [
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
{ label: "Mumble", path: "/clan/1/service/mumble" },
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
],
},
{
label: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
],
};
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar/Nav",
component: SidebarNav,
render: (_: never, context: StoryContext<SidebarNavProps>) => {
const history = createMemoryHistory();
history.set({ value: "/clan/1/machine/backup" });
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => (
<Suspense>
<SidebarNav {...sidebarNavProps} />
</Suspense>
)}
>
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
</MemoryRouter>
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
};

View File

@@ -1,47 +0,0 @@
import "./SidebarNav.css";
import { SidebarNavHeader } from "@/src/components/Sidebar/SidebarNavHeader";
import { SidebarNavBody } from "@/src/components/Sidebar/SidebarNavBody";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
label: string;
links: LinkProps[];
}
export interface MachineProps {
label: string;
path: string;
status: MachineStatus;
serviceCount: number;
}
export interface ClanLinkProps {
label: string;
path: string;
}
export interface ClanProps {
label: string;
settingsPath: string;
machines: MachineProps[];
}
export interface SidebarNavProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
extraSections: SectionProps[];
}
export const SidebarNav = (props: SidebarNavProps) => {
return (
<div class="sidebar">
<SidebarNavHeader {...props} />
<SidebarNavBody {...props} />
</div>
);
};

View File

@@ -1,96 +0,0 @@
import "./SidebarNavHeader.css";
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 } from "solid-js";
import { ClanLinkProps, ClanProps } from "@/src/components/Sidebar/SidebarNav";
export interface SidebarHeaderProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
}
export const SidebarNavHeader = (props: SidebarHeaderProps) => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
const firstChar = props.clanDetail.label.charAt(0);
return (
<div class="sidebar-header">
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="title">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{firstChar.toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{props.clanDetail.label}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(props.clanDetail.settingsPath)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={props.clanLinks}>
{(clan) => (
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(clan.path)}
>
<Typography hierarchy="label" size="xs" weight="medium">
{clan.label}
</Typography>
</DropdownMenu.Item>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
);
};

View File

@@ -1,5 +1,5 @@
div.sidebar-pane {
@apply h-full w-auto max-w-60 border-none;
@apply w-full max-w-60 border-none;
& > div.header {
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];

View File

@@ -11,7 +11,7 @@ import { Checkbox } from "@/src/components/Form/Checkbox";
import { Combobox } from "../Form/Combobox";
const meta: Meta<SidebarPaneProps> = {
title: "Components/Sidebar/Pane",
title: "Components/SidebarPane",
component: SidebarPane,
};

View File

@@ -0,0 +1,9 @@
div.tooltip-content {
@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

@@ -0,0 +1,40 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
const meta: Meta<TooltipProps> = {
title: "Components/Tooltip",
component: Tooltip,
decorators: [
(Story: StoryObj<TooltipProps>) => (
<div class="p-16">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<TooltipProps>;
export const Default: Story = {
args: {
placement: "top",
inverted: false,
trigger: <Button hierarchy="primary">Trigger</Button>,
children: (
<Typography hierarchy="body" size="xs" inverted={true} weight="medium">
Your Clan is being created
</Typography>
),
},
};
export const AnimateBounce: Story = {
args: {
...Default.args,
animation: "bounce",
},
};

View File

@@ -0,0 +1,34 @@
import "./Tooltip.css";
import {
Tooltip as KTooltip,
TooltipRootProps as KTooltipRootProps,
} from "@kobalte/core/tooltip";
import cx from "classnames";
import { JSX } from "solid-js";
export interface TooltipProps extends KTooltipRootProps {
inverted?: boolean;
trigger: JSX.Element;
children: JSX.Element;
animation?: "bounce";
}
export const Tooltip = (props: TooltipProps) => {
return (
<KTooltip {...props}>
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
<KTooltip.Portal>
<KTooltip.Content
class={cx("tooltip-content", {
inverted: props.inverted,
"animate-bounce": props.animation == "bounce",
})}
>
{props.placement == "bottom" && <KTooltip.Arrow />}
{props.children}
{props.placement == "top" && <KTooltip.Arrow />}
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip>
);
};

View File

@@ -42,7 +42,7 @@ interface BackendReturnType<K extends OperationNames> {
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
*/
interface ApiCall<K extends OperationNames> {
export interface ApiCall<K extends OperationNames> {
uuid: string;
result: Promise<OperationResponse<K>>;
cancel: () => Promise<void>;

View File

@@ -1,6 +1,9 @@
import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator } from "@solidjs/router";
import { Params, Navigator, useParams } from "@solidjs/router";
export const encodeBase64 = (value: string) => window.btoa(value);
export const decodeBase64 = (value: string) => window.atob(value);
export const selectClanFolder = async () => {
const req = callApi("get_clan_folder", {});
@@ -20,10 +23,51 @@ export const selectClanFolder = async () => {
throw new Error("Illegal state exception");
};
export const navigateToClan = (navigate: Navigator, uri: string) => {
navigate("/clan/" + window.btoa(uri));
export const buildClanPath = (clanURI: string) => {
return "/clans/" + encodeBase64(clanURI);
};
export const buildMachinePath = (clanURI: string, machineID: string) => {
return (
"/clans/" + encodeBase64(clanURI) + "/machines/" + encodeBase64(machineID)
);
};
export const navigateToClan = (navigate: Navigator, clanURI: string) => {
const path = buildClanPath(clanURI);
console.log("Navigating to clan", clanURI, path);
navigate(path);
};
export const navigateToMachine = (
navigate: Navigator,
clanURI: string,
machineID: string,
) => {
const path = buildMachinePath(clanURI, machineID);
console.log("Navigating to machine", clanURI, machineID, path);
navigate(path);
};
export const clanURIParam = (params: Params) => {
return window.atob(params.clanURI);
return decodeBase64(params.clanURI);
};
export const useClanURI = () => clanURIParam(useParams());
export const machineIDParam = (params: Params) => {
return decodeBase64(params.machineID);
};
export const useMachineID = (): string => {
const params = useParams();
return machineIDParam(params);
};
export const maybeUseMachineID = (): string | null => {
const params = useParams();
if (params.machineID === undefined) {
return null;
}
return machineIDParam(params);
};

View File

@@ -2,10 +2,11 @@
import { render } from "solid-js/web";
import "./index.css";
import { QueryClient } from "@tanstack/solid-query";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { Routes } from "@/src/routes";
import { Router } from "@solidjs/router";
import { Layout } from "@/src/routes/Layout";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
export const client = new QueryClient();
@@ -18,8 +19,14 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
}
if (import.meta.env.DEV) {
console.log("Development mode");
// Load the debugger in development mode
await import("solid-devtools");
}
render(() => <Router root={Layout}>{Routes}</Router>, root!);
render(
() => (
<QueryClientProvider client={client}>
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
<Router root={Layout}>{Routes}</Router>
</QueryClientProvider>
),
root!,
);

View File

@@ -0,0 +1,76 @@
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
import { callApi, SuccessData } from "../hooks/api";
import { encodeBase64 } from "@/src/hooks/clan";
export type ClanDetails = SuccessData<"get_clan_details">;
export type ListMachines = SuccessData<"list_machines">;
export type MachinesQueryResult = UseQueryResult<ListMachines>;
export const useMachinesQuery = (clanURI: string) =>
useQuery<ListMachines>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "machines"],
queryFn: async () => {
const api = callApi("list_machines", {
flake: {
identifier: clanURI,
},
});
const result = await api.result;
if (result.status === "error") {
console.error("Error fetching machines:", result.errors);
return {};
}
return result.data;
},
}));
export const useClanDetailsQuery = (clanURI: string) =>
useQuery<ClanDetails>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
}));
export const useAllClanDetailsQuery = (clanURIs: string[]) =>
useQueries(() => ({
queries: clanURIs.map((clanURI) => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
enabled: !!clanURI,
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
})),
}));

View File

@@ -0,0 +1,24 @@
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
.create-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);
}
.create-modal {
@apply min-w-96;
}
.sidebar-container {
}
div.sidebar {
@apply absolute top-10 bottom-20 left-4 w-60;
}
div.sidebar-pane {
@apply absolute top-12 bottom-20 left-[16.5rem] w-64;
}

View File

@@ -1,10 +1,277 @@
import { RouteSectionProps, useParams } from "@solidjs/router";
import { Component } from "solid-js";
import { clanURIParam } from "@/src/hooks/clan";
import { RouteSectionProps } from "@solidjs/router";
import {
Component,
JSX,
Show,
createEffect,
createMemo,
createSignal,
on,
onMount,
} from "solid-js";
import {
buildMachinePath,
maybeUseMachineID,
useClanURI,
} from "@/src/hooks/clan";
import { CubeScene } from "@/src/scene/cubes";
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
import { callApi } from "@/src/hooks/api";
import { store, setStore } from "@/src/stores/clan";
import { produce } from "solid-js/store";
import { Button } from "@/src/components/Button/Button";
import { Splash } from "@/src/scene/splash";
import cx from "classnames";
import "./Clan.css";
import { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { useNavigate } from "@solidjs/router";
export const Clan: Component<RouteSectionProps> = (props) => {
const params = useParams();
const clanURI = clanURIParam(params);
return <CubeScene />;
return (
<>
<Sidebar />
{props.children}
<ClanSceneController {...props} />
</>
);
};
interface CreateFormValues extends FieldValues {
name: string;
}
interface MockProps {
onClose: () => void;
onSubmit: (formValues: CreateFormValues) => void;
}
const MockCreateMachine = (props: MockProps) => {
let container: Node;
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
return (
<div ref={(el) => (container = el)} class="create-backdrop">
<Modal
mount={container!}
onClose={() => {
reset(form);
props.onClose();
}}
class="create-modal"
title="Create Machine"
>
{() => (
<Form class="flex flex-col" onSubmit={props.onSubmit}>
<Field name="name">
{(field, props) => (
<>
<TextInput
{...field}
label="Name"
size="s"
required={true}
input={{ ...props, placeholder: "name" }}
/>
</>
)}
</Field>
<div class="mt-4 flex w-full items-center justify-end gap-4">
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
Cancel
</Button>
<Button
size="s"
type="submit"
hierarchy="primary"
onClick={close}
>
Create
</Button>
</div>
</Form>
)}
</Modal>
</div>
);
};
const ClanSceneController = (props: RouteSectionProps) => {
const clanURI = useClanURI();
const navigate = useNavigate();
const [dialogHandlers, setDialogHandlers] = createSignal<{
resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void;
} | null>(null);
const onCreate = async (): Promise<{ id: string }> => {
return new Promise((resolve, reject) => {
setShowModal(true);
setDialogHandlers({ resolve, reject });
});
};
const sendCreate = async (values: CreateFormValues) => {
const api = callApi("create_machine", {
opts: {
clan_dir: {
identifier: clanURI,
},
machine: {
name: values.name,
},
},
});
const res = await api.result;
if (res.status === "error") {
// TODO: Handle displaying errors
console.error("Error creating machine:");
// Important: rejects the promise
throw new Error(res.errors[0].message);
}
return { id: values.name };
};
const [showModal, setShowModal] = createSignal(false);
const [loadingCooldown, setLoadingCooldown] = createSignal(false);
onMount(() => {
setTimeout(() => {
setLoadingCooldown(true);
}, 1500);
});
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
const onMachineSelect = (ids: Set<string>) => {
// Get the first selected ID and navigate to its machine details
const selected = ids.values().next().value;
if (selected) {
navigate(buildMachinePath(clanURI, selected));
}
};
const machine = createMemo(() => maybeUseMachineID());
createEffect(
on(machine, (machineId) => {
if (machineId) {
setSelectedIds(() => {
const res = new Set<string>();
res.add(machineId);
return res;
});
} else {
setSelectedIds(new Set<string>());
}
}),
);
return (
<SceneDataProvider clanURI={clanURI}>
{({ query }) => {
return (
<>
<Show when={showModal()}>
<MockCreateMachine
onClose={() => {
setShowModal(false);
dialogHandlers()?.reject(new Error("User cancelled"));
}}
onSubmit={async (values) => {
try {
const result = await sendCreate(values);
dialogHandlers()?.resolve(result);
setShowModal(false);
} catch (err) {
dialogHandlers()?.reject(err);
setShowModal(false);
}
}}
/>
</Show>
<div
class="flex flex-row"
style={{ position: "absolute", top: "10px", left: "10px" }}
>
<Button
ghost
onClick={() => {
setStore(
produce((s) => {
for (const machineId in s.sceneData[clanURI]) {
// Reset the position of each machine to [0, 0]
s.sceneData[clanURI] = {};
}
}),
);
}}
>
Reset Store
</Button>
<Button
ghost
onClick={() => {
console.log("Refetching API");
query.refetch();
}}
>
Refetch API
</Button>
</div>
{/* TODO: Add minimal display time */}
<div
class={cx({ "fade-out": !query.isLoading && loadingCooldown() })}
>
<Splash />
</div>
<CubeScene
selectedIds={selectedIds}
onSelect={onMachineSelect}
isLoading={query.isLoading}
cubesQuery={query}
onCreate={onCreate}
sceneStore={() => {
const clanURI = useClanURI();
return store.sceneData?.[clanURI];
}}
setMachinePos={(machineId: string, pos: [number, number]) => {
console.log("calling setStore", machineId, pos);
setStore(
produce((s) => {
if (!s.sceneData) {
s.sceneData = {};
}
if (!s.sceneData[clanURI]) {
s.sceneData[clanURI] = {};
}
if (!s.sceneData[clanURI][machineId]) {
s.sceneData[clanURI][machineId] = { position: pos };
} else {
s.sceneData[clanURI][machineId].position = pos;
}
}),
);
}}
/>
</>
);
}}
</SceneDataProvider>
);
};
const SceneDataProvider = (props: {
clanURI: string;
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
}) => {
const machinesQuery = useMachinesQuery(props.clanURI);
// This component can be used to provide scene data or context if needed
return props.children({ query: machinesQuery });
};

View File

@@ -1,6 +1,18 @@
import { Component } from "solid-js";
import { RouteSectionProps } from "@solidjs/router";
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { activeClanURI } from "@/src/stores/clan";
import { navigateToClan } from "@/src/hooks/clan";
export const Layout: Component<RouteSectionProps> = (props) => (
<div class="size-full h-screen">{props.children}</div>
);
export const Layout: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate();
// check for an active clan uri and redirect to it on first load
const activeURI = activeClanURI();
if (!props.location.pathname.startsWith("/clans/") && activeURI) {
navigateToClan(navigate, activeURI);
} else {
navigate("/");
}
return <div class="size-full h-screen">{props.children}</div>;
};

View File

@@ -0,0 +1,19 @@
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { SidebarPane } from "@/src/components/Sidebar/SidebarPane";
import { navigateToClan, useClanURI, useMachineID } from "@/src/hooks/clan";
export const Machine = (props: RouteSectionProps) => {
const navigate = useNavigate();
const clanURI = useClanURI();
const onClose = () => {
// go back to clan route
navigateToClan(navigate, clanURI);
};
return (
<SidebarPane title={useMachineID()} onClose={onClose}>
<h1>Hello world</h1>
</SidebarPane>
);
};

View File

@@ -0,0 +1,663 @@
div.creating {
@apply flex flex-col items-center justify-center;
div.scene {
width: 400px;
height: 400px;
perspective: 1000px;
/*background: red;*/
& > .frame {
position: relative;
top: 100px;
left: 65px;
width: 200px;
height: 200px;
/*background: green;*/
/*transform: rotate3d(-2, -2, 1, 45deg);*/
transform: rotate3d(-1.5, -2, 0.5, 45deg);
transform-style: preserve-3d;
& > .cube {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
transform-style: preserve-3d;
.cube-face {
position: absolute;
width: 100px;
height: 100px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.56) 0%,
rgba(255, 255, 255, 0) 100%
);
border: 1px #10191a solid;
opacity: 1;
&.front {
transform: rotateY(0deg) translateZ(50px);
}
&.right {
transform: rotateY(90deg) translateZ(50px);
}
&.back {
transform: rotateY(180deg) translateZ(50px);
}
&.left {
transform: rotateY(-90deg) translateZ(50px);
}
&.top {
transform: rotateX(90deg) translateZ(50px);
}
&.bottom {
transform: rotateX(-90deg) translateZ(50px);
}
}
&.state-1 {
animation: anim-cube-1-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-1-1 {
transform: translateZ(-120px);
animation: anim-cube-1-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2 {
left: 120px;
animation: anim-cube-2-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2-2 {
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-2-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3 {
top: 120px;
animation: anim-cube-3-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3-3 {
top: 120px;
transform: translateZ(-120px);
animation: anim-cube-3-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4 {
top: 120px;
left: 120px;
animation: anim-cube-4-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4-4 {
top: 120px;
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-4-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
}
}
}
}
@keyframes anim-cube-1-1 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(0px);
}
2.083% {
left: -40px;
transform: translateZ(0px);
}
16.666% {
left: -40px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 0;
transform: translateZ(0px);
}
33.332% {
left: 0;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 0;
transform: translateZ(40px);
}
49.998% {
left: 0;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
left: 0;
transform: translateZ(0);
}
66.664% {
left: 0;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(60px);
}
83.33% {
left: -60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(0px);
}
100% {
left: 0px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-2-1 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(0px);
}
2.083% {
left: 180px;
transform: translateZ(0px);
}
16.666% {
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(0px);
}
33.332% {
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(120px);
}
49.998% {
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(0);
}
66.664% {
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(60px);
}
83.33% {
left: 60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(0px);
}
100% {
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-3-1 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(40px);
}
49.998% {
top: 120px;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-4-1 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-1-2 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(-120px);
}
2.083% {
left: -40px;
transform: translateZ(-120px);
}
16.666% {
left: -40px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 0px;
transform: translateZ(-120px);
}
33.332% {
left: 0px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 0px;
transform: translateZ(-200px);
}
49.998% {
left: 0px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 0px;
transform: translateZ(-120px);
}
66.664% {
left: 0px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(-180px);
}
83.33% {
left: -60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(-120px);
}
100% {
left: 0px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-2-2 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(-120px);
}
2.083% {
left: 180px;
transform: translateZ(-120px);
}
16.666% {
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(-120px);
}
33.332% {
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(-200px);
}
49.998% {
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(-120px);
}
66.664% {
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(-180px);
}
83.33% {
left: 60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(-120px);
}
100% {
left: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-3-2 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-4-2 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
}

View File

@@ -0,0 +1,90 @@
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import { Typography } from "@/src/components/Typography/Typography";
import "./Creating.css";
export const Creating = () => (
<div class="creating">
<Tooltip open={true} placement="top" trigger={<div />}>
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
Your Clan is being created
</Typography>
</Tooltip>
<div class="scene">
<div class="frame">
<div id="cube-1" class="cube state-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2" class="cube state-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3" class="cube state-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4" class="cube state-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-1-1" class="cube state-1-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2-2" class="cube state-2-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3-3" class="cube state-3-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4-4" class="cube state-4-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
</div>
</div>
</div>
);

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