Fix/fe bugs 3 (#2943)

* fix typeo in patch db seed

* show all registries in updates tab, fix required dependnecy display in marketplace, update browser tab title desc

* always show pointer for version select

* chore: fix comments

* support html in action desc and marketplace long desc, only show qr in action res if qr is true

* disable save if smtp creds not edited, show better smtp success message

* dont dismiss login spinner until patchDB returns

* feat: redesign of service dashboard and interface (#2946)

* feat: redesign of service dashboard and interface

* chore: comments

* re-add setup complete

* dibale launch UI when not running, re-style things, rename things

* back to 1000

* fix clearnet docs link and require password retype in setup wiz

* faster hint display

* display dependency ID if title not available

* fix migration

* better init progress view

* fix setup success page by providing VERSION and notifications page fixes

* force uninstall from service error page, soft or hard

* handle error state better

* chore: fixed for install and setup wizards

* chore: fix issues (#2949)

* enable and disable kiosk mode

* minor fixes

* fix dependency mounts

* dismissable tasks

* provide replayId

* default if health check success message is null

* look for wifi interface too

* dash for null user agent in sessions

* add disk repair to diagnostic api

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-05-21 19:04:26 -06:00
committed by GitHub
parent 44560c8da8
commit b40849f672
123 changed files with 1662 additions and 964 deletions

View File

@@ -348,6 +348,7 @@ pub struct ClearTaskParams {
pub package_id: PackageId,
pub replay_id: ReplayId,
#[arg(long)]
#[serde(default)]
pub force: bool,
}

View File

@@ -7,6 +7,7 @@ use rpc_toolkit::{
};
use crate::context::{CliContext, DiagnosticContext, RpcContext};
use crate::disk::repair;
use crate::init::SYSTEM_REBUILD_PATH;
use crate::prelude::*;
use crate::shutdown::Shutdown;
@@ -95,6 +96,15 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Remove disk from filesystem"),
)
.subcommand("repair", from_fn_async(|_: C| repair()).no_cli())
.subcommand(
"repair",
CallRemoteHandler::<CliContext, _, _>::new(
from_fn_async(|_: RpcContext| repair())
.no_display()
.with_about("Repair disk in the event of corruption"),
),
)
}
pub async fn forget_disk<C: Context>(_: C) -> Result<(), Error> {

View File

@@ -3,28 +3,73 @@ use std::path::Path;
use digest::generic_array::GenericArray;
use digest::{Digest, OutputSizeUser};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use ts_rs::TS;
use super::FileSystem;
use crate::prelude::*;
use crate::util::io::create_file;
pub struct Bind<SrcDir: AsRef<Path>> {
src_dir: SrcDir,
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "kebab-case")]
pub enum FileType {
File,
Directory,
Infer,
}
impl<SrcDir: AsRef<Path>> Bind<SrcDir> {
pub fn new(src_dir: SrcDir) -> Self {
Self { src_dir }
pub struct Bind<Src: AsRef<Path>> {
src: Src,
filetype: FileType,
}
impl<Src: AsRef<Path>> Bind<Src> {
pub fn new(src: Src) -> Self {
Self {
src,
filetype: FileType::Directory,
}
}
pub fn with_type(mut self, filetype: FileType) -> Self {
self.filetype = filetype;
self
}
}
impl<SrcDir: AsRef<Path> + Send + Sync> FileSystem for Bind<SrcDir> {
impl<Src: AsRef<Path> + Send + Sync> FileSystem for Bind<Src> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.src_dir))
Ok(Some(&self.src))
}
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> {
["--bind"]
}
async fn pre_mount(&self) -> Result<(), Error> {
tokio::fs::create_dir_all(self.src_dir.as_ref()).await?;
async fn pre_mount(&self, mountpoint: &Path) -> Result<(), Error> {
let from_meta = tokio::fs::metadata(&self.src).await.ok();
let to_meta = tokio::fs::metadata(&mountpoint).await.ok();
if matches!(self.filetype, FileType::File)
|| (matches!(self.filetype, FileType::Infer)
&& from_meta.as_ref().map_or(false, |m| m.is_file()))
{
if to_meta.as_ref().map_or(false, |m| m.is_dir()) {
tokio::fs::remove_dir(mountpoint).await?;
}
if from_meta.is_none() {
create_file(self.src.as_ref()).await?.sync_all().await?;
}
if to_meta.is_none() {
create_file(mountpoint).await?.sync_all().await?;
}
} else {
if to_meta.as_ref().map_or(false, |m| m.is_file()) {
tokio::fs::remove_file(mountpoint).await?;
}
if from_meta.is_none() {
tokio::fs::create_dir_all(self.src.as_ref()).await?;
}
if to_meta.is_none() {
tokio::fs::create_dir_all(mountpoint).await?;
}
}
Ok(())
}
async fn source_hash(
@@ -33,12 +78,12 @@ impl<SrcDir: AsRef<Path> + Send + Sync> FileSystem for Bind<SrcDir> {
let mut sha = Sha256::new();
sha.update("Bind");
sha.update(
tokio::fs::canonicalize(self.src_dir.as_ref())
tokio::fs::canonicalize(self.src.as_ref())
.await
.with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
self.src_dir.as_ref().display().to_string(),
self.src.as_ref().display().to_string(),
)
})?
.as_os_str()

View File

@@ -49,8 +49,7 @@ impl<EncryptedDir: AsRef<Path> + Send + Sync, Key: AsRef<str> + Send + Sync> Fil
mountpoint: P,
mount_type: super::MountType,
) -> Result<(), Error> {
self.pre_mount().await?;
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
self.pre_mount(mountpoint.as_ref()).await?;
Command::new("mount")
.args(
default_mount_command(self, mountpoint, mount_type)

View File

@@ -53,16 +53,15 @@ impl<Fs: FileSystem> FileSystem for IdMapped<Fs> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
self.filesystem.source().await
}
async fn pre_mount(&self) -> Result<(), Error> {
self.filesystem.pre_mount().await
async fn pre_mount(&self, mountpoint: &Path) -> Result<(), Error> {
self.filesystem.pre_mount(mountpoint).await
}
async fn mount<P: AsRef<Path> + Send>(
&self,
mountpoint: P,
mount_type: MountType,
) -> Result<(), Error> {
self.pre_mount().await?;
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
self.pre_mount(mountpoint.as_ref()).await?;
Command::new("mount.next")
.args(
default_mount_command(self, mountpoint, mount_type)

View File

@@ -69,8 +69,7 @@ pub(self) async fn default_mount_impl(
mountpoint: impl AsRef<Path> + Send,
mount_type: MountType,
) -> Result<(), Error> {
fs.pre_mount().await?;
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
fs.pre_mount(mountpoint.as_ref()).await?;
Command::from(default_mount_command(fs, mountpoint, mount_type).await?)
.capture(false)
.invoke(ErrorKind::Filesystem)
@@ -92,8 +91,11 @@ pub trait FileSystem: Send + Sync {
fn source(&self) -> impl Future<Output = Result<Option<impl AsRef<Path>>, Error>> + Send {
async { Ok(None::<&Path>) }
}
fn pre_mount(&self) -> impl Future<Output = Result<(), Error>> + Send {
async { Ok(()) }
fn pre_mount(&self, mountpoint: &Path) -> impl Future<Output = Result<(), Error>> + Send {
async move {
tokio::fs::create_dir_all(mountpoint).await?;
Ok(())
}
}
fn mount<P: AsRef<Path> + Send>(
&self,

View File

@@ -41,9 +41,10 @@ impl<
Box::new(lazy_format!("workdir={}", self.work.as_ref().display())),
]
}
async fn pre_mount(&self) -> Result<(), Error> {
async fn pre_mount(&self, mountpoint: &Path) -> Result<(), Error> {
tokio::fs::create_dir_all(self.upper.as_ref()).await?;
tokio::fs::create_dir_all(self.work.as_ref()).await?;
tokio::fs::create_dir_all(mountpoint).await?;
Ok(())
}
async fn source_hash(

View File

@@ -10,10 +10,10 @@ use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, V
use tokio::process::Command;
use crate::db::model::package::{
TaskEntry, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind,
ManifestPreference,
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
TaskEntry,
};
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::bind::{Bind, FileType};
use crate::disk::mount::filesystem::idmapped::IdMapped;
use crate::disk::mount::filesystem::{FileSystem, MountType};
use crate::disk::mount::util::{is_mountpoint, unmount};
@@ -23,14 +23,6 @@ use crate::util::Invoke;
use crate::volume::data_dir;
use crate::DATA_DIR;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum FileType {
File,
Directory,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@@ -39,8 +31,7 @@ pub struct MountTarget {
volume_id: VolumeId,
subpath: Option<PathBuf>,
readonly: bool,
#[ts(optional)]
filetype: Option<FileType>,
filetype: FileType,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
@@ -67,7 +58,6 @@ pub async fn mount(
let subpath = subpath.unwrap_or_default();
let subpath = subpath.strip_prefix("/").unwrap_or(&subpath);
let source = data_dir(DATA_DIR, &package_id, &volume_id).join(subpath);
let from_meta = tokio::fs::metadata(&source).await.ok();
let location = location.strip_prefix("/").unwrap_or(&location);
let mountpoint = context
.seed
@@ -77,39 +67,7 @@ pub async fn mount(
.or_not_found("lxc container")?
.rootfs_dir()
.join(location);
let to_meta = tokio::fs::metadata(&mountpoint).await.ok();
if matches!(filetype, Some(FileType::File))
|| (filetype.is_none() && from_meta.as_ref().map_or(false, |m| m.is_file()))
{
if to_meta.as_ref().map_or(false, |m| m.is_dir()) {
tokio::fs::remove_dir(&mountpoint).await?;
}
if from_meta.is_none() {
if let Some(parent) = source.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&source, "").await?;
}
if to_meta.is_none() {
if let Some(parent) = mountpoint.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&mountpoint, "").await?;
}
} else {
if to_meta.as_ref().map_or(false, |m| m.is_file()) {
tokio::fs::remove_file(&mountpoint).await?;
}
if from_meta.is_none() {
tokio::fs::create_dir_all(&source).await?;
}
if to_meta.is_none() {
tokio::fs::create_dir_all(&mountpoint).await?;
}
}
tokio::fs::create_dir_all(&mountpoint).await?;
if is_mountpoint(&mountpoint).await? {
unmount(&mountpoint, true).await?;
}
@@ -118,7 +76,7 @@ pub async fn mount(
.arg(&mountpoint)
.invoke(crate::ErrorKind::Filesystem)
.await?;
IdMapped::new(Bind::new(source), 0, 100000, 65536)
IdMapped::new(Bind::new(source).with_type(filetype), 0, 100000, 65536)
.mount(
mountpoint,
if readonly {

View File

@@ -211,27 +211,12 @@ impl VersionT for Version {
}
fn up(self, db: &mut Value, (account, ssh_keys, cifs): Self::PreUpRes) -> Result<(), Error> {
let wifi = json!({
"infterface": db["server-info"]["wifi"]["interface"],
"interface": db["server-info"]["wifi"]["interface"],
"ssids": db["server-info"]["wifi"]["ssids"],
"selected": db["server-info"]["wifi"]["selected"],
"last_region": db["server-info"]["wifi"]["last-region"],
"lastRegion": db["server-info"]["wifi"]["last-region"],
});
let ip_info = {
let mut ip_info = json!({});
let empty = Default::default();
for (k, v) in db["server-info"]["ip-info"].as_object().unwrap_or(&empty) {
let k: &str = k.as_ref();
ip_info[k] = json!({
"ipv4Range": v["ipv4-range"],
"ipv6Range": v["ipv6-range"],
"ipv4": v["ipv4"],
"ipv6": v["ipv6"],
});
}
ip_info
};
let status_info = json!({
"backupProgress": db["server-info"]["status-info"]["backup-progress"],
"updated": db["server-info"]["status-info"]["updated"],
@@ -259,7 +244,7 @@ impl VersionT for Version {
.replace("https://", "")
.replace("http://", "")
.replace(".onion/", ""));
server_info["ipInfo"] = ip_info;
server_info["networkInterfaces"] = json!({});
server_info["statusInfo"] = status_info;
server_info["wifi"] = wifi;
server_info["unreadNotificationCount"] =