Compare commits

..

5 Commits

Author SHA1 Message Date
Aiden McClelland
015ff02d71 fix build 2025-11-20 01:05:50 -07:00
Aiden McClelland
10bfaf5415 fix start-tunnel build 2025-11-20 00:30:33 -07:00
Aiden McClelland
e3e0b85e0c Bugfix/alpha.13 (#3053)
* bugfixes for alpha.13

* minor fixes

* version bump

* start-tunnel workflow

* sdk beta 44

* defaultFilter

* fix reset-password on tunnel auth

* explicitly rebuild types

* fix typo

* ubuntu-latest runner

* add cleanup steps

* fix env on attach
2025-11-19 22:48:49 -07:00
Matt Hill
ad0632892e Various (#3051)
* tell user to restart server after kiosk chnage

* remove unused import

* dont show tor address on server setup

* chore: address comments

* revert mock

* chore: remove uptime block on mobile

* utiliser le futur proche

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-19 10:35:07 -07:00
Aiden McClelland
f26791ba39 fix raspi fsck 2025-11-17 12:17:58 -07:00
52 changed files with 479 additions and 291 deletions

100
.github/workflows/start-tunnel.yaml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Start-Tunnel
on:
workflow_call:
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment
options:
- NONE
- dev
- unstable
- dev-unstable
runner:
type: choice
description: Runner
options:
- standard
- fast
arch:
type: choice
description: Architecture
options:
- ALL
- x86_64
- aarch64
- riscv64
push:
branches:
- master
- next/*
pull_request:
branches:
- master
- next/*
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
compile:
name: Compile Base Binaries
strategy:
fail-fast: true
matrix:
arch: >-
${{
fromJson('{
"x86_64": ["x86_64"],
"aarch64": ["aarch64"],
"riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64", "riscv64"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: make tunnel-deb
env:
PLATFORM: ${{ matrix.arch }}
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4
with:
name: start-tunnel_${{ matrix.arch }}.deb
path: start-tunnel-*_${{ matrix.arch }}.deb

View File

@@ -67,8 +67,13 @@ jobs:
"ALL": ["x86_64", "aarch64"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-22.04", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
@@ -134,7 +139,7 @@ jobs:
${{
fromJson(
format(
'["ubuntu-22.04", "{0}"]',
'["ubuntu-latest", "{0}"]',
fromJson('{
"x86_64": "buildjet-8vcpu-ubuntu-2204",
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
@@ -267,7 +272,7 @@ jobs:
index:
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
needs: [image]
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- run: >-
curl "https://${{

View File

@@ -17,7 +17,7 @@ env:
jobs:
test:
name: Run Automated Tests
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:

View File

@@ -40,7 +40,6 @@ STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_
fi')
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
REBUILD_TYPES = 1
ifeq ($(REMOTE),)
mkdir = mkdir -p $1
@@ -63,7 +62,7 @@ endif
.DELETE_ON_ERROR:
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel ts-bindings
all: $(STARTOS_TARGETS)
@@ -277,10 +276,9 @@ container-runtime/node_modules/.package-lock.json: container-runtime/package-loc
npm --prefix container-runtime ci
touch container-runtime/node_modules/.package-lock.json
sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi)
ts-bindings: core/startos/bindings/index.ts
mkdir -p sdk/base/lib/osBindings
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
touch sdk/base/lib/osBindings/index.ts
core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/startos/bindings

View File

@@ -1,4 +1,3 @@
- grub-common
- grub-efi
+ parted
+ raspberrypi-net-mods

View File

@@ -25,5 +25,5 @@ apply_rule PREROUTING -p tcp -d $sip --dport $sport -j DNAT --to-destination $di
apply_rule OUTPUT -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport
if [ "$UNDO" = 1 ]; then
conntrack -D -p tcp -d $sip --dport $sport
conntrack -D -p tcp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
fi

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43",
"version": "0.4.0-beta.44",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -158,6 +158,8 @@ export class RpcListener {
this.unixSocketServer.listen(SOCKET_PATH)
console.log("Listening on %s", SOCKET_PATH)
this.unixSocketServer.on("connection", (s) => {
let id: IdType = null
const captureId = <X>(x: X) => {

2
core/Cargo.lock generated
View File

@@ -7908,7 +7908,7 @@ dependencies = [
[[package]]
name = "start-os"
version = "0.4.0-alpha.13"
version = "0.4.0-alpha.14"
dependencies = [
"aes 0.7.5",
"arti-client",

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -5,4 +5,4 @@ if tty -s; then
USE_TTY="-it"
fi
alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -e "CFLAGS=-D_FORTIFY_SOURCE=2" -e "CXXFLAGS=-D_FORTIFY_SOURCE=2" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$(pwd)":/workdir -w /workdir -P start9/cargo-zigbuild'
alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -e "AWS_LC_SYS_CMAKE_TOOLCHAIN_FILE_riscv64gc_unknown_linux_musl=/root/cmake-overrides/toolchain-riscv64-musl-clang.cmake" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$(pwd)":/workdir -w /workdir -P start9/cargo-zigbuild'

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..."
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi

View File

@@ -15,7 +15,7 @@ license = "MIT"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.13" # VERSION_BUMP
version = "0.4.0-alpha.14" # VERSION_BUMP
[lib]
name = "startos"

View File

@@ -260,11 +260,7 @@ impl NetworkInterfaceInfo {
}
pub fn secure(&self) -> bool {
self.secure.unwrap_or_else(|| {
self.ip_info.as_ref().map_or(false, |ip_info| {
ip_info.device_type == Some(NetworkInterfaceType::Wireguard)
}) && !self.public()
})
self.secure.unwrap_or(false)
}
}

View File

@@ -366,6 +366,7 @@ impl LxcContainer {
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
tracing::info!("Connected to socket in {:?}", started.elapsed());
Ok(UnixRpcClient::new(sock_path))
}
}

View File

@@ -649,16 +649,6 @@ async fn torctl(
.invoke(ErrorKind::Tor)
.await?;
let logs = journalctl(
LogSource::Unit(SYSTEMD_UNIT),
Some(0),
None,
Some("0"),
false,
true,
)
.await?;
let mut tcp_stream = None;
for _ in 0..60 {
if let Ok(conn) = TcpStream::connect(tor_control).await {
@@ -720,7 +710,7 @@ async fn torctl(
ErrorKind::Tor,
));
}
Ok((connection, logs))
Ok(connection)
};
let pre_handler = async {
while let Some(command) = recv.recv().await {
@@ -745,7 +735,7 @@ async fn torctl(
Ok(())
};
let (mut connection, mut logs) = tokio::select! {
let mut connection = tokio::select! {
res = bootstrap => res?,
res = pre_handler => return res,
};
@@ -851,46 +841,59 @@ async fn torctl(
Ok(())
};
let log_parser = async {
while let Some(log) = logs.try_next().await? {
for (regex, severity) in &*LOG_REGEXES {
if regex.is_match(&log.message) {
let (check, wipe_state) = match severity {
ErrorLogSeverity::Fatal { wipe_state } => (false, *wipe_state),
ErrorLogSeverity::Unknown { wipe_state } => (true, *wipe_state),
};
let addr = hck_key.public().get_onion_address().to_string();
if !check
|| TcpStream::connect(tor_socks)
.map_err(|e| Error::new(e, ErrorKind::Tor))
.and_then(|mut tor_socks| async move {
tokio::time::timeout(
Duration::from_secs(30),
socks5_impl::client::connect(&mut tor_socks, (addr, 80), None)
.map_err(|e| Error::new(e, ErrorKind::Tor)),
)
loop {
let mut logs = journalctl(
LogSource::Unit(SYSTEMD_UNIT),
Some(0),
None,
Some("0"),
false,
true,
)
.await?;
while let Some(log) = logs.try_next().await? {
for (regex, severity) in &*LOG_REGEXES {
if regex.is_match(&log.message) {
let (check, wipe_state) = match severity {
ErrorLogSeverity::Fatal { wipe_state } => (false, *wipe_state),
ErrorLogSeverity::Unknown { wipe_state } => (true, *wipe_state),
};
let addr = hck_key.public().get_onion_address().to_string();
if !check
|| TcpStream::connect(tor_socks)
.map_err(|e| Error::new(e, ErrorKind::Tor))
.await?
})
.await
.with_ctx(|_| (ErrorKind::Tor, "Tor is confirmed to be down"))
.log_err()
.is_some()
{
if wipe_state {
Command::new("systemctl")
.arg("stop")
.arg("tor")
.invoke(ErrorKind::Tor)
.await?;
tokio::fs::remove_dir_all("/var/lib/tor").await?;
.and_then(|mut tor_socks| async move {
tokio::time::timeout(
Duration::from_secs(30),
socks5_impl::client::connect(
&mut tor_socks,
(addr, 80),
None,
)
.map_err(|e| Error::new(e, ErrorKind::Tor)),
)
.map_err(|e| Error::new(e, ErrorKind::Tor))
.await?
})
.await
.with_ctx(|_| (ErrorKind::Tor, "Tor is confirmed to be down"))
.log_err()
.is_some()
{
if wipe_state {
Command::new("systemctl")
.arg("stop")
.arg("tor")
.invoke(ErrorKind::Tor)
.await?;
tokio::fs::remove_dir_all("/var/lib/tor").await?;
}
return Err(Error::new(eyre!("{}", log.message), ErrorKind::Tor));
}
return Err(Error::new(eyre!("{}", log.message), ErrorKind::Tor));
}
}
}
}
// Err(Error::new(eyre!("Log stream terminated"), ErrorKind::Tor))
Ok(())
};
let health_checker = async {
let mut last_success = Instant::now();
@@ -960,20 +963,23 @@ impl TorControl {
_thread: tokio::spawn(async move {
let wipe_state = AtomicBool::new(false);
let mut health_timeout = Duration::from_secs(STARTING_HEALTH_TIMEOUT);
while let Err(e) = torctl(
tor_control,
tor_socks,
&mut recv,
&mut thread_services,
&wipe_state,
&mut health_timeout,
)
.await
{
tracing::error!("{e}: Restarting tor");
tracing::debug!("{e:?}");
loop {
if let Err(e) = torctl(
tor_control,
tor_socks,
&mut recv,
&mut thread_services,
&wipe_state,
&mut health_timeout,
)
.await
{
tracing::error!("TorControl : {e}");
tracing::debug!("{e:?}");
}
tracing::info!("Restarting Tor");
tokio::time::sleep(Duration::from_secs(1)).await;
}
tracing::info!("TorControl is shut down.")
})
.into(),
send,

View File

@@ -106,7 +106,9 @@ pub struct ExecParams {
#[arg(long)]
pty_size: Option<TermSize>,
#[arg(short, long)]
env: Option<PathBuf>,
env: Vec<String>,
#[arg(long)]
env_file: Option<PathBuf>,
#[arg(short, long)]
workdir: Option<PathBuf>,
#[arg(short, long)]
@@ -119,6 +121,7 @@ impl ExecParams {
fn exec(&self) -> Result<(), Error> {
let ExecParams {
env,
env_file,
workdir,
user,
chroot,
@@ -131,14 +134,15 @@ impl ExecParams {
ErrorKind::InvalidRequest,
));
};
let env_string = if let Some(env) = &env {
std::fs::read_to_string(env)
let env_string = if let Some(env_file) = &env_file {
std::fs::read_to_string(env_file)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
} else {
Default::default()
};
let env = env_string
.lines()
.chain(env.iter().map(|l| l.as_str()))
.map(|l| l.trim())
.filter_map(|l| l.split_once("="))
.collect::<BTreeMap<_, _>>();
@@ -199,6 +203,7 @@ pub fn launch(
force_stderr_tty,
pty_size,
env,
env_file,
workdir,
user,
chroot,
@@ -294,8 +299,11 @@ pub fn launch(
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("launch-init");
if let Some(env) = env {
cmd = cmd.arg("--env").arg(env);
for env in env {
cmd = cmd.arg("-e").arg(env)
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir);
@@ -349,8 +357,11 @@ pub fn launch(
} else {
let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("launch-init");
if let Some(env) = env {
cmd.arg("--env").arg(env);
for env in env {
cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);
@@ -441,6 +452,7 @@ pub fn exec(
force_stderr_tty,
pty_size,
env,
env_file,
workdir,
user,
chroot,
@@ -544,8 +556,11 @@ pub fn exec(
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("exec-command");
if let Some(env) = env {
cmd = cmd.arg("--env").arg(env);
for env in env {
cmd = cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir);
@@ -599,8 +614,11 @@ pub fn exec(
} else {
let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("exec-command");
if let Some(env) = env {
cmd.arg("--env").arg(env);
for env in env {
cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
}
if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir);

View File

@@ -885,7 +885,7 @@ pub async fn attach(
.arg("start-container")
.arg("subcontainer")
.arg("exec")
.arg("--env")
.arg("--env-file")
.arg(
Path::new("/media/startos/images")
.join(image_id)

View File

@@ -43,7 +43,7 @@ use crate::util::rpc_client::UnixRpcClient;
use crate::volume::data_dir;
use crate::{ARCH, DATA_DIR, PACKAGE_DATA};
const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct ServiceState {

View File

@@ -117,6 +117,8 @@ impl ServiceMap {
match Service::load(ctx, id, disposition).await {
Ok(s) => *service = s.into(),
Err(e) => {
tracing::error!("Error loading service: {e}");
tracing::debug!("{e:?}");
let e = ErrorData::from(e);
ctx.db
.mutate(|db| {

View File

@@ -3,7 +3,7 @@ use imbl::HashMap;
use imbl_value::InternedString;
use itertools::Itertools;
use patch_db::HasModel;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -113,27 +113,12 @@ impl AuthContext for TunnelContext {
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct SignerInfo {
pub name: InternedString,
}
pub fn auth_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"login",
from_fn_async(crate::auth::login_impl::<TunnelContext>)
.with_metadata("login", Value::Bool(true))
.no_cli(),
)
.subcommand(
"logout",
from_fn_async(crate::auth::logout::<TunnelContext>)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("Log out of current auth session")
.with_call_remote::<CliContext>(),
)
crate::auth::auth::<C, TunnelContext>()
.subcommand("set-password", from_fn_async(set_password_rpc).no_cli())
.subcommand(
"set-password",
@@ -173,19 +158,15 @@ pub fn auth_api<C: Context>() -> ParentHandler<C> {
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "NAME", "KEY"]);
for (key, info) in res {
table.add_row(row![info.name, key]);
}
table.print_tty(false)?;
Ok(())
})
.with_about("List authorized keys")
@@ -194,7 +175,7 @@ pub fn auth_api<C: Context>() -> ParentHandler<C> {
)
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct AddKeyParams {
pub name: InternedString,
@@ -216,7 +197,7 @@ pub async fn add_key(
.result
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct RemoveKeyParams {
pub key: AnyVerifyingKey,
@@ -240,7 +221,7 @@ pub async fn list_keys(ctx: TunnelContext) -> Result<HashMap<AnyVerifyingKey, Si
ctx.db.peek().await.into_auth_pubkeys().de()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
pub struct SetPasswordParams {
pub password: String,
}

View File

@@ -53,8 +53,9 @@ mod v0_4_0_alpha_10;
mod v0_4_0_alpha_11;
mod v0_4_0_alpha_12;
mod v0_4_0_alpha_13;
mod v0_4_0_alpha_14;
pub type Current = v0_4_0_alpha_13::Version; // VERSION_BUMP
pub type Current = v0_4_0_alpha_14::Version; // VERSION_BUMP
impl Current {
#[instrument(skip(self, db))]
@@ -169,7 +170,8 @@ enum Version {
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>),
V0_4_0_alpha_11(Wrapper<v0_4_0_alpha_11::Version>),
V0_4_0_alpha_12(Wrapper<v0_4_0_alpha_12::Version>),
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>), // VERSION_BUMP
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>),
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>), // VERSION_BUMP
Other(exver::Version),
}
@@ -225,7 +227,8 @@ impl Version {
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_12(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
return Err(Error::new(
eyre!("unknown version {v}"),
@@ -273,7 +276,8 @@ impl Version {
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_12(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
}
}

View File

@@ -4,7 +4,7 @@ use exver::{PreReleaseSegment, VersionRange};
use imbl_value::InternedString;
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_11};
use super::{v0_4_0_alpha_11, VersionT};
use crate::net::tor::TorSecretKey;
use crate::prelude::*;
@@ -75,7 +75,10 @@ impl VersionT for Version {
}
fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?;
db["private"]["keyStore"]["localCerts"] = db["private"]["keyStore"]["local_certs"].clone();
if db["private"]["keyStore"]["localCerts"].is_null() {
db["private"]["keyStore"]["localCerts"] =
db["private"]["keyStore"]["local_certs"].clone();
}
Ok(Value::Null)
}

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_13};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_14: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 14.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_13::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_14.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -1,3 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from "./AnyVerifyingKey"
import type { ContactInfo } from "./ContactInfo"
export type SignerInfo = { name: string }
export type SignerInfo = {
name: string
contact: Array<ContactInfo>
keys: Array<AnyVerifyingKey>
}

View File

@@ -3,7 +3,7 @@ import { knownProtocols } from "../interfaces/Host"
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
import { Effects } from "../Effects"
import { DropGenerator, DropPromise } from "./Drop"
import { IPV6_LINK_LOCAL } from "./ip"
import { IpAddress, IPV6_LINK_LOCAL } from "./ip"
export type UrlString = string
export type HostId = string
@@ -17,7 +17,15 @@ export const getHostname = (url: string): Hostname | null => {
return last
}
type FilterKinds = "onion" | "local" | "domain" | "ip" | "ipv4" | "ipv6"
type FilterKinds =
| "onion"
| "local"
| "domain"
| "ip"
| "ipv4"
| "ipv6"
| "localhost"
| "link-local"
export type Filter = {
visibility?: "public" | "private"
kind?: FilterKinds | FilterKinds[]
@@ -72,6 +80,12 @@ type FilterReturnTy<F extends Filter> = F extends {
: Exclude<HostnameInfo, FilterReturnTy<E>>
: HostnameInfo
const defaultFilter = {
exclude: {
kind: ["localhost", "link-local"] as ("localhost" | "link-local")[],
},
}
type Formats = "hostname-info" | "urlstring" | "url"
type FormatReturnTy<
F extends Filter,
@@ -92,8 +106,11 @@ export type Filled = {
sslUrl: UrlString | null
}
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
filter: <
F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format?: Format,
) => FormatReturnTy<F, Format>[]
@@ -215,7 +232,13 @@ function filterRec(
h.kind === "ip" &&
h.hostname.kind === "domain") ||
(kind.has("ipv4") && h.kind === "ip" && h.hostname.kind === "ipv4") ||
(kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6")),
(kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6") ||
(kind.has("localhost") &&
["localhost", "127.0.0.1", "[::1]"].includes(h.hostname.value)) ||
(kind.has("link-local") &&
h.kind === "ip" &&
h.hostname.kind === "ipv6" &&
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
)
}
@@ -239,11 +262,14 @@ export const filledAddress = (
...addressInfo,
hostnames,
toUrls,
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
filter: <
F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format?: Format,
) => {
const filtered = filterRec(hostnames, filter, false)
const filtered = filterRec(hostnames, filter ?? defaultFilter, false)
let res: FormatReturnTy<F, Format>[] = filtered as any
if (format === "hostname-info") return res
const urls = filtered.flatMap(toUrlArray)

View File

@@ -61,7 +61,7 @@ import {
} from "../../base/lib/inits"
import { DropGenerator } from "../../base/lib/util/Drop"
export const OSVersion = testTypeVersion("0.4.0-alpha.13")
export const OSVersion = testTypeVersion("0.4.0-alpha.14")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -410,12 +410,17 @@ export class SubContainerOwned<
workdir = options.cwd
delete options.cwd
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
const child = cp.spawn(
"start-container",
[
"subcontainer",
"exec",
`--env=/media/startos/images/${this.imageId}.env`,
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
@@ -530,6 +535,11 @@ export class SubContainerOwned<
workdir = options.cwd
delete options.cwd
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
await this.killLeader()
this.leaderExited = false
this.leader = cp.spawn(
@@ -537,7 +547,7 @@ export class SubContainerOwned<
[
"subcontainer",
"launch",
`--env=/media/startos/images/${this.imageId}.env`,
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
@@ -574,12 +584,17 @@ export class SubContainerOwned<
workdir = options.cwd
delete options.cwd
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
return cp.spawn(
"start-container",
[
"subcontainer",
"exec",
`--env=/media/startos/images/${this.imageId}.env`,
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43",
"version": "0.4.0-beta.44",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43",
"version": "0.4.0-beta.44",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
@@ -98,6 +98,7 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -1643,6 +1644,7 @@
"integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
}
@@ -1944,6 +1946,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -3053,6 +3056,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -4157,6 +4161,7 @@
"integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"commander": "^10.0.0",
"source-map-generator": "0.8.0"
@@ -4833,6 +4838,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -4953,6 +4959,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43",
"version": "0.4.0-beta.44",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.13",
"version": "0.4.0-alpha.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "startos-ui",
"version": "0.4.0-alpha.13",
"version": "0.4.0-alpha.14",
"license": "MIT",
"dependencies": {
"@angular/animations": "^20.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.13",
"version": "0.4.0-alpha.14",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"license": "MIT",

View File

@@ -39,7 +39,9 @@ import { DocsLinkDirective } from '@start9labs/shared'
"
>
<div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3>
<h2 style="font-variant-caps: all-small-caps">
Root Certificate Authority
</h2>
<p>
Download your server's Root CA and
<a
@@ -47,7 +49,7 @@ import { DocsLinkDirective } from '@start9labs/shared'
path="/user-manual/trust-ca.html"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
follow the instructions
follow instructions
</a>
to establish a secure connection with your server.
</p>
@@ -84,15 +86,15 @@ import { DocsLinkDirective } from '@start9labs/shared'
"
>
<h2 style="font-variant-caps: all-small-caps">
Access from home (LAN)
Permanent Local Address
</h2>
<p>
Visit the address below when you are connected to the same WiFi or
Local Area Network (LAN) as your server.
You must be connected to the same Local Area Network (LAN) as your
server to access this address.
</p>
<p
style="
padding: 16px;
padding: 16px 0;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
@@ -100,33 +102,6 @@ import { DocsLinkDirective } from '@start9labs/shared'
>
<code id="lan-addr"></code>
</p>
<h2 style="font-variant-caps: all-small-caps">
Access on the go (Tor)
</h2>
<p>Visit the address below when you are away from home.</p>
<p>
<span style="font-weight: bold">Note:</span>
This address will only work from a Tor-enabled browser.
<a
docsLink
path="/user-manual/connecting-remotely/tor.html"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="tor-addr"></code>
</p>
</section>
</div>
</body>

View File

@@ -7,7 +7,7 @@ import {
DOCUMENT,
} from '@angular/core'
import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
import { TuiButton, TuiIcon, TuiSurface } from '@taiga-ui/core'
import { TuiButton, TuiIcon, TuiLoader, TuiSurface } from '@taiga-ui/core'
import { TuiCardLarge } from '@taiga-ui/layout'
import { DocumentationComponent } from 'src/app/components/documentation.component'
import { MatrixComponent } from 'src/app/components/matrix.component'
@@ -31,10 +31,16 @@ import { StateService } from 'src/app/services/state.service'
<h3>You can now safely unplug your old StartOS data drive</h3>
}
<h3>
http://start.local was for setup purposes only. It will no longer
work.
</h3>
<button tuiCardLarge tuiSurface="floating" (click)="download()">
<strong class="caps">Download address info</strong>
<span>
start.local was for setup purposes only. It will no longer work.
For future reference, this file contains your server's permanent
local address, as well as its Root Certificate Authority (Root CA).
</span>
<strong class="caps">
Download
@@ -48,17 +54,18 @@ import { StateService } from 'src/app/services/state.service'
target="_blank"
[attr.href]="disableLogin ? null : lanAddress"
>
<strong class="caps">Trust your Root CA</strong>
<span>
In the new tab, follow instructions to trust your server's Root CA
and log in.
</span>
<strong class="caps">
Open
Open Local Address
<tui-icon icon="@tui.external-link" />
</strong>
</a>
<app-documentation hidden [lanAddress]="lanAddress" />
} @else {
<tui-loader />
}
</section>
`,
@@ -97,6 +104,10 @@ import { StateService } from 'src/app/services/state.service'
opacity: var(--tui-disabled-opacity);
pointer-events: none;
}
h3 {
text-align: left;
}
`,
imports: [
TuiCardLarge,
@@ -105,6 +116,7 @@ import { StateService } from 'src/app/services/state.service'
TuiSurface,
MatrixComponent,
DocumentationComponent,
TuiLoader,
],
})
export default class SuccessPage implements AfterViewInit {
@@ -117,7 +129,6 @@ export default class SuccessPage implements AfterViewInit {
readonly stateService = inject(StateService)
torAddresses?: string[]
lanAddress?: string
cert?: string
disableLogin = this.stateService.setupType === 'fresh'
@@ -127,10 +138,8 @@ export default class SuccessPage implements AfterViewInit {
}
download() {
const torElem = this.document.getElementById('tor-addr')
const lanElem = this.document.getElementById('lan-addr')
if (torElem) torElem.innerHTML = this.torAddresses?.join('\n') || ''
if (lanElem) lanElem.innerHTML = this.lanAddress || ''
this.document
@@ -155,9 +164,6 @@ export default class SuccessPage implements AfterViewInit {
try {
const ret = await this.api.complete()
if (!this.stateService.kiosk) {
this.torAddresses = ret.torAddresses.map(a =>
a.replace(/^https:/, 'http:'),
)
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
this.cert = ret.rootCa

View File

@@ -484,7 +484,7 @@ export default {
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
513: 'Aktivieren',
514: 'Deaktivieren',
515: 'Du verwendest derzeit einen Kiosk. Wenn du den Kiosk-Modus deaktivierst, wird die Verbindung zum Kiosk getrennt.',
515: 'Diese Änderung wird nach dem nächsten Neustart wirksam',
516: 'Empfohlen',
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
518: 'Verwerfen',

View File

@@ -483,7 +483,7 @@ export const ENGLISH = {
'Kiosk Mode is unavailable on this device': 512,
'Enable': 513,
'Disable': 514,
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.': 515,
'This change will take effect after the next boot': 515,
'Recommended': 516, // as in, we recommend this
'Are you sure you want to dismiss this task?': 517,
'Dismiss': 518, // as in, dismiss or delete a task

View File

@@ -484,7 +484,7 @@ export default {
512: 'El modo quiosco no está disponible en este dispositivo',
513: 'Activar',
514: 'Desactivar',
515: 'Actualmente estás utilizando un quiosco. Desactivar el modo quiosco provocará su desconexión.',
515: 'Este cambio tendrá efecto después del próximo inicio',
516: 'Recomendado',
517: '¿Estás seguro de que deseas descartar esta tarea?',
518: 'Descartar',

View File

@@ -484,7 +484,7 @@ export default {
512: 'Le mode kiosque nest pas disponible sur cet appareil',
513: 'Activer',
514: 'Désactiver',
515: 'Vous utilisez actuellement un kiosque. Désactiver le mode kiosque entraînera sa déconnexion.',
515: 'Ce changement va prendre effet après le prochain démarrage',
516: 'Recommandé',
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
518: 'Ignorer',

View File

@@ -484,7 +484,7 @@ export default {
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
513: 'Włącz',
514: 'Wyłącz',
515: 'Obecnie używasz kiosku. Wyłączenie trybu kiosku spowoduje jego rozłączenie.',
515: 'Ta zmiana zacznie obowiązywać po następnym uruchomieniu',
516: 'Zalecane',
517: 'Czy na pewno chcesz odrzucić to zadanie?',
518: 'Odrzuć',

View File

@@ -10,7 +10,7 @@ import { I18N, i18nKey } from './i18n.providers'
export class i18nPipe implements PipeTransform {
private readonly i18n = inject(I18N)
transform(englishKey: i18nKey | null | undefined): string {
transform(englishKey: i18nKey | null | undefined | ''): string {
englishKey = englishKey || ('' as i18nKey)
return this.i18n()?.[ENGLISH[englishKey]] || englishKey

View File

@@ -1,17 +1,17 @@
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
computed,
input,
} from '@angular/core'
import { i18nKey, i18nPipe } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { i18nPipe } from '@start9labs/shared'
import { TuiLoader } from '@taiga-ui/core'
import { ServiceUptimeComponent } from 'src/app/routes/portal/routes/services/components/uptime.component'
import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import {
getInstalledPrimaryStatus,
PrimaryRendering,
PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service'
@Component({
@@ -19,23 +19,27 @@ import {
template: `
<header>{{ 'Status' | i18n }}</header>
<div>
@if (installingInfo) {
@if (info()) {
<h3>
<tui-loader size="s" [inheritColor]="true" />
{{ 'Installing' | i18n }}
<span class="loading-dots"></span>
{{ getText(installingInfo.progress.overall) | i18n }}
{{ info() | i18n }}
</h3>
} @else {
<h3 [class]="class">
{{ text | i18n }}
@if (text === 'Task Required') {
<h3 [class]="class()">
{{ text() || 'Unknown' | i18n }}
@if (text() === 'Task Required') {
<small>{{ 'See below' | i18n }}</small>
}
@if (rendering?.showDots) {
@if (rendering().showDots) {
<span class="loading-dots"></span>
}
@if ($any(pkg().status)?.started; as started) {
<service-uptime [started]="started" />
}
</h3>
}
<ng-content />
@@ -76,6 +80,12 @@ import {
margin: 0 0.25rem -0.125rem 0;
}
service-uptime {
display: none;
width: fit-content;
margin: 0.5rem 0.125rem;
}
:host-context(tui-root._mobile) {
:host {
min-height: 0;
@@ -94,32 +104,33 @@ import {
small {
text-align: left;
}
service-uptime {
display: flex;
}
}
`,
host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiLoader, i18nPipe],
imports: [TuiLoader, i18nPipe, ServiceUptimeComponent],
})
export class ServiceStatusComponent {
@Input({ required: true })
status?: PrimaryStatus
readonly pkg = input.required<PackageDataEntry>()
readonly connected = input(false)
@Input()
installingInfo?: InstallingInfo
protected readonly status = computed((pkg = this.pkg()) =>
pkg?.stateInfo.state === 'installed'
? getInstalledPrimaryStatus(pkg)
: pkg?.stateInfo.state,
)
@Input()
connected = false
protected readonly rendering = computed(() => PrimaryRendering[this.status()])
protected readonly text = computed(
() => this.connected() && this.rendering().display,
)
private readonly i18n = inject(i18nPipe)
get text(): i18nKey {
return this.connected ? this.rendering?.display || 'Unknown' : 'Unknown'
}
get class(): string | null {
if (!this.connected) return null
switch (this.rendering?.color) {
protected readonly class = computed(() => {
switch (this.connected() && this.rendering().color) {
case 'danger':
return 'g-negative'
case 'warning':
@@ -131,13 +142,10 @@ export class ServiceStatusComponent {
default:
return null
}
}
})
get rendering() {
return this.status && PrimaryRendering[this.status]
}
getText(progress: T.Progress): i18nKey {
return getProgressText(progress)
}
protected readonly info = computed(
(progress = this.pkg().stateInfo.installingInfo?.progress.overall) =>
progress ? getProgressText(progress) : '',
)
}

View File

@@ -79,6 +79,10 @@ import { getManifest } from 'src/app/utils/get-package-data'
overflow: hidden;
}
td:not(:last-child) {
padding-inline-end: 1.5rem;
}
td:last-child {
white-space: nowrap;
text-align: right;

View File

@@ -66,7 +66,8 @@ import { distinctUntilChanged } from 'rxjs/operators'
color: var(--tui-text-primary);
}
:host-context(table) {
:host-context(table),
:host-context(service-status) {
padding: 0;
header {

View File

@@ -39,11 +39,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
} @else if (installing()) {
<service-install-progress [pkg]="pkg" />
} @else if (installed()) {
<service-status
[connected]="!!connected()"
[installingInfo]="pkg.stateInfo.installingInfo"
[status]="status()"
>
<service-status [connected]="!!connected()" [pkg]="pkg">
@if (connected()) {
<service-controls [pkg]="pkg" [status]="status()" />
}
@@ -51,10 +47,8 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
@if (status() !== 'backingUp') {
<service-health-checks [checks]="health()" />
<service-uptime
class="g-card"
[started]="$any(pkg.status)?.started"
/>
<service-uptime class="g-card" [started]="$any(pkg.status).started" />
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
@if (errors() | async; as errors) {
<service-dependencies
@@ -63,7 +57,6 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
[errors]="errors"
/>
}
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
<service-tasks
#tasks="elementRef"
@@ -91,7 +84,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
</button>
}
} @else if (removing()) {
<service-status [connected]="!!connected()" [status]="status()" />
<service-status [connected]="!!connected()" [pkg]="pkg" />
}
}
`,
@@ -139,6 +132,10 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
> * {
grid-column: span 1;
}
service-uptime {
display: none;
}
}
`,
host: { class: 'g-subpage' },

View File

@@ -4,7 +4,6 @@ import {
Component,
inject,
INJECTOR,
DOCUMENT,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms'
@@ -151,7 +150,7 @@ import { SystemWipeComponent } from './wipe.component'
</span>
</span>
@if (server.kiosk !== null) {
<button tuiButton appearance="primary" (click)="tryToggleKiosk()">
<button tuiButton appearance="primary" (click)="toggleKiosk()">
{{ server.kiosk ? ('Disable' | i18n) : ('Enable' | i18n) }}
</button>
}
@@ -242,7 +241,6 @@ export default class SystemGeneralComponent {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
private readonly isTor = inject(ConfigService).isTor()
private readonly document = inject(DOCUMENT)
private readonly dialog = inject(DialogService)
private readonly i18n = inject(i18nPipe)
private readonly injector = inject(INJECTOR)
@@ -326,28 +324,6 @@ export default class SystemGeneralComponent {
.subscribe(() => this.resetTor(this.wipe))
}
async tryToggleKiosk() {
if (
this.server()?.kiosk &&
['localhost', '127.0.0.1'].includes(this.document.location.hostname)
) {
return this.dialog
.openConfirm({
label: 'Warning',
data: {
content:
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.',
yes: 'Disable',
no: 'Cancel',
},
})
.pipe(filter(Boolean))
.subscribe(async () => this.toggleKiosk())
}
this.toggleKiosk()
}
async onRepair() {
this.dialog
.openConfirm({
@@ -370,7 +346,7 @@ export default class SystemGeneralComponent {
})
}
private async toggleKiosk() {
async toggleKiosk() {
const kiosk = this.server()?.kiosk
const loader = this.loader
@@ -379,6 +355,11 @@ export default class SystemGeneralComponent {
try {
await this.api.toggleKiosk(!kiosk)
this.dialog
.openAlert('This change will take effect after the next boot', {
label: 'Restart to apply',
})
.subscribe()
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -1,16 +1,20 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
ChangeDetectionStrategy,
Component,
inject,
viewChild,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router'
import { verify } from '@start9labs/argon2'
import {
DialogService,
ErrorService,
i18nKey,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB } from '@start9labs/start-sdk'
import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiAlertService, TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { from } from 'rxjs'
@@ -70,13 +74,14 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
],
})
export default class SystemPasswordComponent {
private readonly dialog = inject(DialogService)
private readonly alerts = inject(TuiAlertService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
private readonly i18n = inject(i18nPipe)
readonly form = viewChild(FormComponent)
readonly spec = toSignal(from(configBuilderToSpec(this.passwordSpec())))
readonly buttons = [
{
@@ -119,7 +124,12 @@ export default class SystemPasswordComponent {
try {
await this.api.resetPassword({ oldPassword, newPassword })
this.dialog.openAlert('Password changed').subscribe()
this.form()?.form.reset()
this.alerts
.open(this.i18n.transform('Password changed'), {
appearance: 'positive',
})
.subscribe()
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: {
aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_aarch64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64.squashfs',
commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
},
'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_aarch64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
},
raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_raspberrypi.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_raspberrypi.squashfs',
commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
},
x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_x86_64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64.squashfs',
commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
},
'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_x86_64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136,
@@ -385,7 +385,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -420,7 +420,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -465,7 +465,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -500,7 +500,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -547,7 +547,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -595,7 +595,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -647,7 +647,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -682,7 +682,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -727,7 +727,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -775,7 +775,7 @@ export namespace Mock {
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43',
sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,