sdk db backups, wifi ux, release notes, minor copy

This commit is contained in:
Matt Hill
2026-03-24 16:39:31 -06:00
parent 53dff95365
commit 186925065d
10 changed files with 222 additions and 144 deletions

View File

@@ -521,7 +521,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
.or_not_found("certificate in chain")?;
println!("📝 Root CA:");
print!("{cert}\n");
println!("Follow instructions to trust your Root CA (recommended): https://docs.start9.com/start-tunnel/installing/index.html#trust-your-root-ca");
println!("Follow instructions to trust your Root CA (recommended): https://docs.start9.com/start-tunnel/installing.html#trust-your-root-ca");
return Ok(());
}

View File

@@ -8,108 +8,120 @@ Previous backups are incompatible with v0.4.0. It is strongly recommended that y
A server is not a toy. It is a critical component of the computing paradigm, and its failure can be catastrophic, resulting in downtime or loss of data. From the beginning, Start9 has taken a "security and reliability first" approach to the development of StartOS, favoring soundness over speed, and prioritizing essential features such as encrypted network connections, simple backups, and a reliable container runtime over nice-to-haves like custom theming and more services.
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible; namely, an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible: an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026.
The difficulty of our endeavor requires making mistakes, and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026.
v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly six years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation that will allow us to deliver on the promise of sovereign computing.
v0.4.0 is a complete rewrite of StartOS almost nothing survived. After nearly six years of building StartOS, we believe we have finally arrived at the correct architecture and foundation to deliver on the promise of sovereign computing.
## Changelog
### New User interface
### User Experience
We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy.
#### New User Interface
### Translations
The StartOS UI has been rewritten to be more performant, more intuitive, and better looking on both mobile and desktop.
StartOS v0.4.0 supports multiple languages and also makes it easy to add more later on.
#### Internationalization
### LXC Container Runtime
StartOS v0.4.0 and available services now support multiple languages and keyboard layouts.
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups.
#### Improved Actions
### Hardware Acceleration
Actions accept arbitrary form input and return arbitrary responses, replacing the old "Config" and "Properties" concepts, which have been removed. The new Actions API gives package developers the ability to break configuration and properties into smaller, more specific forms — or to exclude them entirely without polluting the UI. Improved form design and new input types round out the experience.
Services can take advantage of (and require) the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference purposes. For example, StartOS and Ollama can run natively on The Nvidia DGX Spark and take full advantage of the hardware/firmware stack to perform local inference against open source models.
#### Progress Reporting
### New S9PK archive format
A new progress reporting API enables package developers to define custom phases and provide real-time progress updates for operations such as installing, updating, or backing up a service.
The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk.
#### Email Notifications via SMTP
### Improved Actions
You can now add your Gmail, SES, or other SMTP credentials to StartOS to deliver email notifications from StartOS and from installed services that support SMTP.
Actions take arbitrary form input and return arbitrary responses, thus satisfying the needs of both "Config" and "Properties", which have now been removed. The new actions API gives package developers the ability to break up Config and Properties into smaller, more specific formats, or to exclude them entirely without polluting the UI. Improved form design and new input types round out the new actions experience.
### Networking & Connectivity
### Squashfs Images for OS Updates
#### LAN Port Forwarding
StartOS now uses squashfs images instead of rsync for OS updates. This allows for better update verification and improved reliability.
Perhaps the biggest complaint with prior versions of StartOS was the use of unique `.local` URLs for service interfaces. This has been corrected. Service interfaces are now available on unique ports, supporting non-HTTP traffic on the LAN as well as remote access via VPN.
### Typescript Package API and SDK
#### Gateways
Package developers can now take advantage of StartOS APIs using the new start-sdk, available in Typescript. A barebones StartOS package (s9pk) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences for their service.
Gateways connect your server to the Internet, facilitating inbound and outbound traffic. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet. Outbound traffic can also be overridden on a per-service basis.
### Removed PostgresSQL
#### Private Domains
StartOS itself has miniscule data persistence needs. PostgresSQL was overkill and has been removed in favor of lightweight PatchDB.
A private domain is like your server's `.local` address, except it also works over VPN, and it can be _anything_ — a real domain you control, a made-up domain, or even a domain controlled by someone else.
### Sending Emails via SMTP
Like your local domain, private domains can only be accessed when connected to the same LAN as your server, either physically or via VPN, and they require trusting your server's Root CA.
You can now add your Gmail, SES, or other SMTP credentials to StartOS in order to send deliver email notifications from StartOS and from installed services that support SMTP.
#### Public Domains (Clearnet)
### SSH password auth
It is now easy to expose service interfaces to the public Internet on a domain you control. There are two options:
1. **Open ports on your router.** This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface.
2. **Use a Wireguard reverse tunnel**, such as [StartTunnel](#start-tunnel), to proxy web traffic. This option requires renting a $5$10/month VPS and installing StartTunnel (or similar). The result is a virtual router in the cloud that you can use to expose service interfaces instead of your real router, hiding your IP address from visitors.
#### Let's Encrypt
StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. Visitors to your public websites and APIs will no longer need to download and trust your server's Root CA.
#### Internal DNS Server
StartOS runs its own DNS server and automatically adds records for your private domains. You can configure your router or other gateway to use the StartOS DNS server to resolve these domains locally.
#### Static DNS Servers
By default, StartOS uses the DNS servers it receives via DHCP from its gateway(s). It is now possible to override these with custom, static DNS servers.
#### Tor as a Plugin
With the expanded networking capabilities of StartOS v0.4.0, Tor is now an optional plugin that can be installed from the Marketplace. Users can run their own Tor relay, route outbound connections through Tor, and generate hidden service URLs for any service interface, including vanity addresses.
#### Tor Address Management
StartOS v0.4.0 supports adding and removing Tor addresses for both StartOS itself and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS, enabling vanity addresses.
### System & Infrastructure
#### LXC Container Runtime
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub-containers. This architecture naturally supports multi-container setups.
#### Hardware Acceleration
Services can take advantage of — and require — the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference. For example, StartOS and Ollama can run natively on the Nvidia DGX Spark and take full advantage of its hardware and firmware stack to perform local inference against open source models.
#### Squashfs Images for OS Updates
StartOS now uses squashfs images instead of rsync for OS updates, enabling better update verification and improved reliability.
#### Replaced PostgreSQL with PatchDB
StartOS itself has minimal data persistence needs. PostgreSQL was overkill and has been replaced with the lightweight PatchDB.
#### Improved Backups
The new `start-fs` FUSE module unifies filesystem expectations across platforms, enabling more reliable backups. The system now defaults to rsync differential backups instead of incremental backups, which is both faster and more space-efficient — files deleted from the server are also deleted from the backup.
#### SSH Password Authentication
You can now SSH into your server using your master password. SSH public key authentication is still supported as well.
### Tor Address Management
### Developer Experience
StartOS v0.4.0 supports adding and removing Tor addresses for StartOS and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS. This has the added benefit of permitting vanity addresses.
#### New S9PK Archive Format
### Progress Reporting
The S9PK archive format has been overhauled to support signature verification of partial downloads and direct mounting of container images without unpacking the archive.
A new progress reporting API enabled package developers to create unique phases and provide real-time progress reporting for actions such as installing, updating, or backing up a service.
#### TypeScript Package API and SDK
### Registry Protocol
Package developers can now interact with StartOS APIs using the new `start-sdk`, available in TypeScript. A barebones StartOS package (S9PK) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences for their services.
The new registry protocol bifurcates package indexing (listing/validating) and package hosting (downloading). Registries are now simple indexes of packages that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the curated list of packages comes from Start9. But when someone installs a listed service, the package binary is being downloaded from Github. The registry also validates the binary. This makes it much easier to host a custom registry, since it is just a curated list of services tat reference package binaries hosted on Github or elsewhere.
#### Registry Protocol
### LAN port forwarding
The new registry protocol separates package indexing (listing and validation) from package hosting (downloading). Registries are now simple indexes that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the curated list of packages comes from Start9, but when they install a service, the binary is downloaded from GitHub. The registry also validates the binary. This makes it much easier to host a custom registry, since it is just a curated list of services that reference package binaries hosted on GitHub or elsewhere.
Perhaps the biggest complaint with prior version of StartOS was use of unique .local URLs for service interfaces. This has been corrected. Service interfaces are now available on unique ports, allowing for non-http traffic on the LAN as well as remote access via VPN.
#### Exver and Service Flavors
### Improved Backups
The new start-fs fuse module unifies file system expectations for various platforms, enabling more reliable backups. The new system also defaults to using rsync differential backups instead of incremental backups, which is faster and saves on disk space by also deleting from the backup files that were deleted from the server.
### Exver
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0".
### Let's Encrypt
StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA.
### Gateways
Gateways connect your server to the Internet, facilitating inbound and outbound traffic. Your router is a gateway. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet.
### Static DNS Servers
By default, StartOS uses the DNS servers it receives via DHCP from its gateway(s). It is now possible to override these DNS servers with custom, static ones.
### Internal DNS Server
StartOS runs its own DNS server and automatically adds records for your private domains. You can update your router or other gateway to use StartOS DNS server in order to resolve these domains locally.
### Private Domains
A private domain is like to your server's .local, except it also works for VPN connectivity, and it can be _anything_. It can be a real domain you control, a made up domain, or even a domain controlled by someone else.
Similar to your local domain, private domains can only be accessed when connected to the same LAN as your server, either physically or via VPN, and they require trusting your server's Root CA.
### Public Domains (Clearnet)
It is now easy to expose service interfaces to the public Internet on a public domain you control. There are two options, both of which are easy to accomplish:
1. Open ports on your router. This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface.
2. Use a Wireguard reverse tunnel, such as [StartTunnel](#start-tunnel) to proxy web traffic. This option requires renting a $5-$10/month VPS and installing StartTunnel (or similar). The result is a new gateway, a virtual router in the cloud, that you can use to expose service interfaces instead of your real router, thereby hiding your IP address from visitors.
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a semver-compliant upstream version, (2) a semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors are alternative implementations of a service where a user would typically want only one installed, and data can be migrated between them. Flavors commonly satisfy the same dependency API for downstream packages, though this is not strictly required. A valid Exver looks like: `#knots:29.0:1.0-beta.1` — the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0.

View File

@@ -1,5 +1,12 @@
# Changelog
## 0.4.0-beta.66 (2026-03-24)
- **Breaking:** `withPgDump()` replaces `pgdata` with required `mountpoint` + `pgdataPath`
- Passwordless/trust auth support for `withPgDump()` and `withMysqlDump()`
- New options: `pgOptions` for postgres, `mysqldOptions` for mysql/mariadb
- Fixed MariaDB backup/restore support
## 0.4.0-beta.65 (2026-03-23)
### Added

View File

@@ -10,9 +10,10 @@ const BACKUP_HOST_PATH = '/media/startos/backup'
const BACKUP_CONTAINER_MOUNT = '/backup-target'
/** A password value, or a function that returns one. Functions are resolved lazily (only during restore). */
export type LazyPassword = string | (() => string | Promise<string>)
export type LazyPassword = string | (() => string | Promise<string>) | null
async function resolvePassword(pw: LazyPassword): Promise<string> {
async function resolvePassword(pw: LazyPassword): Promise<string | null> {
if (pw === null) return null
return typeof pw === 'function' ? pw() : pw
}
@@ -22,16 +23,20 @@ export type PgDumpConfig<M extends T.SDKManifest> = {
imageId: keyof M['images'] & T.ImageId
/** Volume ID containing the PostgreSQL data directory */
dbVolume: M['volumes'][number]
/** Path to PGDATA within the container (e.g. '/var/lib/postgresql/data') */
pgdata: string
/** Volume mountpoint (e.g. '/var/lib/postgresql') */
mountpoint: string
/** Subpath from mountpoint to PGDATA (e.g. '/data', '/18/docker') */
pgdataPath: string
/** PostgreSQL database name to dump */
database: string
/** PostgreSQL user */
user: string
/** PostgreSQL password (for restore). Can be a string or a function that returns one — functions are resolved lazily after volumes are restored. */
/** PostgreSQL password (for restore). Can be a string, a function that returns one (resolved lazily after volumes are restored), or null for trust auth. */
password: LazyPassword
/** Additional initdb arguments (e.g. ['--data-checksums']) */
initdbArgs?: string[]
/** Additional options passed to `pg_ctl start -o` (e.g. '-c shared_preload_libraries=vectorchord'). Appended after `-c listen_addresses=`. */
pgOptions?: string
}
/** Configuration for MySQL/MariaDB dump-based backup */
@@ -52,6 +57,8 @@ export type MysqlDumpConfig<M extends T.SDKManifest> = {
engine: 'mysql' | 'mariadb'
/** Custom readiness check command (default: ['mysqladmin', 'ping', ...]) */
readyCommand?: string[]
/** Additional options passed to `mysqld` on startup (e.g. '--innodb-buffer-pool-size=256M'). Appended after `--bind-address=127.0.0.1`. */
mysqldOptions?: string[]
}
/** Bind-mount the backup target into a SubContainer's rootfs */
@@ -154,19 +161,21 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
const {
imageId,
dbVolume,
pgdata,
mountpoint,
pgdataPath,
database,
user,
password,
initdbArgs = [],
pgOptions,
} = config
const pgdata = `${mountpoint}${pgdataPath}`
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
const pgMountpoint = pgdata.replace(/\/data$/, '') || pgdata
function dbMounts() {
return Mounts.of<M>().mountVolume({
volumeId: dbVolume,
mountpoint: pgMountpoint,
mountpoint: mountpoint,
readonly: false,
subpath: null,
})
@@ -193,10 +202,12 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
user: 'root',
})
console.log(`[${label}] starting postgres`)
await sub.execFail(
['pg_ctl', 'start', '-D', pgdata, '-o', '-c listen_addresses='],
{ user: 'postgres' },
)
const pgStartOpts = pgOptions
? `-c listen_addresses= ${pgOptions}`
: '-c listen_addresses='
await sub.execFail(['pg_ctl', 'start', '-D', pgdata, '-o', pgStartOpts], {
user: 'postgres',
})
for (let i = 0; i < 60; i++) {
const { exitCode } = await sub.exec(['pg_isready', '-U', user], {
user: 'postgres',
@@ -249,7 +260,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
async (sub) => {
await mountBackupTarget(sub.rootfs)
await sub.execFail(
['chown', '-R', 'postgres:postgres', pgMountpoint],
['chown', '-R', 'postgres:postgres', mountpoint],
{ user: 'root' },
)
await sub.execFail(
@@ -274,18 +285,20 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
{ user: 'postgres' },
null,
)
await sub.execFail(
[
'psql',
'-U',
user,
'-d',
database,
'-c',
`ALTER USER ${user} WITH PASSWORD '${resolvedPassword}'`,
],
{ user: 'postgres' },
)
if (resolvedPassword !== null) {
await sub.execFail(
[
'psql',
'-U',
user,
'-d',
database,
'-c',
`ALTER USER ${user} WITH PASSWORD '${resolvedPassword}'`,
],
{ user: 'postgres' },
)
}
await sub.execFail(['pg_ctl', 'stop', '-D', pgdata, '-w'], {
user: 'postgres',
})
@@ -318,6 +331,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
password,
engine,
readyCommand,
mysqldOptions = [],
} = config
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
@@ -342,6 +356,42 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
throw new Error('MySQL/MariaDB failed to become ready within 30 seconds')
}
async function startMysql(sub: {
exec(cmd: string[], opts?: any): Promise<{ exitCode: number | null }>
execFail(cmd: string[], opts?: any, timeout?: number | null): Promise<any>
}) {
if (engine === 'mariadb') {
// MariaDB doesn't support --daemonize; fire-and-forget the exec
sub
.exec(
[
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--bind-address=127.0.0.1',
...mysqldOptions,
],
{ user: 'root' },
)
.catch((e) =>
console.error('[mysql-backup] mysqld exited unexpectedly:', e),
)
} else {
await sub.execFail(
[
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--bind-address=127.0.0.1',
'--daemonize',
...mysqldOptions,
],
{ user: 'root' },
null,
)
}
}
return new Backups<M>()
.setPreBackup(async (effects) => {
const pw = await resolvePassword(password)
@@ -350,7 +400,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
'ping',
'-u',
user,
`-p${pw}`,
...(pw !== null ? [`-p${pw}`] : []),
'--silent',
]
await SubContainerRc.withTemp<M, void, BackupEffects>(
@@ -371,24 +421,14 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
user: 'root',
})
}
await sub.execFail(
[
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--skip-networking',
'--daemonize',
],
{ user: 'root' },
null,
)
await startMysql(sub)
await waitForMysql(sub, readyCmd)
await sub.execFail(
[
'mysqldump',
'-u',
user,
`-p${pw}`,
...(pw !== null ? [`-p${pw}`] : []),
'--single-transaction',
`--result-file=${dumpFile}`,
database,
@@ -396,9 +436,15 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
{ user: 'root' },
null,
)
// Graceful shutdown via SIGTERM; wait for exit
await sub.execFail(
['mysqladmin', '-u', user, `-p${pw}`, 'shutdown'],
[
'sh',
'-c',
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
],
{ user: 'root' },
null,
)
},
)
@@ -435,17 +481,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
{ user: 'root' },
)
}
await sub.execFail(
[
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--skip-networking',
'--daemonize',
],
{ user: 'root' },
null,
)
await startMysql(sub)
// After fresh init, root has no password
await waitForMysql(sub, [
'mysqladmin',
@@ -455,29 +491,32 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
'--silent',
])
// Create database, user, and set password
await sub.execFail(
[
'mysql',
'-u',
'root',
'-e',
`CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost' IDENTIFIED BY '${pw}'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; ALTER USER 'root'@'localhost' IDENTIFIED BY '${pw}'; FLUSH PRIVILEGES;`,
],
{ user: 'root' },
)
const grantSql =
pw !== null
? `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost' IDENTIFIED BY '${pw}'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; ALTER USER 'root'@'localhost' IDENTIFIED BY '${pw}'; FLUSH PRIVILEGES;`
: `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; FLUSH PRIVILEGES;`
await sub.execFail(['mysql', '-u', 'root', '-e', grantSql], {
user: 'root',
})
// Restore from dump
await sub.execFail(
[
'sh',
'-c',
`mysql -u root -p'${pw}' \`${database}\` < ${dumpFile}`,
`mysql -u root ${pw !== null ? `-p'${pw}'` : ''} ${database} < ${dumpFile}`,
],
{ user: 'root' },
null,
)
// Graceful shutdown via SIGTERM; wait for exit
await sub.execFail(
['mysqladmin', '-u', 'root', `-p${password}`, 'shutdown'],
[
'sh',
'-c',
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
],
{ user: 'root' },
null,
)
},
)

