mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Compare commits
9 Commits
feat/gener
...
fix/dry-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
392ae2d675 | ||
|
|
b0b4b41c42 | ||
|
|
bbbc8f7440 | ||
|
|
c7a4dd617e | ||
|
|
d6b81f3c9b | ||
|
|
879f953a9f | ||
|
|
782f2e83bf | ||
|
|
6cefc27c5f | ||
|
|
2b676808a9 |
24
.github/workflows/startos-iso.yaml
vendored
24
.github/workflows/startos-iso.yaml
vendored
@@ -89,9 +89,9 @@ jobs:
|
|||||||
"riscv64": "ubuntu-latest"
|
"riscv64": "ubuntu-latest"
|
||||||
}')[matrix.arch],
|
}')[matrix.arch],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "ubuntu-24.04-32-cores",
|
"x86_64": "amd64-fast",
|
||||||
"aarch64": "ubuntu-24.04-arm-32-cores",
|
"aarch64": "aarch64-fast",
|
||||||
"riscv64": "ubuntu-24.04-32-cores"
|
"riscv64": "amd64-fast"
|
||||||
}')[matrix.arch]
|
}')[matrix.arch]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -153,15 +153,15 @@ jobs:
|
|||||||
"riscv64-nonfree": "ubuntu-24.04-arm",
|
"riscv64-nonfree": "ubuntu-24.04-arm",
|
||||||
}')[matrix.platform],
|
}')[matrix.platform],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "ubuntu-24.04-8-cores",
|
"x86_64": "amd64-fast",
|
||||||
"x86_64-nonfree": "ubuntu-24.04-8-cores",
|
"x86_64-nonfree": "amd64-fast",
|
||||||
"x86_64-nvidia": "ubuntu-24.04-8-cores",
|
"x86_64-nvidia": "amd64-fast",
|
||||||
"aarch64": "ubuntu-24.04-arm-8-cores",
|
"aarch64": "aarch64-fast",
|
||||||
"aarch64-nonfree": "ubuntu-24.04-arm-8-cores",
|
"aarch64-nonfree": "aarch64-fast",
|
||||||
"aarch64-nvidia": "ubuntu-24.04-arm-8-cores",
|
"aarch64-nvidia": "aarch64-fast",
|
||||||
"raspberrypi": "ubuntu-24.04-arm-8-cores",
|
"raspberrypi": "aarch64-fast",
|
||||||
"riscv64": "ubuntu-24.04-8-cores",
|
"riscv64": "amd64-fast",
|
||||||
"riscv64-nonfree": "ubuntu-24.04-8-cores",
|
"riscv64-nonfree": "amd64-fast",
|
||||||
}')[matrix.platform]
|
}')[matrix.platform]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const host = new MultiHost({ effects, id })
|
const host = new MultiHost({ effects, id })
|
||||||
const internalPorts = new Set(
|
const internalPorts = new Set(
|
||||||
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
||||||
.map(Number.parseInt)
|
.map((v) => parseInt(v))
|
||||||
.concat(
|
.concat(
|
||||||
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
||||||
(c) => c.internal,
|
(c) => c.internal,
|
||||||
|
|||||||
@@ -125,10 +125,10 @@ impl Public {
|
|||||||
},
|
},
|
||||||
status_info: ServerStatus {
|
status_info: ServerStatus {
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
updated: false,
|
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
|
restart: None,
|
||||||
},
|
},
|
||||||
unread_notification_count: 0,
|
unread_notification_count: 0,
|
||||||
password_hash: account.password.clone(),
|
password_hash: account.password.clone(),
|
||||||
@@ -220,6 +220,16 @@ pub struct ServerInfo {
|
|||||||
pub keyboard: Option<KeyboardOptions>,
|
pub keyboard: Option<KeyboardOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum RestartReason {
|
||||||
|
Mdns,
|
||||||
|
Language,
|
||||||
|
Kiosk,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
@@ -364,12 +374,13 @@ pub struct BackupProgress {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ServerStatus {
|
pub struct ServerStatus {
|
||||||
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
||||||
pub updated: bool,
|
|
||||||
pub update_progress: Option<FullProgress>,
|
pub update_progress: Option<FullProgress>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub shutting_down: bool,
|
pub shutting_down: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub restarting: bool,
|
pub restarting: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub restart: Option<RestartReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ impl OsPartitionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548";
|
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6E6F-744E-656564454649";
|
||||||
|
|
||||||
/// Find the BIOS boot partition on the same disk as `known_part`.
|
/// Find the BIOS boot partition on the same disk as `known_part`.
|
||||||
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tracing::instrument;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::db::model::public::ServerInfo;
|
use crate::db::model::public::{RestartReason, ServerInfo};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
@@ -272,6 +272,7 @@ pub async fn set_hostname_rpc(
|
|||||||
}
|
}
|
||||||
if let Some(hostname) = &hostname {
|
if let Some(hostname) = &hostname {
|
||||||
hostname.save(server_info)?;
|
hostname.save(server_info)?;
|
||||||
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Mdns))?;
|
||||||
}
|
}
|
||||||
ServerHostnameInfo::load(server_info)
|
ServerHostnameInfo::load(server_info)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -371,11 +371,11 @@ pub async fn init(
|
|||||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||||
let devices = lshw().await?;
|
let devices = lshw().await?;
|
||||||
let status_info = ServerStatus {
|
let status_info = ServerStatus {
|
||||||
updated: false,
|
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
|
restart: None,
|
||||||
};
|
};
|
||||||
db.mutate(|v| {
|
db.mutate(|v| {
|
||||||
let server_info = v.as_public_mut().as_server_info_mut();
|
let server_info = v.as_public_mut().as_server_info_mut();
|
||||||
|
|||||||
@@ -765,6 +765,7 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
});
|
});
|
||||||
|
gc_policy_routing(&ifaces).await;
|
||||||
for result in futures::future::join_all(jobs).await {
|
for result in futures::future::join_all(jobs).await {
|
||||||
result.log_err();
|
result.log_err();
|
||||||
}
|
}
|
||||||
@@ -806,15 +807,43 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, E
|
|||||||
Ok(Some(trimmed.parse()?))
|
Ok(Some(trimmed.parse()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PolicyRoutingCleanup {
|
struct PolicyRoutingGuard {
|
||||||
table_id: u32,
|
table_id: u32,
|
||||||
iface: String,
|
|
||||||
}
|
}
|
||||||
impl Drop for PolicyRoutingCleanup {
|
|
||||||
fn drop(&mut self) {
|
/// Remove stale per-interface policy-routing state (fwmark rules, routing
|
||||||
let table_str = self.table_id.to_string();
|
/// tables, iptables CONNMARK rules) for interfaces that no longer exist.
|
||||||
let iface = std::mem::take(&mut self.iface);
|
async fn gc_policy_routing(active_ifaces: &BTreeSet<GatewayId>) {
|
||||||
tokio::spawn(async move {
|
let active_tables: BTreeSet<u32> = active_ifaces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|iface| if_nametoindex(iface.as_str()).ok().map(|idx| 1000 + idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// GC fwmark ip rules at priority 50 and their routing tables.
|
||||||
|
if let Ok(rules) = Command::new("ip")
|
||||||
|
.arg("rule")
|
||||||
|
.arg("show")
|
||||||
|
.invoke(ErrorKind::Network)
|
||||||
|
.await
|
||||||
|
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
||||||
|
{
|
||||||
|
for line in rules.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if !line.starts_with("50:") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(pos) = line.find("lookup ") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let token = line[pos + 7..].split_whitespace().next().unwrap_or("");
|
||||||
|
let Ok(table_id) = token.parse::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if table_id < 1000 || active_tables.contains(&table_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let table_str = table_id.to_string();
|
||||||
|
tracing::debug!("gc_policy_routing: removing stale table {table_id}");
|
||||||
Command::new("ip")
|
Command::new("ip")
|
||||||
.arg("rule")
|
.arg("rule")
|
||||||
.arg("del")
|
.arg("del")
|
||||||
@@ -835,25 +864,46 @@ impl Drop for PolicyRoutingCleanup {
|
|||||||
.invoke(ErrorKind::Network)
|
.invoke(ErrorKind::Network)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
Command::new("iptables")
|
}
|
||||||
.arg("-t")
|
}
|
||||||
.arg("mangle")
|
|
||||||
.arg("-D")
|
// GC iptables CONNMARK set-mark rules for defunct interfaces.
|
||||||
.arg("PREROUTING")
|
if let Ok(rules) = Command::new("iptables")
|
||||||
.arg("-i")
|
.arg("-t")
|
||||||
.arg(&iface)
|
.arg("mangle")
|
||||||
.arg("-m")
|
.arg("-S")
|
||||||
.arg("conntrack")
|
.arg("PREROUTING")
|
||||||
.arg("--ctstate")
|
.invoke(ErrorKind::Network)
|
||||||
.arg("NEW")
|
.await
|
||||||
.arg("-j")
|
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
||||||
.arg("CONNMARK")
|
{
|
||||||
.arg("--set-mark")
|
// Rules look like:
|
||||||
.arg(&table_str)
|
// -A PREROUTING -i wg0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1005
|
||||||
.invoke(ErrorKind::Network)
|
for line in rules.lines() {
|
||||||
.await
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
.ok();
|
if parts.first() != Some(&"-A") {
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
if !parts.contains(&"--set-mark") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(iface_idx) = parts.iter().position(|&p| p == "-i") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(&iface) = parts.get(iface_idx + 1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if active_ifaces.contains(&GatewayId::from(InternedString::intern(iface))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracing::debug!("gc_policy_routing: removing stale iptables rule for {iface}");
|
||||||
|
let mut cmd = Command::new("iptables");
|
||||||
|
cmd.arg("-t").arg("mangle").arg("-D");
|
||||||
|
for &arg in &parts[1..] {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
cmd.invoke(ErrorKind::Network).await.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,11 +1035,8 @@ async fn watch_ip(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let policy_guard: Option<PolicyRoutingCleanup> =
|
let policy_guard: Option<PolicyRoutingGuard> =
|
||||||
policy_table_id.map(|t| PolicyRoutingCleanup {
|
policy_table_id.map(|t| PolicyRoutingGuard { table_id: t });
|
||||||
table_id: t,
|
|
||||||
iface: iface.as_str().to_owned(),
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
until
|
until
|
||||||
@@ -1016,7 +1063,7 @@ async fn watch_ip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_policy_routing(
|
async fn apply_policy_routing(
|
||||||
guard: &PolicyRoutingCleanup,
|
guard: &PolicyRoutingGuard,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
lan_ip: &OrdSet<IpAddr>,
|
lan_ip: &OrdSet<IpAddr>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -1250,7 +1297,7 @@ async fn poll_ip_info(
|
|||||||
ip4_proxy: &Ip4ConfigProxy<'_>,
|
ip4_proxy: &Ip4ConfigProxy<'_>,
|
||||||
ip6_proxy: &Ip6ConfigProxy<'_>,
|
ip6_proxy: &Ip6ConfigProxy<'_>,
|
||||||
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
||||||
policy_guard: &Option<PolicyRoutingCleanup>,
|
policy_guard: &Option<PolicyRoutingGuard>,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
||||||
db: Option<&TypedPatchDb<Database>>,
|
db: Option<&TypedPatchDb<Database>>,
|
||||||
@@ -1299,6 +1346,49 @@ async fn poll_ip_info(
|
|||||||
apply_policy_routing(guard, iface, &lan_ip).await?;
|
apply_policy_routing(guard, iface, &lan_ip).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write IP info to the watch immediately so the gateway appears in the
|
||||||
|
// DB without waiting for the (slow) WAN IP fetch. The echoip HTTP
|
||||||
|
// request has a 5-second timeout per URL and is easily cancelled by
|
||||||
|
// D-Bus signals via the Until mechanism, which would prevent the
|
||||||
|
// gateway from ever appearing if we waited.
|
||||||
|
let mut ip_info = IpInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
scope_id,
|
||||||
|
device_type,
|
||||||
|
subnets: subnets.clone(),
|
||||||
|
lan_ip,
|
||||||
|
wan_ip: None,
|
||||||
|
ntp_servers,
|
||||||
|
dns_servers,
|
||||||
|
};
|
||||||
|
|
||||||
|
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
||||||
|
let (name, secure, gateway_type, prev_wan_ip) =
|
||||||
|
m.get(iface).map_or((None, None, None, None), |i| {
|
||||||
|
(
|
||||||
|
i.name.clone(),
|
||||||
|
i.secure,
|
||||||
|
i.gateway_type,
|
||||||
|
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
ip_info.wan_ip = prev_wan_ip;
|
||||||
|
let ip_info = Arc::new(ip_info);
|
||||||
|
m.insert(
|
||||||
|
iface.clone(),
|
||||||
|
NetworkInterfaceInfo {
|
||||||
|
name,
|
||||||
|
secure,
|
||||||
|
ip_info: Some(ip_info.clone()),
|
||||||
|
gateway_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.filter(|old| &old.ip_info == &Some(ip_info))
|
||||||
|
.is_none()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now fetch the WAN IP in a second pass. Even if this is slow or
|
||||||
|
// gets cancelled, the gateway already has valid ip_info above.
|
||||||
let echoip_urls = if let Some(db) = db {
|
let echoip_urls = if let Some(db) = db {
|
||||||
db.peek()
|
db.peek()
|
||||||
.await
|
.await
|
||||||
@@ -1349,41 +1439,25 @@ async fn poll_ip_info(
|
|||||||
);
|
);
|
||||||
tracing::debug!("{e:?}");
|
tracing::debug!("{e:?}");
|
||||||
}
|
}
|
||||||
let mut ip_info = IpInfo {
|
|
||||||
name: name.clone(),
|
|
||||||
scope_id,
|
|
||||||
device_type,
|
|
||||||
subnets,
|
|
||||||
lan_ip,
|
|
||||||
wan_ip,
|
|
||||||
ntp_servers,
|
|
||||||
dns_servers,
|
|
||||||
};
|
|
||||||
|
|
||||||
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
// Update with WAN IP if we obtained one
|
||||||
let (name, secure, gateway_type, prev_wan_ip) =
|
if wan_ip.is_some() {
|
||||||
m.get(iface).map_or((None, None, None, None), |i| {
|
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
||||||
(
|
let Some(entry) = m.get_mut(iface) else {
|
||||||
i.name.clone(),
|
return false;
|
||||||
i.secure,
|
};
|
||||||
i.gateway_type,
|
let Some(ref existing_ip) = entry.ip_info else {
|
||||||
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
return false;
|
||||||
)
|
};
|
||||||
});
|
if existing_ip.wan_ip == wan_ip {
|
||||||
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip);
|
return false;
|
||||||
let ip_info = Arc::new(ip_info);
|
}
|
||||||
m.insert(
|
let mut updated = (**existing_ip).clone();
|
||||||
iface.clone(),
|
updated.wan_ip = wan_ip;
|
||||||
NetworkInterfaceInfo {
|
entry.ip_info = Some(Arc::new(updated));
|
||||||
name,
|
true
|
||||||
secure,
|
});
|
||||||
ip_info: Some(ip_info.clone()),
|
}
|
||||||
gateway_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.filter(|old| &old.ip_info == &Some(ip_info))
|
|
||||||
.is_none()
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,6 +615,7 @@ fn check_matching_info_short() {
|
|||||||
sdk_version: None,
|
sdk_version: None,
|
||||||
hardware_acceleration: false,
|
hardware_acceleration: false,
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
},
|
},
|
||||||
icon: DataUrl::from_vec("image/png", vec![]),
|
icon: DataUrl::from_vec("image/png", vec![]),
|
||||||
dependency_metadata: BTreeMap::new(),
|
dependency_metadata: BTreeMap::new(),
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ pub struct PackageMetadata {
|
|||||||
pub hardware_acceleration: bool,
|
pub hardware_acceleration: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub plugins: BTreeSet<PluginId>,
|
pub plugins: BTreeSet<PluginId>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub satisfies: BTreeSet<VersionString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
version: version.into(),
|
version: version.into(),
|
||||||
satisfies: BTreeSet::new(),
|
|
||||||
can_migrate_from: VersionRange::any(),
|
can_migrate_from: VersionRange::any(),
|
||||||
can_migrate_to: VersionRange::none(),
|
can_migrate_to: VersionRange::none(),
|
||||||
metadata: PackageMetadata {
|
metadata: PackageMetadata {
|
||||||
@@ -219,6 +218,7 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
PackageProcedure::Script(_) => false,
|
PackageProcedure::Script(_) => false,
|
||||||
},
|
},
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
},
|
},
|
||||||
images: BTreeMap::new(),
|
images: BTreeMap::new(),
|
||||||
volumes: value
|
volumes: value
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ pub(crate) fn current_version() -> Version {
|
|||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
pub id: PackageId,
|
pub id: PackageId,
|
||||||
pub version: VersionString,
|
pub version: VersionString,
|
||||||
pub satisfies: BTreeSet<VersionString>,
|
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub can_migrate_to: VersionRange,
|
pub can_migrate_to: VersionRange,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ pub async fn check_dependencies(
|
|||||||
};
|
};
|
||||||
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
||||||
let installed_version = manifest.as_version().de()?.into_version();
|
let installed_version = manifest.as_version().de()?.into_version();
|
||||||
let satisfies = manifest.as_satisfies().de()?;
|
let satisfies = manifest.as_metadata().as_satisfies().de()?;
|
||||||
let installed_version = Some(installed_version.clone().into());
|
let installed_version = Some(installed_version.clone().into());
|
||||||
let is_running = package
|
let is_running = package
|
||||||
.as_status_info()
|
.as_status_info()
|
||||||
|
|||||||
@@ -134,8 +134,7 @@ pub async fn list_service_interfaces(
|
|||||||
.expect("valid json pointer");
|
.expect("valid json pointer");
|
||||||
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
||||||
|
|
||||||
let res = imbl_value::from_value(watch.peek_and_mark_seen()?)
|
let res = from_value(watch.peek_and_mark_seen()?)?;
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if let Some(callback) = callback {
|
if let Some(callback) = callback {
|
||||||
let callback = callback.register(&context.seed.persistent_container);
|
let callback = callback.register(&context.seed.persistent_container);
|
||||||
@@ -174,9 +173,7 @@ pub async fn clear_service_interfaces(
|
|||||||
.as_idx_mut(&package_id)
|
.as_idx_mut(&package_id)
|
||||||
.or_not_found(&package_id)?
|
.or_not_found(&package_id)?
|
||||||
.as_service_interfaces_mut()
|
.as_service_interfaces_mut()
|
||||||
.mutate(|s| {
|
.mutate(|s| Ok(s.retain(|id, _| except.contains(id))))
|
||||||
Ok(s.retain(|id, _| except.contains(id)))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::bins::set_locale;
|
use crate::bins::set_locale;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
|
use crate::db::model::public::RestartReason;
|
||||||
use crate::disk::util::{get_available, get_used};
|
use crate::disk::util::{get_available, get_used};
|
||||||
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -351,10 +352,9 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info.as_kiosk_mut().ser(&Some(true))?;
|
||||||
.as_kiosk_mut()
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
||||||
.ser(&Some(true))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -369,10 +369,9 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info.as_kiosk_mut().ser(&Some(false))?;
|
||||||
.as_kiosk_mut()
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
||||||
.ser(&Some(false))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -1367,10 +1366,11 @@ pub async fn set_language(
|
|||||||
save_language(&*language).await?;
|
save_language(&*language).await?;
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info
|
||||||
.as_language_mut()
|
.as_language_mut()
|
||||||
.ser(&Some(language.clone()))
|
.ser(&Some(language.clone()))?;
|
||||||
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Language))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::PLATFORM;
|
use crate::PLATFORM;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
|
use crate::db::model::public::RestartReason;
|
||||||
use crate::notifications::{NotificationLevel, notify};
|
use crate::notifications::{NotificationLevel, notify};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::{
|
use crate::progress::{
|
||||||
@@ -81,8 +82,9 @@ pub async fn update_system(
|
|||||||
.into_public()
|
.into_public()
|
||||||
.into_server_info()
|
.into_server_info()
|
||||||
.into_status_info()
|
.into_status_info()
|
||||||
.into_updated()
|
.into_restart()
|
||||||
.de()?
|
.de()?
|
||||||
|
== Some(RestartReason::Update)
|
||||||
{
|
{
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
@@ -281,10 +283,18 @@ async fn maybe_do_update(
|
|||||||
|
|
||||||
let start_progress = progress.snapshot();
|
let start_progress = progress.snapshot();
|
||||||
|
|
||||||
let status = ctx
|
ctx.db
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let mut status = peeked.as_public().as_server_info().as_status_info().de()?;
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
|
|
||||||
|
if server_info.as_status_info().as_restart().de()?.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
|
crate::ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut status = server_info.as_status_info().de()?;
|
||||||
if status.update_progress.is_some() {
|
if status.update_progress.is_some() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updating")),
|
eyre!("{}", t!("update.already-updating")),
|
||||||
@@ -293,22 +303,12 @@ async fn maybe_do_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
status.update_progress = Some(start_progress);
|
status.update_progress = Some(start_progress);
|
||||||
db.as_public_mut()
|
server_info.as_status_info_mut().ser(&status)?;
|
||||||
.as_server_info_mut()
|
Ok(())
|
||||||
.as_status_info_mut()
|
|
||||||
.ser(&status)?;
|
|
||||||
Ok(status)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
if status.updated {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
|
||||||
crate::ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
||||||
ctx.db.clone(),
|
ctx.db.clone(),
|
||||||
|db| {
|
|db| {
|
||||||
@@ -338,10 +338,15 @@ async fn maybe_do_update(
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let status_info =
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
db.as_public_mut().as_server_info_mut().as_status_info_mut();
|
server_info
|
||||||
status_info.as_update_progress_mut().ser(&None)?;
|
.as_status_info_mut()
|
||||||
status_info.as_updated_mut().ser(&true)
|
.as_update_progress_mut()
|
||||||
|
.ser(&None)?;
|
||||||
|
server_info
|
||||||
|
.as_status_info_mut()
|
||||||
|
.as_restart_mut()
|
||||||
|
.ser(&Some(RestartReason::Update))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -40,6 +40,102 @@ lazy_static::lazy_static! {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the LC_COLLATE / LC_CTYPE the cluster was created with and generate
|
||||||
|
/// those locales if they are missing from the running system. Older installs
|
||||||
|
/// may have been initialized with a locale (e.g. en_GB.UTF-8) that the current
|
||||||
|
/// image does not ship. Without it PostgreSQL starts but refuses
|
||||||
|
/// connections, breaking the migration.
|
||||||
|
async fn ensure_cluster_locale(pg_version: u32) -> Result<(), Error> {
|
||||||
|
let cluster_dir = format!("/var/lib/postgresql/{pg_version}/main");
|
||||||
|
let pg_controldata = format!("/usr/lib/postgresql/{pg_version}/bin/pg_controldata");
|
||||||
|
|
||||||
|
let output = Command::new(&pg_controldata)
|
||||||
|
.arg(&cluster_dir)
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.with_kind(crate::ErrorKind::Database)?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
tracing::warn!("pg_controldata failed, skipping locale check: {stderr}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut locales_needed = Vec::new();
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let locale = if let Some(rest) = line.strip_prefix("LC_COLLATE:") {
|
||||||
|
rest.trim()
|
||||||
|
} else if let Some(rest) = line.strip_prefix("LC_CTYPE:") {
|
||||||
|
rest.trim()
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !locale.is_empty() && locale != "C" && locale != "POSIX" {
|
||||||
|
locales_needed.push(locale.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locales_needed.sort();
|
||||||
|
locales_needed.dedup();
|
||||||
|
|
||||||
|
if locales_needed.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which locales are already available.
|
||||||
|
let available = Command::new("locale")
|
||||||
|
.arg("-a")
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut need_gen = false;
|
||||||
|
for locale in &locales_needed {
|
||||||
|
// locale -a normalizes e.g. "en_GB.UTF-8" → "en_GB.utf8"
|
||||||
|
let normalized = locale.replace("-", "").to_lowercase();
|
||||||
|
if available.lines().any(|l| l.replace("-", "").to_lowercase() == normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Debian's locale-gen ignores positional args — the locale must be
|
||||||
|
// uncommented in /etc/locale.gen or appended to it.
|
||||||
|
tracing::info!("Enabling missing locale for PostgreSQL cluster: {locale}");
|
||||||
|
let locale_gen_path = Path::new("/etc/locale.gen");
|
||||||
|
let contents = tokio::fs::read_to_string(locale_gen_path)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
// Try to uncomment an existing entry first, otherwise append.
|
||||||
|
let entry = format!("{locale} UTF-8");
|
||||||
|
let commented = format!("# {entry}");
|
||||||
|
if contents.contains(&commented) {
|
||||||
|
let updated = contents.replace(&commented, &entry);
|
||||||
|
tokio::fs::write(locale_gen_path, updated).await?;
|
||||||
|
} else if !contents.contains(&entry) {
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
let mut f = tokio::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(locale_gen_path)
|
||||||
|
.await?;
|
||||||
|
f.write_all(format!("\n{entry}\n").as_bytes()).await?;
|
||||||
|
}
|
||||||
|
need_gen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if need_gen {
|
||||||
|
Command::new("locale-gen")
|
||||||
|
.invoke(crate::ErrorKind::Database)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
||||||
let db_dir = datadir.as_ref().join("main/postgresql");
|
let db_dir = datadir.as_ref().join("main/postgresql");
|
||||||
@@ -91,6 +187,12 @@ async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
|||||||
|
|
||||||
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
||||||
|
|
||||||
|
// The cluster may have been created with a locale not present on the
|
||||||
|
// current image (e.g. en_GB.UTF-8 on a server that predates the trixie
|
||||||
|
// image). Detect and generate it before starting PostgreSQL, otherwise
|
||||||
|
// PG will start but refuse connections.
|
||||||
|
ensure_cluster_locale(pg_version).await?;
|
||||||
|
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("start")
|
.arg("start")
|
||||||
.arg(format!("postgresql@{pg_version}-main.service"))
|
.arg(format!("postgresql@{pg_version}-main.service"))
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ impl VersionT for Version {
|
|||||||
&V0_3_0_COMPAT
|
&V0_3_0_COMPAT
|
||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
|
let status_info = db["public"]["serverInfo"]["statusInfo"]
|
||||||
|
.as_object_mut();
|
||||||
|
if let Some(m) = status_info {
|
||||||
|
m.remove("updated");
|
||||||
|
m.insert("restart".into(), Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Value::Null)
|
Ok(Value::Null)
|
||||||
}
|
}
|
||||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import type { VolumeId } from './VolumeId'
|
|||||||
export type Manifest = {
|
export type Manifest = {
|
||||||
id: PackageId
|
id: PackageId
|
||||||
version: Version
|
version: Version
|
||||||
satisfies: Array<Version>
|
|
||||||
canMigrateTo: string
|
canMigrateTo: string
|
||||||
canMigrateFrom: string
|
canMigrateFrom: string
|
||||||
images: { [key: ImageId]: ImageConfig }
|
images: { [key: ImageId]: ImageConfig }
|
||||||
@@ -37,4 +36,5 @@ export type Manifest = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
|
satisfies: Array<Version>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { MerkleArchiveCommitment } from './MerkleArchiveCommitment'
|
|||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
import type { PluginId } from './PluginId'
|
import type { PluginId } from './PluginId'
|
||||||
import type { RegistryAsset } from './RegistryAsset'
|
import type { RegistryAsset } from './RegistryAsset'
|
||||||
|
import type { Version } from './Version'
|
||||||
|
|
||||||
export type PackageVersionInfo = {
|
export type PackageVersionInfo = {
|
||||||
icon: DataUrl
|
icon: DataUrl
|
||||||
@@ -31,4 +32,5 @@ export type PackageVersionInfo = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
|
satisfies: Array<Version>
|
||||||
}
|
}
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/RestartReason.ts
Normal file
3
sdk/base/lib/osBindings/RestartReason.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type RestartReason = 'mdns' | 'language' | 'kiosk' | 'update'
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
import type { BackupProgress } from './BackupProgress'
|
import type { BackupProgress } from './BackupProgress'
|
||||||
import type { FullProgress } from './FullProgress'
|
import type { FullProgress } from './FullProgress'
|
||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
|
import type { RestartReason } from './RestartReason'
|
||||||
|
|
||||||
export type ServerStatus = {
|
export type ServerStatus = {
|
||||||
backupProgress: { [key: PackageId]: BackupProgress } | null
|
backupProgress: { [key: PackageId]: BackupProgress } | null
|
||||||
updated: boolean
|
|
||||||
updateProgress: FullProgress | null
|
updateProgress: FullProgress | null
|
||||||
shuttingDown: boolean
|
shuttingDown: boolean
|
||||||
restarting: boolean
|
restarting: boolean
|
||||||
|
restart: RestartReason | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ export { RenameGatewayParams } from './RenameGatewayParams'
|
|||||||
export { ReplayId } from './ReplayId'
|
export { ReplayId } from './ReplayId'
|
||||||
export { RequestCommitment } from './RequestCommitment'
|
export { RequestCommitment } from './RequestCommitment'
|
||||||
export { ResetPasswordParams } from './ResetPasswordParams'
|
export { ResetPasswordParams } from './ResetPasswordParams'
|
||||||
|
export { RestartReason } from './RestartReason'
|
||||||
export { RestorePackageParams } from './RestorePackageParams'
|
export { RestorePackageParams } from './RestorePackageParams'
|
||||||
export { RunActionParams } from './RunActionParams'
|
export { RunActionParams } from './RunActionParams'
|
||||||
export { Security } from './Security'
|
export { Security } from './Security'
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Performs a deep structural equality check across all provided arguments.
|
* Performs a deep structural equality check across all provided arguments.
|
||||||
* Returns true only if every argument is deeply equal to every other argument.
|
* Returns true only if every argument is deeply equal to every other argument.
|
||||||
* Handles primitives, arrays, and plain objects recursively.
|
* Handles primitives, arrays, and plain objects (JSON-like) recursively.
|
||||||
|
*
|
||||||
|
* Non-plain objects (Set, Map, Date, etc.) are compared by reference only,
|
||||||
|
* since Object.keys() does not enumerate their contents.
|
||||||
*
|
*
|
||||||
* @param args - Two or more values to compare for deep equality
|
* @param args - Two or more values to compare for deep equality
|
||||||
* @returns True if all arguments are deeply equal
|
* @returns True if all arguments are deeply equal
|
||||||
@@ -23,6 +26,18 @@ export function deepEqual(...args: unknown[]) {
|
|||||||
}
|
}
|
||||||
if (objects.length !== args.length) return false
|
if (objects.length !== args.length) return false
|
||||||
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
||||||
|
if (
|
||||||
|
objects.some(
|
||||||
|
(x) => !Array.isArray(x) && Object.getPrototypeOf(x) !== Object.prototype,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
objects.reduce<object | null>(
|
||||||
|
(a, b) => (a === b ? a : null),
|
||||||
|
objects[0],
|
||||||
|
) !== null
|
||||||
|
)
|
||||||
|
}
|
||||||
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
||||||
for (const key of allKeys) {
|
for (const key of allKeys) {
|
||||||
for (const x of objects) {
|
for (const x of objects) {
|
||||||
|
|||||||
@@ -485,7 +485,6 @@ export default {
|
|||||||
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
|
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
|
||||||
513: 'Aktivieren',
|
513: 'Aktivieren',
|
||||||
514: 'Deaktivieren',
|
514: 'Deaktivieren',
|
||||||
515: 'Diese Änderung wird nach dem nächsten Neustart wirksam',
|
|
||||||
516: 'Empfohlen',
|
516: 'Empfohlen',
|
||||||
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
|
||||||
518: 'Verwerfen',
|
518: 'Verwerfen',
|
||||||
@@ -717,11 +716,12 @@ export default {
|
|||||||
799: 'Nach Klick auf "Enroll MOK":',
|
799: 'Nach Klick auf "Enroll MOK":',
|
||||||
800: 'Geben Sie bei Aufforderung Ihr StartOS-Passwort ein',
|
800: 'Geben Sie bei Aufforderung Ihr StartOS-Passwort ein',
|
||||||
801: 'Ihr System hat Secure Boot aktiviert, was erfordert, dass alle Kernel-Module mit einem vertrauenswürdigen Schlüssel signiert sind. Einige Hardware-Treiber \u2014 wie die für NVIDIA-GPUs \u2014 sind nicht mit dem Standard-Distributionsschlüssel signiert. Die Registrierung des StartOS-Signaturschlüssels ermöglicht es Ihrer Firmware, diesen Modulen zu vertrauen, damit Ihre Hardware vollständig genutzt werden kann.',
|
801: 'Ihr System hat Secure Boot aktiviert, was erfordert, dass alle Kernel-Module mit einem vertrauenswürdigen Schlüssel signiert sind. Einige Hardware-Treiber \u2014 wie die für NVIDIA-GPUs \u2014 sind nicht mit dem Standard-Distributionsschlüssel signiert. Die Registrierung des StartOS-Signaturschlüssels ermöglicht es Ihrer Firmware, diesen Modulen zu vertrauen, damit Ihre Hardware vollständig genutzt werden kann.',
|
||||||
802: 'Die Übersetzungen auf Betriebssystemebene sind bereits aktiv. Ein Neustart ist erforderlich, damit die Übersetzungen auf Dienstebene wirksam werden.',
|
|
||||||
803: 'Dieses Laufwerk verwendet ext4 und wird automatisch in btrfs konvertiert. Ein Backup wird dringend empfohlen, bevor Sie fortfahren.',
|
803: 'Dieses Laufwerk verwendet ext4 und wird automatisch in btrfs konvertiert. Ein Backup wird dringend empfohlen, bevor Sie fortfahren.',
|
||||||
804: 'Ich habe ein Backup meiner Daten',
|
804: 'Ich habe ein Backup meiner Daten',
|
||||||
805: 'Öffentliche Domain hinzufügen',
|
805: 'Öffentliche Domain hinzufügen',
|
||||||
806: 'Ergebnis',
|
806: 'Ergebnis',
|
||||||
807: 'Nach dem Öffnen der neuen Adresse werden Sie zum Neustart aufgefordert.',
|
807: 'Download abgeschlossen. Neustart zum Anwenden.',
|
||||||
808: 'Ein Neustart ist erforderlich, damit die Dienstschnittstellen den neuen Hostnamen verwenden.',
|
808: 'Hostname geändert, Neustart damit installierte Dienste die neue Adresse verwenden',
|
||||||
|
809: 'Sprache geändert, Neustart damit installierte Dienste die neue Sprache verwenden',
|
||||||
|
810: 'Kioskmodus geändert, Neustart zum Anwenden',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -484,7 +484,6 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Kiosk Mode is unavailable on this device': 512,
|
'Kiosk Mode is unavailable on this device': 512,
|
||||||
'Enable': 513,
|
'Enable': 513,
|
||||||
'Disable': 514,
|
'Disable': 514,
|
||||||
'This change will take effect after the next boot': 515,
|
|
||||||
'Recommended': 516, // as in, we recommend this
|
'Recommended': 516, // as in, we recommend this
|
||||||
'Are you sure you want to dismiss this task?': 517,
|
'Are you sure you want to dismiss this task?': 517,
|
||||||
'Dismiss': 518, // as in, dismiss or delete a task
|
'Dismiss': 518, // as in, dismiss or delete a task
|
||||||
@@ -718,11 +717,12 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'After clicking "Enroll MOK":': 799,
|
'After clicking "Enroll MOK":': 799,
|
||||||
'When prompted, enter your StartOS password': 800,
|
'When prompted, enter your StartOS password': 800,
|
||||||
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers \u2014 such as those for NVIDIA GPUs \u2014 are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.': 801,
|
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers \u2014 such as those for NVIDIA GPUs \u2014 are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.': 801,
|
||||||
'OS-level translations are already in effect. A restart is required for service-level translations to take effect.': 802,
|
|
||||||
'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803,
|
'This drive uses ext4 and will be automatically converted to btrfs. A backup is strongly recommended before proceeding.': 803,
|
||||||
'I have a backup of my data': 804,
|
'I have a backup of my data': 804,
|
||||||
'Add Public Domain': 805,
|
'Add Public Domain': 805,
|
||||||
'Result': 806,
|
'Result': 806,
|
||||||
'After opening the new address, you will be prompted to restart.': 807,
|
'Download complete. Restart to apply.': 807,
|
||||||
'A restart is required for service interfaces to use the new hostname.': 808,
|
'Hostname changed, restart for installed services to use the new address': 808,
|
||||||
|
'Language changed, restart for installed services to use the new language': 809,
|
||||||
|
'Kiosk mode changed, restart to apply': 810,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,7 +485,6 @@ export default {
|
|||||||
512: 'El modo quiosco no está disponible en este dispositivo',
|
512: 'El modo quiosco no está disponible en este dispositivo',
|
||||||
513: 'Activar',
|
513: 'Activar',
|
||||||
514: 'Desactivar',
|
514: 'Desactivar',
|
||||||
515: 'Este cambio tendrá efecto después del próximo inicio',
|
|
||||||
516: 'Recomendado',
|
516: 'Recomendado',
|
||||||
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
517: '¿Estás seguro de que deseas descartar esta tarea?',
|
||||||
518: 'Descartar',
|
518: 'Descartar',
|
||||||
@@ -717,11 +716,12 @@ export default {
|
|||||||
799: 'Después de hacer clic en "Enroll MOK":',
|
799: 'Después de hacer clic en "Enroll MOK":',
|
||||||
800: 'Cuando se le solicite, ingrese su contraseña de StartOS',
|
800: 'Cuando se le solicite, ingrese su contraseña de StartOS',
|
||||||
801: 'Su sistema tiene Secure Boot habilitado, lo que requiere que todos los módulos del kernel estén firmados con una clave de confianza. Algunos controladores de hardware \u2014 como los de las GPU NVIDIA \u2014 no están firmados con la clave de distribución predeterminada. Registrar la clave de firma de StartOS permite que su firmware confíe en estos módulos para que su hardware pueda utilizarse completamente.',
|
801: 'Su sistema tiene Secure Boot habilitado, lo que requiere que todos los módulos del kernel estén firmados con una clave de confianza. Algunos controladores de hardware \u2014 como los de las GPU NVIDIA \u2014 no están firmados con la clave de distribución predeterminada. Registrar la clave de firma de StartOS permite que su firmware confíe en estos módulos para que su hardware pueda utilizarse completamente.',
|
||||||
802: 'Las traducciones a nivel del sistema operativo ya están en vigor. Se requiere un reinicio para que las traducciones a nivel de servicio surtan efecto.',
|
|
||||||
803: 'Esta unidad usa ext4 y se convertirá automáticamente a btrfs. Se recomienda encarecidamente hacer una copia de seguridad antes de continuar.',
|
803: 'Esta unidad usa ext4 y se convertirá automáticamente a btrfs. Se recomienda encarecidamente hacer una copia de seguridad antes de continuar.',
|
||||||
804: 'Tengo una copia de seguridad de mis datos',
|
804: 'Tengo una copia de seguridad de mis datos',
|
||||||
805: 'Agregar dominio público',
|
805: 'Agregar dominio público',
|
||||||
806: 'Resultado',
|
806: 'Resultado',
|
||||||
807: 'Después de abrir la nueva dirección, se le pedirá que reinicie.',
|
807: 'Descarga completa. Reiniciar para aplicar.',
|
||||||
808: 'Se requiere un reinicio para que las interfaces de servicio utilicen el nuevo nombre de host.',
|
808: 'Nombre de host cambiado, reiniciar para que los servicios instalados usen la nueva dirección',
|
||||||
|
809: 'Idioma cambiado, reiniciar para que los servicios instalados usen el nuevo idioma',
|
||||||
|
810: 'Modo kiosco cambiado, reiniciar para aplicar',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -485,7 +485,6 @@ export default {
|
|||||||
512: 'Le mode kiosque n’est pas disponible sur cet appareil',
|
512: 'Le mode kiosque n’est pas disponible sur cet appareil',
|
||||||
513: 'Activer',
|
513: 'Activer',
|
||||||
514: 'Désactiver',
|
514: 'Désactiver',
|
||||||
515: 'Ce changement va prendre effet après le prochain démarrage',
|
|
||||||
516: 'Recommandé',
|
516: 'Recommandé',
|
||||||
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
|
||||||
518: 'Ignorer',
|
518: 'Ignorer',
|
||||||
@@ -717,11 +716,12 @@ export default {
|
|||||||
799: 'Après avoir cliqué sur "Enroll MOK" :',
|
799: 'Après avoir cliqué sur "Enroll MOK" :',
|
||||||
800: 'Lorsque vous y êtes invité, entrez votre mot de passe StartOS',
|
800: 'Lorsque vous y êtes invité, entrez votre mot de passe StartOS',
|
||||||
801: "Votre système a Secure Boot activé, ce qui exige que tous les modules du noyau soient signés avec une clé de confiance. Certains pilotes matériels \u2014 comme ceux des GPU NVIDIA \u2014 ne sont pas signés par la clé de distribution par défaut. L'enregistrement de la clé de signature StartOS permet à votre firmware de faire confiance à ces modules afin que votre matériel puisse être pleinement utilisé.",
|
801: "Votre système a Secure Boot activé, ce qui exige que tous les modules du noyau soient signés avec une clé de confiance. Certains pilotes matériels \u2014 comme ceux des GPU NVIDIA \u2014 ne sont pas signés par la clé de distribution par défaut. L'enregistrement de la clé de signature StartOS permet à votre firmware de faire confiance à ces modules afin que votre matériel puisse être pleinement utilisé.",
|
||||||
802: "Les traductions au niveau du système d'exploitation sont déjà en vigueur. Un redémarrage est nécessaire pour que les traductions au niveau des services prennent effet.",
|
|
||||||
803: 'Ce disque utilise ext4 et sera automatiquement converti en btrfs. Il est fortement recommandé de faire une sauvegarde avant de continuer.',
|
803: 'Ce disque utilise ext4 et sera automatiquement converti en btrfs. Il est fortement recommandé de faire une sauvegarde avant de continuer.',
|
||||||
804: "J'ai une sauvegarde de mes données",
|
804: "J'ai une sauvegarde de mes données",
|
||||||
805: 'Ajouter un domaine public',
|
805: 'Ajouter un domaine public',
|
||||||
806: 'Résultat',
|
806: 'Résultat',
|
||||||
807: 'Après avoir ouvert la nouvelle adresse, vous serez invité à redémarrer.',
|
807: 'Téléchargement terminé. Redémarrer pour appliquer.',
|
||||||
808: "Un redémarrage est nécessaire pour que les interfaces de service utilisent le nouveau nom d'hôte.",
|
808: "Nom d'hôte modifié, redémarrer pour que les services installés utilisent la nouvelle adresse",
|
||||||
|
809: 'Langue modifiée, redémarrer pour que les services installés utilisent la nouvelle langue',
|
||||||
|
810: 'Mode kiosque modifié, redémarrer pour appliquer',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -485,7 +485,6 @@ export default {
|
|||||||
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
|
512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
|
||||||
513: 'Włącz',
|
513: 'Włącz',
|
||||||
514: 'Wyłącz',
|
514: 'Wyłącz',
|
||||||
515: 'Ta zmiana zacznie obowiązywać po następnym uruchomieniu',
|
|
||||||
516: 'Zalecane',
|
516: 'Zalecane',
|
||||||
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
517: 'Czy na pewno chcesz odrzucić to zadanie?',
|
||||||
518: 'Odrzuć',
|
518: 'Odrzuć',
|
||||||
@@ -717,11 +716,12 @@ export default {
|
|||||||
799: 'Po kliknięciu "Enroll MOK":',
|
799: 'Po kliknięciu "Enroll MOK":',
|
||||||
800: 'Po wyświetleniu monitu wprowadź swoje hasło StartOS',
|
800: 'Po wyświetleniu monitu wprowadź swoje hasło StartOS',
|
||||||
801: 'Twój system ma włączony Secure Boot, co wymaga, aby wszystkie moduły jądra były podpisane zaufanym kluczem. Niektóre sterowniki sprzętowe \u2014 takie jak te dla GPU NVIDIA \u2014 nie są podpisane domyślnym kluczem dystrybucji. Zarejestrowanie klucza podpisu StartOS pozwala firmware ufać tym modułom, aby sprzęt mógł być w pełni wykorzystany.',
|
801: 'Twój system ma włączony Secure Boot, co wymaga, aby wszystkie moduły jądra były podpisane zaufanym kluczem. Niektóre sterowniki sprzętowe \u2014 takie jak te dla GPU NVIDIA \u2014 nie są podpisane domyślnym kluczem dystrybucji. Zarejestrowanie klucza podpisu StartOS pozwala firmware ufać tym modułom, aby sprzęt mógł być w pełni wykorzystany.',
|
||||||
802: 'Tłumaczenia na poziomie systemu operacyjnego są już aktywne. Wymagane jest ponowne uruchomienie, aby tłumaczenia na poziomie usług zaczęły obowiązywać.',
|
|
||||||
803: 'Ten dysk używa ext4 i zostanie automatycznie skonwertowany na btrfs. Zdecydowanie zaleca się wykonanie kopii zapasowej przed kontynuowaniem.',
|
803: 'Ten dysk używa ext4 i zostanie automatycznie skonwertowany na btrfs. Zdecydowanie zaleca się wykonanie kopii zapasowej przed kontynuowaniem.',
|
||||||
804: 'Mam kopię zapasową moich danych',
|
804: 'Mam kopię zapasową moich danych',
|
||||||
805: 'Dodaj domenę publiczną',
|
805: 'Dodaj domenę publiczną',
|
||||||
806: 'Wynik',
|
806: 'Wynik',
|
||||||
807: 'Po otwarciu nowego adresu zostaniesz poproszony o ponowne uruchomienie.',
|
807: 'Pobieranie zakończone. Uruchom ponownie, aby zastosować.',
|
||||||
808: 'Ponowne uruchomienie jest wymagane, aby interfejsy usług używały nowej nazwy hosta.',
|
808: 'Nazwa hosta zmieniona, uruchom ponownie, aby zainstalowane usługi używały nowego adresu',
|
||||||
|
809: 'Język zmieniony, uruchom ponownie, aby zainstalowane usługi używały nowego języka',
|
||||||
|
810: 'Tryb kiosku zmieniony, uruchom ponownie, aby zastosować',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -50,45 +50,32 @@ import { CHANGE_PASSWORD } from './change-password'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div tuiCell>
|
</div>
|
||||||
<span tuiTitle>
|
<div tuiCardLarge [style.align-items]="'start'">
|
||||||
<strong>Change password</strong>
|
<button tuiButton size="s" (click)="onChangePassword()">
|
||||||
</span>
|
Change password
|
||||||
<button tuiButton size="s" (click)="onChangePassword()">Change</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
<div tuiCell>
|
tuiButton
|
||||||
<span tuiTitle>
|
size="s"
|
||||||
<strong>Restart</strong>
|
iconStart="@tui.rotate-cw"
|
||||||
<span tuiSubtitle>Restart the VPS</span>
|
[loading]="restarting()"
|
||||||
</span>
|
(click)="onRestart()"
|
||||||
<button
|
>
|
||||||
tuiButton
|
Reboot VPS
|
||||||
size="s"
|
</button>
|
||||||
appearance="secondary"
|
<button tuiButton size="s" iconStart="@tui.log-out" (click)="onLogout()">
|
||||||
iconStart="@tui.rotate-cw"
|
Logout
|
||||||
[loading]="restarting()"
|
</button>
|
||||||
(click)="onRestart()"
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div tuiCell>
|
|
||||||
<span tuiTitle>
|
|
||||||
<strong>Logout</strong>
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
size="s"
|
|
||||||
appearance="secondary-destructive"
|
|
||||||
iconStart="@tui.log-out"
|
|
||||||
(click)="onLogout()"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
[tuiCardLarge] {
|
[tuiCardLarge] {
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
|
|
||||||
@@ -148,9 +135,9 @@ export default class Settings {
|
|||||||
await this.api.restart()
|
await this.api.restart()
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(
|
.open(
|
||||||
'The VPS is restarting. Please wait 1\u20132 minutes, then refresh the page.',
|
'The VPS is rebooting. Please wait 1\u20132 minutes, then refresh the page.',
|
||||||
{
|
{
|
||||||
label: 'Restarting',
|
label: 'Rebooting',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ body {
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background:
|
background:
|
||||||
conic-gradient(var(--tui-background-base)),
|
linear-gradient(var(--tui-background-base, #171717), var(--tui-background-base, #171717)),
|
||||||
radial-gradient(circle at top right, #5240a8, transparent 40%),
|
radial-gradient(circle at top right, #5240a8, transparent 40%),
|
||||||
radial-gradient(circle at bottom right, #9236c9, transparent),
|
radial-gradient(circle at bottom right, #9236c9, transparent),
|
||||||
radial-gradient(circle at 25% 100%, #5b65d5, transparent 30%),
|
radial-gradient(circle at 25% 100%, #5b65d5, transparent 30%),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { RouterOutlet } from '@angular/router'
|
import { RouterOutlet } from '@angular/router'
|
||||||
import { ErrorService } from '@start9labs/shared'
|
import { ErrorService, i18nPipe } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
@@ -39,10 +39,7 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
@if (update(); as update) {
|
@if (update(); as update) {
|
||||||
<tui-action-bar *tuiPopup="bar()">
|
<tui-action-bar *tuiPopup="bar()">
|
||||||
<span tuiCell="m">
|
<span tuiCell="m">
|
||||||
@if (update === true) {
|
@if (
|
||||||
<tui-icon icon="@tui.check" class="g-positive" />
|
|
||||||
Download complete, restart to apply changes
|
|
||||||
} @else if (
|
|
||||||
update.overall && update.overall !== true && update.overall.total
|
update.overall && update.overall !== true && update.overall.total
|
||||||
) {
|
) {
|
||||||
<tui-progress-circle
|
<tui-progress-circle
|
||||||
@@ -58,9 +55,36 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
Calculating download size
|
Calculating download size
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@if (update === true) {
|
</tui-action-bar>
|
||||||
<button tuiButton size="s" (click)="restart()">Restart</button>
|
}
|
||||||
}
|
@if (restartReason(); as reason) {
|
||||||
|
<tui-action-bar *tuiPopup="bar()">
|
||||||
|
<span tuiCell="m">
|
||||||
|
<tui-icon icon="@tui.refresh-cw" />
|
||||||
|
@switch (reason) {
|
||||||
|
@case ('update') {
|
||||||
|
{{ 'Download complete. Restart to apply.' | i18n }}
|
||||||
|
}
|
||||||
|
@case ('mdns') {
|
||||||
|
{{
|
||||||
|
'Hostname changed, restart for installed services to use the new address'
|
||||||
|
| i18n
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
@case ('language') {
|
||||||
|
{{
|
||||||
|
'Language changed, restart for installed services to use the new language'
|
||||||
|
| i18n
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
@case ('kiosk') {
|
||||||
|
{{ 'Kiosk mode changed, restart to apply' | i18n }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<button tuiButton size="s" appearance="primary" (click)="restart()">
|
||||||
|
{{ 'Restart' | i18n }}
|
||||||
|
</button>
|
||||||
</tui-action-bar>
|
</tui-action-bar>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -114,6 +138,7 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
TuiPopup,
|
TuiPopup,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
|
i18nPipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PortalComponent {
|
export class PortalComponent {
|
||||||
@@ -124,6 +149,9 @@ export class PortalComponent {
|
|||||||
|
|
||||||
readonly name = toSignal(this.patch.watch$('serverInfo', 'name'))
|
readonly name = toSignal(this.patch.watch$('serverInfo', 'name'))
|
||||||
readonly update = toSignal(inject(OSService).updating$)
|
readonly update = toSignal(inject(OSService).updating$)
|
||||||
|
readonly restartReason = toSignal(
|
||||||
|
this.patch.watch$('serverInfo', 'statusInfo', 'restart'),
|
||||||
|
)
|
||||||
readonly bar = signal(true)
|
readonly bar = signal(true)
|
||||||
|
|
||||||
getProgress(size: number, downloaded: number): number {
|
getProgress(size: number, downloaded: number): number {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
|||||||
|
|
||||||
import { MarketplaceAlertsService } from '../services/alerts.service'
|
import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||||
|
|
||||||
type KEYS = 'id' | 'version' | 'alerts' | 'flavor'
|
type KEYS = 'id' | 'version' | 'alerts' | 'flavor' | 'satisfies'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-controls',
|
selector: 'marketplace-controls',
|
||||||
@@ -185,9 +185,13 @@ export class MarketplaceControlsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async dryInstall(url: string | null) {
|
private async dryInstall(url: string | null) {
|
||||||
const { id, version } = this.pkg()
|
const { id, version, satisfies } = this.pkg()
|
||||||
const packages = await getAllPackages(this.patch)
|
const packages = await getAllPackages(this.patch)
|
||||||
const breakages = dryUpdate({ id, version }, packages, this.exver)
|
const breakages = dryUpdate(
|
||||||
|
{ id, version, satisfies: satisfies || [] },
|
||||||
|
packages,
|
||||||
|
this.exver,
|
||||||
|
)
|
||||||
|
|
||||||
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
|
if (!breakages.length || (await this.alerts.alertBreakages(breakages))) {
|
||||||
this.installOrUpload(url)
|
this.installOrUpload(url)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
TuiNotification,
|
TuiNotification,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||||
import * as json from 'fast-json-patch'
|
|
||||||
import { compare } from 'fast-json-patch'
|
import { compare } from 'fast-json-patch'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
||||||
@@ -191,9 +190,7 @@ export class ActionInputModal {
|
|||||||
task.actionId === this.actionId &&
|
task.actionId === this.actionId &&
|
||||||
task.when?.condition === 'input-not-matches' &&
|
task.when?.condition === 'input-not-matches' &&
|
||||||
task.input &&
|
task.input &&
|
||||||
json
|
conflicts(task.input.value, input),
|
||||||
.compare(input, task.input.value)
|
|
||||||
.some(op => op.op === 'add' || op.op === 'replace'),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map(id => id)
|
.map(id => id)
|
||||||
@@ -214,3 +211,26 @@ export class ActionInputModal {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors the Rust backend's `conflicts()` function in core/src/service/action.rs.
|
||||||
|
// A key in the partial that is missing from the full input is NOT a conflict.
|
||||||
|
function conflicts(left: unknown, right: unknown): boolean {
|
||||||
|
if (
|
||||||
|
typeof left === 'object' &&
|
||||||
|
left !== null &&
|
||||||
|
!Array.isArray(left) &&
|
||||||
|
typeof right === 'object' &&
|
||||||
|
right !== null &&
|
||||||
|
!Array.isArray(right)
|
||||||
|
) {
|
||||||
|
const l = left as Record<string, unknown>
|
||||||
|
const r = right as Record<string, unknown>
|
||||||
|
return Object.keys(l).some(k => (k in r ? conflicts(l[k], r[k]) : false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(left) && Array.isArray(right)) {
|
||||||
|
return left.some(v => right.every(vr => conflicts(v, vr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return left !== right
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
inject,
|
inject,
|
||||||
INJECTOR,
|
INJECTOR,
|
||||||
OnInit,
|
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -48,6 +47,7 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { OSService } from 'src/app/services/os.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
@@ -96,14 +96,10 @@ import { UPDATE } from './update.component'
|
|||||||
[disabled]="os.updatingOrBackingUp$ | async"
|
[disabled]="os.updatingOrBackingUp$ | async"
|
||||||
(click)="onUpdate()"
|
(click)="onUpdate()"
|
||||||
>
|
>
|
||||||
@if (server.statusInfo.updated) {
|
@if (os.showUpdate$ | async) {
|
||||||
{{ 'Restart to apply' | i18n }}
|
{{ 'Update' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@if (os.showUpdate$ | async) {
|
{{ 'Check for updates' | i18n }}
|
||||||
{{ 'Update' | i18n }}
|
|
||||||
} @else {
|
|
||||||
{{ 'Check for updates' | i18n }}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +274,7 @@ import { UPDATE } from './update.component'
|
|||||||
TuiAnimated,
|
TuiAnimated,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SystemGeneralComponent implements OnInit {
|
export default class SystemGeneralComponent {
|
||||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||||
private readonly loader = inject(TuiNotificationMiddleService)
|
private readonly loader = inject(TuiNotificationMiddleService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -288,20 +284,7 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
private readonly injector = inject(INJECTOR)
|
private readonly injector = inject(INJECTOR)
|
||||||
private readonly win = inject(WA_WINDOW)
|
private readonly win = inject(WA_WINDOW)
|
||||||
private readonly route = inject(ActivatedRoute)
|
private readonly config = inject(ConfigService)
|
||||||
private readonly router = inject(Router)
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.route.queryParams
|
|
||||||
.pipe(filter(params => params['restart'] === 'hostname'))
|
|
||||||
.subscribe(async () => {
|
|
||||||
await this.router.navigate([], {
|
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: {},
|
|
||||||
})
|
|
||||||
this.promptHostnameRestart()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
@@ -321,7 +304,6 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
|
|
||||||
onLanguageChange(language: Language) {
|
onLanguageChange(language: Language) {
|
||||||
this.i18nService.setLang(language.name)
|
this.i18nService.setLang(language.name)
|
||||||
this.promptLanguageRestart()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose shared utilities for template use
|
// Expose shared utilities for template use
|
||||||
@@ -371,9 +353,7 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
if (this.server()?.statusInfo.updated) {
|
if (this.os.updateAvailable$.value) {
|
||||||
this.restart()
|
|
||||||
} else if (this.os.updateAvailable$.value) {
|
|
||||||
this.update()
|
this.update()
|
||||||
} else {
|
} else {
|
||||||
this.check()
|
this.check()
|
||||||
@@ -400,7 +380,7 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subscribe(result => {
|
.subscribe(result => {
|
||||||
if (this.win.location.hostname.endsWith('.local')) {
|
if (this.config.accessType === 'mdns') {
|
||||||
this.confirmNameChange(result)
|
this.confirmNameChange(result)
|
||||||
} else {
|
} else {
|
||||||
this.saveName(result)
|
this.saveName(result)
|
||||||
@@ -433,24 +413,18 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
await this.api.setHostname({ name, hostname })
|
await this.api.setHostname({ name, hostname })
|
||||||
|
|
||||||
if (wasLocal) {
|
if (wasLocal) {
|
||||||
const { protocol, port } = this.win.location
|
|
||||||
const portSuffix = port ? ':' + port : ''
|
|
||||||
const newUrl = `${protocol}//${hostname}.local${portSuffix}/system/general?restart=hostname`
|
|
||||||
|
|
||||||
this.dialog
|
this.dialog
|
||||||
.openConfirm({
|
.openConfirm({
|
||||||
label: 'Hostname Changed',
|
label: 'Hostname Changed',
|
||||||
data: {
|
data: {
|
||||||
content:
|
content:
|
||||||
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local. ${this.i18n.transform('After opening the new address, you will be prompted to restart.')}` as i18nKey,
|
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local` as i18nKey,
|
||||||
yes: 'Open new address',
|
yes: 'Open new address',
|
||||||
no: 'Dismiss',
|
no: 'Dismiss',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.pipe(filter(Boolean))
|
.pipe(filter(Boolean))
|
||||||
.subscribe(() => this.win.open(newUrl, '_blank'))
|
.subscribe(() => this.win.open(`https://${hostname}.local`, '_blank'))
|
||||||
} else {
|
|
||||||
this.promptHostnameRestart()
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
@@ -526,7 +500,6 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.toggleKiosk(true)
|
await this.api.toggleKiosk(true)
|
||||||
this.promptRestart()
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -546,7 +519,6 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
options: [],
|
options: [],
|
||||||
})
|
})
|
||||||
await this.api.toggleKiosk(true)
|
await this.api.toggleKiosk(true)
|
||||||
this.promptRestart()
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -559,7 +531,6 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.toggleKiosk(false)
|
await this.api.toggleKiosk(false)
|
||||||
this.promptRestart()
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -567,50 +538,6 @@ export default class SystemGeneralComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private promptRestart() {
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({
|
|
||||||
label: 'Restart to apply',
|
|
||||||
data: {
|
|
||||||
content: 'This change will take effect after the next boot',
|
|
||||||
yes: 'Restart now',
|
|
||||||
no: 'Later',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.restart())
|
|
||||||
}
|
|
||||||
|
|
||||||
private promptHostnameRestart() {
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({
|
|
||||||
label: 'Restart to apply',
|
|
||||||
data: {
|
|
||||||
content:
|
|
||||||
'A restart is required for service interfaces to use the new hostname.',
|
|
||||||
yes: 'Restart now',
|
|
||||||
no: 'Later',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.restart())
|
|
||||||
}
|
|
||||||
|
|
||||||
private promptLanguageRestart() {
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({
|
|
||||||
label: 'Restart to apply',
|
|
||||||
data: {
|
|
||||||
content:
|
|
||||||
'OS-level translations are already in effect. A restart is required for service-level translations to take effect.',
|
|
||||||
yes: 'Restart now',
|
|
||||||
no: 'Later',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.restart())
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(UPDATE, {
|
.open(UPDATE, {
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export namespace Mock {
|
|||||||
export const ServerUpdated: T.ServerStatus = {
|
export const ServerUpdated: T.ServerStatus = {
|
||||||
backupProgress: null,
|
backupProgress: null,
|
||||||
updateProgress: null,
|
updateProgress: null,
|
||||||
updated: true,
|
|
||||||
restarting: false,
|
restarting: false,
|
||||||
shuttingDown: false,
|
shuttingDown: false,
|
||||||
|
restart: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegistryOSUpdate: T.OsVersionInfoMap = {
|
export const RegistryOSUpdate: T.OsVersionInfoMap = {
|
||||||
@@ -459,6 +459,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -501,6 +502,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -553,6 +555,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -595,6 +598,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -649,6 +653,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -704,6 +709,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -763,6 +769,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -805,6 +812,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: BTC_ICON,
|
icon: BTC_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {},
|
dependencyMetadata: {},
|
||||||
donationUrl: null,
|
donationUrl: null,
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -857,6 +865,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: LND_ICON,
|
icon: LND_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
'btc-rpc-proxy': ProxyDep,
|
'btc-rpc-proxy': ProxyDep,
|
||||||
@@ -912,6 +921,7 @@ export namespace Mock {
|
|||||||
gitHash: 'fakehash',
|
gitHash: 'fakehash',
|
||||||
icon: PROXY_ICON,
|
icon: PROXY_ICON,
|
||||||
sourceVersion: null,
|
sourceVersion: null,
|
||||||
|
satisfies: [],
|
||||||
dependencyMetadata: {
|
dependencyMetadata: {
|
||||||
bitcoind: BitcoinDep,
|
bitcoind: BitcoinDep,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -435,14 +435,20 @@ export class MockApiService extends ApiService {
|
|||||||
async toggleKiosk(enable: boolean): Promise<null> {
|
async toggleKiosk(enable: boolean): Promise<null> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch = [
|
this.mockRevision([
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/kiosk',
|
path: '/serverInfo/kiosk',
|
||||||
value: enable,
|
value: enable,
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
this.mockRevision(patch)
|
this.mockRevision([
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/statusInfo/restart',
|
||||||
|
value: 'kiosk',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -450,7 +456,7 @@ export class MockApiService extends ApiService {
|
|||||||
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
const patch = [
|
this.mockRevision([
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/name',
|
path: '/serverInfo/name',
|
||||||
@@ -461,8 +467,14 @@ export class MockApiService extends ApiService {
|
|||||||
path: '/serverInfo/hostname',
|
path: '/serverInfo/hostname',
|
||||||
value: params.hostname,
|
value: params.hostname,
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
this.mockRevision(patch)
|
this.mockRevision([
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/statusInfo/restart',
|
||||||
|
value: 'mdns',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -485,14 +497,20 @@ export class MockApiService extends ApiService {
|
|||||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
const patch = [
|
this.mockRevision([
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/language',
|
path: '/serverInfo/language',
|
||||||
value: params.language,
|
value: params.language,
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
this.mockRevision(patch)
|
this.mockRevision([
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/statusInfo/restart',
|
||||||
|
value: 'language',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1831,11 +1849,11 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch3: Operation<boolean>[] = [
|
const patch3: Operation<string>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/statusInfo/updated',
|
path: '/serverInfo/statusInfo/restart',
|
||||||
value: true,
|
value: 'update',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
op: PatchOp.REMOVE,
|
||||||
|
|||||||
@@ -227,11 +227,11 @@ export const mockPatchData: DataModel = {
|
|||||||
postInitMigrationTodos: {},
|
postInitMigrationTodos: {},
|
||||||
statusInfo: {
|
statusInfo: {
|
||||||
// currentBackup: null,
|
// currentBackup: null,
|
||||||
updated: false,
|
|
||||||
updateProgress: null,
|
updateProgress: null,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
shuttingDown: false,
|
shuttingDown: false,
|
||||||
backupProgress: null,
|
backupProgress: null,
|
||||||
|
restart: null,
|
||||||
},
|
},
|
||||||
name: 'Random Words',
|
name: 'Random Words',
|
||||||
hostname: 'random-words',
|
hostname: 'random-words',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class OSService {
|
|||||||
.pipe(shareReplay({ bufferSize: 1, refCount: true }))
|
.pipe(shareReplay({ bufferSize: 1, refCount: true }))
|
||||||
|
|
||||||
readonly updating$ = this.statusInfo$.pipe(
|
readonly updating$ = this.statusInfo$.pipe(
|
||||||
map(status => status.updateProgress ?? status.updated),
|
map(status => status.updateProgress ?? false),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { DataModel } from '../services/patch-db/data-model'
|
|||||||
import { getManifest } from './get-package-data'
|
import { getManifest } from './get-package-data'
|
||||||
|
|
||||||
export function dryUpdate(
|
export function dryUpdate(
|
||||||
{ id, version }: { id: string; version: string },
|
{
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
satisfies,
|
||||||
|
}: { id: string; version: string; satisfies: string[] },
|
||||||
pkgs: DataModel['packageData'],
|
pkgs: DataModel['packageData'],
|
||||||
exver: Exver,
|
exver: Exver,
|
||||||
): string[] {
|
): string[] {
|
||||||
@@ -13,10 +17,24 @@ export function dryUpdate(
|
|||||||
Object.keys(pkg.currentDependencies || {}).some(
|
Object.keys(pkg.currentDependencies || {}).some(
|
||||||
pkgId => pkgId === id,
|
pkgId => pkgId === id,
|
||||||
) &&
|
) &&
|
||||||
!exver.satisfies(
|
!versionSatisfies(
|
||||||
version,
|
version,
|
||||||
|
satisfies,
|
||||||
pkg.currentDependencies[id]?.versionRange || '',
|
pkg.currentDependencies[id]?.versionRange || '',
|
||||||
|
exver,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map(pkg => getManifest(pkg).title)
|
.map(pkg => getManifest(pkg).title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function versionSatisfies(
|
||||||
|
version: string,
|
||||||
|
satisfies: string[],
|
||||||
|
range: string,
|
||||||
|
exver: Exver,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
exver.satisfies(version, range) ||
|
||||||
|
satisfies.some(v => exver.satisfies(v, range))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user