Compare commits

...

10 Commits

Author SHA1 Message Date
Aiden McClelland
d966e35054 fix migration 2023-03-17 18:58:49 -06:00
Aiden McClelland
1675570291 fix test 2023-03-17 14:42:32 -06:00
Aiden McClelland
9b88de656e version bump (#2232)
* version bump

* welcome notes

* 0341 release notes

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-03-17 12:55:21 -06:00
Aiden McClelland
3d39b5653d don't blow up if s9pk fails to load (#2231) 2023-03-17 12:09:24 -06:00
J H
eb5f7f64ad feat: Default to no owner for rsync (#2230) 2023-03-17 12:09:13 -06:00
Aiden McClelland
9fc0164c4d better logging of health (#2228) 2023-03-17 12:09:01 -06:00
Aiden McClelland
65eb520cca disable apt and add script for persisting apt pkgs (#2225)
* disable apt and add script for persisting apt pkgs

* fix typo

* exit 1 on fake-apt

* readd fake-apt after upgrade

* fix typo

* remove finicky protection

* fix build
2023-03-17 12:08:49 -06:00
Aiden McClelland
f7f07932b4 update registry rsync script (#2227) 2023-03-17 10:05:58 -06:00
Aiden McClelland
de52494039 fix loading authcookie into cookie store on ssh (#2226) 2023-03-17 10:05:12 -06:00
Matt Hill
4d87ee2bb6 update display obj on union change (#2224)
* update display obj on union change

* deelete unnecessary changes

* more efficient

* fix: properly change height of form object

* more config examples

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2023-03-17 11:57:26 -04:00
28 changed files with 383 additions and 135 deletions

2
backend/Cargo.lock generated
View File

@@ -1354,7 +1354,7 @@ dependencies = [
[[package]]
name = "embassy-os"
version = "0.3.4"
version = "0.3.4-rev.1"
dependencies = [
"aes",
"async-compression",

View File

@@ -14,7 +14,7 @@ keywords = [
name = "embassy-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/embassy-os"
version = "0.3.4"
version = "0.3.4-rev.1"
[lib]
name = "embassy"

View File

@@ -54,7 +54,8 @@ impl Drop for CliContextSeed {
true,
)
.unwrap();
let store = self.cookie_store.lock().unwrap();
let mut store = self.cookie_store.lock().unwrap();
store.remove("localhost", "", "local");
store.save_json(&mut *writer).unwrap();
writer.sync_all().unwrap();
std::fs::rename(tmp, &self.cookie_path).unwrap();
@@ -101,19 +102,22 @@ impl CliContext {
.unwrap_or(Path::new("/"))
.join(".cookies.json")
});
let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() {
let mut store = CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
.map_err(|e| eyre!("{}", e))
.with_kind(crate::ErrorKind::Deserialization)?;
let cookie_store = Arc::new(CookieStoreMutex::new({
let mut store = if cookie_path.exists() {
CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
.map_err(|e| eyre!("{}", e))
.with_kind(crate::ErrorKind::Deserialization)?
} else {
CookieStore::default()
};
if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) {
store
.insert_raw(&Cookie::new("local", local), &"http://localhost".parse()?)
.with_kind(crate::ErrorKind::Network)?;
}
store
} else {
CookieStore::default()
}));
Ok(CliContext(Arc::new(CliContextSeed {
base_url: url.clone(),
rpc_url: {

View File

@@ -1435,16 +1435,22 @@ pub fn load_images<'a, P: AsRef<Path> + 'a + Send + Sync>(
copy_and_shutdown(&mut File::open(&path).await?, load_in)
.await?
}
Some("s9pk") => {
copy_and_shutdown(
&mut S9pkReader::open(&path, false)
.await?
.docker_images()
.await?,
load_in,
)
.await?
Some("s9pk") => match async {
let mut reader = S9pkReader::open(&path, true).await?;
copy_and_shutdown(&mut reader.docker_images().await?, load_in)
.await?;
Ok::<_, Error>(())
}
.await
{
Ok(()) => (),
Err(e) => {
tracing::error!(
"Error loading docker images from s9pk: {e}"
);
tracing::debug!("{e:?}");
}
},
_ => unreachable!(),
};

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use itertools::Itertools;
use patch_db::{DbHandle, LockReceipt, LockType};
use tracing::instrument;
@@ -111,6 +112,7 @@ pub async fn check<Db: DbHandle>(
};
let health_results = if let Some(started) = started {
tracing::debug!("Checking health of {}", id);
manifest
.health_checks
.check_all(
@@ -129,6 +131,24 @@ pub async fn check<Db: DbHandle>(
if !should_commit.load(Ordering::SeqCst) {
return Ok(());
}
if !health_results
.iter()
.any(|(_, res)| matches!(res, HealthCheckResult::Failure { .. }))
{
tracing::debug!("All health checks succeeded for {}", id);
} else {
tracing::debug!(
"Some health checks failed for {}: {}",
id,
health_results
.iter()
.filter(|(_, res)| matches!(res, HealthCheckResult::Failure { .. }))
.map(|(id, _)| &*id)
.join(", ")
);
}
let current_dependents = {
let mut checkpoint = tx.begin().await?;
let receipts = HealthCheckStatusReceipt::new(&mut checkpoint, id).await?;
@@ -153,9 +173,7 @@ pub async fn check<Db: DbHandle>(
current_dependents
};
tracing::debug!("Checking health of {}", id);
let receipts = crate::dependencies::BreakTransitiveReceipts::new(&mut tx).await?;
tracing::debug!("Got receipts {}", id);
for (dependent, info) in (current_dependents).0.iter() {
let failures: BTreeMap<HealthCheckId, HealthCheckResult> = health_results

View File

@@ -226,7 +226,6 @@ impl DockerProcedure {
let name = name.docker_name();
let name: Option<&str> = name.as_ref().map(|x| &**x);
let mut cmd = tokio::process::Command::new("docker");
tracing::debug!("{:?} is run", name);
let container_name = Self::container_name(pkg_id, name);
cmd.arg("run")
.arg("--rm")
@@ -408,7 +407,6 @@ impl DockerProcedure {
let name: Option<&str> = name.as_deref();
let mut cmd = tokio::process::Command::new("docker");
tracing::debug!("{:?} is exec", name);
cmd.arg("exec");
cmd.args(self.docker_args_inject(pkg_id).await?);

View File

@@ -429,6 +429,7 @@ async fn migrate(
ignore_existing: false,
exclude: Vec::new(),
no_permissions: false,
no_owner: false,
},
)
.await?;
@@ -441,6 +442,7 @@ async fn migrate(
ignore_existing: false,
exclude: vec!["tmp".to_owned()],
no_permissions: false,
no_owner: false,
},
)
.await?;

View File

@@ -306,6 +306,7 @@ async fn sync_boot() -> Result<(), Error> {
ignore_existing: false,
exclude: Vec::new(),
no_permissions: false,
no_owner: false,
},
)
.await?

View File

@@ -20,8 +20,9 @@ mod v0_3_2;
mod v0_3_2_1;
mod v0_3_3;
mod v0_3_4;
mod v0_3_4_1;
pub type Current = v0_3_4::Version;
pub type Current = v0_3_4_1::Version;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(untagged)]
@@ -37,6 +38,7 @@ enum Version {
V0_3_2_1(Wrapper<v0_3_2_1::Version>),
V0_3_3(Wrapper<v0_3_3::Version>),
V0_3_4(Wrapper<v0_3_4::Version>),
V0_3_4_1(Wrapper<v0_3_4_1::Version>),
Other(emver::Version),
}
@@ -63,6 +65,7 @@ impl Version {
Version::V0_3_2_1(Wrapper(x)) => x.semver(),
Version::V0_3_3(Wrapper(x)) => x.semver(),
Version::V0_3_4(Wrapper(x)) => x.semver(),
Version::V0_3_4_1(Wrapper(x)) => x.semver(),
Version::Other(x) => x.clone(),
}
}
@@ -244,6 +247,10 @@ pub async fn init<Db: DbHandle>(
v.0.migrate_to(&Current::new(), db, secrets, receipts)
.await?
}
Version::V0_3_4_1(v) => {
v.0.migrate_to(&Current::new(), db, secrets, receipts)
.await?
}
Version::Other(_) => {
return Err(Error::new(
eyre!("Cannot downgrade"),
@@ -287,6 +294,7 @@ mod tests {
Just(Version::V0_3_2_1(Wrapper(v0_3_2_1::Version::new()))),
Just(Version::V0_3_3(Wrapper(v0_3_3::Version::new()))),
Just(Version::V0_3_4(Wrapper(v0_3_4::Version::new()))),
Just(Version::V0_3_4_1(Wrapper(v0_3_4_1::Version::new()))),
em_version().prop_map(Version::Other),
]
}

View File

@@ -0,0 +1,30 @@
use async_trait::async_trait;
use emver::VersionRange;
use super::v0_3_0::V0_3_0_COMPAT;
use super::*;
const V0_3_4_1: emver::Version = emver::Version::new(0, 3, 4, 1);
#[derive(Clone, Debug)]
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_3_4::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> emver::Version {
V0_3_4_1
}
fn compat(&self) -> &'static VersionRange {
&*V0_3_0_COMPAT
}
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
Ok(())
}
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -32,3 +32,4 @@ rsync
systemd-timesyncd
magic-wormhole
nyx
bash-completion

View File

@@ -13,17 +13,28 @@ mkdir -p /media/embassy/next/run
mkdir -p /media/embassy/next/dev
mkdir -p /media/embassy/next/sys
mkdir -p /media/embassy/next/proc
mkdir -p /media/embassy/next/boot
mount --bind /run /media/embassy/next/run
mount --bind /dev /media/embassy/next/dev
mount --bind /sys /media/embassy/next/sys
mount --bind /proc /media/embassy/next/proc
mount --bind /boot /media/embassy/next/boot
chroot /media/embassy/next
rm /media/embassy/next/usr/local/bin/apt
rm /media/embassy/next/usr/local/bin/apt-get
rm /media/embassy/next/usr/local/bin/aptitude
chroot /media/embassy/next $@
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/apt
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/apt-get
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/aptitude
umount /media/embassy/next/run
umount /media/embassy/next/dev
umount /media/embassy/next/sys
umount /media/embassy/next/proc
umount /media/embassy/next/boot
echo 'Upgrading...'

View File

@@ -3,8 +3,8 @@
set -e
# install dependencies
apt update
apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools
/usr/bin/apt update
/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools
# create kiosk script
cat > /home/start9/kiosk.sh << 'EOF'

21
build/lib/scripts/fake-apt Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
>&2 echo 'THIS IS NOT A STANDARD DEBIAN SYSTEM'
>&2 echo 'USING apt COULD CAUSE IRREPARABLE DAMAGE TO YOUR EMBASSY'
>&2 echo 'PLEASE TURN BACK NOW!!!'
if [ "$1" == "upgrade" ] && [ "$(whoami)" == "root" ]; then
>&2 echo 'IF YOU THINK RUNNING "sudo apt upgrade" IS A REASONABLE THING TO DO ON THIS SYSTEM, YOU PROBABLY SHOULDN'"'"'T BE ON THE COMMAND LINE.'
>&2 echo 'YOU ARE BEING REMOVED FROM THIS SESSION FOR YOUR OWN SAFETY.'
pkill -9 -t $(tty | sed 's|^/dev/||g')
fi
>&2 echo
>&2 echo 'If you are SURE you know what you are doing, and are willing to accept the DIRE CONSEQUENCES of doing so, you can run the following command to disable this protection:'
>&2 echo ' sudo rm /usr/local/bin/apt'
>&2 echo
>&2 echo 'Otherwise, what you probably want to do is run:'
>&2 echo ' sudo /usr/lib/embassy/scripts/chroot-and-upgrade'
>&2 echo 'You can run apt in this context to add packages to your system.'
>&2 echo 'When you are done with your changes, type "exit" and the device will reboot into a system with the changes applied.'
>&2 echo 'This is still NOT RECOMMENDED if you don'"'"'t know what you are doing, but at least isn'"'"'t guaranteed to break things.'
exit 1

View File

@@ -0,0 +1,20 @@
#!/bin/bash
if [ -z "$1" ]; then
>&2 echo "usage: $0 <PACKAGE_NAME>"
exit 1
fi
TO_INSTALL=()
while [ -n "$1" ]; do
if ! dpkg -s "$1"; then
TO_INSTALL+=("$1")
fi
shift
done
if [ ${#TO_INSTALL[@]} -ne 0 ]; then
/usr/lib/embassy/scripts/chroot-and-upgrade << EOF
apt-get update && apt-get install -y ${TO_INSTALL[@]}
EOF
fi

View File

@@ -112,3 +112,7 @@ rm -f /etc/motd
ln -sf /usr/lib/embassy/motd /etc/update-motd.d/00-embassy
chmod -x /etc/update-motd.d/*
chmod +x /etc/update-motd.d/00-embassy
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/apt
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/apt-get
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/aptitude

View File

@@ -11,10 +11,9 @@ cat > /etc/rsyncd.conf << RD
uid = root
gid = root
use chroot = yes
max connections = 4
max connections = 50
pid file = /var/run/rsyncd.pid
exclude = lost+found/
transfer logging = yes
timeout = 900
ignore nonreadable = yes
dont compress = *.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2
@@ -35,12 +34,13 @@ do
echo "Making new dir $new_dir"
mkdir -p $new_dir
if ! test -n "$(mount -l | grep $new_dir)"
then
echo "Mounting $filename to $new_dir"
mount $filename $new_dir
if test -n "$(mount -l | grep $new_dir)"; then
umount $new_dir
fi
echo "Mounting $filename to $new_dir"
mount $filename $new_dir
cat >> /etc/rsyncd.conf << INSERTING
[$version]
path = $version_dir

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-os",
"version": "0.3.4",
"version": "0.3.4.1",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": null,
"ack-welcome": "0.3.4",
"ack-welcome": "0.3.4.1",
"marketplace": {
"selected-url": "https://registry.start9.com/",
"known-hosts": {

View File

@@ -22,8 +22,7 @@
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
>
</ion-textarea>
></ion-textarea>
<ng-template #notTextArea>
<ion-input
type="text"
@@ -38,8 +37,7 @@
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
>
</ion-input>
></ion-input>
</ng-template>
<ion-button
*ngIf="spec.type === 'string' && spec.masked"
@@ -59,8 +57,9 @@
slot="end"
color="light"
style="font-size: medium"
>{{ spec.units }}</ion-note
>
{{ spec.units }}
</ion-note>
</ion-item>
<p class="error-message">
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
@@ -92,11 +91,11 @@
*ngIf="original?.[entry.key] === undefined"
color="success"
>
(New)</ion-text
>
(New)
</ion-text>
<ion-text *ngIf="entry.value.dirty" color="warning">
(Edited)</ion-text
>
(Edited)
</ion-text>
</b>
</ion-label>
<!-- boolean -->
@@ -157,14 +156,9 @@
></ion-icon>
</ion-item-divider>
<!-- body -->
<div
<tui-expand
[expanded]="objectDisplay[entry.key].expanded"
[id]="objectId | toElementId: entry.key"
[ngStyle]="{
'max-height': objectDisplay[entry.key].height,
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '.42s'
}"
>
<div class="nested-wrapper">
<form-object
@@ -172,11 +166,10 @@
[formGroup]="$any(entry.value)"
[current]="current?.[entry.key]"
[original]="original?.[entry.key]"
(onResize)="resize(entry.key)"
(hasNewOptions)="setHasNew(entry.key)"
></form-object>
</div>
</div>
</tui-expand>
</ng-container>
<!-- union -->
<form-union
@@ -259,15 +252,10 @@
></ion-icon>
</ion-item>
<!-- object/union body -->
<div
<tui-expand
style="padding-left: 24px"
[expanded]="objectListDisplay[entry.key][i].expanded"
[id]="objectId | toElementId: entry.key:i"
[ngStyle]="{
'max-height': objectListDisplay[entry.key][i].height,
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '.42s'
}"
>
<form-object
*ngIf="spec.subtype === 'object'"
@@ -278,7 +266,6 @@
(onInputChange)="
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
"
(onResize)="resize(entry.key, i)"
></form-object>
<form-union
*ngIf="spec.subtype === 'union'"
@@ -289,7 +276,6 @@
(onInputChange)="
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
"
(onResize)="resize(entry.key, i)"
></form-union>
<div style="text-align: right; padding-top: 12px">
<ion-button
@@ -301,7 +287,7 @@
Delete
</ion-button>
</div>
</div>
</tui-expand>
</ng-container>
<!-- string or number -->
<div
@@ -318,8 +304,7 @@
$any(spec.spec).placeholder || 'Enter ' + spec.name
"
[formControlName]="i"
>
</ion-input>
></ion-input>
<ion-button
strong
fill="clear"

View File

@@ -18,6 +18,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { SharedPipesModule } from '@start9labs/shared'
import { TuiElasticContainerModule } from '@taiga-ui/kit'
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
import { TuiExpandModule } from '@taiga-ui/core'
@NgModule({
declarations: [
@@ -39,6 +40,7 @@ import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
SharedPipesModule,
EnumListPageModule,
TuiElasticContainerModule,
TuiExpandModule,
],
exports: [FormObjectComponent, FormLabelComponent],
})

View File

@@ -6,6 +6,7 @@ import {
ChangeDetectionStrategy,
Inject,
inject,
SimpleChanges,
} from '@angular/core'
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
@@ -41,15 +42,14 @@ export class FormObjectComponent {
@Input() current?: Config
@Input() original?: Config
@Output() onInputChange = new EventEmitter<void>()
@Output() onResize = new EventEmitter<void>()
@Output() hasNewOptions = new EventEmitter<void>()
warningAck: { [key: string]: boolean } = {}
unmasked: { [key: string]: boolean } = {}
objectDisplay: {
[key: string]: { expanded: boolean; height: string; hasNewOptions: boolean }
[key: string]: { expanded: boolean; hasNewOptions: boolean }
} = {}
objectListDisplay: {
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
[key: string]: { expanded: boolean; displayAs: string }[]
} = {}
objectId = v4()
@@ -63,6 +63,37 @@ export class FormObjectComponent {
) {}
ngOnInit() {
this.setDisplays()
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (
this.original &&
Object.keys(this.current || {}).some(
key => this.original![key] === undefined,
)
)
this.hasNewOptions.emit()
})
}
ngOnChanges(changes: SimpleChanges) {
const specChanges = changes['objectSpec']
if (!specChanges) return
if (
!specChanges.firstChange &&
Object.keys({
...specChanges.previousValue,
...specChanges.currentValue,
}).length !== Object.keys(specChanges.previousValue).length
) {
this.setDisplays()
}
}
private setDisplays() {
Object.keys(this.objectSpec).forEach(key => {
const spec = this.objectSpec[key]
@@ -74,7 +105,6 @@ export class FormObjectComponent {
]
this.objectListDisplay[key][index] = {
expanded: false,
height: '0px',
displayAs: displayAs
? (Mustache as any).render(displayAs, obj)
: '',
@@ -83,33 +113,10 @@ export class FormObjectComponent {
} else if (spec.type === 'object') {
this.objectDisplay[key] = {
expanded: false,
height: '0px',
hasNewOptions: false,
}
}
})
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (this.original) {
Object.keys(this.current || {}).forEach(key => {
if ((this.original as Config)[key] === undefined) {
this.hasNewOptions.emit()
}
})
}
}, 10)
}
resize(key: string, i?: number): void {
setTimeout(() => {
if (i !== undefined) {
this.objectListDisplay[key][i].height = this.getScrollHeight(key, i)
} else {
this.objectDisplay[key].height = this.getScrollHeight(key)
}
this.onResize.emit()
}, 250) // 250 to match transition-duration defined in html, for smooth recursive resize
}
addListItemWrapper<T extends ValueSpec>(
@@ -121,20 +128,11 @@ export class FormObjectComponent {
toggleExpandObject(key: string) {
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
this.objectDisplay[key].height = this.objectDisplay[key].expanded
? this.getScrollHeight(key)
: '0px'
this.onResize.emit()
}
toggleExpandListObject(key: string, i: number) {
this.objectListDisplay[key][i].expanded =
!this.objectListDisplay[key][i].expanded
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
.expanded
? this.getScrollHeight(key, i)
: '0px'
this.onResize.emit()
}
updateLabel(key: string, i: number, displayAs: string) {
@@ -275,7 +273,6 @@ export class FormObjectComponent {
'display-as'
]
this.objectListDisplay[key].push({
height: '0px',
expanded: false,
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',
})
@@ -296,8 +293,8 @@ export class FormObjectComponent {
}
private deleteListItem(key: string, index: number, markDirty = true): void {
if (this.objectListDisplay[key])
this.objectListDisplay[key][index].height = '0px'
// if (this.objectListDisplay[key])
// this.objectListDisplay[key][index].height = '0px'
const arr = this.formGroup.get(key) as UntypedFormArray
if (markDirty) arr.markAsDirty()
pauseFor(250).then(() => {
@@ -328,13 +325,6 @@ export class FormObjectComponent {
arr.markAsDirty()
}
private getScrollHeight(key: string, index = 0): string {
const element = this.document.getElementById(
getElementId(this.objectId, key, index),
)
return `${element?.scrollHeight}px`
}
asIsOrder() {
return 0
}
@@ -352,8 +342,6 @@ export class FormUnionComponent {
@Input() current?: Config
@Input() original?: Config
@Output() onResize = new EventEmitter<void>()
get unionValue() {
return this.formGroup.get(this.spec.tag.id)?.value
}
@@ -374,10 +362,7 @@ export class FormUnionComponent {
objectId = v4()
constructor(
private readonly formService: FormService,
@Inject(DOCUMENT) private readonly document: Document,
) {}
constructor(private readonly formService: FormService) {}
updateUnion(e: any): void {
const tagId = this.spec.tag.id
@@ -397,12 +382,6 @@ export class FormUnionComponent {
this.formGroup.addControl(control, unionGroup.controls[control])
})
}
resize(): void {
setTimeout(() => {
this.onResize.emit()
}, 250) // 250 to match transition-duration, defined in html
}
}
@Component({

View File

@@ -37,7 +37,6 @@
[formGroup]="formGroup"
[current]="current"
[original]="original"
(onResize)="resize()"
></form-object>
</tui-elastic-container>
</div>

View File

@@ -10,7 +10,27 @@
</ion-header>
<ion-content class="ion-padding">
<h2>This release</h2>
<h2>This Release</h2>
<h4>0.3.4.1</h4>
<p class="note-padding">
View the complete
<a
href="https://github.com/Start9Labs/embassy-os/releases/tag/v0.3.4.1"
target="_blank"
noreferrer
>
release notes
</a>
for more details.
</p>
<h6>Highlights</h6>
<ul class="spaced-list">
<li>0.3.4 bug fixes</li>
</ul>
<h2>Previous Releases</h2>
<h4>0.3.4</h4>
<p class="note-padding">
View the complete

View File

@@ -1169,6 +1169,113 @@ export module Mock {
} as any // @TODO why is this necessary?
export const ConfigSpec: RR.GetPackageConfigRes['spec'] = {
bitcoin: {
type: 'object',
name: 'Bitcoin Settings',
description:
'RPC and P2P interface configuration options for Bitcoin Core',
spec: {
'bitcoind-p2p': {
type: 'union',
tag: {
id: 'type',
name: 'Bitcoin Core P2P',
description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
'variant-names': {
internal: 'Bitcoin Core',
external: 'External Node',
},
},
default: 'internal',
variants: {
internal: {},
external: {
'p2p-host': {
type: 'string',
name: 'Public Address',
description: 'The public address of your Bitcoin Core server',
nullable: false,
masked: false,
copyable: false,
},
'p2p-port': {
type: 'number',
name: 'P2P Port',
description:
'The port that your Bitcoin Core P2P server is bound to',
nullable: false,
range: '[0,65535]',
integral: true,
default: 8333,
},
},
},
},
},
},
advanced: {
name: 'Advanced',
type: 'object',
description: 'Advanced settings',
spec: {
rpcsettings: {
name: 'RPC Settings',
type: 'object',
description: 'rpc username and password',
warning:
'Adding RPC users gives them special permissions on your node.',
spec: {
rpcuser2: {
name: 'RPC Username',
type: 'string',
description: 'rpc username',
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
},
rpcuser: {
name: 'RPC Username',
type: 'string',
description: 'rpc username',
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
masked: false,
copyable: true,
},
rpcpass: {
name: 'RPC User Password',
type: 'string',
description: 'rpc password',
nullable: false,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
masked: true,
copyable: true,
},
rpcpass2: {
name: 'RPC User Password',
type: 'string',
description: 'rpc password',
nullable: false,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
masked: true,
copyable: true,
},
},
},
},
},
testnet: {
name: 'Testnet',
type: 'boolean',
@@ -1449,6 +1556,29 @@ export module Mock {
},
},
external: {
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
spec: {
name: {
type: 'string',
name: 'Name',
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
},
email: {
type: 'string',
name: 'Email',
nullable: false,
masked: false,
copyable: true,
},
},
},
'public-domain': {
name: 'Public Domain',
type: 'string',
@@ -1520,8 +1650,8 @@ export module Mock {
copyable: false,
},
},
advanced: {
name: 'Advanced',
'more-advanced': {
name: 'More Advanced',
type: 'object',
description: 'Advanced settings',
spec: {
@@ -1726,8 +1856,7 @@ export module Mock {
rulemakers: [],
},
'bitcoin-node': {
type: 'external',
'public-domain': 'hello.com',
type: 'internal',
},
port: 20,
rpcallowip: undefined,

View File

@@ -21,13 +21,17 @@ export class ThemeSwitcherService extends BehaviorSubject<string> {
.watch$('ui', 'theme')
.pipe(take(1), filter(Boolean))
.subscribe(theme => {
this.next(theme)
this.updateTheme(theme)
})
}
override next(currentTheme: string): void {
this.embassyApi.setDbValue(['theme'], currentTheme)
this.windowRef.document.body.setAttribute('data-theme', currentTheme)
super.next(currentTheme)
override next(theme: string): void {
this.embassyApi.setDbValue(['theme'], theme)
this.updateTheme(theme)
}
private updateTheme(theme: string): void {
this.windowRef.document.body.setAttribute('data-theme', theme)
super.next(theme)
}
}

View File

@@ -23,6 +23,8 @@ pub struct RsyncOptions {
pub exclude: Vec<String>,
#[serde(default = "const_true")]
pub no_permissions: bool,
#[serde(default = "const_true")]
pub no_owner: bool,
}
impl Default for RsyncOptions {
fn default() -> Self {
@@ -32,6 +34,7 @@ impl Default for RsyncOptions {
ignore_existing: false,
exclude: Vec::new(),
no_permissions: false,
no_owner: false,
}
}
}
@@ -61,6 +64,9 @@ impl Rsync {
if options.no_permissions {
cmd.arg("--no-perms");
}
if options.no_owner {
cmd.arg("--no-owner");
}
for exclude in options.exclude {
cmd.arg(format!("--exclude={}", exclude));
}

View File

@@ -1171,7 +1171,7 @@ dependencies = [
[[package]]
name = "embassy-os"
version = "0.3.4"
version = "0.3.4-rev.1"
dependencies = [
"aes",
"async-compression",