mirror of
https://github.com/pockethost/pockethost.git
synced 2025-03-30 15:08:30 +00:00
refactor: logging and async services
This commit is contained in:
parent
f5c9f349e3
commit
80f10f528d
27
packages/common/src/Logger.ts
Normal file
27
packages/common/src/Logger.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export type Config = {
|
||||
debug: boolean
|
||||
}
|
||||
|
||||
export type Logger = ReturnType<typeof createLogger>
|
||||
|
||||
export const createLogger = (config: Config) => {
|
||||
const { debug } = config
|
||||
const dbg = (...args: any[]) => {
|
||||
if (!debug) return
|
||||
console.log(`[DBG]`, ...args)
|
||||
}
|
||||
|
||||
const warn = (...args: any[]) => {
|
||||
console.log(`[WARN]`, ...args)
|
||||
}
|
||||
|
||||
const info = (...args: any[]) => {
|
||||
console.log(`[INFO]`, ...args)
|
||||
}
|
||||
|
||||
const error = (...args: any[]) => {
|
||||
console.error(`[ERROR]`, ...args)
|
||||
}
|
||||
|
||||
return { dbg, warn, info, error }
|
||||
}
|
57
packages/common/src/PromiseHelper.ts
Normal file
57
packages/common/src/PromiseHelper.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { ClientResponseError } from 'pocketbase'
|
||||
import { Logger } from './Logger'
|
||||
|
||||
export type PromiseHelperConfig = {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type PromiseHelper = ReturnType<typeof createPromiseHelper>
|
||||
|
||||
export const createPromiseHelper = (config: PromiseHelperConfig) => {
|
||||
const { logger } = config
|
||||
const { dbg, error, warn } = logger
|
||||
|
||||
let inside = ''
|
||||
let c = 0
|
||||
const safeCatch = <TIn extends any[], TOut>(
|
||||
name: string,
|
||||
cb: (...args: TIn) => Promise<TOut>
|
||||
) => {
|
||||
return (...args: TIn) => {
|
||||
const _c = c++
|
||||
const uuid = `${name}:${_c}`
|
||||
const pfx = `[safeCatch:${uuid}]`
|
||||
// dbg(uuid, ...args)
|
||||
const tid = setTimeout(() => {
|
||||
warn(pfx, `timeout waiting for ${pfx}`)
|
||||
}, 100)
|
||||
|
||||
inside = pfx
|
||||
return cb(...args)
|
||||
.then((res) => {
|
||||
// dbg(uuid, `finished`)
|
||||
inside = ''
|
||||
clearTimeout(tid)
|
||||
return res
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (e instanceof ClientResponseError) {
|
||||
error(pfx, `PocketBase API error ${e}`)
|
||||
error(pfx, JSON.stringify(e.data, null, 2))
|
||||
if (e.status === 400) {
|
||||
error(
|
||||
pfx,
|
||||
`It looks like you don't have permission to make this request.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
error(pfx, `failed: ${e}`)
|
||||
}
|
||||
error(pfx, e)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { safeCatch }
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
export * from './assert'
|
||||
export * from './Logger'
|
||||
export * from './pocketbase-client-helpers'
|
||||
export * from './PromiseHelper'
|
||||
export * from './releases'
|
||||
export * from './schema'
|
||||
export * from './TimerManager'
|
||||
|
110
packages/common/src/pocketbase-client-helpers/RpcHelper.ts
Normal file
110
packages/common/src/pocketbase-client-helpers/RpcHelper.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import type pocketbaseEs from 'pocketbase'
|
||||
import type { RecordSubscription } from 'pocketbase'
|
||||
import type { JsonObject } from 'type-fest'
|
||||
import { Logger } from '../Logger'
|
||||
import { PromiseHelper } from '../PromiseHelper'
|
||||
import { RecordId, RpcCommands, UserId } from '../schema'
|
||||
import type { WatchHelper } from './WatchHelper'
|
||||
|
||||
export type RpcHelperConfig = {
|
||||
client: pocketbaseEs
|
||||
watchHelper: WatchHelper
|
||||
promiseHelper: PromiseHelper
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type RpcHelper = ReturnType<typeof createRpcHelper>
|
||||
|
||||
export enum RpcStatus {
|
||||
New = 'new',
|
||||
Queued = 'queued',
|
||||
Running = 'running',
|
||||
Starting = 'starting',
|
||||
FinishedSuccess = 'finished-success',
|
||||
FinishedError = 'finished-error',
|
||||
}
|
||||
|
||||
export type RpcPayloadBase = JsonObject
|
||||
|
||||
export type RpcRecord_In<TRecord extends RpcRecord<any, any>> = Pick<
|
||||
TRecord,
|
||||
'userId' | 'payload' | 'cmd'
|
||||
>
|
||||
|
||||
export type RpcRecord<
|
||||
TPayload extends RpcPayloadBase,
|
||||
TRes extends JsonObject
|
||||
> = {
|
||||
id: RecordId
|
||||
userId: UserId
|
||||
cmd: string
|
||||
payload: TPayload
|
||||
status: RpcStatus
|
||||
message: string
|
||||
result: TRes
|
||||
}
|
||||
|
||||
export const RPC_COLLECTION = `rpc`
|
||||
|
||||
export const createRpcHelper = (config: RpcHelperConfig) => {
|
||||
const {
|
||||
client,
|
||||
watchHelper: { watchById },
|
||||
promiseHelper: { safeCatch },
|
||||
} = config
|
||||
|
||||
const mkRpc = <TPayload extends JsonObject, TResult extends JsonObject>(
|
||||
cmd: RpcCommands
|
||||
) => {
|
||||
type ConcreteRpcRecord = RpcRecord<TPayload, TResult>
|
||||
|
||||
return safeCatch(
|
||||
cmd,
|
||||
async (
|
||||
payload: TPayload,
|
||||
cb?: (data: RecordSubscription<ConcreteRpcRecord>) => void
|
||||
) => {
|
||||
const _user = client.authStore.model
|
||||
if (!_user) {
|
||||
throw new Error(`Expected authenticated user here.`)
|
||||
}
|
||||
const { id: userId } = _user
|
||||
const rpcIn: RpcRecord_In<ConcreteRpcRecord> = {
|
||||
cmd,
|
||||
userId,
|
||||
payload,
|
||||
}
|
||||
const rec = await client
|
||||
.collection(RPC_COLLECTION)
|
||||
.create<ConcreteRpcRecord>(rpcIn)
|
||||
return new Promise<ConcreteRpcRecord['result']>(
|
||||
async (resolve, reject) => {
|
||||
const unsub = watchById<ConcreteRpcRecord>(
|
||||
RPC_COLLECTION,
|
||||
rec.id,
|
||||
(data) => {
|
||||
if (data.record.status === RpcStatus.FinishedSuccess) {
|
||||
unsub.then((u) => {
|
||||
u()
|
||||
resolve(data.record.result)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (data.record.status === RpcStatus.FinishedError) {
|
||||
unsub.then((u) => {
|
||||
reject(data.record.message)
|
||||
u()
|
||||
})
|
||||
return
|
||||
}
|
||||
cb?.(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return { mkRpc }
|
||||
}
|
72
packages/common/src/pocketbase-client-helpers/WatchHelper.ts
Normal file
72
packages/common/src/pocketbase-client-helpers/WatchHelper.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { RecordId } from '@pockethost/common'
|
||||
import type pocketbaseEs from 'pocketbase'
|
||||
import type { RecordSubscription, UnsubscribeFunc } from 'pocketbase'
|
||||
import { Logger } from '../Logger'
|
||||
import { PromiseHelper } from '../PromiseHelper'
|
||||
|
||||
export type WatchHelperConfig = {
|
||||
client: pocketbaseEs
|
||||
promiseHelper: PromiseHelper
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type WatchHelper = ReturnType<typeof createWatchHelper>
|
||||
|
||||
export const createWatchHelper = (config: WatchHelperConfig) => {
|
||||
const {
|
||||
client,
|
||||
promiseHelper: { safeCatch },
|
||||
} = config
|
||||
|
||||
const watchById = safeCatch(
|
||||
`subscribe`,
|
||||
async <TRec>(
|
||||
collectionName: string,
|
||||
id: RecordId,
|
||||
cb: (data: RecordSubscription<TRec>) => void,
|
||||
initialFetch = true
|
||||
) => {
|
||||
const unsub = await client
|
||||
.collection(collectionName)
|
||||
.subscribe<TRec>(id, cb)
|
||||
if (initialFetch) {
|
||||
const initial = await client.collection(collectionName).getOne<TRec>(id)
|
||||
if (!initial) {
|
||||
throw new Error(`Expected ${collectionName}.${id} to exist.`)
|
||||
}
|
||||
cb({ action: 'update', record: initial })
|
||||
}
|
||||
return unsub
|
||||
}
|
||||
)
|
||||
|
||||
const watchAllById = safeCatch(
|
||||
`watchAllById`,
|
||||
async <TRec>(
|
||||
collectionName: string,
|
||||
idName: keyof TRec,
|
||||
idValue: RecordId,
|
||||
cb: (data: RecordSubscription<TRec>) => void,
|
||||
initialFetch = true
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
const unsub = client
|
||||
.collection(collectionName)
|
||||
.subscribe<TRec>('*', (e) => {
|
||||
// console.log(e.record.instanceId, id)
|
||||
if (e.record[idName] !== idValue) return
|
||||
cb(e)
|
||||
})
|
||||
if (initialFetch) {
|
||||
const existing = await client
|
||||
.collection(collectionName)
|
||||
.getFullList<TRec>(100, {
|
||||
filter: `${idName.toString()} = '${idValue}'`,
|
||||
})
|
||||
existing.forEach((record) => cb({ action: 'init', record }))
|
||||
}
|
||||
return unsub
|
||||
}
|
||||
)
|
||||
|
||||
return { watchById, watchAllById }
|
||||
}
|
2
packages/common/src/pocketbase-client-helpers/index.ts
Normal file
2
packages/common/src/pocketbase-client-helpers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './RpcHelper'
|
||||
export * from './WatchHelper'
|
@ -7,7 +7,7 @@ import {
|
||||
InstanceId,
|
||||
InstancesRecord,
|
||||
} from '@pockethost/common'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { MixinContext } from './PbClient'
|
||||
|
||||
export type BackupApi = ReturnType<typeof createBackupMixin>
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
import { reduce } from '@s-libs/micro-dash'
|
||||
import Bottleneck from 'bottleneck'
|
||||
import { endOfMonth, startOfMonth } from 'date-fns'
|
||||
import { dbg } from '../util/dbg'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { dbg } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { MixinContext } from './PbClient'
|
||||
|
||||
export type InstanceApi = ReturnType<typeof createInstanceMixin>
|
||||
|
@ -3,8 +3,8 @@ import {
|
||||
InvocationRecord,
|
||||
pocketNow,
|
||||
} from '@pockethost/common'
|
||||
import { dbg } from '../util/dbg'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { dbg } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { InstanceApi } from './InstanceMIxin'
|
||||
import { MixinContext } from './PbClient'
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { JobRecord, JobStatus } from '@pockethost/common'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { MixinContext } from './PbClient'
|
||||
|
||||
export enum RecordSubscriptionActions {
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
} from 'pocketbase'
|
||||
import { DAEMON_PB_DATA_DIR, PUBLIC_PB_SUBDOMAIN } from '../constants'
|
||||
import { Collection_Serialized } from '../migrate/schema'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { info } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { createBackupMixin } from './BackupMixin'
|
||||
import { createInstanceMixin } from './InstanceMIxin'
|
||||
import { createInvocationMixin } from './InvocationMixin'
|
||||
@ -18,7 +19,7 @@ export type PocketbaseClientApi = ReturnType<typeof createPbClient>
|
||||
export type MixinContext = { client: pocketbaseEs; rawDb: Knex }
|
||||
|
||||
export const createPbClient = (url: string) => {
|
||||
console.log(`Initializing client: ${url}`)
|
||||
info(`Initializing client: ${url}`)
|
||||
const rawDb = createRawPbClient(
|
||||
`${DAEMON_PB_DATA_DIR}/${PUBLIC_PB_SUBDOMAIN}/pb_data/data.db`
|
||||
)
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
PUBLIC_PB_SUBDOMAIN,
|
||||
} from '../constants'
|
||||
import { backupInstance } from '../util/backupInstance'
|
||||
import { error } from '../util/dbg'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { dbg, error, info } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { pexec } from './pexec'
|
||||
import { schema } from './schema'
|
||||
import { withInstance } from './withInstance'
|
||||
@ -20,11 +20,11 @@ safeCatch(`root`, async () => {
|
||||
PUBLIC_PB_SUBDOMAIN,
|
||||
`${+new Date()}`,
|
||||
async (progress) => {
|
||||
console.log(progress)
|
||||
dbg(progress)
|
||||
}
|
||||
)
|
||||
|
||||
console.log(`Upgrading`)
|
||||
info(`Upgrading`)
|
||||
await pexec(`${PB_BIN} upgrade --dir=pb_data`)
|
||||
|
||||
await withInstance(async (client) => {
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { exec } from 'child_process'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { dbg, error } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
|
||||
export const pexec = safeCatch(`pexec`, (cmd: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log(cmd)
|
||||
dbg(cmd)
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
console.log(stdout)
|
||||
console.error(stderr)
|
||||
dbg(stdout)
|
||||
if (err) {
|
||||
error(`${err}`)
|
||||
error(stderr)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ import {
|
||||
} from '../constants'
|
||||
import { createPbClient, PocketbaseClientApi } from '../db/PbClient'
|
||||
import { mkInternalUrl } from '../util/internal'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { error, info } from '../util/logger'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { spawnInstance } from '../util/spawnInstance'
|
||||
import { tryFetch } from '../util/tryFetch'
|
||||
|
||||
@ -31,18 +32,16 @@ export const withInstance = safeCatch(
|
||||
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)
|
||||
await cb(client)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
error(
|
||||
`***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/`
|
||||
)
|
||||
console.error(
|
||||
`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`
|
||||
)
|
||||
error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`)
|
||||
} finally {
|
||||
console.log(`Exiting process`)
|
||||
info(`Exiting process`)
|
||||
mainProcess.kill()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${e}`)
|
||||
error(`${e}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -13,6 +13,7 @@ import { createInstanceService } from './services/InstanceService'
|
||||
import { createJobService } from './services/JobService'
|
||||
import { createProxyService } from './services/ProxyService'
|
||||
import { mkInternalUrl } from './util/internal'
|
||||
import { dbg, error, info } from './util/logger'
|
||||
import { spawnInstance } from './util/spawnInstance'
|
||||
// npm install eventsource --save
|
||||
global.EventSource = require('eventsource')
|
||||
@ -36,14 +37,12 @@ global.EventSource = require('eventsource')
|
||||
const instanceService = await createInstanceService(client)
|
||||
try {
|
||||
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)
|
||||
console.log(`Logged in`)
|
||||
dbg(`Logged in`)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
error(
|
||||
`***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/`
|
||||
)
|
||||
console.error(
|
||||
`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`
|
||||
)
|
||||
error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`)
|
||||
}
|
||||
|
||||
const proxyService = await createProxyService(instanceService)
|
||||
@ -51,7 +50,7 @@ global.EventSource = require('eventsource')
|
||||
const backupService = await createBackupService(client, jobService)
|
||||
|
||||
process.once('SIGUSR2', async () => {
|
||||
console.log(`SIGUSR2 detected`)
|
||||
info(`SIGUSR2 detected`)
|
||||
proxyService.shutdown()
|
||||
instanceService.shutdown()
|
||||
jobService.shutdown()
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import Bottleneck from 'bottleneck'
|
||||
import { PocketbaseClientApi } from '../db/PbClient'
|
||||
import { backupInstance } from '../util/backupInstance'
|
||||
import { dbg } from '../util/dbg'
|
||||
import { dbg } from '../util/logger'
|
||||
import { JobServiceApi } from './JobService'
|
||||
|
||||
export const createBackupService = async (
|
||||
@ -66,7 +66,7 @@ export const createBackupService = async (
|
||||
tm.repeat(async () => {
|
||||
const backupRec = await client.getNextBackupJob()
|
||||
if (!backupRec) {
|
||||
dbg(`No backups requested`)
|
||||
// dbg(`No backups requested`)
|
||||
return true
|
||||
}
|
||||
const instance = await client.getInstance(backupRec.instanceId)
|
||||
|
@ -16,10 +16,10 @@ import {
|
||||
PUBLIC_APP_PROTOCOL,
|
||||
} from '../constants'
|
||||
import { PocketbaseClientApi } from '../db/PbClient'
|
||||
import { dbg } from '../util/dbg'
|
||||
import { mkInternalUrl } from '../util/internal'
|
||||
import { dbg, error, warn } from '../util/logger'
|
||||
import { now } from '../util/now'
|
||||
import { safeCatch } from '../util/safeAsync'
|
||||
import { safeCatch } from '../util/promiseHelper'
|
||||
import { PocketbaseProcess, spawnInstance } from '../util/spawnInstance'
|
||||
|
||||
type InstanceApi = {
|
||||
@ -68,7 +68,7 @@ export const createInstanceService = async (client: PocketbaseClientApi) => {
|
||||
port: DAEMON_PB_PORT_BASE,
|
||||
exclude,
|
||||
}).catch((e) => {
|
||||
console.error(`Failed to get port for ${subdomain}`)
|
||||
error(`Failed to get port for ${subdomain}`)
|
||||
throw e
|
||||
})
|
||||
dbg(`Found port for ${subdomain}: ${newPort}`)
|
||||
@ -81,7 +81,7 @@ export const createInstanceService = async (client: PocketbaseClientApi) => {
|
||||
port: newPort,
|
||||
bin: binFor(instance.platform, instance.version),
|
||||
onUnexpectedStop: (code) => {
|
||||
console.warn(`${subdomain} exited unexpectedly with ${code}`)
|
||||
warn(`${subdomain} exited unexpectedly with ${code}`)
|
||||
api.shutdown()
|
||||
},
|
||||
})
|
||||
|
@ -11,7 +11,7 @@ import { default as knexFactory } from 'knex'
|
||||
import pocketbaseEs from 'pocketbase'
|
||||
import { AsyncReturnType } from 'type-fest'
|
||||
import { PocketbaseClientApi } from '../db/PbClient'
|
||||
import { error } from '../util/dbg'
|
||||
import { dbg, error } from '../util/logger'
|
||||
|
||||
export type JobServiceApi = AsyncReturnType<typeof createJobService>
|
||||
|
||||
@ -46,7 +46,7 @@ export const createJobService = async (client: PocketbaseClientApi) => {
|
||||
if (!handler) {
|
||||
throw new Error(`Job handler ${cmd} is not registered`)
|
||||
}
|
||||
console.log(`Running job ${job.id}`, job)
|
||||
dbg(`Running job ${job.id}`, job)
|
||||
await client.setJobStatus(job, JobStatus.Running)
|
||||
await handler(job)
|
||||
await client.setJobStatus(job, JobStatus.FinishedSuccess)
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
PUBLIC_APP_PROTOCOL,
|
||||
PUBLIC_PB_SUBDOMAIN,
|
||||
} from '../constants'
|
||||
import { dbg, info } from '../util/dbg'
|
||||
import { mkInternalUrl } from '../util/internal'
|
||||
import { dbg, error, info } from '../util/logger'
|
||||
import { InstanceServiceApi } from './InstanceService'
|
||||
|
||||
export type ProxyServiceApi = AsyncReturnType<typeof createProxyService>
|
||||
@ -22,7 +22,7 @@ export const createProxyService = async (
|
||||
dbg(`Incoming request ${req.headers.host}/${req.url}`)
|
||||
|
||||
const die = (msg: string) => {
|
||||
console.error(`ERROR: ${msg}`)
|
||||
error(msg)
|
||||
res.writeHead(403, {
|
||||
'Content-Type': `text/plain`,
|
||||
})
|
||||
|
@ -6,9 +6,9 @@ import { Database } from 'sqlite3'
|
||||
import tmp from 'tmp'
|
||||
import { DAEMON_PB_DATA_DIR } from '../constants'
|
||||
import { pexec } from '../migrate/pexec'
|
||||
import { dbg, error } from './dbg'
|
||||
import { ensureDirExists } from './ensureDirExists'
|
||||
import { safeCatch } from './safeAsync'
|
||||
import { dbg, error } from './logger'
|
||||
import { safeCatch } from './promiseHelper'
|
||||
|
||||
export type BackupProgress = {
|
||||
current: number
|
||||
@ -75,7 +75,7 @@ export const backupInstance = safeCatch(
|
||||
unsafeCleanup: true,
|
||||
})
|
||||
const backupTmpTargetRoot = resolve(tmpObj.name)
|
||||
console.log({
|
||||
dbg({
|
||||
instanceId,
|
||||
dataRoot,
|
||||
backupTgzRoot,
|
||||
@ -109,7 +109,7 @@ export const backupInstance = safeCatch(
|
||||
error(`${e}`)
|
||||
throw e
|
||||
} finally {
|
||||
console.log(`Removing again ${backupTmpTargetRoot}`)
|
||||
dbg(`Removing again ${backupTmpTargetRoot}`)
|
||||
tmpObj.removeCallback()
|
||||
chdir(_cwd)
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { DEBUG } from '../constants'
|
||||
|
||||
export const dbg = (...args: any[]) => {
|
||||
if (!DEBUG) return
|
||||
console.log(...args)
|
||||
}
|
||||
|
||||
export const info = (...args: any[]) => {
|
||||
console.log(...args)
|
||||
}
|
||||
|
||||
export const error = (...args: any[]) => {
|
||||
console.error(...args)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { mkdirSync } from 'fs'
|
||||
import { dbg } from './dbg'
|
||||
import { dbg } from './logger'
|
||||
|
||||
export const ensureDirExists = (path: string) => {
|
||||
try {
|
||||
|
5
packages/daemon/src/util/logger.ts
Normal file
5
packages/daemon/src/util/logger.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createLogger } from '@pockethost/common'
|
||||
import { DEBUG } from '../constants'
|
||||
|
||||
export const logger = createLogger({ debug: DEBUG })
|
||||
export const { dbg, info, warn, error } = logger
|
5
packages/daemon/src/util/promiseHelper.ts
Normal file
5
packages/daemon/src/util/promiseHelper.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createPromiseHelper } from '@pockethost/common'
|
||||
import { logger } from './logger'
|
||||
|
||||
export const promiseHelper = createPromiseHelper({ logger })
|
||||
export const { safeCatch } = promiseHelper
|
@ -1,66 +0,0 @@
|
||||
import Bottleneck from 'bottleneck'
|
||||
import { ClientResponseError } from 'pocketbase'
|
||||
import { dbg, error } from './dbg'
|
||||
|
||||
const limiter = new Bottleneck({ maxConcurrent: 1 })
|
||||
|
||||
let inside = ''
|
||||
export const serialAsync = <TIn extends any[], TOut>(
|
||||
name: string,
|
||||
cb: (...args: TIn) => Promise<TOut>
|
||||
) => {
|
||||
const _cb = safeCatch(name, cb)
|
||||
|
||||
return (...args: TIn) => {
|
||||
return limiter.schedule(() => {
|
||||
if (inside) {
|
||||
throw new Error(
|
||||
`Already in async function ${inside}, can't execute ${name}`
|
||||
)
|
||||
}
|
||||
return _cb(...args).finally(() => {
|
||||
inside = ''
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let c = 0
|
||||
export const safeCatch = <TIn extends any[], TOut>(
|
||||
name: string,
|
||||
cb: (...args: TIn) => Promise<TOut>
|
||||
) => {
|
||||
return (...args: TIn) => {
|
||||
const _c = c++
|
||||
const uuid = `${name}:${_c}`
|
||||
dbg(uuid, ...args)
|
||||
const tid = setTimeout(() => {
|
||||
dbg(uuid, `WARNING: timeout waiting for ${uuid}`)
|
||||
}, 100)
|
||||
|
||||
inside = uuid
|
||||
return cb(...args)
|
||||
.then((res) => {
|
||||
dbg(uuid, `finished`)
|
||||
inside = ''
|
||||
clearTimeout(tid)
|
||||
return res
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (e instanceof ClientResponseError) {
|
||||
error(uuid, `PocketBase API error ${e}`)
|
||||
error(uuid, JSON.stringify(e.data, null, 2))
|
||||
if (e.status === 400) {
|
||||
error(
|
||||
uuid,
|
||||
`It looks like you don't have permission to make this request.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
error(uuid, `failed: ${e}`)
|
||||
}
|
||||
error(uuid, e)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
@ -2,9 +2,9 @@ import { spawn } from 'child_process'
|
||||
import { existsSync } from 'fs'
|
||||
import { AsyncReturnType } from 'type-fest'
|
||||
import { DAEMON_PB_BIN_DIR, DAEMON_PB_DATA_DIR } from '../constants'
|
||||
import { dbg } from './dbg'
|
||||
import { mkInternalAddress, mkInternalUrl } from './internal'
|
||||
import { safeCatch } from './safeAsync'
|
||||
import { dbg, error } from './logger'
|
||||
import { safeCatch } from './promiseHelper'
|
||||
import { tryFetch } from './tryFetch'
|
||||
export type PocketbaseProcess = AsyncReturnType<typeof spawnInstance>
|
||||
|
||||
@ -40,7 +40,7 @@ export const spawnInstance = safeCatch(`spawnInstance`, async (cfg: Config) => {
|
||||
})
|
||||
|
||||
ls.stderr.on('data', (data) => {
|
||||
console.error(`${subdomain} stderr: ${data}`)
|
||||
error(`${subdomain} stderr: ${data}`)
|
||||
})
|
||||
|
||||
ls.on('close', (code) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { dbg, error } from './dbg'
|
||||
import { safeCatch } from './safeAsync'
|
||||
import { dbg, error } from './logger'
|
||||
import { safeCatch } from './promiseHelper'
|
||||
|
||||
export const tryFetch = safeCatch(
|
||||
`tryFetch`,
|
||||
|
@ -39,4 +39,4 @@
|
||||
"sass": "^1.54.9",
|
||||
"svelte-highlight": "^6.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import CopyButton from '$components/CopyButton.svelte'
|
||||
import { dbg } from '$util/logger'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { typescript } from 'svelte-highlight/languages'
|
||||
import 'svelte-highlight/styles/github.css'
|
||||
import CopyButton from '$components/CopyButton.svelte'
|
||||
|
||||
export let code: string
|
||||
const handleCopy = () => {
|
||||
console.log('copied')
|
||||
dbg('copied')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { client } from '$src/pocketbase'
|
||||
import { warn } from '$util/logger'
|
||||
import publicRoutes from '$util/public-routes.json'
|
||||
import { getRouter } from '$util/utilities'
|
||||
import { onMount } from 'svelte'
|
||||
@ -11,7 +12,7 @@
|
||||
|
||||
const { pathname } = router
|
||||
if (!publicRoutes.includes(pathname)) {
|
||||
console.warn(`${pathname} is a private route`)
|
||||
warn(`${pathname} is a private route`)
|
||||
window.location.href = '/'
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createGenericSyncEvent } from '$util/events'
|
||||
import {
|
||||
assertExists,
|
||||
createWatchHelper,
|
||||
JobCommands,
|
||||
JobStatus,
|
||||
type BackupRecord,
|
||||
@ -13,6 +14,8 @@ import {
|
||||
type InstancesRecord_New,
|
||||
type JobRecord,
|
||||
type JobRecord_In,
|
||||
type Logger,
|
||||
type PromiseHelper,
|
||||
type UserRecord
|
||||
} from '@pockethost/common'
|
||||
import { keys, map } from '@s-libs/micro-dash'
|
||||
@ -24,7 +27,6 @@ import PocketBase, {
|
||||
type RecordSubscription,
|
||||
type UnsubscribeFunc
|
||||
} from 'pocketbase'
|
||||
import { safeCatch } from '../util/safeCatch'
|
||||
|
||||
export type AuthChangeHandler = (user: BaseAuthStore) => void
|
||||
|
||||
@ -35,9 +37,18 @@ export type AuthStoreProps = {
|
||||
isValid: boolean
|
||||
}
|
||||
|
||||
export type PocketbaseClientConfig = {
|
||||
url: string
|
||||
logger: Logger
|
||||
promiseHelper: PromiseHelper
|
||||
}
|
||||
export type PocketbaseClient = ReturnType<typeof createPocketbaseClient>
|
||||
|
||||
export const createPocketbaseClient = (url: string) => {
|
||||
export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
|
||||
const { url, logger, promiseHelper } = config
|
||||
const { dbg, error } = logger
|
||||
const { safeCatch } = promiseHelper
|
||||
|
||||
const client = new PocketBase(url)
|
||||
|
||||
const { authStore } = client
|
||||
@ -57,7 +68,7 @@ export const createPocketbaseClient = (url: string) => {
|
||||
passwordConfirm: password
|
||||
})
|
||||
.then(() => {
|
||||
// console.log(`Sending verification email to ${email}`)
|
||||
// dbg(`Sending verification email to ${email}`)
|
||||
return client.collection('users').requestVerification(email)
|
||||
})
|
||||
)
|
||||
@ -99,6 +110,9 @@ export const createPocketbaseClient = (url: string) => {
|
||||
client.collection('users').authRefresh()
|
||||
)
|
||||
|
||||
const watchHelper = createWatchHelper({ client, promiseHelper, logger })
|
||||
const { watchById, watchAllById } = watchHelper
|
||||
|
||||
const createInstance = safeCatch(
|
||||
`createInstance`,
|
||||
(payload: InstancesRecord_New): Promise<InstancesRecord> => {
|
||||
@ -115,41 +129,15 @@ export const createPocketbaseClient = (url: string) => {
|
||||
const watchInstanceById = async (
|
||||
id: InstanceId,
|
||||
cb: (data: RecordSubscription<InstancesRecord>) => void
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
getInstanceById(id).then((record) => {
|
||||
// console.log(`Got instnace`, record)
|
||||
assertExists(record, `Expected instance ${id} here`)
|
||||
cb({ action: 'init', record })
|
||||
})
|
||||
return client.collection('instances').subscribe<InstancesRecord>(id, cb)
|
||||
}
|
||||
): Promise<UnsubscribeFunc> => watchById('instances', id, cb)
|
||||
|
||||
const watchBackupsByInstanceId = async (
|
||||
id: InstanceId,
|
||||
cb: (data: RecordSubscription<BackupRecord>) => void
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
const unsub = client.collection('backups').subscribe<BackupRecord>('*', (e) => {
|
||||
// console.log(e.record.instanceId, id)
|
||||
if (e.record.instanceId !== id) return
|
||||
cb(e)
|
||||
})
|
||||
const existingBackups = await client
|
||||
.collection('backups')
|
||||
.getFullList<BackupRecord>(100, { filter: `instanceId = '${id}'` })
|
||||
existingBackups.forEach((record) => cb({ action: 'init', record }))
|
||||
return unsub
|
||||
}
|
||||
): Promise<UnsubscribeFunc> => watchAllById('backups', 'instanceId', id, cb)
|
||||
|
||||
const getAllInstancesById = safeCatch(`getAllInstancesById`, async () =>
|
||||
(
|
||||
await client
|
||||
.collection('instances')
|
||||
.getFullList()
|
||||
.catch((e) => {
|
||||
// console.error(`getAllInstancesById failed with ${e}`)
|
||||
throw e
|
||||
})
|
||||
).reduce((c, v) => {
|
||||
(await client.collection('instances').getFullList()).reduce((c, v) => {
|
||||
c[v.id] = v
|
||||
return c
|
||||
}, {} as Record)
|
||||
@ -169,7 +157,7 @@ export const createPocketbaseClient = (url: string) => {
|
||||
|
||||
const getAuthStoreProps = (): AuthStoreProps => {
|
||||
const { token, model, isValid } = client.authStore as AuthStoreProps
|
||||
// console.log(`current authStore`, { token, model, isValid })
|
||||
// dbg(`current authStore`, { token, model, isValid })
|
||||
if (model instanceof Admin) throw new Error(`Admin models not supported`)
|
||||
if (model && !model.email) throw new Error(`Expected model to be a user here`)
|
||||
return {
|
||||
@ -202,7 +190,7 @@ export const createPocketbaseClient = (url: string) => {
|
||||
*/
|
||||
refreshAuthToken()
|
||||
.catch((e) => {
|
||||
// console.error(`Clearing auth store: ${e}`)
|
||||
dbg(`Clearing auth store: ${e}`)
|
||||
client.authStore.clear()
|
||||
})
|
||||
.finally(() => {
|
||||
@ -218,7 +206,7 @@ export const createPocketbaseClient = (url: string) => {
|
||||
* watch on the user record and update auth accordingly.
|
||||
*/
|
||||
const unsub = onAuthChange((authStore) => {
|
||||
// console.log(`onAuthChange`, { ...authStore })
|
||||
// dbg(`onAuthChange`, { ...authStore })
|
||||
const { model } = authStore
|
||||
if (!model) return
|
||||
if (model instanceof Admin) return
|
||||
@ -230,9 +218,9 @@ export const createPocketbaseClient = (url: string) => {
|
||||
setTimeout(_check, 1000)
|
||||
|
||||
// FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8
|
||||
// console.log(`watching _users`)
|
||||
// dbg(`watching _users`)
|
||||
// unsub = subscribe<User>(`users/${model.id}`, (user) => {
|
||||
// console.log(`realtime _users change`, { ...user })
|
||||
// dbg(`realtime _users change`, { ...user })
|
||||
// fireAuthChange({ ...authStore, model: user })
|
||||
// })
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { browser } from '$app/environment'
|
||||
import { browser, dev } from '$app/environment'
|
||||
import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env'
|
||||
import { createLogger, createPromiseHelper } from '@pockethost/common'
|
||||
import { createPocketbaseClient, type PocketbaseClient } from './PocketbaseClient'
|
||||
|
||||
export const client = (() => {
|
||||
@ -7,9 +8,11 @@ export const client = (() => {
|
||||
return () => {
|
||||
if (!browser) throw new Error(`PocketBase client not supported in SSR`)
|
||||
if (clientInstance) return clientInstance
|
||||
console.log(`Initializing pocketbase client`)
|
||||
const logger = createLogger({ debug: dev })
|
||||
logger.info(`Initializing pocketbase client`)
|
||||
const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}`
|
||||
clientInstance = createPocketbaseClient(url)
|
||||
const promiseHelper = createPromiseHelper({ logger })
|
||||
clientInstance = createPocketbaseClient({ url, logger, promiseHelper })
|
||||
return clientInstance
|
||||
}
|
||||
})()
|
||||
|
@ -3,7 +3,8 @@
|
||||
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { createCleanupManagerSync } from '$util/CleanupManager'
|
||||
import { assertExists } from '@pockethost/common/src/assert'
|
||||
import { dbg } from '$util/logger'
|
||||
import { assertExists } from '@pockethost/common'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { instance } from './store'
|
||||
|
||||
@ -14,7 +15,7 @@
|
||||
onMount(async () => {
|
||||
const { watchInstanceById } = client()
|
||||
watchInstanceById(instanceId, (r) => {
|
||||
console.log(`Handling instance update`, r)
|
||||
dbg(`Handling instance update`, r)
|
||||
const { action, record } = r
|
||||
assertExists(record, `Expected instance here`)
|
||||
instance.set(record)
|
||||
|
@ -23,7 +23,7 @@
|
||||
onMount(async () => {
|
||||
const { watchBackupsByInstanceId } = client()
|
||||
watchBackupsByInstanceId(instance.id, (r) => {
|
||||
// console.log(`Handling backup update`, r)
|
||||
// dbg(`Handling backup update`, r)
|
||||
const { action, record } = r
|
||||
const _backups = reduce(
|
||||
$backups,
|
||||
@ -43,7 +43,7 @@
|
||||
return Date.parse(e.created)
|
||||
}).reverse()
|
||||
)
|
||||
// console.log(record.id)
|
||||
// dbg(record.id)
|
||||
}).then(cm.add)
|
||||
})
|
||||
onDestroy(cm.cleanupAll)
|
||||
|
@ -6,6 +6,7 @@
|
||||
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { createCleanupManagerSync } from '$util/CleanupManager'
|
||||
import { error } from '$util/logger'
|
||||
import { humanVersion, type InstanceRecordById, type InstancesRecord } from '@pockethost/common'
|
||||
import { forEach, values } from '@s-libs/micro-dash'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
@ -51,7 +52,7 @@
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Failed to fetch instances`)
|
||||
error(`Failed to fetch instances`)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { InstanceStatus, LATEST_PLATFORM, USE_LATEST_VERSION } from '@pockethost/common'
|
||||
import { dbg, error, warn } from './logger'
|
||||
|
||||
export type FormErrorHandler = (value: string) => void
|
||||
|
||||
export const handleFormError = (error: any, setError?: FormErrorHandler) => {
|
||||
const { parseError } = client()
|
||||
console.error(`Form error: ${error}`, { error })
|
||||
error(`Form error: ${error}`, { error })
|
||||
|
||||
if (setError) {
|
||||
const message = parseError(error)[0]
|
||||
@ -177,10 +178,10 @@ export const handleInstanceGeneratorWidget = async (
|
||||
// populated the form with their existing login. Try using it.
|
||||
await handleLogin(email, password, undefined, false)
|
||||
.then(() => {
|
||||
console.log(`Account ${email} already exists. Logged in.`)
|
||||
dbg(`Account ${email} already exists. Logged in.`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`Login failed, attempting account creation.`)
|
||||
warn(`Login failed, attempting account creation.`)
|
||||
// This means login has failed.
|
||||
// Either their credentials were incorrect, or the account
|
||||
// did not exist, or there is a system issue.
|
||||
@ -188,15 +189,15 @@ export const handleInstanceGeneratorWidget = async (
|
||||
// is already in use.
|
||||
return handleRegistration(email, password)
|
||||
.then(() => {
|
||||
console.log(`Account created, proceeding to log in.`)
|
||||
dbg(`Account created, proceeding to log in.`)
|
||||
// This means registration succeeded. That's good.
|
||||
// Log in using the new credentials
|
||||
return handleLogin(email, password, undefined, false)
|
||||
.then(() => {
|
||||
console.log(`Logged in after account creation`)
|
||||
dbg(`Logged in after account creation`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Panic, auth system down`)
|
||||
error(`Panic, auth system down`)
|
||||
// This should never happen.
|
||||
// If registration succeeds, login should always succeed.
|
||||
// If a login fails at this point, the system is broken.
|
||||
@ -206,7 +207,7 @@ export const handleInstanceGeneratorWidget = async (
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`User input error`)
|
||||
warn(`User input error`)
|
||||
// This is just for clarity
|
||||
// If registration fails at this point, it means both
|
||||
// login and account creation failed.
|
||||
@ -218,16 +219,16 @@ export const handleInstanceGeneratorWidget = async (
|
||||
})
|
||||
})
|
||||
|
||||
console.log(`User before instance creation is `, user())
|
||||
dbg(`User before instance creation is `, user())
|
||||
// We can only get here if we are successfully logged in using the credentials
|
||||
// provided by the user.
|
||||
// Instance creation could still fail if the name is taken
|
||||
await handleCreateNewInstance(instanceName)
|
||||
.then(() => {
|
||||
console.log(`Creation of ${instanceName} succeeded`)
|
||||
dbg(`Creation of ${instanceName} succeeded`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`Creation of ${instanceName} failed`)
|
||||
warn(`Creation of ${instanceName} failed`)
|
||||
// The instance creation could most likely fail if the name is taken.
|
||||
// In any case, bail out to show errors.
|
||||
if (e.data?.data?.subdomain?.code === 'validation_not_unique') {
|
||||
@ -240,7 +241,7 @@ export const handleInstanceGeneratorWidget = async (
|
||||
throw new Error(`Instance creation: ${messages[0]}`)
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error(`Caught widget error`, { error })
|
||||
error(`Caught widget error`, { error })
|
||||
handleFormError(error, setError)
|
||||
}
|
||||
}
|
||||
|
5
packages/pockethost.io/src/util/logger.ts
Normal file
5
packages/pockethost.io/src/util/logger.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createLogger } from '@pockethost/common'
|
||||
import { PUBLIC_DEBUG } from '../env'
|
||||
|
||||
export const logger = createLogger({ debug: PUBLIC_DEBUG })
|
||||
export const { dbg, info, warn, error } = logger
|
@ -1,16 +0,0 @@
|
||||
import { dev } from '$app/environment'
|
||||
|
||||
export const safeCatch = <TIn extends any[], TOut>(
|
||||
name: string,
|
||||
cb: (...args: TIn) => Promise<TOut>
|
||||
) => {
|
||||
return (...args: TIn) => {
|
||||
if (dev) {
|
||||
console.log(`${name}`)
|
||||
}
|
||||
return cb(...args).catch((e: any) => {
|
||||
console.error(`${name} failed: ${e}`)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { browser } from '$app/environment'
|
||||
import { client } from '$src/pocketbase'
|
||||
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
|
||||
import { writable } from 'svelte/store'
|
||||
import { dbg } from './logger'
|
||||
|
||||
export const authStoreState = writable<AuthStoreProps>({ isValid: false, model: null, token: '' })
|
||||
export const isUserLoggedIn = writable(false)
|
||||
@ -15,14 +16,14 @@ if (browser) {
|
||||
* Listen for auth change events. When we get at least one, the auth state is initialized.
|
||||
*/
|
||||
onAuthChange((authStoreProps) => {
|
||||
console.log(`onAuthChange in store`, { ...authStoreProps })
|
||||
dbg(`onAuthChange in store`, { ...authStoreProps })
|
||||
authStoreState.set(authStoreProps)
|
||||
isAuthStateInitialized.set(true)
|
||||
})
|
||||
|
||||
// Update derived stores when authStore changes
|
||||
authStoreState.subscribe((authStoreProps) => {
|
||||
console.log(`subscriber change`, authStoreProps)
|
||||
dbg(`subscriber change`, authStoreProps)
|
||||
isUserLoggedIn.set(authStoreProps.isValid)
|
||||
isUserVerified.set(!!authStoreProps.model?.verified)
|
||||
})
|
||||
|
@ -139,7 +139,7 @@ open https://pockethost.io
|
||||
- [x] fix: incorrect instance information displaying on dashboard details
|
||||
- [x] fix: more helpful error message when backup fails for nonexistent instance
|
||||
- [x] chore: move version number to base package.json
|
||||
- [ ] chore: refactor logging and async helpers
|
||||
- [x] refctor: logging and async helpers
|
||||
- [x] chore: restore autocancellation
|
||||
- [x] chore: rebuild with go 1.19.3 and include in bin name
|
||||
- [ ] fix: Disallow backups if data dir doesn't exist
|
||||
|
Loading…
x
Reference in New Issue
Block a user