mok ux, autofill device and pf forms, docss for st, docs for start-sdk

This commit is contained in:
Matt Hill
2026-03-12 14:15:45 -06:00
parent 50004da782
commit 0fa069126b
21 changed files with 1147 additions and 154 deletions

View File

@@ -1,31 +1,72 @@
import { Component } from '@angular/core'
import { Component, inject } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiIcon } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
import { StateService } from '../services/state.service'
@Component({
standalone: true,
imports: [TuiButton, TuiIcon, i18nPipe],
template: `
<div class="icon-container">
<tui-icon icon="@tui.shield-check" class="mok-icon" />
@if (!stateService.kiosk) {
<div class="animation-container">
<div class="port">
<div class="port-inner"></div>
</div>
<div class="cable">
<div class="cable-connector"></div>
<div class="cable-body"></div>
</div>
</div>
<p>
{{
'Connect a monitor and keyboard to your server before rebooting.'
| i18n
}}
</p>
} @else {
<div class="icon-container">
<tui-icon icon="@tui.monitor" class="monitor-icon" />
</div>
<p>
{{ 'Keep your monitor connected for the next reboot.' | i18n }}
</p>
}
<div class="mok-info">
<p>
{{
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers — such as those for NVIDIA GPUs — 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.'
| i18n
}}
</p>
<p>
{{
'On the next boot, a blue screen (MokManager) will appear. You will have 10 seconds to select "Enroll MOK" before it dismisses.'
| i18n
}}
</p>
<p>
{{
'If you miss the window, simply reboot to try again. The blue screen will appear on every boot until the key is enrolled.'
| i18n
}}
</p>
<p class="steps-label">
{{ 'After clicking "Enroll MOK":' | i18n }}
</p>
<ol>
<li>Click "Continue"</li>
<li>
{{ 'When prompted, enter your StartOS password' | i18n }}
</li>
<li>Click "Reboot"</li>
</ol>
</div>
<h3>{{ 'Secure Boot Key Enrollment' | i18n }}</h3>
<p>
{{
'A signing key was enrolled for Secure Boot. On the next reboot, a blue screen (MokManager) will appear.'
| i18n
}}
</p>
<ol>
<li>Select "Enroll MOK"</li>
<li>Select "Continue"</li>
<li>{{ 'Enter your StartOS master password when prompted' | i18n }}</li>
<li>Select "Reboot"</li>
</ol>
<footer>
<button tuiButton (click)="context.completeWith(true)">
{{ 'Got it' | i18n }}
{{ 'Ok' | i18n }}
</button>
</footer>
`,
@@ -41,29 +82,114 @@ import { injectContext } from '@taiga-ui/polymorpheus'
margin-bottom: 1rem;
}
.mok-icon {
.monitor-icon {
width: 3rem;
height: 3rem;
color: var(--tui-status-info);
}
h3 {
margin: 0 0 0.5rem;
.animation-container {
position: relative;
width: 160px;
height: 69px;
}
.port {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 18px;
background: var(--tui-background-neutral-1);
border: 2px solid var(--tui-border-normal);
border-radius: 2px;
}
.port-inner {
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--tui-background-neutral-2);
border-radius: 1px;
}
.cable {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
animation: slide-in 2s ease-in-out 0.5s infinite;
left: 130px;
}
.cable-connector {
width: 18px;
height: 12px;
background: var(--tui-text-secondary);
border-radius: 1px;
}
.cable-body {
width: 50px;
height: 6px;
background: var(--tui-text-tertiary);
border-radius: 0 3px 3px 0;
}
@keyframes slide-in {
0% {
left: 130px;
opacity: 0;
}
5% {
left: 130px;
opacity: 1;
}
60% {
left: 32px;
opacity: 1;
}
80% {
left: 32px;
opacity: 1;
}
100% {
left: 32px;
opacity: 0;
}
}
.mok-info {
text-align: left;
margin-top: 0.5rem;
p {
margin: 0 0 0.75rem;
color: var(--tui-text-secondary);
}
.steps-label {
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--tui-text-primary);
}
ol {
margin: 0 0 1rem;
padding-left: 1.5rem;
li {
margin-bottom: 0.25rem;
}
}
}
p {
margin: 0 0 1rem;
color: var(--tui-text-secondary);
}
ol {
text-align: left;
margin: 0 0 1.5rem;
padding-left: 1.5rem;
li {
margin-bottom: 0.25rem;
}
}
footer {
@@ -74,4 +200,5 @@ import { injectContext } from '@taiga-ui/polymorpheus'
})
export class MokEnrollmentDialog {
protected readonly context = injectContext<TuiDialogContext<boolean>>()
readonly stateService = inject(StateService)
}

View File

