Compare commits
81 Commits
feat/optio
...
migrate-sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
998aaec269 | ||
|
|
77bb690b87 | ||
|
|
85e968c4f7 | ||
|
|
f84da1cf62 | ||
|
|
af8f4f00c2 | ||
|
|
e3247d9c36 | ||
|
|
4055508588 | ||
|
|
ff65dfc883 | ||
|
|
1f5ef04a61 | ||
|
|
89f0e90910 | ||
|
|
137aa71529 | ||
|
|
4b5273fbc1 | ||
|
|
aed48be645 | ||
|
|
5fdc9823d1 | ||
|
|
f6284a7ac2 | ||
|
|
72473746ff | ||
|
|
4b36b3e07c | ||
|
|
5a63eeed4e | ||
|
|
ac96d67f09 | ||
|
|
d01342aa79 | ||
|
|
2d404254da | ||
|
|
71b69c1010 | ||
|
|
f155c68efe | ||
|
|
e57741b60c | ||
|
|
c9cacfcf62 | ||
|
|
2d937b80b1 | ||
|
|
e8b91e63bc | ||
|
|
a9d6fa7712 | ||
|
|
65a23983c2 | ||
|
|
c181400267 | ||
|
|
e8ff0d1ad4 | ||
|
|
f9f8a947e2 | ||
|
|
c5b0154af7 | ||
|
|
864742f05f | ||
|
|
38b043f625 | ||
|
|
174e66ef95 | ||
|
|
315049de20 | ||
|
|
2e577dbd1e | ||
|
|
a9b457e063 | ||
|
|
4281770ec7 | ||
|
|
1bd950fa39 | ||
|
|
e37b61240b | ||
|
|
23d2975bb5 | ||
|
|
d441d4c1c1 | ||
|
|
840cb7e2cb | ||
|
|
cf232e1002 | ||
|
|
7414dc6e7e | ||
|
|
d97f997349 | ||
|
|
0621ae1ca6 | ||
|
|
992048e1b2 | ||
|
|
261cad7674 | ||
|
|
a012e4b1af | ||
|
|
158b98ee05 | ||
|
|
14d367e50f | ||
|
|
48c575699e | ||
|
|
60768cc537 | ||
|
|
c26dff282b | ||
|
|
5022f6f26c | ||
|
|
94b93074bc | ||
|
|
d962033236 | ||
|
|
a548851245 | ||
|
|
b32e61bb6d | ||
|
|
e731322af3 | ||
|
|
fd21c6b4ee | ||
|
|
5a86862f47 | ||
|
|
1d1a2563c3 | ||
|
|
4bc57980ff | ||
|
|
3afd0c0971 | ||
|
|
e6a6cb27ec | ||
|
|
dcd78c5d84 | ||
|
|
2a1ad66292 | ||
|
|
5d0d4404b8 | ||
|
|
7b369c77b5 | ||
|
|
06b70a982b | ||
|
|
c9b1b0fb94 | ||
|
|
66bdbb0959 | ||
|
|
752f030d03 | ||
|
|
8c7e93c92e | ||
|
|
579885a6e2 | ||
|
|
45f7ebc0c9 | ||
|
|
997d675f8c |
20
.gitea/workflows/build-clan-app-darwin.yml
Normal file
20
.gitea/workflows/build-clan-app-darwin.yml
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Dont import this file
|
||||
# It is only here for backwards compatibility.
|
||||
# Dont author new modules with this file.
|
||||
{
|
||||
imports = [ ./roles/peer.nix ];
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
../shared.nix
|
||||
];
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
instanceNames = builtins.attrNames config.clan.inventory.services.syncthing;
|
||||
instanceName = builtins.head instanceNames;
|
||||
instance = config.clan.inventory.services.syncthing.${instanceName};
|
||||
introducer = builtins.head instance.roles.introducer.machines;
|
||||
|
||||
introducerId = "${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../shared.nix
|
||||
];
|
||||
|
||||
clan.syncthing.introducer = lib.strings.removeSuffix "\n" (
|
||||
if builtins.pathExists introducerId then
|
||||
builtins.readFile introducerId
|
||||
else
|
||||
throw "${introducerId} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
|
||||
);
|
||||
}
|
||||
47
clanServices/internet/default.nix
Normal file
47
clanServices/internet/default.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
clanServices/internet/flake-module.nix
Normal file
9
clanServices/internet/flake-module.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
internet = module;
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
---
|
||||
description = "A secure, file synchronization app for devices over networks, offering a private alternative to cloud services."
|
||||
features = [ "inventory" ]
|
||||
|
||||
[constraints]
|
||||
roles.introducer.min = 1
|
||||
roles.introducer.max = 1
|
||||
---
|
||||
|
||||
**Warning**: This module was written with our VM integration in mind likely won't work outside of this context. They will be generalized in future.
|
||||
|
||||
## Usage
|
||||
@@ -26,7 +22,7 @@ We recommend configuring this module as an sync-service through the provided opt
|
||||
- **Share Folders**: Select folders to share with connected devices and configure permissions and synchronization parameters.
|
||||
|
||||
!!! info
|
||||
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#clan.syncthing.introducer)
|
||||
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#roles.introducer)
|
||||
|
||||
If that is not the case you can add the other device by its Device ID manually.
|
||||
You can find and share Device IDs under the "Add Device" button in the Web GUI. (`127.0.0.1:8384`)
|
||||
@@ -37,4 +33,4 @@ We recommend configuring this module as an sync-service through the provided opt
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).
|
||||
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).
|
||||
174
clanServices/syncthing/default.nix
Normal file
174
clanServices/syncthing/default.nix
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
# lib,
|
||||
# config,
|
||||
# pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/syncthing";
|
||||
manifest.description = "A secure, file synchronization app for devices over networks";
|
||||
|
||||
roles.introducer = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
|
||||
# TODO is this option even needed or used anywhere?
|
||||
id = lib.mkOption {
|
||||
description = ''
|
||||
The ID of the machine.
|
||||
It is generated automatically by default.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
|
||||
# default = config.clan.core.vars.generators.syncthing.files."id".value;
|
||||
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
|
||||
};
|
||||
introducer = lib.mkOption {
|
||||
description = ''
|
||||
The introducer for the machine.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
};
|
||||
autoAcceptDevices = lib.mkOption {
|
||||
description = ''
|
||||
Auto accept incoming device requests.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
autoShares = lib.mkOption {
|
||||
description = ''
|
||||
Auto share the following Folders by their ID's with introduced devices.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"folder1"
|
||||
"folder2"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
# instanceName,
|
||||
roles,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
_module.args = {
|
||||
inherit settings roles;
|
||||
introducerID = null;
|
||||
};
|
||||
imports = [
|
||||
./shared.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.peer = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
|
||||
# TODO is this option even needed or used anywhere?
|
||||
id = lib.mkOption {
|
||||
description = ''
|
||||
The ID of the machine.
|
||||
It is generated automatically by default.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
|
||||
# default = config.clan.core.vars.generators.syncthing.files."id".value;
|
||||
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
|
||||
};
|
||||
introducer = lib.mkOption {
|
||||
description = ''
|
||||
The introducer for the machine.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
};
|
||||
autoAcceptDevices = lib.mkOption {
|
||||
description = ''
|
||||
Auto accept incoming device requests.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
autoShares = lib.mkOption {
|
||||
description = ''
|
||||
Auto share the following Folders by their ID's with introduced devices.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"folder1"
|
||||
"folder2"
|
||||
];
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
# instanceName,
|
||||
roles,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
_module.args =
|
||||
|
||||
let
|
||||
introducer = builtins.head (lib.attrNames roles.introducer.machines);
|
||||
|
||||
introducerIDPath =
|
||||
if settings.introducer == null then
|
||||
"${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value"
|
||||
else
|
||||
"${config.clan.core.settings.directory}/vars/per-machine/${settings.introducer}/syncthing/id/value";
|
||||
|
||||
introducerID = lib.strings.removeSuffix "\n" (
|
||||
if builtins.pathExists introducerIDPath then
|
||||
builtins.readFile introducerIDPath
|
||||
else
|
||||
throw "${introducerIDPath} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit settings roles introducerID;
|
||||
};
|
||||
|
||||
imports = [
|
||||
./shared.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
23
clanServices/syncthing/flake-module.nix
Normal file
23
clanServices/syncthing/flake-module.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.modules.syncthing = module;
|
||||
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.syncthing = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/syncthing" = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -2,49 +2,11 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
settings,
|
||||
introducerID,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options.clan.syncthing = {
|
||||
id = lib.mkOption {
|
||||
description = ''
|
||||
The ID of the machine.
|
||||
It is generated automatically by default.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
|
||||
default = config.clan.core.vars.generators.syncthing.files."id".value;
|
||||
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
|
||||
};
|
||||
introducer = lib.mkOption {
|
||||
description = ''
|
||||
The introducer for the machine.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
};
|
||||
autoAcceptDevices = lib.mkOption {
|
||||
description = ''
|
||||
Auto accept incoming device requests.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
autoShares = lib.mkOption {
|
||||
description = ''
|
||||
Auto share the following Folders by their ID's with introduced devices.
|
||||
Should only be used on the introducer.
|
||||
'';
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"folder1"
|
||||
"folder2"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
{
|
||||
# Syncthing ports: 8384 for remote access to GUI
|
||||
@@ -65,7 +27,7 @@
|
||||
{
|
||||
assertion = lib.all (
|
||||
attr: builtins.hasAttr attr config.services.syncthing.settings.folders
|
||||
) config.clan.syncthing.autoShares;
|
||||
) settings.autoShares;
|
||||
message = ''
|
||||
Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device.
|
||||
'';
|
||||
@@ -80,12 +42,8 @@
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
|
||||
overrideFolders = lib.mkDefault (
|
||||
if (config.clan.syncthing.introducer == null) then true else false
|
||||
);
|
||||
overrideDevices = lib.mkDefault (
|
||||
if (config.clan.syncthing.introducer == null) then true else false
|
||||
);
|
||||
overrideFolders = lib.mkDefault (if (introducerID == null) then true else false);
|
||||
overrideDevices = lib.mkDefault (if (introducerID == null) then true else false);
|
||||
|
||||
key = lib.mkDefault config.clan.core.vars.generators.syncthing.files."key".path or null;
|
||||
cert = lib.mkDefault config.clan.core.vars.generators.syncthing.files."cert".path or null;
|
||||
@@ -98,13 +56,13 @@
|
||||
devices =
|
||||
{ }
|
||||
// (
|
||||
if (config.clan.syncthing.introducer == null) then
|
||||
if (introducerID == null) then
|
||||
{ }
|
||||
else
|
||||
{
|
||||
"${config.clan.syncthing.introducer}" = {
|
||||
"${introducerID}" = {
|
||||
name = "introducer";
|
||||
id = config.clan.syncthing.introducer;
|
||||
id = introducerID;
|
||||
introducer = true;
|
||||
autoAcceptFolders = true;
|
||||
};
|
||||
@@ -112,6 +70,7 @@
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.syncthing-auto-accept =
|
||||
let
|
||||
baseAddress = "127.0.0.1:8384";
|
||||
@@ -120,7 +79,7 @@
|
||||
SharedFolderById = "/rest/config/folders/";
|
||||
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
|
||||
in
|
||||
lib.mkIf config.clan.syncthing.autoAcceptDevices {
|
||||
lib.mkIf settings.autoAcceptDevices {
|
||||
description = "Syncthing auto accept devices";
|
||||
requisite = [ "syncthing.service" ];
|
||||
after = [ "syncthing.service" ];
|
||||
@@ -138,7 +97,7 @@
|
||||
${lib.getExe pkgs.curl} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice}
|
||||
|
||||
# get all shared folders by their ID
|
||||
for folder in ${builtins.toString config.clan.syncthing.autoShares}; do
|
||||
for folder in ${builtins.toString settings.autoShares}; do
|
||||
SHARED_IDS=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${lib.getExe pkgs.jq} ."devices")
|
||||
PATCHED_IDS=$(echo $SHARED_IDS | ${lib.getExe pkgs.jq} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]")
|
||||
${lib.getExe pkgs.curl} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder"
|
||||
@@ -147,7 +106,7 @@
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.syncthing-auto-accept = lib.mkIf config.clan.syncthing.autoAcceptDevices {
|
||||
systemd.timers.syncthing-auto-accept = lib.mkIf settings.autoAcceptDevices {
|
||||
description = "Syncthing Auto Accept";
|
||||
|
||||
wantedBy = [ "syncthing-auto-accept.service" ];
|
||||
@@ -162,7 +121,7 @@
|
||||
let
|
||||
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
|
||||
in
|
||||
lib.mkIf config.clan.syncthing.autoAcceptDevices {
|
||||
lib.mkIf settings.autoAcceptDevices {
|
||||
description = "Set the api key";
|
||||
after = [ "syncthing-init.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
@@ -182,7 +141,6 @@
|
||||
};
|
||||
|
||||
clan.core.vars.generators.syncthing = {
|
||||
migrateFact = "syncthing";
|
||||
|
||||
files."key".group = config.services.syncthing.group;
|
||||
files."key".owner = config.services.syncthing.user;
|
||||
99
clanServices/syncthing/tests/vm/default.nix
Normal file
99
clanServices/syncthing/tests/vm/default.nix
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
name = "syncthing";
|
||||
|
||||
clan = {
|
||||
|
||||
test.useContainers = false;
|
||||
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
|
||||
machines.introducer = { };
|
||||
machines.peer1 = { };
|
||||
machines.peer2 = { };
|
||||
|
||||
instances."test" = {
|
||||
module.name = "@clan/syncthing";
|
||||
module.input = "self";
|
||||
|
||||
roles.introducer.machines.introducer.settings = {
|
||||
autoAcceptDevices = true;
|
||||
autoShares = [ "Shared" ];
|
||||
};
|
||||
roles.peer.machines.peer1 = { };
|
||||
roles.peer.machines.peer2 = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
|
||||
# peer2.console.keyMap = "colemak";
|
||||
|
||||
peer1.services.syncthing.openDefaultPorts = true;
|
||||
peer2.services.syncthing.openDefaultPorts = true;
|
||||
|
||||
introducer = {
|
||||
|
||||
services.syncthing.openDefaultPorts = true;
|
||||
|
||||
# For faster Tests
|
||||
systemd.timers.syncthing-auto-accept.timerConfig = {
|
||||
OnActiveSec = 1;
|
||||
OnUnitActiveSec = 1;
|
||||
};
|
||||
|
||||
services.syncthing.settings.folders = {
|
||||
"Shared" = {
|
||||
enable = true;
|
||||
path = "~/Shared";
|
||||
versioning = {
|
||||
type = "trashcan";
|
||||
params = {
|
||||
cleanoutDays = "30";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# import time
|
||||
# time.sleep(500000)
|
||||
|
||||
introducer.wait_for_unit("syncthing.service")
|
||||
peer1.wait_for_unit("syncthing.service")
|
||||
peer2.wait_for_unit("syncthing.service")
|
||||
|
||||
# Check that syncthing web interface is accessible
|
||||
introducer.wait_for_open_port(8384)
|
||||
peer1.wait_for_open_port(8384)
|
||||
peer2.wait_for_open_port(8384)
|
||||
|
||||
# Basic connectivity test
|
||||
introducer.succeed("curl -s http://127.0.0.1:8384")
|
||||
peer1.succeed("curl -s http://127.0.0.1:8384")
|
||||
peer2.succeed("curl -s http://127.0.0.1:8384")
|
||||
|
||||
# Check that folders are created correctly
|
||||
peer1.execute("ls -la /var/lib/syncthing")
|
||||
peer2.execute("ls -la /var/lib/syncthing")
|
||||
|
||||
peer1.wait_for_file("/var/lib/syncthing/Shared")
|
||||
peer2.wait_for_file("/var/lib/syncthing/Shared")
|
||||
|
||||
# Check file synchronisation from peer1 to peer2
|
||||
introducer.shutdown()
|
||||
|
||||
peer1.execute("echo hello > /var/lib/syncthing/Shared/hello")
|
||||
|
||||
# peer2.wait_until_succeeds("timeout 5 cat /var/lib/syncthing/Shared")
|
||||
peer2.wait_for_file("/var/lib/syncthing/Shared/hello")
|
||||
|
||||
out = peer2.succeed("cat /var/lib/syncthing/Shared/hello")
|
||||
assert "hello" in out
|
||||
'';
|
||||
}
|
||||
6
clanServices/syncthing/tests/vm/sops/machines/introducer/key.json
Executable file
6
clanServices/syncthing/tests/vm/sops/machines/introducer/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
clanServices/syncthing/tests/vm/sops/machines/peer1/key.json
Executable file
6
clanServices/syncthing/tests/vm/sops/machines/peer1/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
6
clanServices/syncthing/tests/vm/sops/machines/peer2/key.json
Executable file
6
clanServices/syncthing/tests/vm/sops/machines/peer2/key.json
Executable file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"publickey": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:JbpTTfHD92NlaUR7xAyJFoqD+4mYDlpE1gdWuCsrMyar8rUzS6vX7i7ymd69K0tPAT/UUZAmNacPFwvjTkZmdv+/719FNBkowrc=,iv:ZHTcm+V1dNZ07kRQEDNFYh8NMMwZ5g5cq0Tg281Aaec=,tag:tjAJRuQrRC0JYhS0tA+VUw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZWV1ZUZlODZ2OXlXbzVX\nSTkzV0VFL1NJVXpzKy9hTy9NN3gzeWxJbzE4CnJZc0JYd2s3ci85aDFuQ3pJbmtT\nSWZGRmREM25nWjhkZ1hGNE0rcVpMbFEKLS0tIDk3aHZ6ZVlaTmhOV3B4b3g5MzV4\nUUNWcXdHcTlVVEw3UGlxQWtBSDdpMk0K6nCih/rHq4vLS/oDz8cbjY8TVVsQmzaW\nivSd3WhpUaRdigyw/u3/5Lmaii1awy2qJdyREbzzUVgJPfoZ87pabw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:15Z",
|
||||
"mac": "ENC[AES256_GCM,data:KoxJwNfRO1SDlgCc5p9+ZDP6rXOAUXG48ousVXKgNfR+qyS9i0FIYjgJxsSxzsYyn0Md7fbbJdX8MEnJZkgkTn0pJ46HfHsD4oiE66AF4pcgdIssTo4BX6RvoqbCdtS6hi6dpyrW7j1PPhwO3DRhaFIO58Nk1fxcVpyATzm8Gyg=,iv:SYotgqC8fA80mmjYZxUM0p+MUGxRYKHCd1pscS3HVt0=,tag:3XrQabwt+KEtk8JLZ4HTPA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:Lr3bPR9MjNvwBYPIQg0D4qIDhRbD++ZOpuGvz093d+DWva1b4h1jhcsnmziOvINZQ3vVpizklkASRWo757FOJLLV1LiXNqiZAbY=,iv:HbsNN9Who9BFTHEUrRVAA5MAkadXVqTGEsq5kTPZdQo=,tag:pGW8oINEGRjDgg1JoHdUEQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBM1Q5UEpvaDQ4K25oTWN3\nbmdPaTdZUTFFR0xUVEttajNXOSt1T3BnTjJnCkE2eUVleXJKckovdkw3ak1xUGZj\nK3hET0E0OU5KYlZLL2daZFNaZ3cyS00KLS0tIHFwMW9LTHNLR1JwaHdiMnc2bWhH\nMWtwMElUYW50ZXlGZG1US1hiWlNoRjAKTg62lhjMCG1uPtxAmq5L7QGwmlwvGnxG\n+qTZHAPAoTUaWtnJfJpueGB1OJbr4HbUH0gN0cBqq7Y0DGIyvGqidA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:27Z",
|
||||
"mac": "ENC[AES256_GCM,data:1fh6Jp+y6jGI3NgZMgGcuVHX3GLXYH9LKbPG4cVwOk1otX94zr0UcrVOSgH3m9J7QpGlFl48HwCfoNZzkRVmX633Px1UZQopOSZLao8Ao7ZcAwP3EmIowwJBC8//pYIhE6JwPlIlRbHOQRDd8HhIM1VwNjc8dUBBX0VyTAgyT4w=,iv:0Z722NhqETyVY+mkerERVw9TmKx0aASdSdYYdNucmCg=,tag:PT56bX50YajhlHQttz+Ffw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:XgQnGe/dmXyir8gDOgcsdYok1d1blDBx+AFDLw+tXBzv5FY1pSbonSuOKmEVWDEmRCR3o1D8qiuUrDsa68D3als69A/bRhYHR+A=,iv:JzNB2VjE+HHAOQXkN3t95wQPiBBj/c93X5JHP8fosHg=,tag:fcAG53Tp+38NAfqSObIa4A==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6amIrbk9uV1IyQ20zMExo\nL0RrMmxOa3dEOGNmZFhIWXF1Sjc1K3VtdkdFCklLdEVNYnpsclRUdUtyTmVteWhu\nMHpPQmYzSEVldytGazJ1azBnQWJsaEUKLS0tIEZFY0hMNnV1TE9jODdpdy80eXRx\nMDZWSGt1MnhYQWJoMmNoaW1KWGVsVk0Ki0VQLz79+QQeiOri0aBqHEsVessIyjX8\nv3OZAjwMglPNv3j4CIqY/F4sfrAYxKUNB7g0Ui56BZlrG/i68EupAg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:39Z",
|
||||
"mac": "ENC[AES256_GCM,data:081iGB26aJv0067lLJVetcKOyzzaHys6W70hcxB9010kpyU6AkxNt+HCa2lvJNAv+lls4WXgM6WsD/KODkDvbZsP4U3P9sJqY4RbTqJvypN4yjmzogneB7GVOenMQ8ywbm+ILM54nx8Enn6GPm4YX6yTat4WVTFFd+dJYmfBBmM=,iv:rXOqKtBSZvwozT49Zhp6aBpLXWlim7KLRGg6yIb2vkQ=,tag:vmjlvW0PNlHYs7syshJETg==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../users/admin
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/introducer
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:/dsV5uaNTBmB82lbzlQHGyRbFeXM9l32igrLcfGG13td,iv:6PB0LqjRtvrSYxeOPN+261VqacGg0jczCyyF4FZQa/I=,tag:4GlcYRhYhHVdSkwDSzcT6Q==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYmJIUVhVQ3c3ZnhweUhU\nZnR6THB1Y2RjWExPckIwNEl1SlRGeVJORUVVCkk1ZzZwbGNwc1JQOFRpN2MwT0xG\nZTErNzVmSnhPazJJVHBKUnl6d2lYaVkKLS0tIHJRclM2WEYzQTRwVXRHcUUxSTlt\na1pHamdpdm9hUU9PSndybzdlMWY3NkkKcTnYp/5fUTdiNr0ajJeAPuLwhgWAdlAE\ngZ4soLdZoBFUHSsh0LhEm6jO2DXEKUZY7oLZi1gSZZbPA7PI/5k7OA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3THZibDFMaTZrKzJVWjd3\nd3RScjZVZmNlbitmc2FkRXl0UlVmZlRoM2dJCnlkUW1pa3dHZlNqTklxc3dZTkha\nQUpWck5BcEk4NjRjSXI3TlFzQ0pzVVUKLS0tIFpJdHJzKzVkeGFBcnRvR3pDeFN2\nNFNFaW9kY3JXdzk0Q3hPS29iOFRkK1UKoQSqFLIAeU6aRL+rDQ+oJ9PS6aAtAPeo\n5Kpwoi3KQHVrDDIRBaxvZ2BXObOyU0tBqjzBdrOgtRn+96HmnZ6JDw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:15Z",
|
||||
"mac": "ENC[AES256_GCM,data:4T1+t0hZwN0ThdXoCcNeUiQNj2RosRP6HL6SKmsnECn3pmDZqJEt/0xEQWDeDcvfHyZkJAFbU0RfhYsyz2wtYZwuMDL8WEbHURa1GQ4uKNWfUPPeu88eTwnYsbvtUS6TdRZ26eDoQKDNEed1cy9TfI4Bugdt0rrl6O+su2Ikvyo=,iv:aMLJ4r55UY5cIB1rQNns3jN2U/ZjfFN1vl6ZsXpcBbo=,tag:P2Dv7LeGB1fT4TYHwJgLGw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/introducer
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:6bI+k3BUKfpOnYSZx6ucCEKLvPCC2+LVrHzxQyN7MvLKk43LnKsfSSI8lpp5l8CE8Bpcpq9BqWVU3ZhnwmM4RbiHg/+C7R374KqoV0TLXMwq4lkeVM5oh4On2qBbnxsXtBz6NbJmIiJPPTT5YARdDAdIVUZ21jOj7G5jvm1BbAWojVkrKpb5IpOTmuxSA0CPEQRsvP9NHH+5Btm3J8Py4bHcRpgNeKaFfTrJjtXN2te8hTjJb8sGmFvuOr92Tdh/+O5qeA4SjMXqRJEiJiE+Fc9X9ukrxlZkDwsSn2gXQu7ahGhjOZCvQ/Th9j55/ZPiu5zrypfBIltSfGaMzHTqSECSGozfNSqbAAuu/ybCMmp87HDg9RQ3M/2yYqmSs2cRSB1/jN7ha3LTEnPhPKWnpOmnNvQT/rSX0wyfGtdKVk0bIgEc28wIKANyJC+HPPtENdcmdUhx6q2CRi1h+nFz92zkRGzWySgCcl51WawFDD6KRKLvqpq9LWv/bNl4NOQd307hPZguLLB6/Q/yV29j+XgGrZcmlOnn5NfAlVfF+F6ckebPxDKMg0Z3DD/83DPM18iO5SeDcaNVS3c3KoeSpURWMHr5+2n+qtu3wVvKsPPhAsBV8diwGxp5z00Rr84mHXPfnz56Tr+m/rYLfflryrybxTujcuWFCGgzwMdeaYx4tz7lQAY1XtdKSNTYjhWdDfFbqThP3HVGJsRQshCd+8Q4miEfJzDmUgbrUuoF3VjkMApZ4wULV5BCcLrDYIXkwu1lyDt5fk4G0TyZCP8JvBV3zcVxjAPW86tSHNcX0V56Yd090s51u0iJyM/eqNxoNXa/DWBuLd4jpWJRz2c48pUO1XRgqvZHFmUcO5q7SPL/bCvdpjqaopnOqt3501zGB2q+TGfyZUZ3sqtylTPz4C7tv/hrCzmRCTzEFlfsmUOi08DL/6K65ihe6uXjoH2fB7ypjUy4u5L1XcBIvxSitcSacosJHSUtWcFZ6mWIgVyChTjWRfCAF4eB3aVctW+OmVTeiTcdAfbDiPR5FNyDWPTMnnorjBVZkpU=,iv:RGNo6ipQplIsHL7avQpTgEKMDifKnB5W96vYf6X96cQ=,tag:k8oObQ1m3aVnG94MR7dhHw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5NFVRb3cvZEZXdW1xWlBP\nWWNCRjlxYmpPQ1k2aFBmOUR4M0F2ai9Iam5ZCkxzU2dUamZXRmRqV3ZyWTRvdUda\ncTlHa1BuSDQ0em9lMXRwUlc5V2E1WUUKLS0tIDRkamJVZ3NIVlJGRlNrK0EyT09x\nbHVsMzlKVzJGNS8vVVlHRlFJRmUvUlUK1OSBTQi6R3XYEZEQmpGCrMr8m4jCBRiV\nj/G9sCahhTUc0D7E7DTXD3fwfXnZk1bD7buA99f908DT2Bjv4TfeKQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZkJFbWhINkxmK3g4RHFN\neko4VTZHVGZOK1E2dFVqbWdVZktnTEdqT1JJCnBraG1acHVrNzlBWmJhNEMya1Fi\ndUw0NDdKakdLcGE1Z0tsSHVvQ0l1NFkKLS0tIDZvek42eXVEbEdWeG1vSENnakQ4\nQzlGaUNKMVhEM2QwY3ZPTW5nYXNLUkUK2laHy33U+hcQMT4jlUOqtVRCy+hNHyaS\nyuSk1i7Am3VeProaXccREjHjYRHn/l/B1oLRQQQT4cLcComxOArz0w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:16Z",
|
||||
"mac": "ENC[AES256_GCM,data:CVyuCoiArDYnoz/GQ0OZ2K0rJ1+Y1xoznp+v4rxAfL22fv3mY29EDw7ByYXxye0ARCD8gBFCpKeUvWbfOPJGfjsfAFT0AxLYFbUONgejYZpVZbnlElfLUSi39ZhaSwcImqe4RLJ2TND/HuJ6jwz0Lb1h8BWOq5/NeF8JJH2tHFo=,iv:n2liiufvBjCjASBIAmOu2Q5IONsX99GFmZw7YDeHJ+4=,tag:0bYMs8k362w7jApCird4yA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
UHILKRC-BZSMZT7-CA5ANWX-DJTR55R-NBGUBK4-SOZJG5Z-MONIHKP-ZHFGOAL
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/introducer
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:YOzFYRjbmtSqzz+gaimswru/Xl3h2bZ0zkkM+QokycDIGhk5wrdlrGNJiaiQk3xJ1JUEbJewmsBFPz0Xcl8Nh2aAj58s0fMg/H0lb6WfF0n7B4u3TZ6urWir4ViUPdVxcS7oxKhJCU2KoqFNpXS/NjRCmi4G12j42VGBStLNxCFZ8EH4oK/nV0tYpJ++meKA9IlQbuEKmAv6Xt7Ry1isW4THl7jzYwE6y7I1ThqudnDtvqguY+Nl27vMmFed+WmpE4jsXjeW7T/CTynafen81uL7gP6xiy/n+3nZNYgutHCTqa1AonITDIJrzRjAPMmYxwv0k1ebYImBCT0wH7rnmbLF7y4EMNFuA3O5nAvpZ8tkkLzooVf0PCYrtWbwsxOm,iv:T6aRMEHnOezxswMnJXBXoHVkVDFtDqvtCRhqvQHLOWU=,tag:2zTGHZWCt6SQrESyC3f9Rg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSK2pOU0Zoam52V00vdnFi\nR2hpenpHQ1BDaHlyODZ0cmgvdzlLMTZ6em5VCi9sdG9zdzY0Z1pDQVF3dnNSREdt\nQVVUQ29uaE03SWM3bnA3blRHZWJmUmMKLS0tIGwyNUhVMTJ5SkxudlZaVUJaM0xJ\nSEpoaFZaUlczMkYxWlZhb1RBbUZ3SFUKGmjG+r998QLDJylznGhuqa6magj9x9PM\nly7PlqaoZ1diLuFklqFVExK3cXwvA6xdOScZqd8/P/sEdzAodDuKQw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnMlpRK043Rm0zbTJxUGlv\nbE5mOUhhZDQ3a2UrNVhHZithUTJMRlo2Q2hZCjQ2aUt6VXpuOVE4dVVyVFc0WjBm\nMWI0SHVqYUNjdkQvSXY5QWdFTDRjdGcKLS0tIDBleURzYVBlZVcwQk85YzhOWE1z\nd2ZwYVRMM0dxM3U0RWI0Z2kxSjlETWsK+/m7xmcoXlnfYkRL3RK1VATGY6RtkmHA\nfg+YeAENLT1Mr9SnJCWcodxFicz8hiN5PinjynjqWgfv/xLMuh2G9Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:16Z",
|
||||
"mac": "ENC[AES256_GCM,data:dKjYJ4ogUztSwuP6gkyXj/gYd3TDztuwivhtRzLYwoJLJ7c4anEeZDA51tslnhdDDXQ101JDEt0tD05wAo1YdLTNg9YR0eAh4wZ/dkJ+U92U7DYEW40YXxy8co/WWW50eYqbD6SnZbZAMjf4SbaH6kqtS2MTqeba2B1G/y8sb94=,iv:Z8tzsvSTRjondcN0Vnc3bcyVlVnpqLYRoaOCe6dLQGQ=,tag:leifNnUovWvO5cqzyN2PNw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:W48JccasfJGc+WJOgGJf+T5OfVYSAfvghuXC7wUqVkkI,iv:Ukhb8+d1FrZPSmerFZUbuL/G2yHpBNSJeRL4nYG2Cm8=,tag:FCf3VI+eBsmrXvbsI62B7w==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbkFSTTBYVlNjVjNyYmIx\nSjl3UUp5VVlJZ2xudW8veS94VUVwbS94NHkwCnZ3LzZocFBnYkN3K0wwelhYalM5\nOVRmN3g1Tk5rRTViZXkwT244b1I3bzgKLS0tIEpKdCtiSTdjREV4OVZDbU1rdDk1\nTnZZTEk5OEZlWFRFTTVITkRCTFVFQXMKV8SdppHqwCEIyRTNUxjG7AtGZyVZuKBr\ndXURED8uLw37i2gvAzlUZLzQieV+F//muVF4fFONucBq5wnRskyBTA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWb1p5WEM5d3h1RVRpRVVk\nMXArMk9DOTNoYm9yT3pXbWV1YmN4MDFXd2hvClJpUEwwM0pDSUtqZHRvaTI1RlNF\nTkRBYlo4R1lpR0Y1YnRLWGIxNThFMFUKLS0tIGIzeTZ1QVJTZ3VlbjgwN1Z2Y3Bw\nTWxOT2xtVmR0Zm1jeW8wdE1xTEQ0aDAKkMuwZwMPUPO8kGmc7xCElxvpTFfGqag4\nXm6KaoyGNJO5OoWoXgJawynRNyJX2JFGjcI1M1xK4ItYU5NF4Po8ig==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:27Z",
|
||||
"mac": "ENC[AES256_GCM,data:uOaOVlLb+qLshDC4Y1KxYFgsPpHmNlfDaaPHcBBvNs7brBfsfZOPwVXqPxj5M9IzhD87l7kbYisaeBCqDKPM+IdZV1C3WstRu70a26onLl+JMXYpJSUljQwZKWnL7e48o4MZqcOtY0ghcAajI2if7nnGdKXIF5djdSLwSWHx9e4=,iv:Alb7j8UVmfLHmMzwBpJ9Aor+lffm3P3DAJaipvX6LH4=,tag:L4pTMxjA6dRhK6nl2Kokhw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:n5rS499IeNmb7or1zApWyDa74HcFhCGv35zn1DsYED8lWGEnmSFMbOUFlKwa3cBWiGRyQ2UjGzdb7lmDmoNLNzd7JB4ARgI5DapVtj+ymn56VIK5GTBOsRzJgtT4lqh3vEu4Spuv290aBHHg+lsPfdTfsJuOgAr4PVgnNY3O6Exz6EZwOSSvbUX5if7G96Y1bemF2Hh1AbdOg5KvB/Uh6mBW17hnUnbqxT7O7fepYmsSLB/qjBJWVivoMtMQWoQL8IkOapmuQYDf8ig+2FMxzcAQ71wTpTlXLGJ/XAuFSx1tJzpLbFCOpvLtGnemoUIB8viBjgfyTqN5iboJvk/WX110609yx0ew/F45CfpBfjQ66iI1shiCidfPm3ZHCO7KSjXzXEmjX5dVFp3xpz58yJH8LwQtrIQB1OQsLKe2ZmrYfjydoDuN9N+A0a6qN6KJUvIGVxNKnQ1rjj8uq4toVhxsSLQrfNkTFPA1Q624rCOu04oz3wd/oPS28war85atfMjmz9JU+Hw5u5qaxgl7UJno1VYknsrrYDIAGPxUwO56t9kHIgOZpNuD6JqDBJAfGJ1oPT+zVIBcNwNg0Z577g0l9rCLNdKInVCVahxZLCVe3LidtxL/bNzvSYHb09RbkJIhdWn7ouYCHwRRWN5kWbUopm2hHVZjRhC2CGJLBZ3+aLkEJzyZGcQCLF6Jc+/659FD8dXdaqfbR/vEKaVhoLL/WGKeo/lZL6c7TaVwDSIIa7dkk/QvGvmWhO+qG8yIs4vciGa06I62P1Y+9DsMrrWu/yqLTSkWtOpKdvIBh/jPOC4t56JEb/ksIvDLKSdrtrtHyRdCkzZ95XQDAM/a2nv+LnH75Yz9q/R0Caex/OSATCFpNcoyeSJwC5sSs6nV/NJ18YynzclSaKmAvMM8XVx8jrtUEp1yIzgBogxTFJ+yTsvEa6y/yCF3i80RrJNIkOKJDR48g7R/RJTufR+YxQj+RjWpgHbkc/Uhb0cR3X5A0ptA2X6Qz+cW9NXr5dA3A4lVsFzkZLzulqPhCxT9+g8OINx2+ayNx/M=,iv:xy7UNpO48flvQE+otJfHUJLhZlOuUueXLP03cDigXFQ=,tag:HeWayOjoZmVa0dr8GPN7Iw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITjNQcHMweFY1ekNkdWFs\nak1wcm5td29BcTI1OFBtMnFXcTg4SEJlUTFZCjc1YlovcSsxOUNFWGIxZWVsRU1L\neS9wNE1rWU05WDdSZEQzbEZib2Yxa3cKLS0tICt4TjVTOG1JUENSNW8xNVlkZHkx\nem1EMEI3Skx5R3BmLzVRYjBkK1BRODQKfX1nexjFXyXxGBvDRlssjw3XcwvtGxGC\narZ2/kmF2nETLoNDVcNIxV5KUPohhTwWdlrmPGksquBrUCjElMOVLw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMFZ0clBPaE1Xc1NGRTFK\nWlFGTWErdEtoSUhEeG1jWWNycGZsd1h0S0RrClQyQ1dRWGI4VzZRQXlvd3ErU00w\nTjNnQ2VJZEhpZ0RQdUxBUHpONjVNNjAKLS0tIGF1enFGWnhFL3pCM21nSTlhUUxs\naFQreFI2NzJmZVRZeVBxNVBQYVhTWEkKkGUPL0hOBAX2L7Vezq7Sf1V9Yu//X1/x\nNHndFsjTBcaFbDWCxcZpqVvJ0paKm46Nm+AQyhThR0OGCsKbRJhSlQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:27Z",
|
||||
"mac": "ENC[AES256_GCM,data:NieKHtjtSL9NsDsFnIU5MN8pVp55r05Qu7Qf9Pde8W/60xkF1A7kML1zpbUbMfp05VS26aIENps8BOyDZhCJgZG/89NLXccGoJTKFLDUyKDqzwCPuEK+enbRm80VQnPpMLxRSMacCl/Qm3QxpQzLuasQvLH1c18BBhy+jpwuFBY=,iv:NbA65YXGIlkWXu9tmDHdvNL4xxlfp2FFFpjckJ86E80=,tag:XX6T/Hs8W0htQyp7DJhahQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
LFXPW4H-IIDB5K4-SZ54YJL-PVRW2Q7-52YPHEI-6VZQOQA-PSAVYPV-QF5BCQA
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer1
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:QnnXrKYiJMtNQlHBBjwZeosvLjcuMwWH+YjRwy3OeaPQ9gJbj2sAUrkoZSqc5jzi577nTympol92bGIRP51TKJ3k/i0cq7P/UyGIWCjPUi5jmJs3tY8rfztlcyiXr0TPYVYyZDp1ErPfFW96xWjfaSgopjScYGRS/NGkaRedQuP39Hwg4rIPpdrkEbPgVIMCHJdvMcPm2a2RmqX1PzoBj+TEQMCFxyuRg2cTVoqyyYv95w19T+5GddvbVDmB9c8OhG4VS9nqfgdL0IJ3TMUnJAC/S1wfj64YFdhrt7lzR00xriht19ZS+HPhLlBjSruZXa4SrSEtdIMfTAXlVwtw5e/LENhiKumyhmUMqlPRJPANbgZTxa6g1Wq3NmGIOQB6,iv:2I5JcaYbEA4W9zLWTTR4j4I1hREDqb7Nh4Wg9AVEaHE=,tag:WiJhtiPjTMN+CgJU+KNmgw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL1pYVmdUclF3dXhlTjJ5\nRFl1WXRrWUwwYUwxMnNvdXdCbFVZRmp0bDFvCnRNd3p1MzV3WHMxUFlkdUt6SHJj\nTHhSYTkzcGZqNnRWMFBPTTVmNGJaa00KLS0tIHNtWkxzdkEwelVqdzRHM2lqT05H\nN1EvdDgrOG5OUXEvdTlwMDJMekdFa3MKzlxMUKl97RGAcilKQuxC34Pzpr9qPsG9\nS5qhziP2xOn+6rXO21/klVQ1pJDZZcsVI+fCFPHTbwJvXvS6VNuLCg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL3ZCelVSd1ZxTXE3TEFs\ndmdEejl2bE5CTXhqVXZnbEVOOUlaYzhNMnlVClJ5K0hmMXVBTzJUVnVybHY0WCtw\na2tnUm5jR0xGN0M0SHVSSzVvejRZQ1EKLS0tIFNnS2EweEs2V3VXVnA5T0hQVWNq\nREFZckQ5cStZamZBdmZ2WHNzUzdQa0kK6774xhCCzaODxymwg2B+3Y6aRoXyBW1G\n7jTYWEsnf9eUCcBZP+jexT3obituZENtque5Ov1zblaGIBYnxi87Fg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:27Z",
|
||||
"mac": "ENC[AES256_GCM,data:HXcgcj09lBf9NPX5htqHZuU3yLKkKMt+RZtABLPFtzsZzXV6nE/ajlHm6+xI7Fo36u1b3KN9QfQ7i70LP2UKUf1L/JTVZiExkDmcfJCluCaq+/bDpY54ICw1jLZ0ckXgMHSTNwJbvM/UbvqdcB0cCt6MzxRUREHjTW4wk08qpPM=,iv:n24kpaCvODWE6+0cjo5fHCkFYHbz/zSjj9wqbVpd/dg=,tag:9x8wboXQB/YrMUU6L8jXqQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer2
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:MMjnYNzHE7QSxrXY7aN8XSFkFwK/lpRhL4qV0vn6cwyn,iv:kB8m6ZhkNta6Y+gsYufvo45B+Q5lHuJBmYN3sk7ALKE=,tag:zbgynC6xhrFGcq+mE2pftA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4S2FEd2R1Z2hUU3BtSGdZ\nS2I3NjRrNHhGaW9SL3Q3bkJuMFNlM25ya0dNCmhpVmk0eUpWWkpkQWErRUJKdks2\nSzUvMnVQTVJVbC9WeWpkd3VHandrNW8KLS0tIFFWOUlIZFlkYi9NSjJpbXNNWE9I\nMEYxeGJEcGNLaHJOSmR2TytaWDBtaVEKhEfTi3ESLw6I2Uu3ejCGWHMv3LmRvjbg\nZ30CoUmktB7z5/RbdwwiFyV+ijuNq0RPxrlBX0VGEkd/+BrIY4BYxQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwSXpGRkIzU0hwaXBaYXdM\nNTdnZmJTSmVzbVVWZHNCd1VSNjQ4OGdzTkFzClh2Vnl1RWlLbnE0YnVsYjNvZ2hs\nOE04a1QxM0pZVUsvMkpPdlA0Tjdpb28KLS0tIEpMVmFsTVpFcVVvcDdRVk5xZFdS\nMXBTdkt4SlV5UnJTcG4wQU11YjZrS1UKcLyVL6PknecAOLEhjYYbZ38+e+g5jXFd\nJcu7fHEs5O464vgM58SKJQ5m77rGzwqRGh6MjqJTfAl8nPbvw0t2cg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:39Z",
|
||||
"mac": "ENC[AES256_GCM,data:957Dud8YuagJRf8I0pAe4Tpk+En4ac99TAjRVv5eYAQtyDR1iHUwu/NIc0I14wZj7bKP6UqbOntST3jl33xhM2NQGG/FskJppsbS355OirpVfNahqlhcy0Jy5p1L9TikY89SlFgO6kx2eYs67inbk3UXnyeVxAcEIq6qnmh0u60=,iv:rfLaPYlcnBLecIYTIqJbaI2DNERS1MfU1C+7xH51kwY=,tag:5lQ1Ouf40MyB/Q3B6EwMzg==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer2
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:65FnMTSkJxWZrik97mTIIZBH7Y+8bxTlV/wIA2jTV96oQebGWN6kI3lkR562e9ZVoiB5xyTQayU+NZhgIFEzyC5xAnXmLnKlZ2cvcjDJ51TlAujoHD5+R36tb6FrsBkuki3UI/v0xeWUhJSKpIoPL3/1Tq4l8EWVRR/JTf8fuC06HxwyiXJeMU3CkLpbt/txL2VGM9nNov+CbEZL+WJ99kCuRQ/QPAVglj0AulttXBPztNnUtPZBnBZzbFpxE4nRZ7ufjweVrRYgqYNj1CT0gdOJnyId9uu7vkHpZvGiwB3CaEeG3VyOGidfocmC1Yq/bQw26djx6npuiKoXPEgJmyaoi9DPuOvrvMwuYMpuueZCM7+54fbk7VTKusRtp0iOddSP1pevg1zM/w3NjRnnzgicahzxRf2EEGBRea9P4/znmPMHdbylXp1mnuqGpYZEteweOyFQFkjEmjIASI/ZI04JSgTGs37D3WrpD+Utjh/nVOtmeFHbeqbhk7vngDf++PW9nNM3xNhsALhd1w313agzUffQjRI96zXGj4SkUoPXkO9v9uPY+bZUxP4VncO4FbgxcEkO9YS57LiLp56YUzC/72Z30zsLrCnl2YZTXQ8nc7fvFVqfl61HmUC7xaEckQ0YXxH1yXQj+YTsk1qOVY/AyKjjq33j96de0i4AxYCEgZGU0pTkbT92MNezs7Atz066YIi1YC9YsMMMOXdtlz0FvQIN0WTzSqo1AtuyAUXo+otBX9IeMzX3hQ++E7CuHDMjR+I+jwwouAlrNCcn1Ean48cj6UkhNFHem/fysDL/2OYUZqeUfB7Qn8r0Ffewo55/87J0o2+8zlxdEOWPYiKOsT91G8Lq77OaadZCGFnNWlBrzqUkt+TuUa7UM9ok8uyZLX4R+f+hvIuvp+T+IAcz6r0R+1ndHai59r5gfgBeFxB5qnTPYTgujcdAiEeqfg7Dx0DPKEBiIj5awgsdd057oVf+eEY4Cu/mXuKb70TlbgEvelNmB5MRDlWTRm6ybrbnb7NfPmt5UYaTcPX6l/kP5ek95Q==,iv:uAE/AVleWSNp6kY/anvkJOeUqlfiuAgtHBgry6IqcJc=,tag:+J8jTvKqEULP8GW3IUqyTQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqdmdzU3Y2V1MzaE51a2Fj\nbFBIUWF3eXJrTUhKVW1tYzFSeGpad1BwRDBvCk96Z1lIMDg0ZnMrNnJkUzA4VXJ1\nZEtzbGNJU3ZzVmFYMzRBNmo5TnEybUkKLS0tIHErQ2dYZlVuckdML3Job3dwM0hM\nVEVWek40Y0F0bStzd09XenhWaVFIYVkKToXp2wUPBe9LtuLMlBPfpQRq4WHLbnSt\nueOxJvflzQveUhdpuHi6IbtE9HyCwtn6joeOnBTNFxt12Dy/s247Aw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3bGpQQ1N0cmRmcURGV3hq\nUGdHOWh0S1VIQkQ2aFFVNXVnZmpFc050S240Ck03Z1NXNDVISTRvNnhkVnFZdzg0\nVFlXZ1g4citlV21PZ2lEbmRhMFhJaUUKLS0tIDIyTXZNTWpIUFhSYXR4VXBUN284\nYTFqWkQza0V3M05lZDlBQzBtWWFlaTAKfJViXG1KULBXTmDC4PLSWRy9LAVchE47\n1rBtT+r/KlbnOTYYEo7zCMagkPhDdPD5WSegmtghUGkXRtqaTZcTBA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:39Z",
|
||||
"mac": "ENC[AES256_GCM,data:HsnTHpXkyqJMlpbKzHvBH7bxg+ssDwXzjA7JTdpioP74gD8u+oPTHJOlgYI4Qbvr04JbZiRy/cko3BTUXj8C6nCV5gyXhDG3O0rkAGd/v3108ff7WiOWbXpNJ5QhLBnG4Ny1/ITfzu7MQtNkvGFnoktyMtvqHkVeJf5IhOuT6Fo=,iv:T1IYJyi8eXGdVE/JQSvyoyU/Iw/RlpO4TQCx1naZXXI=,tag:Z3J/313RG337U9HdXidrCA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
@@ -0,0 +1 @@
|
||||
5N6ZTHC-FZKLOY7-DJCORO3-BG3C4Q3-KD3PCMP-2DM7EHQ-2FS25FK-DCOTSAI
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/machines/peer2
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:rDPNgUOmTINCoQnTQMjEXXrkvIYT7Ju6tX56aUA3cjlRGSzGmJb3Vmec58RS2QHmuOPUykydPTuAHxKs+SNZoXbdzDn75gAjS+eGNXGB9H9TFS2+iLpaSC5XTnfSdyzuGVyPRNvceI/C8FIvEauEpJ9dbS2kkTFTD15u2iI6zTmm/Mtvy2C18BwwEtu+m1TWB90oVduX05rrdnxoMjtscBCKR8fz6CbqYrbnpag9AUGUk8Z+cIMYGlSjDRlyaLWkh33aV/TWx/QOzfAWCyCVLMnU500ch0c6l2fg8daKDJJas2jHxo8KnlBIP2b6SLrcEKtlOSaPicizlJ9UDilDqHQfbucEJTgkQ2eEwJWSmXhU3q3ttGm3mxhQgej5FeIW,iv:6P1hFhDmHs4IYeyxVA/lYu2OL0ciaZNPEmMdBudacds=,tag:9MjdSKKjUO3VlTh/QmAPtA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUWYyQXVUMUMwaUt4S3RO\nYkNzd3FxU29qYk1wWVlrTUNPK0lKSTU2RFJrCmZaeGg5Nlg5azhaRSs5SzVDZlpV\nKzJuUmlSekYrdzdGQ3JDejNwUmZ3cWMKLS0tIFhGT29pUC9wNlpGaHBMSHROV0lP\nNXNjQytQVDlOQmI1UThUTm5Qd1BmNmsKlvG3ZENmjPUDHRUB2o4RTCueIdtXZjxU\nz3Xg/RWZjaDInneg0z/jXyhKoqLRk1J0h9f/36CHT7RtVHIjtz2KIQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3MWovZlBoVk1HdUhhSXNy\nYXgxZVdIaDhqMTk0dFRCVFptaUE0ZmZ4ZHlBClpDNS85a2U0WE12TXVzOFhDcTRM\nWG1yU0dDaS9pUWVUT3pnWXNxRjlSVmcKLS0tIGxiTWVIREE4SlQ0Mm8xQW0ycnB2\na2Q4WGY2eDl0UGVTdXo1bWhMQjdNNzgKorezVfxaH/Kx3lpX57zF/0gAeCu2NzWX\nAm7RAFeHDJ+w0jIlZa2RM6mXOTKDzaF+h4j27GcLLbXsSRNhlFMqvg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-07-22T13:55:39Z",
|
||||
"mac": "ENC[AES256_GCM,data:w9GKY762/ikgNU0ypIOzlnXZqnW3tN1Fz+cVCsgPMQhZbf2OIb5zHgQAcZMHJII5+WqPxLE+PThhZ2hstuz0hvvqXF37KvuT5MblcGShCNOCTJiVwLVhhEUQl3c3C9iXKV+A/aG9psagjHAlhxsdv1UKqVPhQ0bgrl92EZZPdwo=,iv:3XuVGnMu2tOLvkjY8G78qk+mJWZShRsgWx7LwU3qaCI=,tag:+tQcEjZttN9cPxAOer4IoQ==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../../sops/users/admin
|
||||
110
clanServices/tor/default.nix
Normal file
110
clanServices/tor/default.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
clanServices/tor/flake-module.nix
Normal file
9
clanServices/tor/flake-module.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
tor = module;
|
||||
};
|
||||
}
|
||||
@@ -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" ];
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -97,6 +97,7 @@ nav:
|
||||
- reference/clanServices/packages.md
|
||||
- reference/clanServices/sshd.md
|
||||
- reference/clanServices/state-version.md
|
||||
- reference/clanServices/syncthing.md
|
||||
- reference/clanServices/trusted-nix-caches.md
|
||||
- reference/clanServices/users.md
|
||||
- reference/clanServices/wifi.md
|
||||
@@ -135,7 +136,6 @@ nav:
|
||||
- reference/clanModules/static-hosts.md
|
||||
- reference/clanModules/sunshine.md
|
||||
- reference/clanModules/syncthing-static-peers.md
|
||||
- reference/clanModules/syncthing.md
|
||||
- reference/clanModules/thelounge.md
|
||||
- reference/clanModules/trusted-nix-caches.md
|
||||
- reference/clanModules/user-password.md
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{% extends "base.html" %} {% block extrahead %}
|
||||
<style>
|
||||
|
||||
.md-main__inner {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.md-content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.md-main__inner {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block site_nav %}{% endblock %} {% block content %} {{
|
||||
page.content }} {% endblock %}
|
||||
|
||||
@@ -2,86 +2,5 @@
|
||||
template: options.html
|
||||
---
|
||||
|
||||
<script>
|
||||
|
||||
const variables = [
|
||||
'--md-default-bg-color',
|
||||
'--md-default-fg-color',
|
||||
'--md-default-fg-color--light',
|
||||
'--md-default-fg-color--lightest'
|
||||
];
|
||||
|
||||
let colorScheme = 'default';
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-md-color-scheme') {
|
||||
colorScheme = mutation.target.getAttribute('data-md-color-scheme');
|
||||
console.log('color scheme changed', colorScheme);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-md-color-scheme']
|
||||
});
|
||||
|
||||
function syncCSSVariables() {
|
||||
const iframe = document.getElementById('options-frame');
|
||||
console.log('syncing css variables', iframe);
|
||||
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
const iframeRoot = iframeDoc.documentElement;
|
||||
|
||||
const targetElement = document.querySelector(`[data-md-color-scheme="${colorScheme}"]`);
|
||||
const parentStyles = getComputedStyle(targetElement);
|
||||
|
||||
console.log('parent styles', parentStyles);
|
||||
|
||||
variables.forEach(varName => {
|
||||
const value = parentStyles.getPropertyValue(varName);
|
||||
if (value.trim()) {
|
||||
console.log('setting', varName, value);
|
||||
iframeRoot.style.setProperty(varName, value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// add our custom styling
|
||||
addCustomCSS(iframe);
|
||||
}
|
||||
|
||||
|
||||
function addCustomCSS(iframe) {
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
|
||||
const cssLink = iframeDoc.createElement('link');
|
||||
cssLink.id = "clan-css";
|
||||
cssLink.rel = "stylesheet";
|
||||
cssLink.type = "text/css";
|
||||
cssLink.href = "/static/options.css";
|
||||
|
||||
iframeDoc.head.appendChild(cssLink);
|
||||
}
|
||||
|
||||
function onIFrameLoad() {
|
||||
const iframe = document.getElementById('options-frame');
|
||||
|
||||
// initial sync of css variables
|
||||
syncCSSVariables(iframe);
|
||||
|
||||
// listen for theme changes
|
||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const lightModeQuery = window.matchMedia('(prefers-color-scheme: light)');
|
||||
|
||||
darkModeQuery.addEventListener('change', syncCSSVariables);
|
||||
lightModeQuery.addEventListener('change', syncCSSVariables);
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<iframe id="options-frame" src="/options-page/" onload="onIFrameLoad()" height="1000" width="100%"></iframe>
|
||||
|
||||
[asciinema-player](static/asciinema-player)
|
||||
<iframe src="/options-page/" height="1000" width="100%"></iframe>
|
||||
|
||||
@@ -20,7 +20,3 @@
|
||||
.md-nav__item.md-nav__item--section > label > span {
|
||||
color: var(--md-typeset-a-color);
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url(./Roboto-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: url(./FiraCode-VF.ttf) format("truetype");
|
||||
}
|
||||
|
||||
:root {
|
||||
--f-family: "Roboto";
|
||||
--f-family-mono: "Fira Code";
|
||||
|
||||
--c-page: var(--md-default-bg-color);
|
||||
--c-card: transparent;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
div.card {
|
||||
border: .05rem solid var(--md-default-fg-color--lightest) !important;
|
||||
}
|
||||
|
||||
|
||||
form {
|
||||
label {
|
||||
gap: 1rem;
|
||||
|
||||
input {
|
||||
background-color: #00000042 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffffff1f !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
flake.lock
generated
20
flake.lock
generated
@@ -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": 1752718651,
|
||||
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "2bf3421f7fed5c84d9392b62dcb9d76ef09796a7",
|
||||
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
|
||||
"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": {
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
inputs = {
|
||||
flake-parts.follows = "flake-parts";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
systems.follows = "systems";
|
||||
treefmt-nix.follows = "treefmt-nix";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 -
|
||||
|
||||
|
||||
@@ -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 =
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ in
|
||||
{
|
||||
options = {
|
||||
instances = lib.mkOption {
|
||||
default = { };
|
||||
# instances.<instanceName>...
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
@@ -57,6 +58,7 @@ in
|
||||
};
|
||||
# instances.<machineName>...
|
||||
machines = lib.mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
config.exportsModule
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
pkgs/clan-app/ui/logos/darknet-builder-logo.svg
Normal file
1
pkgs/clan-app/ui/logos/darknet-builder-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -138,6 +138,10 @@
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
& > span.typography {
|
||||
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* button group */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,8 +46,6 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
value={value()}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
@@ -43,16 +54,54 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
{...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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,28 @@ 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">
|
||||
{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>
|
||||
|
||||
@@ -14,35 +14,35 @@ 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" },
|
||||
{ label: "Brian's Clan", path: "/clans/1" },
|
||||
{ label: "Dave's Clan", path: "/clans/2" },
|
||||
{ label: "Mic92's Clan", path: "/clans/3" },
|
||||
],
|
||||
clanDetail: {
|
||||
label: "Brian's Clan",
|
||||
settingsPath: "/clan/1/settings",
|
||||
settingsPath: "/clans/1/settings",
|
||||
machines: [
|
||||
{
|
||||
label: "Backup & Home",
|
||||
path: "/clan/1/machine/backup",
|
||||
path: "/clans/1/machine/backup",
|
||||
serviceCount: 3,
|
||||
status: "Online",
|
||||
},
|
||||
{
|
||||
label: "Raspberry Pi",
|
||||
path: "/clan/1/machine/pi",
|
||||
path: "/clans/1/machine/pi",
|
||||
serviceCount: 1,
|
||||
status: "Offline",
|
||||
},
|
||||
{
|
||||
label: "Mom's Laptop",
|
||||
path: "/clan/1/machine/moms-laptop",
|
||||
path: "/clans/1/machine/moms-laptop",
|
||||
serviceCount: 2,
|
||||
status: "Installed",
|
||||
},
|
||||
{
|
||||
label: "Dad's Laptop",
|
||||
path: "/clan/1/machine/dads-laptop",
|
||||
path: "/clans/1/machine/dads-laptop",
|
||||
serviceCount: 4,
|
||||
status: "Not Installed",
|
||||
},
|
||||
@@ -52,10 +52,10 @@ const sidebarNavProps: SidebarNavProps = {
|
||||
{
|
||||
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: "Borgbackup", path: "/clans/1/service/borgbackup" },
|
||||
{ label: "Syncthing", path: "/clans/1/service/syncthing" },
|
||||
{ label: "Mumble", path: "/clans/1/service/mumble" },
|
||||
{ label: "Minecraft", path: "/clans/1/service/minecraft" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -81,7 +81,7 @@ const meta: Meta<RouteSectionProps> = {
|
||||
component: SidebarNav,
|
||||
render: (_: never, context: StoryContext<SidebarNavProps>) => {
|
||||
const history = createMemoryHistory();
|
||||
history.set({ value: "/clan/1/machine/backup" });
|
||||
history.set({ value: "/clans/1/machine/backup" });
|
||||
|
||||
return (
|
||||
<div style="height: 670px;">
|
||||
@@ -93,7 +93,7 @@ const meta: Meta<RouteSectionProps> = {
|
||||
</Suspense>
|
||||
)}
|
||||
>
|
||||
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
|
||||
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
|
||||
</MemoryRouter>
|
||||
</div>
|
||||
);
|
||||
|
||||
9
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.css
Normal file
9
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.css
Normal 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;
|
||||
}
|
||||
}
|
||||
40
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.stories.tsx
Normal file
40
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.stories.tsx
Normal 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",
|
||||
},
|
||||
};
|
||||
34
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.tsx
Normal file
34
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 selectClanFolder = async () => {
|
||||
const req = callApi("get_clan_folder", {});
|
||||
@@ -21,9 +21,37 @@ export const selectClanFolder = async () => {
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||
navigate("/clan/" + window.btoa(uri));
|
||||
navigate("/clans/" + window.btoa(uri));
|
||||
};
|
||||
|
||||
export const clanURIParam = (params: Params) => {
|
||||
return window.atob(params.clanURI);
|
||||
};
|
||||
|
||||
export function useClanURI(opts: { force: true }): string;
|
||||
export function useClanURI(opts: { force: boolean }): string | null;
|
||||
export function useClanURI(
|
||||
opts: { force: boolean } = { force: false },
|
||||
): string | null {
|
||||
const maybe = () => {
|
||||
const params = useParams();
|
||||
if (!params.clanURI) {
|
||||
return null;
|
||||
}
|
||||
const clanURI = clanURIParam(params);
|
||||
if (!clanURI) {
|
||||
throw new Error(
|
||||
"Could not decode clan URI from params: " + params.clanURI,
|
||||
);
|
||||
}
|
||||
return clanURI;
|
||||
};
|
||||
|
||||
const uri = maybe();
|
||||
if (!uri && opts.force) {
|
||||
throw new Error(
|
||||
"ClanURI is not set. Use this function only within contexts, where clanURI is guaranteed to have been set.",
|
||||
);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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";
|
||||
@@ -22,4 +22,11 @@ if (import.meta.env.DEV) {
|
||||
await import("solid-devtools");
|
||||
}
|
||||
|
||||
render(() => <Router root={Layout}>{Routes}</Router>, root!);
|
||||
render(
|
||||
() => (
|
||||
<QueryClientProvider client={client}>
|
||||
<Router root={Layout}>{Routes}</Router>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
root!,
|
||||
);
|
||||
|
||||
31
pkgs/clan-app/ui/src/queries/queries.ts
Normal file
31
pkgs/clan-app/ui/src/queries/queries.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/solid-query";
|
||||
import { callApi, SuccessData } from "../hooks/api";
|
||||
|
||||
export type ListMachines = SuccessData<"list_machines">;
|
||||
|
||||
export type MachinesQueryResult = UseQueryResult<ListMachines>;
|
||||
|
||||
interface MachinesQueryParams {
|
||||
clanURI: string | null;
|
||||
}
|
||||
export const useMachinesQuery = (props: MachinesQueryParams) =>
|
||||
useQuery<ListMachines>(() => ({
|
||||
queryKey: ["clans", props.clanURI, "machines"],
|
||||
enabled: !!props.clanURI,
|
||||
queryFn: async () => {
|
||||
if (!props.clanURI) {
|
||||
return {};
|
||||
}
|
||||
const api = callApi("list_machines", {
|
||||
flake: {
|
||||
identifier: props.clanURI,
|
||||
},
|
||||
});
|
||||
const result = await api.result;
|
||||
if (result.status === "error") {
|
||||
console.error("Error fetching machines:", result.errors);
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
13
pkgs/clan-app/ui/src/routes/Clan/Clan.css
Normal file
13
pkgs/clan-app/ui/src/routes/Clan/Clan.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,10 +1,231 @@
|
||||
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, createSignal } from "solid-js";
|
||||
import { 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";
|
||||
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <CubeScene />;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
<ClanSceneController />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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="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 = () => {
|
||||
const clanURI = useClanURI({ force: true });
|
||||
|
||||
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);
|
||||
|
||||
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] = {}; // Clear the entire object
|
||||
// delete s.sceneData[clanURI][machineId];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
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 })}>
|
||||
<Splash />
|
||||
</div>
|
||||
|
||||
<CubeScene
|
||||
isLoading={query.isLoading}
|
||||
cubesQuery={query}
|
||||
onCreate={onCreate}
|
||||
sceneStore={() => {
|
||||
const clanURI = useClanURI({ force: true });
|
||||
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 | null;
|
||||
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
|
||||
}) => {
|
||||
const machinesQuery = useMachinesQuery({ clanURI: props.clanURI });
|
||||
|
||||
// This component can be used to provide scene data or context if needed
|
||||
return props.children({ query: machinesQuery });
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
663
pkgs/clan-app/ui/src/routes/Onboarding/Creating.css
Normal file
663
pkgs/clan-app/ui/src/routes/Onboarding/Creating.css
Normal 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);
|
||||
}
|
||||
}
|
||||
90
pkgs/clan-app/ui/src/routes/Onboarding/Creating.tsx
Normal file
90
pkgs/clan-app/ui/src/routes/Onboarding/Creating.tsx
Normal 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>
|
||||
);
|
||||
@@ -1,18 +1,29 @@
|
||||
import { Component, createSignal, Match, Setter, Show, Switch } from "solid-js";
|
||||
import {
|
||||
Accessor,
|
||||
Component,
|
||||
createSignal,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import "./Onboarding.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Logo } from "@/src/components/Logo/Logo";
|
||||
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
||||
import { activeClanURI } from "@/src/stores/clan";
|
||||
import { activeClanURI, addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import {
|
||||
createForm,
|
||||
FormStore,
|
||||
getError,
|
||||
getErrors,
|
||||
getValue,
|
||||
SubmitHandler,
|
||||
valiForm,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
@@ -20,23 +31,32 @@ import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import * as v from "valibot";
|
||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { Creating } from "./Creating";
|
||||
|
||||
type State = "welcome" | "setup";
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
const SetupSchema = v.object({
|
||||
name: v.pipe(v.string(), v.nonEmpty("Please enter a name.")),
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Please enter a name."),
|
||||
v.regex(
|
||||
new RegExp("^[a-zA-Z0-9_\\-]+$"),
|
||||
"Name must be alphanumeric and can contain underscores and dashes, without spaces.",
|
||||
),
|
||||
),
|
||||
description: v.pipe(v.string(), v.nonEmpty("Please describe your clan.")),
|
||||
directory: v.pipe(v.string(), v.nonEmpty("Please select a directory.")),
|
||||
directory: v.pipe(
|
||||
// initial value is undefined, and I can't see how to handle this better in valibot, so for now when the type
|
||||
// is incorrect we treat it as empty
|
||||
v.string("Please select a directory."),
|
||||
v.nonEmpty("Please select a directory."),
|
||||
),
|
||||
});
|
||||
|
||||
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||
|
||||
interface backgroundProps {
|
||||
state: State;
|
||||
form: FormStore<SetupForm>;
|
||||
}
|
||||
|
||||
const background = (props: backgroundProps) => (
|
||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
@@ -71,7 +91,11 @@ const background = (props: backgroundProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const welcome = (setState: Setter<State>) => {
|
||||
const welcome = (props: {
|
||||
setState: Setter<State>;
|
||||
welcomeError: Accessor<string | undefined>;
|
||||
setWelcomeError: Setter<string | undefined>;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const selectFolder = async () => {
|
||||
@@ -91,7 +115,23 @@ const welcome = (setState: Setter<State>) => {
|
||||
Build your <br />
|
||||
own darknet
|
||||
</Typography>
|
||||
<Button hierarchy="secondary" onClick={() => setState("setup")}>
|
||||
{props.welcomeError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Your Clan creation failed"
|
||||
description={props.welcomeError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
onClick={() => {
|
||||
// reset welcome error
|
||||
props.setWelcomeError(undefined);
|
||||
// move to next step
|
||||
props.setState("setup");
|
||||
}}
|
||||
>
|
||||
Start building
|
||||
</Button>
|
||||
<div class="separator">
|
||||
@@ -126,13 +166,89 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
|
||||
const [state, setState] = createSignal<State>("welcome");
|
||||
|
||||
// used to display an error in the welcome screen in the event of a failed
|
||||
// clan creation
|
||||
const [welcomeError, setWelcomeError] = createSignal<string | undefined>();
|
||||
|
||||
//
|
||||
const [setupForm, { Form, Field }] = createForm<SetupForm>({
|
||||
validate: valiForm(SetupSchema),
|
||||
});
|
||||
|
||||
const metaError = () => {
|
||||
const errors = getErrors(setupForm, ["name", "description"]);
|
||||
return errors ? errors.name || errors.description : undefined;
|
||||
const formError = () => {
|
||||
const formErrors = getErrors(setupForm);
|
||||
return (
|
||||
formErrors.name ||
|
||||
formErrors.description ||
|
||||
formErrors.directory ||
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectFile = async () => {
|
||||
const req = callApi("get_system_file", {
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Select a folder for you new Clan",
|
||||
},
|
||||
});
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
// just throw the first error, I can't imagine why there would be multiple
|
||||
// errors for this call
|
||||
throw new Error(resp.errors[0].message);
|
||||
}
|
||||
|
||||
if (resp.status === "success" && resp.data) {
|
||||
return resp.data[0];
|
||||
}
|
||||
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<SetupForm> = async (
|
||||
{ name, description, directory },
|
||||
event,
|
||||
) => {
|
||||
const path = `${directory}/${name}`;
|
||||
|
||||
const req = callApi("create_clan", {
|
||||
opts: {
|
||||
dest: path,
|
||||
// todo allow users to select a template
|
||||
template: "minimal",
|
||||
initial: {
|
||||
meta: {
|
||||
name: name,
|
||||
description: description,
|
||||
// todo it tries to 'delete' icon if it's not provided
|
||||
// this logic is unexpected, and needs reviewed.
|
||||
icon: null,
|
||||
},
|
||||
machines: {},
|
||||
instances: {},
|
||||
services: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setState("creating");
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
setWelcomeError(resp.errors[0].message);
|
||||
setState("welcome");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.status === "success") {
|
||||
addClanURI(path);
|
||||
setActiveClanURI(path);
|
||||
navigateToClan(navigate, path);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -140,7 +256,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
{background({ form: setupForm, state: state() })}
|
||||
<div class="container">
|
||||
<Switch>
|
||||
<Match when={state() === "welcome"}>{welcome(setState)}</Match>
|
||||
<Match when={state() === "welcome"}>
|
||||
{welcome({
|
||||
setState,
|
||||
welcomeError,
|
||||
setWelcomeError,
|
||||
})}
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "setup"}>
|
||||
<div class="setup">
|
||||
@@ -155,8 +277,16 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
Setup
|
||||
</Typography>
|
||||
</div>
|
||||
<Form>
|
||||
<Fieldset name="meta" error={metaError()}>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{formError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Form error"
|
||||
description={formError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Fieldset name="meta">
|
||||
<Field name="name">
|
||||
{(field, input) => (
|
||||
<TextInput
|
||||
@@ -195,15 +325,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
name="location"
|
||||
error={getError(setupForm, "directory")}
|
||||
>
|
||||
<Fieldset name="location">
|
||||
<Field name="directory">
|
||||
{(field, input) => (
|
||||
<HostFileInput
|
||||
onSelectFile={async () => "test"}
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
orientation="horizontal"
|
||||
required={true}
|
||||
@@ -228,6 +356,10 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Form>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "creating"}>
|
||||
<Creating />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -8,7 +8,41 @@ export const Routes: RouteDefinition[] = [
|
||||
component: Onboarding,
|
||||
},
|
||||
{
|
||||
path: "/clan/:clanURI",
|
||||
component: Clan,
|
||||
path: "/clans",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => (
|
||||
<h1>
|
||||
Clans (index) - (Doesnt really exist, just to keep the scene
|
||||
mounted)
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/:clanURI",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: Clan,
|
||||
},
|
||||
{
|
||||
path: "/machines",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => <h1>Machines (Index)</h1>,
|
||||
},
|
||||
{
|
||||
path: "/:machineID",
|
||||
component: (props) => (
|
||||
<h1>Machine ID: {props.params.machineID}</h1>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
15
pkgs/clan-app/ui/src/scene/cubes.css
Normal file
15
pkgs/clan-app/ui/src/scene/cubes.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.cubes-scene-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { CubeScene } from "./cubes";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "scene/cubes",
|
||||
component: CubeScene,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
45
pkgs/clan-app/ui/src/scene/splash.css
Normal file
45
pkgs/clan-app/ui/src/scene/splash.css
Normal file
@@ -0,0 +1,45 @@
|
||||
#splash {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(to top, #e3e7e7, #edf1f1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#splash .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply h-8 mb-8;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@apply h-3 w-60 mb-3;
|
||||
width: 18rem;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#bfd0d2 0px,
|
||||
#bfd0d2 10px,
|
||||
#f7f9fa 10px,
|
||||
#f7f9fa 20px
|
||||
);
|
||||
animation: stripe-move 1s linear infinite;
|
||||
background-size: 28px 28px; /* Sqrt(20^2 + 20^2) ~= 28 */
|
||||
|
||||
@apply border-2 border-solid rounded-[3px] border-bg-def-1;
|
||||
}
|
||||
|
||||
@keyframes stripe-move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 28px 0;
|
||||
}
|
||||
}
|
||||
15
pkgs/clan-app/ui/src/scene/splash.stories.tsx
Normal file
15
pkgs/clan-app/ui/src/scene/splash.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Splash } from "./splash";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "scene/splash",
|
||||
component: Splash,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user