View File

@@ -1,15 +1,15 @@
import { z } from 'zod'
import * as YAML from 'yaml'
import * as TOML from '@iarna/toml'
import * as INI from 'ini'
import {
XMLParser,
XMLBuilder,
XMLParser,
type X2jOptions,
type XmlBuilderOptions,
} from 'fast-xml-parser'
import * as T from '../../../base/lib/types'
import * as INI from 'ini'
import * as fs from 'node:fs/promises'
import * as YAML from 'yaml'
import { z } from 'zod'
import * as T from '../../../base/lib/types'
import { asError, deepEqual } from '../../../base/lib/util'
import { Watchable } from '../../../base/lib/util/Watchable'
import { PathBase } from './Volume'
@@ -382,7 +382,7 @@ export class FileHelper<A> {
const mergeData = this.validate(fileMerge({}, fileData, data))
const toWrite = this.writeData(mergeData)
if (toWrite !== fileDataRaw) {
this.writeFile(mergeData)
await this.writeFile(mergeData)
if (!options.allowWriteAfterConst && effects.constRetry) {
const records = this.consts.filter(([c]) => c === effects.constRetry)
for (const record of records) {

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.65",
"version": "0.4.0-beta.66",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.65",
"version": "0.4.0-beta.66",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

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

View File

@@ -202,7 +202,7 @@ export class ActionInputModal {
const message = `${this.i18n.transform('As a result of this change, the following services will no longer work properly and may crash')}:<ul>`
const content =
`${message}${breakages.map(id => `<li><b>${getManifest(packages[id]!).title}</b></li>`)}</ul>` as i18nKey
`${message}${breakages.map(id => `<li><b>${getManifest(packages[id]!).title}</b></li>`).join('')}</ul>` as i18nKey
return firstValueFrom(
this.dialog

View File

@@ -26,6 +26,7 @@ import { PatchDB } from 'patch-db-client'
import {
catchError,
defer,
exhaustMap,
finalize,
first,
map,
@@ -34,7 +35,10 @@ import {
of,
Subject,
switchMap,
takeUntil,
takeWhile,
tap,
timer,
} from 'rxjs'
import {
FormComponent,
@@ -184,7 +188,7 @@ export default class SystemWifiComponent {
),
this.refresh$.pipe(
tap(() => this.refreshing.set(true)),
switchMap(() =>
exhaustMap(() =>
this.getWifi$().pipe(finalize(() => this.refreshing.set(false))),
),
),
@@ -205,6 +209,10 @@ export default class SystemWifiComponent {
try {
await this.api.enableWifi({ enabled: enable })
if (enable) {
this.update$.next({ known: [], available: [] })
this.pollForNetworks()
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -259,6 +267,15 @@ export default class SystemWifiComponent {
}
}
private pollForNetworks(): void {
timer(0, 500)
.pipe(
takeWhile(() => !this.wifi()?.available?.length),
takeUntil(timer(5000)),
)
.subscribe(() => this.refresh$.next())
}
private async confirmWifi(ssid: string): Promise<void> {
const maxAttempts = 5
let attempts = 0

View File

@@ -71,13 +71,16 @@ export function getInstalledPrimaryStatus({
tasks,
statusInfo,
}: T.PackageDataEntry): PrimaryStatus {
const base = getInstalledBaseStatus(statusInfo)
if (
!INACTIVE_STATUSES.includes(base) &&
Object.values(tasks).some(t => t.active && t.task.severity === 'critical')
) {
return 'task-required'
}
return getInstalledBaseStatus(statusInfo)
return base
}
function getHealthStatus(statusInfo: T.StatusInfo): T.HealthStatus | null {