@@ -1,10 +1,10 @@
import {
AfterViewInit,
Component,
DOCUMENT,
ElementRef,
inject,
ViewChild,
DOCUMENT,
} from '@angular/core'
import {
DialogService,
@@ -12,17 +12,17 @@ import {
ErrorService,
i18nPipe,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
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 { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { DocumentationComponent } from '../components/documentation.component'
import { MatrixComponent } from '../components/matrix.component'
import { MokEnrollmentDialog } from '../components/mok-enrollment.dialog'
import { RemoveMediaDialog } from '../components/remove-media.dialog'
import { T } from '@start9labs/start-sdk'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
@Component({
template: `
@@ -50,7 +50,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
} @else {
<!-- Step: Download Address Info (non-kiosk only) -->
@if (!stateService.kiosk) {
<button tuiCell="l" [disabled]="downloaded" (click)="download()">
<button tuiCell="l" (click)="download()">
<tui-avatar appearance="secondary" src="@tui.download" />
<div tuiTitle>
{{ 'Download Address Info' | i18n }}
@@ -67,12 +67,12 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
</button>
}
<!-- Step: Remove USB Media (when restart needed) -->
<!-- Step: Restart flow -->
@if (result.needsRestart) {
<button
tuiCell="l"
[class.disabled]="!stateService.kiosk && !downloaded"
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
[disabled]="!stateService.kiosk && !downloaded"
(click)="removeMedia()"
>
<tui-avatar appearance="secondary" src="@tui.usb" />
@@ -90,11 +90,39 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
}
</button>
<!-- Step: Secure Boot Enrollment (when MOK enrolled) -->
@if (stateService.mokEnrolled) {
<button
tuiCell="l"
[class.disabled]="!usbRemoved"
[disabled]="!usbRemoved"
(click)="acknowledgeMok()"
>
<tui-avatar appearance="secondary" src="@tui.shield-check" />
<div tuiTitle>
{{ 'Secure Boot Enrollment' | i18n }}
<div tuiSubtitle>
{{
'Prepare for Secure Boot key enrollment on the next reboot'
| i18n
}}
</div>
</div>
@if (mokAcknowledged) {
<tui-icon icon="@tui.circle-check" class="g-positive" />
}
</button>
}
<!-- Step: Restart Server -->
<button
tuiCell="l"
[class.disabled]="!usbRemoved"
[disabled]="!usbRemoved || rebooted || rebooting"
[class.disabled]="
!usbRemoved || (stateService.mokEnrolled && !mokAcknowledged)
"
[disabled]="
!usbRemoved || (stateService.mokEnrolled && !mokAcknowledged)
"
(click)="reboot()"
>
<tui-avatar appearance="secondary" src="@tui.rotate-cw" />
@@ -116,6 +144,16 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
<tui-icon icon="@tui.circle-check" class="g-positive" />
}
</button>
} @else if (stateService.kiosk) {
<button tuiCell="l" (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>
}
<!-- Step: Open Local Address (non-kiosk only) -->
@@ -137,22 +175,6 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
}
<!-- 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>
`,
@@ -198,6 +220,7 @@ export default class SuccessPage implements AfterViewInit {
lanAddress = ''
downloaded = false
usbRemoved = false
mokAcknowledged = false
rebooting = false
rebooted = false
@@ -212,8 +235,6 @@ export default class SuccessPage implements AfterViewInit {
}
download() {
if (this.downloaded) return
const lanElem = this.document.getElementById('lan-addr')
if (lanElem) lanElem.innerHTML = this.lanAddress
@@ -243,6 +264,19 @@ export default class SuccessPage implements AfterViewInit {
})
}
acknowledgeMok() {
this.dialogs
.openComponent<boolean>(new PolymorpheusComponent(MokEnrollmentDialog), {
label: 'Secure Boot',
size: 'm',
dismissible: false,
closeable: false,
})
.subscribe(() => {
this.mokAcknowledged = true
})
}
exitKiosk() {
this.api.exit()
}
@@ -252,6 +286,8 @@ export default class SuccessPage implements AfterViewInit {
}
async reboot() {
if (this.rebooting || this.rebooted) return
this.rebooting = true
try {
@@ -276,20 +312,6 @@ export default class SuccessPage implements AfterViewInit {
await this.api.exit()
}
}
if (this.stateService.mokEnrolled && this.result.needsRestart) {
this.dialogs
.openComponent<boolean>(
new PolymorpheusComponent(MokEnrollmentDialog),
{
label: 'Secure Boot',
size: 's',
dismissible: false,
closeable: true,
},
)
.subscribe()
}
} catch (e: any) {
this.errorService.handleError(e)
}

View File

@@ -116,7 +116,7 @@ export class MockApiService extends ApiService {
return {
guid: 'mock-data-guid',
attach: !params.dataDrive.wipe,
mokEnrolled: false,
mokEnrolled: true,
}
}