feat: update start-tunnel web app for typed tunnel API

- Use generated TS types for tunnel API params and data models
- Simplify API service methods to use typed RPC calls
- Update port forward UI for optional labels
This commit is contained in:
Aiden McClelland
2026-03-12 13:38:42 -06:00
parent 6091314981
commit 517bf80fc8
9 changed files with 107 additions and 179 deletions

View File

@@ -1,8 +1,7 @@
import { Signal } from '@angular/core' import { Signal } from '@angular/core'
import { AbstractControl } from '@angular/forms' import { AbstractControl } from '@angular/forms'
import { utils } from '@start9labs/start-sdk' import { T, utils } from '@start9labs/start-sdk'
import { IpNet } from '@start9labs/start-sdk/util' import { IpNet } from '@start9labs/start-sdk/util'
import { WgServer } from 'src/app/services/patch-db/data-model'
export interface MappedDevice { export interface MappedDevice {
readonly subnet: { readonly subnet: {
@@ -16,7 +15,7 @@ export interface MappedDevice {
export interface MappedSubnet { export interface MappedSubnet {
readonly range: string readonly range: string
readonly name: string readonly name: string
readonly clients: WgServer['subnets']['']['clients'] readonly clients: T.Tunnel.WgSubnetClients
} }
export interface DeviceData { export interface DeviceData {

View File

@@ -15,11 +15,12 @@ import {
import { TuiFieldErrorPipe } from '@taiga-ui/kit' import { TuiFieldErrorPipe } from '@taiga-ui/kit'
import { TuiForm } from '@taiga-ui/layout' import { TuiForm } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { T } from '@start9labs/start-sdk'
import { ApiService } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
export interface EditLabelData { export interface EditLabelData {
readonly source: string readonly source: string
readonly label: string readonly label: T.Tunnel.PortForwardEntry['label']
} }
@Component({ @Component({

View File

@@ -1,4 +1,5 @@
import { Signal } from '@angular/core' import { Signal } from '@angular/core'
import { T } from '@start9labs/start-sdk'
export interface MappedDevice { export interface MappedDevice {
readonly ip: string readonly ip: string
@@ -10,8 +11,8 @@ export interface MappedForward {
readonly externalport: string readonly externalport: string
readonly device: MappedDevice readonly device: MappedDevice
readonly internalport: string readonly internalport: string
readonly label: string readonly label: T.Tunnel.PortForwardEntry['label']
readonly enabled: boolean readonly enabled: T.Tunnel.PortForwardEntry['enabled']
} }
export interface PortForwardsData { export interface PortForwardsData {

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { Dump } from 'patch-db-client' import { Dump } from 'patch-db-client'
import { TunnelData } from '../patch-db/data-model'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { TunnelData } from '../patch-db/data-model'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -10,77 +11,43 @@ export abstract class ApiService {
abstract openWebsocket$<T>(guid: string): Observable<T> abstract openWebsocket$<T>(guid: string): Observable<T>
abstract subscribe(): Promise<SubscribeRes> // db.subscribe abstract subscribe(): Promise<SubscribeRes> // db.subscribe
// auth // auth
abstract login(params: LoginReq): Promise<null> // auth.login abstract login(params: T.Tunnel.SetPasswordParams): Promise<null> // auth.login
abstract logout(): Promise<null> // auth.logout abstract logout(): Promise<null> // auth.logout
abstract setPassword(params: LoginReq): Promise<null> // auth.set-password abstract setPassword(params: T.Tunnel.SetPasswordParams): Promise<null> // auth.set-password
// subnets // subnets
abstract addSubnet(params: UpsertSubnetReq): Promise<null> // subnet.add abstract addSubnet(
abstract editSubnet(params: UpsertSubnetReq): Promise<null> // subnet.add params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams,
abstract deleteSubnet(params: DeleteSubnetReq): Promise<null> // subnet.remove ): Promise<null> // subnet.add
abstract editSubnet(
params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams,
): Promise<null> // subnet.edit
abstract deleteSubnet(params: T.Tunnel.SubnetParams): Promise<null> // subnet.remove
// devices // devices
abstract addDevice(params: UpsertDeviceReq): Promise<null> // device.add abstract addDevice(params: T.Tunnel.AddDeviceParams): Promise<null> // device.add
abstract editDevice(params: UpsertDeviceReq): Promise<null> // device.add abstract editDevice(params: T.Tunnel.AddDeviceParams): Promise<null> // device.edit
abstract deleteDevice(params: DeleteDeviceReq): Promise<null> // device.remove abstract deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise<null> // device.remove
abstract showDeviceConfig(params: DeleteDeviceReq): Promise<string> // device.show-config abstract showDeviceConfig(
params: T.Tunnel.RemoveDeviceParams,
): Promise<string> // device.show-config
// forwards // forwards
abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add abstract addForward(
abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove params: T.Tunnel.AddPortForwardParams,
abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> // port-forward.update-label ): Promise<null> // port-forward.add
abstract setForwardEnabled(params: SetForwardEnabledReq): Promise<null> // port-forward.set-enabled abstract deleteForward(
params: T.Tunnel.RemovePortForwardParams,
): Promise<null> // port-forward.remove
abstract updateForwardLabel(
params: T.Tunnel.UpdatePortForwardLabelParams,
): Promise<null> // port-forward.update-label
abstract setForwardEnabled(
params: T.Tunnel.SetPortForwardEnabledParams,
): Promise<null> // port-forward.set-enabled
// update // update
abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check abstract checkUpdate(): Promise<T.Tunnel.TunnelUpdateResult> // update.check
abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply abstract applyUpdate(): Promise<T.Tunnel.TunnelUpdateResult> // update.apply
} }
export type SubscribeRes = { export type SubscribeRes = {
dump: Dump<TunnelData> dump: Dump<TunnelData>
guid: string guid: string
} }
export type LoginReq = { password: string }
export type UpsertSubnetReq = {
name: string
subnet: string
}
export type DeleteSubnetReq = {
subnet: string
}
export type UpsertDeviceReq = {
name: string
subnet: string
ip: string
}
export type DeleteDeviceReq = {
subnet: string
ip: string
}
export type AddForwardReq = {
source: string // externalip:port
target: string // internalip:port
label: string
}
export type DeleteForwardReq = {
source: string
}
export type UpdateForwardLabelReq = {
source: string
label: string
}
export type SetForwardEnabledReq = {
source: string
enabled: boolean
}
export type TunnelUpdateResult = {
status: string
installed: string
candidate: string
}

View File

@@ -8,20 +8,8 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { filter, firstValueFrom, Observable } from 'rxjs' import { filter, firstValueFrom, Observable } from 'rxjs'
import { webSocket } from 'rxjs/webSocket' import { webSocket } from 'rxjs/webSocket'
import { import { T } from '@start9labs/start-sdk'
AddForwardReq, import { ApiService, SubscribeRes } from './api.service'
ApiService,
DeleteDeviceReq,
DeleteForwardReq,
DeleteSubnetReq,
LoginReq,
SubscribeRes,
TunnelUpdateResult,
SetForwardEnabledReq,
UpdateForwardLabelReq,
UpsertDeviceReq,
UpsertSubnetReq,
} from './api.service'
import { AuthService } from '../auth.service' import { AuthService } from '../auth.service'
import { PATCH_CACHE } from '../patch-db/patch-db-source' import { PATCH_CACHE } from '../patch-db/patch-db-source'
@@ -54,7 +42,7 @@ export class LiveApiService extends ApiService {
// auth // auth
async login(params: LoginReq): Promise<null> { async login(params: T.Tunnel.SetPasswordParams): Promise<null> {
return this.rpcRequest({ method: 'auth.login', params }) return this.rpcRequest({ method: 'auth.login', params })
} }
@@ -62,75 +50,87 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'auth.logout', params: {} }) return this.rpcRequest({ method: 'auth.logout', params: {} })
} }
async setPassword(params: LoginReq): Promise<null> { async setPassword(params: T.Tunnel.SetPasswordParams): Promise<null> {
return this.rpcRequest({ method: 'auth.set-password', params }) return this.rpcRequest({ method: 'auth.set-password', params })
} }
async addSubnet(params: UpsertSubnetReq): Promise<null> { async addSubnet(
params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams,
): Promise<null> {
return this.upsertSubnet(params) return this.upsertSubnet(params)
} }
async editSubnet(params: UpsertSubnetReq): Promise<null> { async editSubnet(
params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams,
): Promise<null> {
return this.upsertSubnet(params) return this.upsertSubnet(params)
} }
async deleteSubnet(params: DeleteSubnetReq): Promise<null> { async deleteSubnet(params: T.Tunnel.SubnetParams): Promise<null> {
return this.rpcRequest({ method: 'subnet.remove', params }) return this.rpcRequest({ method: 'subnet.remove', params })
} }
// devices // devices
async addDevice(params: UpsertDeviceReq): Promise<null> { async addDevice(params: T.Tunnel.AddDeviceParams): Promise<null> {
return this.upsertDevice(params) return this.upsertDevice(params)
} }
async editDevice(params: UpsertDeviceReq): Promise<null> { async editDevice(params: T.Tunnel.AddDeviceParams): Promise<null> {
return this.upsertDevice(params) return this.upsertDevice(params)
} }
async deleteDevice(params: DeleteDeviceReq): Promise<null> { async deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise<null> {
return this.rpcRequest({ method: 'device.remove', params }) return this.rpcRequest({ method: 'device.remove', params })
} }
async showDeviceConfig(params: DeleteDeviceReq): Promise<string> { async showDeviceConfig(params: T.Tunnel.RemoveDeviceParams): Promise<string> {
return this.rpcRequest({ method: 'device.show-config', params }) return this.rpcRequest({ method: 'device.show-config', params })
} }
// forwards // forwards
async addForward(params: AddForwardReq): Promise<null> { async addForward(params: T.Tunnel.AddPortForwardParams): Promise<null> {
return this.rpcRequest({ method: 'port-forward.add', params }) return this.rpcRequest({ method: 'port-forward.add', params })
} }
async deleteForward(params: DeleteForwardReq): Promise<null> { async deleteForward(
params: T.Tunnel.RemovePortForwardParams,
): Promise<null> {
return this.rpcRequest({ method: 'port-forward.remove', params }) return this.rpcRequest({ method: 'port-forward.remove', params })
} }
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> { async updateForwardLabel(
params: T.Tunnel.UpdatePortForwardLabelParams,
): Promise<null> {
return this.rpcRequest({ method: 'port-forward.update-label', params }) return this.rpcRequest({ method: 'port-forward.update-label', params })
} }
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> { async setForwardEnabled(
params: T.Tunnel.SetPortForwardEnabledParams,
): Promise<null> {
return this.rpcRequest({ method: 'port-forward.set-enabled', params }) return this.rpcRequest({ method: 'port-forward.set-enabled', params })
} }
// update // update
async checkUpdate(): Promise<TunnelUpdateResult> { async checkUpdate(): Promise<T.Tunnel.TunnelUpdateResult> {
return this.rpcRequest({ method: 'update.check', params: {} }) return this.rpcRequest({ method: 'update.check', params: {} })
} }
async applyUpdate(): Promise<TunnelUpdateResult> { async applyUpdate(): Promise<T.Tunnel.TunnelUpdateResult> {
return this.rpcRequest({ method: 'update.apply', params: {} }) return this.rpcRequest({ method: 'update.apply', params: {} })
} }
// private // private
private async upsertSubnet(params: UpsertSubnetReq): Promise<null> { private async upsertSubnet(
params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams,
): Promise<null> {
return this.rpcRequest({ method: 'subnet.add', params }) return this.rpcRequest({ method: 'subnet.add', params })
} }
private async upsertDevice(params: UpsertDeviceReq): Promise<null> { private async upsertDevice(params: T.Tunnel.AddDeviceParams): Promise<null> {
return this.rpcRequest({ method: 'device.add', params }) return this.rpcRequest({ method: 'device.add', params })
} }

View File

@@ -1,21 +1,9 @@
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { shareReplay, Subject, tap } from 'rxjs' import { shareReplay, Subject, tap } from 'rxjs'
import { WebSocketSubject } from 'rxjs/webSocket' import { WebSocketSubject } from 'rxjs/webSocket'
import { import { ApiService, SubscribeRes } from './api.service'
AddForwardReq,
ApiService,
DeleteDeviceReq,
DeleteForwardReq,
DeleteSubnetReq,
LoginReq,
SubscribeRes,
TunnelUpdateResult,
SetForwardEnabledReq,
UpdateForwardLabelReq,
UpsertDeviceReq,
UpsertSubnetReq,
} from './api.service'
import { pauseFor } from '@start9labs/shared' import { pauseFor } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { AuthService } from '../auth.service' import { AuthService } from '../auth.service'
import { import {
AddOperation, AddOperation,
@@ -26,12 +14,7 @@ import {
Revision, Revision,
} from 'patch-db-client' } from 'patch-db-client'
import { toObservable } from '@angular/core/rxjs-interop' import { toObservable } from '@angular/core/rxjs-interop'
import { import { mockTunnelData } from '../patch-db/data-model'
mockTunnelData,
PortForwardEntry,
WgClient,
WgSubnet,
} from '../patch-db/data-model'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -66,7 +49,7 @@ export class MockApiService extends ApiService {
} }
} }
async login(params: LoginReq): Promise<null> { async login(params: T.Tunnel.SetPasswordParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
return null return null
} }
@@ -76,15 +59,15 @@ export class MockApiService extends ApiService {
return null return null
} }
async setPassword(params: LoginReq): Promise<null> { async setPassword(params: T.Tunnel.SetPasswordParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
return null return null
} }
async addSubnet(params: UpsertSubnetReq): Promise<null> { async addSubnet(params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: AddOperation<WgSubnet>[] = [ const patch: AddOperation<T.Tunnel.WgSubnetConfig>[] = [
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/wg/subnets/${replaceSlashes(params.subnet)}`, path: `/wg/subnets/${replaceSlashes(params.subnet)}`,
@@ -96,7 +79,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async editSubnet(params: UpsertSubnetReq): Promise<null> { async editSubnet(params: T.Tunnel.SubnetParams & T.Tunnel.AddSubnetParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: ReplaceOperation<string>[] = [ const patch: ReplaceOperation<string>[] = [
@@ -111,7 +94,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async deleteSubnet(params: DeleteSubnetReq): Promise<null> { async deleteSubnet(params: T.Tunnel.SubnetParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: RemoveOperation[] = [ const patch: RemoveOperation[] = [
@@ -125,14 +108,14 @@ export class MockApiService extends ApiService {
return null return null
} }
async addDevice(params: UpsertDeviceReq): Promise<null> { async addDevice(params: T.Tunnel.AddDeviceParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: AddOperation<WgClient>[] = [ const patch: AddOperation<T.Tunnel.WgConfig>[] = [
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/wg/subnets/${replaceSlashes(params.subnet)}/clients/${params.ip}`, path: `/wg/subnets/${replaceSlashes(params.subnet)}/clients/${params.ip}`,
value: { name: params.name }, value: { name: params.name, key: '', psk: '' },
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)
@@ -140,7 +123,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async editDevice(params: UpsertDeviceReq): Promise<null> { async editDevice(params: T.Tunnel.AddDeviceParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: ReplaceOperation<string>[] = [ const patch: ReplaceOperation<string>[] = [
@@ -155,7 +138,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async deleteDevice(params: DeleteDeviceReq): Promise<null> { async deleteDevice(params: T.Tunnel.RemoveDeviceParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: RemoveOperation[] = [ const patch: RemoveOperation[] = [
@@ -169,22 +152,22 @@ export class MockApiService extends ApiService {
return null return null
} }
async showDeviceConfig(params: DeleteDeviceReq): Promise<string> { async showDeviceConfig(params: T.Tunnel.RemoveDeviceParams): Promise<string> {
await pauseFor(1000) await pauseFor(1000)
return MOCK_CONFIG return MOCK_CONFIG
} }
async addForward(params: AddForwardReq): Promise<null> { async addForward(params: T.Tunnel.AddPortForwardParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: AddOperation<PortForwardEntry>[] = [ const patch: AddOperation<T.Tunnel.PortForwardEntry>[] = [
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/portForwards/${params.source}`, path: `/portForwards/${params.source}`,
value: { value: {
target: params.target, target: params.target,
label: params.label || '', label: params.label || null,
enabled: true, enabled: true,
}, },
}, },
@@ -194,10 +177,10 @@ export class MockApiService extends ApiService {
return null return null
} }
async updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> { async updateForwardLabel(params: T.Tunnel.UpdatePortForwardLabelParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: ReplaceOperation<string>[] = [ const patch: ReplaceOperation<string | null>[] = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: `/portForwards/${params.source}/label`, path: `/portForwards/${params.source}/label`,
@@ -209,7 +192,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> { async setForwardEnabled(params: T.Tunnel.SetPortForwardEnabledParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: ReplaceOperation<boolean>[] = [ const patch: ReplaceOperation<boolean>[] = [
@@ -224,7 +207,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async deleteForward(params: DeleteForwardReq): Promise<null> { async deleteForward(params: T.Tunnel.RemovePortForwardParams): Promise<null> {
await pauseFor(1000) await pauseFor(1000)
const patch: RemoveOperation[] = [ const patch: RemoveOperation[] = [
@@ -238,7 +221,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async checkUpdate(): Promise<TunnelUpdateResult> { async checkUpdate(): Promise<T.Tunnel.TunnelUpdateResult> {
await pauseFor(1000) await pauseFor(1000)
return { return {
status: 'update-available', status: 'update-available',
@@ -247,7 +230,7 @@ export class MockApiService extends ApiService {
} }
} }
async applyUpdate(): Promise<TunnelUpdateResult> { async applyUpdate(): Promise<T.Tunnel.TunnelUpdateResult> {
await pauseFor(2000) await pauseFor(2000)
return { return {
status: 'updating', status: 'updating',

View File

@@ -1,45 +1,21 @@
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
export type PortForwardEntry = { export type TunnelData = Pick<
target: string T.Tunnel.TunnelDatabase,
label: string 'wg' | 'portForwards' | 'gateways'
enabled: boolean >
}
export type TunnelData = {
wg: WgServer
portForwards: Record<string, PortForwardEntry>
gateways: Record<string, T.NetworkInterfaceInfo>
}
export type WgServer = {
subnets: Record<string, WgSubnet>
}
export type WgSubnet = {
name: string
clients: Record<string, WgClient>
}
export type WgClient = {
name: string
}
export const mockTunnelData: TunnelData = { export const mockTunnelData: TunnelData = {
wg: { wg: {
port: 51820,
key: '',
subnets: { subnets: {
'10.59.0.0/24': { '10.59.0.0/24': {
name: 'Family', name: 'Family',
clients: { clients: {
'10.59.0.2': { '10.59.0.2': { name: 'Start9 Server', key: '', psk: '' },
name: 'Start9 Server', '10.59.0.3': { name: 'Phone', key: '', psk: '' },
}, '10.59.0.4': { name: 'Laptop', key: '', psk: '' },
'10.59.0.3': {
name: 'Phone',
},
'10.59.0.4': {
name: 'Laptop',
},
}, },
}, },
}, },

View File

@@ -14,7 +14,8 @@ import {
switchMap, switchMap,
takeWhile, takeWhile,
} from 'rxjs' } from 'rxjs'
import { ApiService, TunnelUpdateResult } from './api/api.service' import { T } from '@start9labs/start-sdk'
import { ApiService } from './api/api.service'
import { AuthService } from './auth.service' import { AuthService } from './auth.service'
@Component({ @Component({
@@ -34,7 +35,7 @@ export class UpdateService {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
readonly result = signal<TunnelUpdateResult | null>(null) readonly result = signal<T.Tunnel.TunnelUpdateResult | null>(null)
readonly hasUpdate = computed( readonly hasUpdate = computed(
() => this.result()?.status === 'update-available', () => this.result()?.status === 'update-available',
) )
@@ -60,7 +61,7 @@ export class UpdateService {
this.setResult(result) this.setResult(result)
} }
private setResult(result: TunnelUpdateResult): void { private setResult(result: T.Tunnel.TunnelUpdateResult): void {
this.result.set(result) this.result.set(result)
if (result.status === 'updating') { if (result.status === 'updating') {

View File

@@ -239,7 +239,7 @@ export const mockPatchData: DataModel = {
caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15', caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15',
ntpSynced: false, ntpSynced: false,
smtp: null, smtp: null,
ifconfigUrl: 'https://ifconfig.co', echoipUrls: ['https://ipconfig.me', 'https://ifconfig.co'],
platform: 'x86_64-nonfree', platform: 'x86_64-nonfree',
zram: true, zram: true,
governor: 'performance', governor: 'performance',