mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
Feature/consolidate setup (#3092)
* start consolidating * add start-cli flash-os * combine install and setup and refactor all * use http * undo mock * fix translation * translations * use dialogservice wrapper * better ST messaging on setup * only warn on update if breakages (#3097) * finish setup wizard and ui language-keyboard feature * fix typo * wip: localization * remove start-tunnel readme * switch to posix strings for language internal * revert mock * translate backend strings * fix missing about text * help text for args * feat: add "Add new gateway" option (#3098) * feat: add "Add new gateway" option * Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add translation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix dns selection * keyboard keymap also * ability to shutdown after install * revert mock * working setup flow + manifest localization * (mostly) redundant localization on frontend * version bump * omit live medium from disk list and better space management * ignore missing package archive on 035 migration * fix device migration * add i18n helper to sdk * fix install over 0.3.5.1 * fix grub config --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,155 +6,224 @@ import {
|
||||
ViewChild,
|
||||
DOCUMENT,
|
||||
} from '@angular/core'
|
||||
import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiIcon, TuiLoader, TuiSurface } from '@taiga-ui/core'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
import { DocumentationComponent } from 'src/app/components/documentation.component'
|
||||
import { MatrixComponent } from 'src/app/components/matrix.component'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
|
||||
import { ApiService } from '../services/api.service'
|
||||
import { StateService } from '../services/state.service'
|
||||
import { DocumentationComponent } from '../components/documentation.component'
|
||||
import { MatrixComponent } from '../components/matrix.component'
|
||||
import { SetupCompleteRes } from '../types'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<canvas matrix></canvas>
|
||||
<section tuiCardLarge>
|
||||
<h1 class="heading">
|
||||
<tui-icon icon="@tui.circle-check-big" class="g-positive" />
|
||||
Setup Complete!
|
||||
</h1>
|
||||
@if (stateService.kiosk) {
|
||||
<button tuiButton (click)="exitKiosk()">Continue to Login</button>
|
||||
} @else if (lanAddress) {
|
||||
@if (stateService.setupType === 'restore') {
|
||||
<h3>You can now safely unplug your backup drive</h3>
|
||||
} @else if (stateService.setupType === 'transfer') {
|
||||
<h3>You can now safely unplug your old StartOS data drive</h3>
|
||||
<header tuiHeader>
|
||||
<h2 tuiTitle>
|
||||
<span class="inline-title">
|
||||
<tui-icon icon="@tui.circle-check-big" class="g-positive" />
|
||||
{{ 'Setup Complete!' | i18n }}
|
||||
</span>
|
||||
@if (!stateService.kiosk) {
|
||||
<span tuiSubtitle>
|
||||
{{
|
||||
stateService.setupType === 'restore'
|
||||
? ('You can unplug your backup drive' | i18n)
|
||||
: stateService.setupType === 'transfer'
|
||||
? ('You can unplug your transfer drive' | i18n)
|
||||
: ('http://start.local was for setup only. It will no longer work.'
|
||||
| i18n)
|
||||
}}
|
||||
</span>
|
||||
}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
@if (!result) {
|
||||
<tui-loader />
|
||||
} @else {
|
||||
<!-- Step: Download Address Info (non-kiosk only) -->
|
||||
@if (!stateService.kiosk) {
|
||||
<button tuiCell="l" [disabled]="downloaded" (click)="download()">
|
||||
<tui-avatar appearance="secondary" src="@tui.download" />
|
||||
<div tuiTitle>
|
||||
{{ 'Download Address Info' | i18n }}
|
||||
<div tuiSubtitle>
|
||||
{{
|
||||
"Contains your server's permanent local address and Root CA"
|
||||
| i18n
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@if (downloaded) {
|
||||
<tui-icon icon="@tui.circle-check" class="g-positive" />
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<h3>
|
||||
http://start.local was for setup purposes only. It will no longer
|
||||
work.
|
||||
</h3>
|
||||
<!-- Step: Remove USB Media (when restart needed) -->
|
||||
@if (result.needsRestart) {
|
||||
<button
|
||||
tuiCell="l"
|
||||
[class.disabled]="!stateService.kiosk && !downloaded"
|
||||
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
|
||||
(click)="usbRemoved = true"
|
||||
>
|
||||
<tui-avatar appearance="secondary" src="@tui.usb" />
|
||||
<div tuiTitle>
|
||||
{{ 'USB Removed' | i18n }}
|
||||
<div tuiSubtitle>
|
||||
{{
|
||||
'Remove the USB installation media from your server' | i18n
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@if (usbRemoved) {
|
||||
<tui-icon icon="@tui.circle-check" class="g-positive" />
|
||||
}
|
||||
</button>
|
||||
|
||||
<button tuiCardLarge tuiSurface="floating" (click)="download()">
|
||||
<strong class="caps">Download address info</strong>
|
||||
<span>
|
||||
For future reference, this file contains your server's permanent
|
||||
local address, as well as its Root Certificate Authority (Root CA).
|
||||
</span>
|
||||
<strong class="caps">
|
||||
Download
|
||||
<tui-icon icon="@tui.download" />
|
||||
</strong>
|
||||
</button>
|
||||
<!-- Step: Restart Server -->
|
||||
<button
|
||||
tuiCell="l"
|
||||
[class.disabled]="!usbRemoved"
|
||||
[disabled]="!usbRemoved || rebooted || rebooting"
|
||||
(click)="reboot()"
|
||||
>
|
||||
<tui-avatar appearance="secondary" src="@tui.rotate-cw" />
|
||||
<div tuiTitle>
|
||||
{{ 'Restart Server' | i18n }}
|
||||
<div tuiSubtitle>
|
||||
@if (rebooting) {
|
||||
{{ 'Waiting for server to come back online' | i18n }}
|
||||
} @else if (rebooted) {
|
||||
{{ 'Server is back online' | i18n }}
|
||||
} @else {
|
||||
{{ 'Restart your server to complete setup' | i18n }}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (rebooting) {
|
||||
<tui-loader />
|
||||
} @else if (rebooted) {
|
||||
<tui-icon icon="@tui.circle-check" class="g-positive" />
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<a
|
||||
tuiCardLarge
|
||||
tuiSurface="floating"
|
||||
target="_blank"
|
||||
[attr.href]="disableLogin ? null : lanAddress"
|
||||
>
|
||||
<span>
|
||||
In the new tab, follow instructions to trust your server's Root CA
|
||||
and log in.
|
||||
</span>
|
||||
<strong class="caps">
|
||||
Open Local Address
|
||||
<tui-icon icon="@tui.external-link" />
|
||||
</strong>
|
||||
</a>
|
||||
<app-documentation hidden [lanAddress]="lanAddress" />
|
||||
} @else {
|
||||
<tui-loader />
|
||||
<!-- Step: Open Local Address (non-kiosk only) -->
|
||||
@if (!stateService.kiosk) {
|
||||
<button
|
||||
tuiCell="l"
|
||||
[class.disabled]="!canOpenAddress"
|
||||
[disabled]="!canOpenAddress"
|
||||
(click)="openLocalAddress()"
|
||||
>
|
||||
<tui-avatar appearance="secondary" src="@tui.external-link" />
|
||||
<div tuiTitle>
|
||||
{{ 'Open Local Address' | i18n }}
|
||||
<div tuiSubtitle>{{ lanAddress }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<app-documentation hidden [lanAddress]="lanAddress" />
|
||||
}
|
||||
|
||||
<!-- Step: Continue to Login (kiosk only) -->
|
||||
@if (stateService.kiosk) {
|
||||
<button
|
||||
tuiCell="l"
|
||||
[class.disabled]="result.needsRestart && !rebooted"
|
||||
[disabled]="result.needsRestart && !rebooted"
|
||||
(click)="exitKiosk()"
|
||||
>
|
||||
<tui-avatar appearance="secondary" src="@tui.log-in" />
|
||||
<div tuiTitle>
|
||||
{{ 'Continue to Login' | i18n }}
|
||||
<div tuiSubtitle>
|
||||
{{ 'Proceed to the StartOS login screen' | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
.heading {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
.inline-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font: var(--tui-font-heading-4);
|
||||
}
|
||||
|
||||
.caps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
[tuiCardLarge] {
|
||||
color: var(--tui-text-primary);
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
|
||||
&[data-appearance='floating'] {
|
||||
background: var(--tui-background-neutral-1);
|
||||
|
||||
&:hover {
|
||||
background: var(--tui-background-neutral-1-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a[tuiCardLarge]:not([href]) {
|
||||
[tuiCell].disabled {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: left;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TuiCardLarge,
|
||||
TuiCell,
|
||||
TuiIcon,
|
||||
TuiButton,
|
||||
TuiSurface,
|
||||
TuiLoader,
|
||||
TuiAvatar,
|
||||
MatrixComponent,
|
||||
DocumentationComponent,
|
||||
TuiLoader,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export default class SuccessPage implements AfterViewInit {
|
||||
@ViewChild(DocumentationComponent, { read: ElementRef })
|
||||
private readonly documentation?: ElementRef<HTMLElement>
|
||||
|
||||
private readonly document = inject(DOCUMENT)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly downloadHtml = inject(DownloadHTMLService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly stateService = inject(StateService)
|
||||
|
||||
lanAddress?: string
|
||||
cert?: string
|
||||
disableLogin = this.stateService.setupType === 'fresh'
|
||||
result?: SetupCompleteRes
|
||||
lanAddress = ''
|
||||
downloaded = false
|
||||
usbRemoved = false
|
||||
rebooting = false
|
||||
rebooted = false
|
||||
|
||||
get canOpenAddress(): boolean {
|
||||
if (!this.downloaded) return false
|
||||
if (this.result?.needsRestart && !this.rebooted) return false
|
||||
return true
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.complete(), 1000)
|
||||
setTimeout(() => this.complete(), 500)
|
||||
}
|
||||
|
||||
download() {
|
||||
const lanElem = this.document.getElementById('lan-addr')
|
||||
if (this.downloaded) return
|
||||
|
||||
if (lanElem) lanElem.innerHTML = this.lanAddress || ''
|
||||
const lanElem = this.document.getElementById('lan-addr')
|
||||
if (lanElem) lanElem.innerHTML = this.lanAddress
|
||||
|
||||
this.document
|
||||
.getElementById('cert')
|
||||
?.setAttribute(
|
||||
'href',
|
||||
URL.createObjectURL(
|
||||
new Blob([this.cert!], { type: 'application/octet-stream' }),
|
||||
new Blob([this.result!.rootCa], { type: 'application/octet-stream' }),
|
||||
),
|
||||
)
|
||||
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
|
||||
this.downloadHtml.download('StartOS-info.html', html).then(_ => {
|
||||
this.disableLogin = false
|
||||
this.downloadHtml.download('StartOS-info.html', html).then(() => {
|
||||
this.downloaded = true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,17 +231,58 @@ export default class SuccessPage implements AfterViewInit {
|
||||
this.api.exit()
|
||||
}
|
||||
|
||||
openLocalAddress() {
|
||||
window.open(this.lanAddress, '_blank')
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
this.rebooting = true
|
||||
|
||||
try {
|
||||
await this.api.exit()
|
||||
await this.pollForServer()
|
||||
this.rebooted = true
|
||||
this.rebooting = false
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
this.rebooting = false
|
||||
}
|
||||
}
|
||||
|
||||
private async complete() {
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.stateService.kiosk) {
|
||||
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
|
||||
this.cert = ret.rootCa
|
||||
this.result = await this.api.complete()
|
||||
|
||||
await this.api.exit()
|
||||
if (!this.stateService.kiosk) {
|
||||
this.lanAddress = `http://${this.result.hostname}.local`
|
||||
|
||||
if (!this.result.needsRestart) {
|
||||
await this.api.exit()
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private async pollForServer(): Promise<void> {
|
||||
const maxAttempts = 60
|
||||
let attempts = 0
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
await this.api.echo({ message: 'ping' }, this.lanAddress)
|
||||
return
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
this.i18n.transform(
|
||||
'Server did not come back online. Please check your server and try accessing it manually.',
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user