Compare commits
20 commits
2a9e8e6c03
...
c0004409d7
| Author | SHA1 | Date | |
|---|---|---|---|
| c0004409d7 | |||
| 004832fc06 | |||
| 83371117d4 | |||
| e1c02d7a91 | |||
| 4dfc898140 | |||
| 21dc584199 | |||
| 6c80606b7e | |||
| 907f2cabca | |||
| 5c13051b4b | |||
| bc3269a814 | |||
| 63d9d6b004 | |||
| 9a821fda94 | |||
| 8157d0d561 | |||
| 024a6bdbe2 | |||
| 4bb20124a7 | |||
| 509684d0bd | |||
| c782bd5e53 | |||
| 4f8249b780 | |||
| d1a8e7222f | |||
| 402c847f3c |
18 changed files with 201 additions and 24 deletions
|
|
@ -16,7 +16,8 @@
|
||||||
ShareURL = "https://${shareFqdn}";
|
ShareURL = "https://${shareFqdn}";
|
||||||
EnableSharing = true;
|
EnableSharing = true;
|
||||||
DataFolder = "/persist/navidrome";
|
DataFolder = "/persist/navidrome";
|
||||||
MusicFolder = "/binds/music";
|
MusicFolder = "/binds/music/main";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
systemd.services.navidrome.serviceConfig.BindReadOnlyPaths = ["/binds/music"];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@
|
||||||
shareFqdn = "muse.lava.moe";
|
shareFqdn = "muse.lava.moe";
|
||||||
subnetId = "5";
|
subnetId = "5";
|
||||||
|
|
||||||
subnet = x: "fd0d:1::${subnetId}:${toString x}";
|
subnet = x: "fd0d:2::${subnetId}:${toString x}";
|
||||||
host = subnet 1;
|
host = subnet 1;
|
||||||
client = subnet 2;
|
client = subnet 2;
|
||||||
|
|
||||||
subnet4 = x: "10.30.${subnetId}.${toString x}";
|
subnet4 = x: "10.32.${subnetId}.${toString x}";
|
||||||
host4 = subnet4 1;
|
host4 = subnet4 1;
|
||||||
client4 = subnet4 2;
|
client4 = subnet4 2;
|
||||||
|
|
||||||
|
|
@ -39,13 +39,7 @@
|
||||||
useACMEHost = "lava.moe";
|
useACMEHost = "lava.moe";
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
locations."/".proxyPass = "http://[${client}]:4533";
|
locations."/".proxyPass = "http://[${client}]:4533";
|
||||||
listenAddresses = [ "10.0.0.1" "[fd0d::1]" "100.67.1.1" ];
|
listenAddresses = [ "100.67.2.1" ];
|
||||||
};
|
|
||||||
services.nginx.virtualHosts."${shareFqdn}" = {
|
|
||||||
useACMEHost = "lava.moe";
|
|
||||||
forceSSL = true;
|
|
||||||
locations."/".return = "404";
|
|
||||||
locations."/share/".proxyPass = "http://[${client}]:4533";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [ "d /persist/containers/${name} 755 root users" ];
|
systemd.tmpfiles.rules = [ "d /persist/containers/${name} 755 root users" ];
|
||||||
|
|
@ -68,7 +62,7 @@
|
||||||
isReadOnly = false;
|
isReadOnly = false;
|
||||||
};
|
};
|
||||||
bindMounts."music" = {
|
bindMounts."music" = {
|
||||||
hostPath = "/persist/media/music";
|
hostPath = "/flower/media/music";
|
||||||
mountPoint = "/binds/music";
|
mountPoint = "/binds/music";
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
age.secrets = {
|
age.secrets = {
|
||||||
acme_dns.file = ../../secrets/acme_dns.age;
|
acme_dns.file = ../../secrets/acme_dns.age;
|
||||||
|
passwd.file = ../../secrets/passwd.age;
|
||||||
|
navidrome_env.file = ../../secrets/navidrome_env.age;
|
||||||
wpa_conf = {
|
wpa_conf = {
|
||||||
file = ../../secrets/wpa_conf.age;
|
file = ../../secrets/wpa_conf.age;
|
||||||
path = "/etc/wpa_supplicant/imperative.conf";
|
path = "/etc/wpa_supplicant/imperative.conf";
|
||||||
|
|
@ -26,11 +28,14 @@
|
||||||
modules.services.nginx
|
modules.services.nginx
|
||||||
modules.services.syncthing
|
modules.services.syncthing
|
||||||
|
|
||||||
|
inputs.c-emerald.nixosModule
|
||||||
inputs.c-garnet.nixosModule
|
inputs.c-garnet.nixosModule
|
||||||
|
|
||||||
./filesystem.nix
|
./filesystem.nix
|
||||||
./kernel.nix
|
./kernel.nix
|
||||||
./networking.nix
|
./networking.nix
|
||||||
|
./home.syncthing.nix
|
||||||
|
./samba.nix
|
||||||
|
|
||||||
../../users/hana
|
../../users/hana
|
||||||
];
|
];
|
||||||
|
|
|
||||||
39
hosts/alyssum/home.syncthing.nix
Normal file
39
hosts/alyssum/home.syncthing.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
configOn = user: port: {
|
||||||
|
me.binds."/home/${user}/.config/syncthing" = "${user}/syncthing/config";
|
||||||
|
me.binds."/home/${user}/.local/state/syncthing" = "${user}/syncthing/state";
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [ "d /flower/syncthing/${user} 700 ${user} users" ];
|
||||||
|
|
||||||
|
users.users.${user} = {
|
||||||
|
hashedPasswordFile = config.age.secrets.passwd.path;
|
||||||
|
isNormalUser = true;
|
||||||
|
linger = true;
|
||||||
|
};
|
||||||
|
home-manager.users.${user} = { ... }: {
|
||||||
|
home = {
|
||||||
|
username = "${user}";
|
||||||
|
homeDirectory = "/home/${user}";
|
||||||
|
stateVersion = "26.05";
|
||||||
|
};
|
||||||
|
services.syncthing = {
|
||||||
|
enable = true;
|
||||||
|
guiAddress = "[::]:${toString port}";
|
||||||
|
overrideDevices = false;
|
||||||
|
overrideFolders = false;
|
||||||
|
settings = {
|
||||||
|
options.listenAddresses = [
|
||||||
|
"tcp://0.0.0.0:2${toString port}"
|
||||||
|
"quic://0.0.0.0:2${toString port}"
|
||||||
|
"dynamic+https://relays.syncthing.net/endpoint"
|
||||||
|
];
|
||||||
|
defaults.folder.path = "/flower/syncthing/${user}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in lib.mkMerge [
|
||||||
|
(configOn "kujira" 8385)
|
||||||
|
(configOn "cilly" 8386)
|
||||||
|
]
|
||||||
84
hosts/alyssum/samba.nix
Normal file
84
hosts/alyssum/samba.nix
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
configOn = user: let
|
||||||
|
passwd_fname = "passwd_smb${user}";
|
||||||
|
in {
|
||||||
|
age.secrets.${passwd_fname}.file = ../../secrets/${passwd_fname}.age;
|
||||||
|
me.binds."/flower/smb/${user}/music" = "/flower/media/music/${user}";
|
||||||
|
me.binds."/flower/smb/${user}/syncthing" = "/flower/syncthing/${user}";
|
||||||
|
|
||||||
|
users.users.${user} = {
|
||||||
|
hashedPasswordFile = config.age.secrets.passwd.path;
|
||||||
|
isNormalUser = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
system.activationScripts = {
|
||||||
|
init_smbpasswd.text = let
|
||||||
|
smbpasswd = "${config.services.samba.package}/bin/smbpasswd";
|
||||||
|
in ''
|
||||||
|
printf "$(cat ${config.age.secrets.${passwd_fname}.path})\n$(cat ${config.age.secrets.${passwd_fname}.path})\n" | ${smbpasswd} -sa ${user}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
services.samba.settings."${user}" = {
|
||||||
|
"path" = "/flower/smb/${user}";
|
||||||
|
"browseable" = "yes";
|
||||||
|
"read only" = "no";
|
||||||
|
"guest ok" = "no";
|
||||||
|
"create mask" = "0644";
|
||||||
|
"directory mask" = "0755";
|
||||||
|
"force user" = user;
|
||||||
|
"force group" = "users";
|
||||||
|
"valid users" = user;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in lib.mkMerge [
|
||||||
|
(configOn "cilly")
|
||||||
|
(configOn "kujira")
|
||||||
|
{
|
||||||
|
me.binds."/flower/smb/kujira/opencloud" = "/flower/opencloud/data/storage/users/users/a8e29fc0-673c-4c67-be00-2442904acb43";
|
||||||
|
|
||||||
|
networking.firewall.allowPing = true;
|
||||||
|
|
||||||
|
services.samba = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.samba4Full;
|
||||||
|
openFirewall = true;
|
||||||
|
settings = {
|
||||||
|
global = {
|
||||||
|
"server smb encrypt" = "required";
|
||||||
|
"workgroup" = "WORKGROUP";
|
||||||
|
"server string" = "smbnix";
|
||||||
|
"netbios name" = "smbnix";
|
||||||
|
"security" = "user";
|
||||||
|
"hosts allow" = "100.64.0.0/10 127.0.0.1 alyssum localhost";
|
||||||
|
"hosts deny" = "0.0.0.0/0";
|
||||||
|
"guest account" = "nobody";
|
||||||
|
"map to guest" = "bad user";
|
||||||
|
};
|
||||||
|
"public" = {
|
||||||
|
"path" = "/flower/smb/public";
|
||||||
|
"browseable" = "yes";
|
||||||
|
"read only" = "no";
|
||||||
|
"guest ok" = "yes";
|
||||||
|
"create mask" = "0644";
|
||||||
|
"directory mask" = "0755";
|
||||||
|
"force user" = "hana";
|
||||||
|
"force group" = "users";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.samba-wsdd = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.avahi = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
nssmdns4 = true;
|
||||||
|
publish.enable = true;
|
||||||
|
publish.userServices = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
age.secrets = {
|
age.secrets = {
|
||||||
acme_dns.file = ../../secrets/acme_dns.age;
|
acme_dns.file = ../../secrets/acme_dns.age;
|
||||||
navidrome_env.file = ../../secrets/navidrome_env.age;
|
|
||||||
slskd_env.file = ../../secrets/slskd_env.age;
|
slskd_env.file = ../../secrets/slskd_env.age;
|
||||||
wg_dandelion.file = ../../secrets/wg_dandelion.age;
|
wg_dandelion.file = ../../secrets/wg_dandelion.age;
|
||||||
};
|
};
|
||||||
|
|
@ -31,12 +30,12 @@
|
||||||
inputs.c-beryllium.nixosModule
|
inputs.c-beryllium.nixosModule
|
||||||
inputs.c-citrine.nixosModule
|
inputs.c-citrine.nixosModule
|
||||||
inputs.c-diamond.nixosModule
|
inputs.c-diamond.nixosModule
|
||||||
inputs.c-emerald.nixosModule
|
|
||||||
inputs.c-fluorite.nixosModule
|
inputs.c-fluorite.nixosModule
|
||||||
|
|
||||||
./filesystem.nix
|
./filesystem.nix
|
||||||
./kernel.nix
|
./kernel.nix
|
||||||
./networking.nix
|
./networking.nix
|
||||||
|
./nginx.nix
|
||||||
|
|
||||||
../../users/hana
|
../../users/hana
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ in {
|
||||||
"/" = {
|
"/" = {
|
||||||
device = "rootfs";
|
device = "rootfs";
|
||||||
fsType = "tmpfs";
|
fsType = "tmpfs";
|
||||||
options = [ "defaults" "size=12G" "mode=755" ];
|
options = [ "defaults" "size=6G" "mode=755" ];
|
||||||
};
|
};
|
||||||
"/boot" = mkLabelMount "UEFI" "vfat";
|
"/boot" = mkLabelMount "UEFI" "vfat";
|
||||||
|
|
||||||
|
|
|
||||||
8
hosts/dandelion/nginx.nix
Normal file
8
hosts/dandelion/nginx.nix
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{ ... }: {
|
||||||
|
services.nginx.virtualHosts."muse.lava.moe" = {
|
||||||
|
useACMEHost = "lava.moe";
|
||||||
|
forceSSL = true;
|
||||||
|
locations."/".return = "404";
|
||||||
|
locations."/share/".proxyPass = "http://[fd0d:2::5:2]:4533";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
{ config, lib, ...}: {
|
{ config, lib, ...}: {
|
||||||
imports = [ ./options.nix ];
|
imports = [ ./options.nix ];
|
||||||
fileSystems = lib.mapAttrs (dest: key: {
|
fileSystems = lib.mapAttrs (dest: key: let
|
||||||
|
target = if (lib.strings.hasPrefix "/" key)
|
||||||
|
then key
|
||||||
|
else "/persist/binds/${key}";
|
||||||
|
in {
|
||||||
depends = [ "/persist" ];
|
depends = [ "/persist" ];
|
||||||
device = "/persist/binds/${key}";
|
device = target;
|
||||||
fsType = "none";
|
fsType = "none";
|
||||||
options = [ "bind" ];
|
options = [ "bind" ];
|
||||||
}) config.me.binds;
|
}) config.me.binds;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{ config, lib, pkgs, ... }: {
|
{ config, inputs, pkgs, ... }: {
|
||||||
nix = {
|
nix = {
|
||||||
|
nixPath = [ "nixpkgs=${inputs.nixpkgs}" ];
|
||||||
package = pkgs.nixVersions.latest;
|
package = pkgs.nixVersions.latest;
|
||||||
|
|
||||||
settings = rec {
|
settings = rec {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, sysConfig, ... }:
|
||||||
let
|
let
|
||||||
luaconf = pkgs.writeText "config.lua"
|
luaconf = pkgs.writeText "config.lua"
|
||||||
(lib.replaceStrings
|
(lib.replaceStrings
|
||||||
["{{OMNISHARP_PATH}}" "{{DART_PATH}}" "{{CATPPUCCIN_FLAVOUR}}"]
|
["{{OMNISHARP_PATH}}" "{{DART_PATH}}" "{{CATPPUCCIN_FLAVOUR}}" "{{USERNAME}}" "{{HOSTNAME}}"]
|
||||||
["${pkgs.omnisharp-roslyn}/bin/OmniSharp" "${pkgs.dart}/bin/dart" config.catppuccin.nvim.flavor]
|
["${pkgs.omnisharp-roslyn}/bin/OmniSharp" "${pkgs.dart}/bin/dart" config.catppuccin.nvim.flavor config.home.username sysConfig.networking.hostName]
|
||||||
(builtins.readFile ../../res/config.lua));
|
(builtins.readFile ../../res/config.lua));
|
||||||
in {
|
in {
|
||||||
systemd.user.tmpfiles.rules = [
|
systemd.user.tmpfiles.rules = [
|
||||||
|
|
@ -21,6 +21,7 @@ in {
|
||||||
withRuby = false;
|
withRuby = false;
|
||||||
|
|
||||||
extraPackages = with pkgs; [
|
extraPackages = with pkgs; [
|
||||||
|
nixd
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
texlab
|
texlab
|
||||||
astro-language-server
|
astro-language-server
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ vim.diagnostic.config({
|
||||||
|
|
||||||
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
|
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
|
||||||
|
|
||||||
local servers = { 'astro', 'clangd', 'cssls', 'html', 'nil_ls', 'tailwindcss', 'texlab', 'ts_ls', 'yamlls' }
|
local servers = { 'astro', 'clangd', 'cssls', 'html', 'tailwindcss', 'texlab', 'ts_ls', 'yamlls' }
|
||||||
for _, lsp in ipairs(servers) do
|
for _, lsp in ipairs(servers) do
|
||||||
vim.lsp.config(lsp, {
|
vim.lsp.config(lsp, {
|
||||||
capabilities = capabilities,
|
capabilities = capabilities,
|
||||||
|
|
@ -292,6 +292,32 @@ vim.lsp.config("diagnosticls", {
|
||||||
})
|
})
|
||||||
vim.lsp.enable("diagnosticls")
|
vim.lsp.enable("diagnosticls")
|
||||||
|
|
||||||
|
-- LSP/nixd
|
||||||
|
vim.lsp.config("nixd", {
|
||||||
|
cmd = { "nixd" },
|
||||||
|
filetypes = { "nix" },
|
||||||
|
root_markers = { "flake.nix", ".git" },
|
||||||
|
settings = {
|
||||||
|
nixd = {
|
||||||
|
nixpkgs = {
|
||||||
|
expr = "import <nixpkgs> { }",
|
||||||
|
},
|
||||||
|
formatting = {
|
||||||
|
command = { "nixfmt" },
|
||||||
|
},
|
||||||
|
options = {
|
||||||
|
nixos = {
|
||||||
|
expr = '(builtins.getFlake (toString ./.)).nixosConfigurations.{{HOSTNAME}}.options',
|
||||||
|
},
|
||||||
|
home_manager = {
|
||||||
|
expr = '(builtins.getFlake (builtins.toString ./.)).nixosConfigurations."{{USERNAME}}@{{HOSTNAME}}".options.home-manager.users.type.getSubOptions []',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
vim.lsp.enable("nixd")
|
||||||
|
|
||||||
-- LSP/Signatures
|
-- LSP/Signatures
|
||||||
require("lsp_signature").setup {
|
require("lsp_signature").setup {
|
||||||
hint_enable = false,
|
hint_enable = false,
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ let
|
||||||
|
|
||||||
rin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPru5eTBvHJ4ZmrrzPRHCGM09wQP/ZHSaKYalDuBVO15";
|
rin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPru5eTBvHJ4ZmrrzPRHCGM09wQP/ZHSaKYalDuBVO15";
|
||||||
in {
|
in {
|
||||||
"secrets/passwd.age".publicKeys = [ anemone blossom rin ];
|
"secrets/passwd.age".publicKeys = [ alyssum anemone blossom rin ];
|
||||||
|
"secrets/passwd_smbcilly.age".publicKeys = [ alyssum rin ];
|
||||||
|
"secrets/passwd_smbkujira.age".publicKeys = [ alyssum rin ];
|
||||||
"secrets/wpa_conf.age".publicKeys = [ alyssum blossom rin ];
|
"secrets/wpa_conf.age".publicKeys = [ alyssum blossom rin ];
|
||||||
|
|
||||||
"secrets/acme_dns.age".publicKeys = [ alyssum dandelion hazel rin ];
|
"secrets/acme_dns.age".publicKeys = [ alyssum dandelion hazel rin ];
|
||||||
"secrets/navidrome_env.age".publicKeys = [ anemone dandelion rin ];
|
"secrets/navidrome_env.age".publicKeys = [ alyssum dandelion rin ];
|
||||||
"secrets/slskd_env.age".publicKeys = [ anemone dandelion rin ];
|
"secrets/slskd_env.age".publicKeys = [ anemone dandelion rin ];
|
||||||
"secrets/tailscale_auth.age".publicKeys = [ alyssum anemone blossom dandelion rin ];
|
"secrets/tailscale_auth.age".publicKeys = [ alyssum anemone blossom dandelion rin ];
|
||||||
"secrets/warden_admin.age".publicKeys = [ rin ];
|
"secrets/warden_admin.age".publicKeys = [ rin ];
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
7
secrets/passwd_smbcilly.age
Normal file
7
secrets/passwd_smbcilly.age
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 kOMSPw CQaXT9/nw3NGD2/H/ctSQGXIoacgjfKQ24wkpEieLSQ
|
||||||
|
i4xEXgWGQ7xgQyaDQQIeDuiCLjA6Le23qSnv8C1cbcI
|
||||||
|
-> ssh-ed25519 U9FXlg GL4dCSCku/FA6ipb9XI1AxO4lhm2r/1lRAeqaGrB32o
|
||||||
|
+pPgqwnoPi3wJLobTimVMj0rng+XRapRG6jTYFXSsDM
|
||||||
|
--- eVgn3ON19pqq+L832bqlbkHUQXdaTI+LfSL4bYfEdew
|
||||||
|
Æ*Œl\ÈWç!J7E/´»îò"f@%\ìüÏ[¨òj8fÓ¶›ž
|
||||||
7
secrets/passwd_smbkujira.age
Normal file
7
secrets/passwd_smbkujira.age
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 kOMSPw Kn+LPMoyOrVwI/nrGgnxgVA3D+tVY9Tccg/Yx/jL+E8
|
||||||
|
IfWiSBh7KgNvgcHlcDzfdcB9nxm1zy12Ae7AGm39fdE
|
||||||
|
-> ssh-ed25519 U9FXlg 6eIIGEIYDo02FBsgBnwbuOeR8t4xB6jSmLfIL73UCDg
|
||||||
|
QOc0ddunQQcVEVD20DKKpn3wZWUSveFJSUTBnv+xnNk
|
||||||
|
--- MjN2i0FNzbUpBGUDNgWGXrRsYl2gtsQX+JlzZV/fYdw
|
||||||
|
TÎ <ç‘R#d<>Ć̎lLkáN¦½º8´cÃ_N¬)±ŠT
|
||||||
|
|
@ -15,7 +15,6 @@ in {
|
||||||
ffmpeg
|
ffmpeg
|
||||||
gnupg
|
gnupg
|
||||||
kitty
|
kitty
|
||||||
nil
|
|
||||||
nodejs_latest
|
nodejs_latest
|
||||||
pamixer
|
pamixer
|
||||||
pnpm
|
pnpm
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue