linter fixes

This commit is contained in:
Ben Allfree 2025-07-18 17:27:20 -07:00
parent 60307e6ca7
commit 297a6e45ee
213 changed files with 28142 additions and 36109 deletions

View File

@ -1,11 +1,14 @@
{
"semi": false,
"useTabs": false,
"singleQuote": true,
"trailingComma": "all",
"trailingComma": "es5",
"semi": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"plugins": [
"./node_modules/prettier-plugin-organize-imports/index.js",
"./node_modules/prettier-plugin-svelte/plugin.js",
"./node_modules/prettier-plugin-jsdoc/dist/index.js"
]
"prettier-plugin-organize-imports",
"prettier-plugin-svelte",
"prettier-plugin-jsdoc"
],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -20,7 +20,7 @@
"prettier": "^3.4.2",
"prettier-plugin-jsdoc": "^1.3.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-svelte": "^3.4.0",
"tslib": "^2.8.1",
"tsx": "^4.19.2",
"typescript": "^5.7.3"

View File

@ -58,11 +58,7 @@
</script>
{#if message && !isHidden}
<div
class="alert mb-4 {alertTypeClass} {additionalClasses} justify-center"
transition:slide
role="alert"
>
<div class="alert mb-4 {alertTypeClass} {additionalClasses} justify-center" transition:slide role="alert">
<Fa icon={alertTypeIcon} />
<span>{@html message}</span>
</div>

View File

@ -2,7 +2,7 @@
import CopyButton from '$components/CopyButton.svelte'
import { Highlight } from 'svelte-highlight'
import { typescript, type LanguageType } from 'svelte-highlight/languages'
import a11yDark from "svelte-highlight/styles/a11y-dark";
import a11yDark from 'svelte-highlight/styles/a11y-dark'
export let code: string
export let language: LanguageType<'typescript' | 'bash' | 'dns'> = typescript

View File

@ -13,7 +13,5 @@
</script>
<Clipboard text={code} let:copy on:copy={handleCopy}>
<TinyButton click={copy} style={isCopied ? 'success' : 'primary'}
>{isCopied ? 'Copied!' : 'Copy'}</TinyButton
>
<TinyButton click={copy} style={isCopied ? 'success' : 'primary'}>{isCopied ? 'Copied!' : 'Copy'}</TinyButton>
</Clipboard>

View File

@ -4,10 +4,7 @@
import PricingCard from '$components/PricingCard.svelte'
const TOTAL_QTY = 150
export let priceMonthly: [number, string?, number?] = [
359,
'once, use forever',
]
export let priceMonthly: [number, string?, number?] = [359, 'once, use forever']
export let startDate: Date | null = null
export let endDate: Date | null = null

View File

@ -38,9 +38,7 @@
}
const days = Math.floor(difference / (1000 * 60 * 60 * 24))
const hours = Math.floor(
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
)
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((difference % (1000 * 60)) / 1000)
@ -167,9 +165,7 @@
${priceAnnually[0]}
</span>
{/if}
<span
class={`text-sm font-semibold leading-6 ${qtyRemaining <= 0 ? 'text-gray-600' : 'text-gray-300'}`}
>
<span class={`text-sm font-semibold leading-6 ${qtyRemaining <= 0 ? 'text-gray-600' : 'text-gray-300'}`}>
/ {priceAnnually[1]}
</span>
</a>
@ -194,11 +190,7 @@
${priceMonthly[0]}
</span>
{/if}
<span
class="text-sm font-semibold leading-6 ${qtyRemaining <= 0
? 'text-gray-600'
: 'text-gray-300'}"
>
<span class="text-sm font-semibold leading-6 ${qtyRemaining <= 0 ? 'text-gray-600' : 'text-gray-300'}">
/ {priceMonthly[1]}</span
>
</a>

View File

@ -32,8 +32,7 @@
{
name: 'Giovanni Cruz',
title: 'Software developer',
quote:
'Been inlove with this from the first moment I used it. Excellent offering.',
quote: 'Been inlove with this from the first moment I used it. Excellent offering.',
},
{
name: 'Anele Mbanga',

View File

@ -42,11 +42,7 @@
<Fa icon={faCheck} /> Sent!
</div>
{:else}
<button
type="button"
class="btn btn-outline-secondary btn-sm"
on:click={handleClick}>Resend Email</button
>
<button type="button" class="btn btn-outline-secondary btn-sm" on:click={handleClick}>Resend Email</button>
{/if}
{#if formError}

View File

@ -6,9 +6,7 @@
<!-- Setting the `container-type` allows us to use Container Queries -->
<div
class="card card-body bg-base-200 {block
? 'block'
: ''} {height} {marginBottom}"
class="card card-body bg-base-200 {block ? 'block' : ''} {height} {marginBottom}"
style="container-type: inline-size"
>
<slot />

View File

@ -9,14 +9,8 @@
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<h3 class="text-xl font-bold"><slot /></h3>
<a
href={documentation}
class="btn btn-sm btn-outline btn-primary"
target="_blank"
>Full documentation <Fa
icon={faArrowUpRightFromSquare}
class="opacity-50 text-sm"
/></a
<a href={documentation} class="btn btn-sm btn-outline btn-primary" target="_blank"
>Full documentation <Fa icon={faArrowUpRightFromSquare} class="opacity-50 text-sm" /></a
>
</div>
{/if}

View File

@ -1,9 +1,5 @@
<script lang="ts">
import {
isAuthStateInitialized,
isUserLoggedIn,
userStore,
} from '$util/stores'
import { isAuthStateInitialized, isUserLoggedIn, userStore } from '$util/stores'
export let role = ''
let hasRole = false

View File

@ -1,26 +1,21 @@
import { type InstanceFields, parseBoolean } from 'pockethost/common'
/**
* These environment variables default to pointing to the production build so
* frontend development is easy. If they are specified in .env, those values
* will prevail.
* These environment variables default to pointing to the production build so frontend development is easy. If they are
* specified in .env, those values will prevail.
*/
// The apex domain of this whole operation.
export const PUBLIC_APEX_DOMAIN =
import.meta.env.PUBLIC_APEX_DOMAIN || `pockethost.io`
export const PUBLIC_APEX_DOMAIN = import.meta.env.PUBLIC_APEX_DOMAIN || `pockethost.io`
export const PUBLIC_APP_URL =
import.meta.env.PUBLIC_APP_URL || `https://${PUBLIC_APEX_DOMAIN}`
export const PUBLIC_APP_URL = import.meta.env.PUBLIC_APP_URL || `https://${PUBLIC_APEX_DOMAIN}`
// The protocol to use, almost always will be https
export const PUBLIC_HTTP_PROTOCOL =
import.meta.env.PUBLIC_HTTP_PROTOCOL || `https:`
export const PUBLIC_HTTP_PROTOCOL = import.meta.env.PUBLIC_HTTP_PROTOCOL || `https:`
// The complete URL to the mothership
export const PUBLIC_MOTHERSHIP_URL =
import.meta.env.PUBLIC_MOTHERSHIP_URL ||
`https://pockethost-central.${PUBLIC_APEX_DOMAIN}`
import.meta.env.PUBLIC_MOTHERSHIP_URL || `https://pockethost-central.${PUBLIC_APEX_DOMAIN}`
// Whether we are in debugging mode - default TRUE
export const PUBLIC_DEBUG = parseBoolean(import.meta.env.PUBLIC_DEBUG || 'true')
@ -49,13 +44,10 @@ export const INSTANCE_HOST = (instance: InstanceFields) => {
* INSTANCE_URL('my-cool-instance', 'dashboard') // https://my-cool-instance.pockethost.io/dashboard
*
* @param {string} instance This is the unique instance name
* @param {string[]} paths This is an optional list of additional paths to
* append to the instance URL.
* @param {string[]} paths This is an optional list of additional paths to append to the instance URL.
*/
export const INSTANCE_URL = (instance: InstanceFields, ...paths: string[]) => {
return `${PUBLIC_HTTP_PROTOCOL}//${INSTANCE_HOST(instance)}/${mkPath(
...paths,
)}`
return `${PUBLIC_HTTP_PROTOCOL}//${INSTANCE_HOST(instance)}/${mkPath(...paths)}`
}
/**

View File

@ -6,19 +6,16 @@ interface Window {
/**
* Initialises Lemon.js on your page.
*
* @param options - An object with a single property, eventHandler, which
* is a function that will be called when Lemon.js emits an event.
* @param options - An object with a single property, eventHandler, which is a function that will be called when
* Lemon.js emits an event.
*/
Setup: (options: {
eventHandler: (event: { event: string }) => void
}) => void
Setup: (options: { eventHandler: (event: { event: string }) => void }) => void
/** Refreshes `lemonsqueezy-button` listeners on the page. */
Refresh: () => void
Url: {
/**
* Opens a given Lemon Squeezy URL, typically these are Checkout or
* Payment Details Update overlays.
* Opens a given Lemon Squeezy URL, typically these are Checkout or Payment Details Update overlays.
*
* @param url - The URL to open.
*/

View File

@ -52,8 +52,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const logOut = () => authStore.clear()
/**
* This will register a new user into Pocketbase, and email them a
* verification link
* This will register a new user into Pocketbase, and email them a verification link
*
* @param email {string} The email of the user
* @param password {string} The password of the user
@ -85,8 +84,8 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}
/**
* This will reset an unauthenticated user's password by sending a
* verification link to their email, and includes an optional error handler
* This will reset an unauthenticated user's password by sending a verification link to their email, and includes an
* optional error handler
*
* @param email {string} The email of the user
*/
@ -95,24 +94,17 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}
/**
* This will let an unauthenticated user save a new password after verifying
* their email
* This will let an unauthenticated user save a new password after verifying their email
*
* @param token {string} The token from the verification email
* @param password {string} The new password of the user
*/
const requestPasswordResetConfirm = async (
token: string,
password: string,
) => {
return await client
.collection('users')
.confirmPasswordReset(token, password, password)
const requestPasswordResetConfirm = async (token: string, password: string) => {
return await client.collection('users').confirmPasswordReset(token, password, password)
}
/**
* This will log a user into Pocketbase, and includes an optional error
* handler
* This will log a user into Pocketbase, and includes an optional error handler
*
* @param {string} email The email of the user
* @param {string} password The password of the user
@ -129,32 +121,26 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const createInstance = mkRest<CreateInstancePayload, CreateInstanceResult>(
RestCommands.Instance,
RestMethods.Post,
CreateInstancePayloadSchema,
CreateInstancePayloadSchema
)
const updateInstance = mkRest<UpdateInstancePayload, UpdateInstanceResult>(
RestCommands.Instance,
RestMethods.Put,
UpdateInstancePayloadSchema,
UpdateInstancePayloadSchema
)
const deleteInstance = mkRest<DeleteInstancePayload, DeleteInstanceResult>(
RestCommands.Instance,
RestMethods.Delete,
DeleteInstancePayloadSchema,
DeleteInstancePayloadSchema
)
const getInstanceById = (
id: InstanceId,
): Promise<InstanceFields | undefined> =>
const getInstanceById = (id: InstanceId): Promise<InstanceFields | undefined> =>
client.collection('instances').getOne<InstanceFields>(id)
const getInstanceBySubdomain = (
subdomain: InstanceFields['subdomain'],
): Promise<InstanceFields | undefined> =>
client
.collection('instances')
.getFirstListItem<InstanceFields>(`subdomain='${subdomain}'`)
const getInstanceBySubdomain = (subdomain: InstanceFields['subdomain']): Promise<InstanceFields | undefined> =>
client.collection('instances').getFirstListItem<InstanceFields>(`subdomain='${subdomain}'`)
const getAllInstancesById = async () =>
(await client.collection('instances').getFullList()).reduce(
@ -162,16 +148,13 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
c[v.id] = v as unknown as InstanceFields
return c
},
{} as { [_: InstanceId]: InstanceFields },
{} as { [_: InstanceId]: InstanceFields }
)
const parseError = (e: Error): string[] => {
if (!(e instanceof ClientResponseError)) return [`${e}`]
if (e.data.message && keys(e.data.data).length === 0)
return [e.data.message]
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter(
(v) => !!v,
)
if (e.data.message && keys(e.data.data).length === 0) return [e.data.message]
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v)
}
const resendVerificationEmail = async () => {
@ -184,8 +167,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const { isAdmin, model, token, isValid } = client.authStore
if (isAdmin) throw new Error(`Admin models not supported`)
if (model && !model.email)
throw new Error(`Expected model to be a user here`)
if (model && !model.email) throw new Error(`Expected model to be a user here`)
return {
token,
model,
@ -194,8 +176,8 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}
/**
* Use synthetic event for authStore changers, so we can broadcast just the
* props we want and not the actual authStore object.
* Use synthetic event for authStore changers, so we can broadcast just the props we want and not the actual authStore
* object.
*/
const [onAuthChange, fireAuthChange] = createGenericSyncEvent<BaseAuthStore>()
@ -207,9 +189,8 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
})
/**
* Refresh the auth token immediately upon creating the client. The auth
* token may be out of date, or fields in the user record may have changed
* in the backend.
* Refresh the auth token immediately upon creating the client. The auth token may be out of date, or fields in the
* user record may have changed in the backend.
*/
if (browser) {
refreshAuthToken()
@ -222,12 +203,11 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}
/**
* Listen for auth state changes and subscribe to realtime _user events.
* This way, when the verified flag is flipped, it will appear that the
* authstore model is updated.
* Listen for auth state changes and subscribe to realtime _user events. This way, when the verified flag is
* flipped, it will appear that the authstore model is updated.
*
* Polling is a stopgap til v.0.8. Once 0.8 comes along, we can do a
* realtime watch on the user record and update auth accordingly.
* Polling is a stopgap til v.0.8. Once 0.8 comes along, we can do a realtime watch on the user record and update
* auth accordingly.
*/
const unsub = onAuthChange((authStore) => {
const { model, isAdmin } = authStore
@ -249,7 +229,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const watchInstanceLog = (
instance: InstanceFields,
update: (log: UntrustedInstanceLogFields) => void,
nInitial = 100,
nInitial = 100
): (() => void) => {
const auth = client.authStore.exportToCookie()

View File

@ -1,8 +1,5 @@
import { PUBLIC_MOTHERSHIP_URL } from '$src/env'
import {
createPocketbaseClient,
type PocketbaseClient,
} from './PocketbaseClient'
import { createPocketbaseClient, type PocketbaseClient } from './PocketbaseClient'
export const client = (() => {
let clientInstance: PocketbaseClient | undefined

View File

@ -12,10 +12,7 @@
<div class="flex flex-col space-x-4 items-center justify-center">
<div class="prose">
<p class="text-2xl text-center text-warning">
Instances will not run until you <a
href="/access"
class="link text-primary">upgrade</a
>.
Instances will not run until you <a href="/access" class="link text-primary">upgrade</a>.
</p>
</div>
</div>

View File

@ -12,14 +12,10 @@
Your email: <span class="text-success font-bold">{$userStore?.email}</span>
</div>
<div class="m-4">
Your plan: <span class="text-success font-bold"
>{PLAN_NAMES[$userSubscriptionType]}</span
>
Your plan: <span class="text-success font-bold">{PLAN_NAMES[$userSubscriptionType]}</span>
</div>
<div class="m-4">
Allowed instances: {$userStore?.subscription_quantity} (<a
href="/support"
class="link text-primary">request more</a
Allowed instances: {$userStore?.subscription_quantity} (<a href="/support" class="link text-primary">request more</a
>)
</div>
<div>

View File

@ -43,13 +43,10 @@
<div class="mt-10">
{#if active}
<div class="text-success text-center text-2xl">
This is your current plan.
</div>
<div class="text-success text-center text-2xl">This is your current plan.</div>
<p class="mt-10 text-neutral-content">
To change to a different plan, contact <a
class="link"
href={`"${DISCORD_URL}"`}><code>.noaxis</code> on Discord</a
To change to a different plan, contact <a class="link" href={`"${DISCORD_URL}"`}
><code>.noaxis</code> on Discord</a
>
</p>
{:else if prices.length > 0}
@ -58,16 +55,12 @@
{#if (startLimit > 0 && limit === 0) || !upgradable}
<button class="btn btn-primary" disabled>{price.title}</button>
{:else}
<a class="btn btn-primary" href={price.link} target="_blank"
>{price.title}</a
>
<a class="btn btn-primary" href={price.link} target="_blank">{price.title}</a>
{/if}
{/each}
</div>
{#if !upgradable}
To change to this plan, <a href="/support" class="link"
>contact support</a
>
To change to this plan, <a href="/support" class="link">contact support</a>
{/if}
{/if}
</div>

View File

@ -3,79 +3,63 @@
import FAQItem from '$src/routes/(app)/account/FAQItem.svelte'
</script>
<div
class="mx-auto max-w-7xl divide-y divide-gray-500 px-6 py-24 sm:py-32 lg:px-8 lg:py-40"
>
<h2 class="text-2xl font-bold leading-10 tracking-tight">
Frequently asked questions
</h2>
<div class="mx-auto max-w-7xl divide-y divide-gray-500 px-6 py-24 sm:py-32 lg:px-8 lg:py-40">
<h2 class="text-2xl font-bold leading-10 tracking-tight">Frequently asked questions</h2>
<dl class="mt-10 space-y-8 divide-y divide-gray-500">
<FAQItem question={`What is the "Legacy Plan"?`}>
<p class="mb-4">
Legacy accounts have access to existing projects and features, but
cannot create new projects or use new features.
Legacy accounts have access to existing projects and features, but cannot create new projects or use new
features.
</p>
<p>
If you upgrade to a paid plan and then downgrade again, you will still
have access to your Legacy projects and features, but any new projects
and features created on a paid plan will no longer work.
If you upgrade to a paid plan and then downgrade again, you will still have access to your Legacy projects and
features, but any new projects and features created on a paid plan will no longer work.
</p>
</FAQItem>
<FAQItem question="Giving Back to the Open Source Community">
<p class="mb-4">
PocketHost is committed to giving back to the open source community that
helps create PocketHost and PocketBase.
PocketHost is committed to giving back to the open source community that helps create PocketHost and PocketBase.
</p>
<p class="mb-4">
10% of net proceeds (after expenses) are donated back to the community.
Specifically, PocketHost makes donations to the PocketBase project and
major contributors to the PocketHost project.
</p>
<p>
In addition, 1% of membership fees is collected by Stripe to reduce
carbon footprints around the world.
10% of net proceeds (after expenses) are donated back to the community. Specifically, PocketHost makes donations
to the PocketBase project and major contributors to the PocketHost project.
</p>
<p>In addition, 1% of membership fees is collected by Stripe to reduce carbon footprints around the world.</p>
</FAQItem>
<FAQItem question="Fair Use Policy">
<p class="mb-4">
When we say 'unlimited', we mean it in the Fair Use sense of the word.
Obviously, everything has limits. In our study of PocketHost usage
patterns, we found that even the busiest and most successful PocketHost
instances rarely stress our system.
When we say 'unlimited', we mean it in the Fair Use sense of the word. Obviously, everything has limits. In our
study of PocketHost usage patterns, we found that even the busiest and most successful PocketHost instances
rarely stress our system.
</p>
<p class="mb-4">
PocketHost is a haven for developers who want to launch and iterate
quickly on ideas without worrying about metering and infrastructure.
PocketHost is a haven for developers who want to launch and iterate quickly on ideas without worrying about
metering and infrastructure.
</p>
<p class="mb-4">
If your app gets big and it starts affecting the system, we'll talk
about an enterprise plan or a dedicated setup.
If your app gets big and it starts affecting the system, we'll talk about an enterprise plan or a dedicated
setup.
</p>
<p>
Please enjoy PocketHost knowing that you can use as much storage,
bandwidth, and CPU as your application requires under normal operating
conditions. Let us handle the hosting so you can get back to work.
Please enjoy PocketHost knowing that you can use as much storage, bandwidth, and CPU as your application
requires under normal operating conditions. Let us handle the hosting so you can get back to work.
</p>
</FAQItem>
<FAQItem question="Cancellation and Refunds">
<p class="mb-4">Short version: We only want your money if you are happy.</p>
<p class="mb-4">
Short version: We only want your money if you are happy.
</p>
<p class="mb-4">
Long version: If you need to cancel your membership for any reason,
please <a class="link" href={DISCORD_URL}
Long version: If you need to cancel your membership for any reason, please <a class="link" href={DISCORD_URL}
>contact <code>.noaxis</code> on Discord</a
>. If you cancel within the first 5 days of a signup or renewal, we'll
refund the full amount. Otherwise, we'll pro-rate it. Sound good?
>. If you cancel within the first 5 days of a signup or renewal, we'll refund the full amount. Otherwise, we'll
pro-rate it. Sound good?
</p>
<p>
If you create additional instances and then downgrade to the free plan,
the extra instances will remain accessible in your dashboard, but they
will not run.
If you create additional instances and then downgrade to the free plan, the extra instances will remain
accessible in your dashboard, but they will not run.
</p>
</FAQItem>

View File

@ -23,7 +23,7 @@
}
</script>
{#each values($globalInstancesStore).sort( (a, b) => a.subdomain.localeCompare(b.subdomain), ) as instance, index}
{#each values($globalInstancesStore).sort((a, b) => a.subdomain.localeCompare(b.subdomain)) as instance, index}
<button
class={`card min-w-80 lg:max-w-80 flex-1 m-4 transition hover:bg-base-300 ${instance.power ? 'bg-neutral' : 'bg-base-200'}`}
on:click={(_) => goto(`/instances/${instance.id}`)}
@ -34,9 +34,7 @@
<span>{instance.subdomain.length > 15 ? instance.subdomain.slice(0, 15) + '...' : instance.subdomain}</span>
<input
type="checkbox"
class="toggle {instance.power
? 'toggle-success'
: 'bg-red-500 hover:bg-red-500'}"
class="toggle {instance.power ? 'toggle-success' : 'bg-red-500 hover:bg-red-500'}"
checked={instance.power}
on:click={(e) => e.stopPropagation()}
on:change={handlePowerChange(instance.id)}
@ -46,8 +44,7 @@
<p class="text-left">
<span class="text-gray-400"
>Version {instance.version}
<span class={instance.power ? 'hidden' : ''}>- Powered Off</span
></span
<span class={instance.power ? 'hidden' : ''}>- Powered Off</span></span
>
</p>
@ -63,11 +60,7 @@
target="_blank"
on:click={(e) => e.stopPropagation()}
>
<img
src="/images/pocketbase-logo.svg"
alt="PocketBase Logo"
class="w-6"
/>
<img src="/images/pocketbase-logo.svg" alt="PocketBase Logo" class="w-6" />
<span>Admin</span>
</a>
</div>

View File

@ -1,9 +1,5 @@
<script lang="ts">
import {
globalInstancesStore,
userSubscriptionType,
userStore,
} from '$util/stores'
import { globalInstancesStore, userSubscriptionType, userStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import InstanceList from './InstanceList.svelte'
import { SubscriptionType } from 'pockethost/common'
@ -18,29 +14,21 @@
<title>Dashboard - PocketHost</title>
</svelte:head>
<div
class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-4"
>
<div class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-4">
<h2 class="text-4xl text-base-content font-bold capitalize">Dashboard</h2>
<a href="/instances/new" class="m-3 btn btn-primary">
<Fa icon={faPlus} /> New Instance</a
>
<a href="/instances/new" class="m-3 btn btn-primary"> <Fa icon={faPlus} /> New Instance</a>
</div>
<div class="flex flex-col space-x-4 items-center justify-center">
{#if maxInstances > 0}
{#if instanceCount > maxInstances}
<p class="text-center text-error">
You have exceeded your instance limit.
</p>
<p class="text-center text-error">You have exceeded your instance limit.</p>
{/if}
<div class="flex flex-row space-x-4 items-center justify-center">
<div>Instances</div>
<progress
class="progress {instanceCount > maxInstances
? 'progress-error'
: 'progress-primary'} w-48 md:w-80"
class="progress {instanceCount > maxInstances ? 'progress-error' : 'progress-primary'} w-48 md:w-80"
value={instanceCount}
max={maxInstances}
></progress>

View File

@ -9,10 +9,7 @@
import { type InstanceId } from 'pockethost/common'
import Toggle from './Toggle.svelte'
import Fa from 'svelte-fa'
import {
faExternalLinkAlt,
faTriangleExclamation,
} from '@fortawesome/free-solid-svg-icons'
import { faExternalLinkAlt, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
let isReady = false
$: {
@ -45,20 +42,14 @@
</script>
<svelte:head>
<title
>{isReady ? $instance.subdomain : 'Instance'} overview - PocketHost</title
>
<title>{isReady ? $instance.subdomain : 'Instance'} overview - PocketHost</title>
</svelte:head>
{#if isReady}
<div
class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-2"
>
<div class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-2">
<div>
<div class="flex items-center gap-2">
<h2
class="text-4xl md:text-left text-base-content font-bold mb-3 break-words"
>
<h2 class="text-4xl md:text-left text-base-content font-bold mb-3 break-words">
{$instance.subdomain}
</h2>
</div>
@ -77,19 +68,13 @@
</div>
<div>
<Toggle
checked={$instance.power}
onChange={handlePowerChange($instance.id)}
/>
<Toggle checked={$instance.power} onChange={handlePowerChange($instance.id)} />
</div>
</div>
{#if !$instance.power}
<div class="px-4 mb-8">
<AlertBar
message="This instance is turned off and will not respond to requests"
type="warning"
/>
<AlertBar message="This instance is turned off and will not respond to requests" type="warning" />
</div>
{/if}
@ -101,29 +86,17 @@
<a href={`/instances/${id}`} class={activeClass(id)}>Overview</a>
</li>
<li>
<a href={`/instances/${id}/secrets`} class={activeClass(`secrets`)}
>Secrets</a
>
<a href={`/instances/${id}/secrets`} class={activeClass(`secrets`)}>Secrets</a>
</li>
<li>
<a href={`/instances/${id}/logs`} class={activeClass(`logs`)}>Logs</a>
</li>
<li>
<a href={`/instances/${id}/ftp`} class={activeClass(`ftp`)}
>FTP Access</a
>
<a href={`/instances/${id}/ftp`} class={activeClass(`ftp`)}>FTP Access</a>
</li>
<li>
<a
href={INSTANCE_ADMIN_URL($instance)}
rel="noreferrer"
target="_blank"
>
<img
src="/images/pocketbase-logo.svg"
alt="PocketBase Logo"
class="w-6 inline-block"
/>
<a href={INSTANCE_ADMIN_URL($instance)} rel="noreferrer" target="_blank">
<img src="/images/pocketbase-logo.svg" alt="PocketBase Logo" class="w-6 inline-block" />
Admin
<Fa icon={faExternalLinkAlt} class="ml-2 text-xs" />
</a>
@ -136,36 +109,22 @@
</div>
<ul class="menu text-base-content">
<li>
<a href={`/instances/${id}/version`} class={activeClass(`version`)}
>Change Version</a
>
<a href={`/instances/${id}/version`} class={activeClass(`version`)}>Change Version</a>
</li>
<li>
<a href={`/instances/${id}/domain`} class={activeClass(`domain`)}
>Custom Domain</a
>
<a href={`/instances/${id}/domain`} class={activeClass(`domain`)}>Custom Domain</a>
</li>
<li>
<a
href={`/instances/${id}/admin-sync`}
class={activeClass(`admin-sync`)}>Admin Sync</a
>
<a href={`/instances/${id}/admin-sync`} class={activeClass(`admin-sync`)}>Admin Sync</a>
</li>
<li>
<a href={`/instances/${id}/dev`} class={activeClass(`dev`)}
>Dev Mode</a
>
<a href={`/instances/${id}/dev`} class={activeClass(`dev`)}>Dev Mode</a>
</li>
<li>
<a href={`/instances/${id}/rename`} class={activeClass(`rename`)}
>Rename</a
>
<a href={`/instances/${id}/rename`} class={activeClass(`rename`)}>Rename</a>
</li>
<li>
<a
href={`/instances/${id}/delete`}
class={`text-error ${activeClass(`delete`)}`}>Delete</a
>
<a href={`/instances/${id}/delete`} class={`text-error ${activeClass(`delete`)}`}>Delete</a>
</li>
</ul>
</div>

View File

@ -38,9 +38,10 @@
<div class="text-accent">Notice: You are in Custom Domain mode</div>
{:else}
<div class="text-error">
Notice: You are in Custom Domain mode but it is not active and will not
work. Go find <a href={DISCORD_URL} target="_blank" class="link"
><code>.noaxis</code> on Discord</a
Notice: You are in Custom Domain mode but it is not active and will not work. Go find <a
href={DISCORD_URL}
target="_blank"
class="link"><code>.noaxis</code> on Discord</a
> to get set up.
</div>
{/if}
@ -57,18 +58,10 @@
<p>Additional Resources:</p>
<ul class="list-disc pl-4">
<li>
<a
href={`https://pocketbase.io/docs/api-records/`}
target="_blank"
class="link">PocketBase Web APIs</a
>
<a href={`https://pocketbase.io/docs/api-records/`} target="_blank" class="link">PocketBase Web APIs</a>
</li>
<li>
<a
href="https://www.npmjs.com/package/pocketbase"
target="_blank"
class="link">PocketBase NPM Package</a
>
<a href="https://www.npmjs.com/package/pocketbase" target="_blank" class="link">PocketBase NPM Package</a>
</li>
</ul>
</div>

View File

@ -16,17 +16,12 @@
<div class="form-control w-fit">
<label class="label cursor-pointer">
<span class="label-text text-lg mr-2"
>
<span class="font-bold text-{checked ? onClass : offClass}"
>{checked ? onText : offText}</span
></span
<span class="label-text text-lg mr-2">
<span class="font-bold text-{checked ? onClass : offClass}">{checked ? onText : offText}</span></span
>
<input
type="checkbox"
class="toggle bg-{checked ? onClass : offClass} hover:bg-{checked
? onClass
: offClass}"
class="toggle bg-{checked ? onClass : offClass} hover:bg-{checked ? onClass : offClass}"
{checked}
on:change={handleChange}
/>

View File

@ -25,7 +25,8 @@
<CardHeader documentation={`/docs/admin-sync`}>Admin Sync</CardHeader>
<p class="mb-8">
Admin Sync ensures that your instance always has an admin account that matches the login credentials of your pockethost.io account.
Admin Sync ensures that your instance always has an admin account that matches the login credentials of your
pockethost.io account.
</p>
<ErrorMessage message={errorMessage} />

View File

@ -31,7 +31,7 @@
// Prompt the user to confirm the version change
const confirmVersionChange = confirm(
`LAST CHANCE - Are you sure you want to delete this instance? Your database, all local files, logs, and subdomain will be lost.`,
`LAST CHANCE - Are you sure you want to delete this instance? Your database, all local files, logs, and subdomain will be lost.`
)
// If they select yes, then update the version in pocketbase
@ -66,10 +66,7 @@
<CardHeader documentation={`/docs/delete`}>Delete Instance</CardHeader>
{#if power}
<AlertBar
message="Instance must be powered off before deleting."
type="error"
/>
<AlertBar message="Instance must be powered off before deleting." type="error" />
{/if}
<div class="mb-8">
@ -86,15 +83,8 @@
<ErrorMessage message={errorMessage} />
<form
class="flex change-version-form-container-query gap-4"
on:submit={handleSave}
>
<button
type="submit"
class="btn btn-error"
disabled={power || isButtonDisabled}>Delete Instance</button
>
<form class="flex change-version-form-container-query gap-4" on:submit={handleSave}>
<button type="submit" class="btn btn-error" disabled={power || isButtonDisabled}>Delete Instance</button>
</form>
<style>

View File

@ -23,8 +23,8 @@
<CardHeader documentation={`/docs/dev-mode`}>Dev Mode</CardHeader>
<p class="mb-8">
Starting with PocketBase v0.20.1, your instance will show debugging output in
the instance logs. Performance is degraded while Dev Mode is active.
Starting with PocketBase v0.20.1, your instance will show debugging output in the instance logs. Performance is
degraded while Dev Mode is active.
</p>
<ErrorMessage message={errorMessage} />

View File

@ -4,8 +4,7 @@
import CardHeader from '$components/cards/CardHeader.svelte'
import { INSTANCE_BARE_HOST } from '$src/env'
import { client } from '$src/pocketbase-client'
import { isUserPaid, userSubscriptionType } from '$util/stores'
import { SubscriptionType } from 'pockethost/common'
import { isUserPaid } from '$util/stores'
import { dns } from 'svelte-highlight/languages'
import { instance } from '../store'
@ -26,13 +25,10 @@
let errorMessage = ''
let successMessage = ''
const regex =
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,24}$/
const regex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,24}$/
$: {
isButtonDisabled =
(!!formCname.trim() && !regex.test(formCname)) ||
(!formCname.trim() && !cname.trim())
isButtonDisabled = (!!formCname.trim() && !regex.test(formCname)) || (!formCname.trim() && !cname.trim())
}
const onRename = (e: Event) => {
e.preventDefault()
@ -73,20 +69,13 @@
</script>
<div class="max-w-2xl">
<CardHeader documentation={`/docs/custom-domains`}>
Custom Domain (CNAME)
</CardHeader>
<CardHeader documentation={`/docs/custom-domains`}>Custom Domain (CNAME)</CardHeader>
<div class="mb-8">
Use a custom domain (CNAME) with your PocketHost instance.
</div>
<div class="mb-8">Use a custom domain (CNAME) with your PocketHost instance.</div>
{#if cname && regex.test(formCname.trim())}
<div class="mb-8">Go to your DNS provider and add a CNAME entry.</div>
<div class="mb-4">
<CodeSample
code={`${formCname} CNAME ${INSTANCE_BARE_HOST($instance)}`}
language={dns}
/>
<CodeSample code={`${formCname} CNAME ${INSTANCE_BARE_HOST($instance)}`} language={dns} />
</div>
{/if}
@ -100,18 +89,11 @@
type="warning"
/>
{:else}
<AlertBar
message={`Your custom domain name is active.`}
type="success"
flash
/>
<AlertBar message={`Your custom domain name is active.`} type="success" flash />
{/if}
{/if}
<form
class="flex rename-instance-form-container-query gap-4"
on:submit={onRename}
>
<form class="flex rename-instance-form-container-query gap-4" on:submit={onRename}>
<input
title="Only valid domain name patterns are allowed"
type="text"
@ -119,9 +101,7 @@
class="input input-bordered w-full"
/>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}
>Update Custom Domain</button
>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}>Update Custom Domain</button>
</form>
</div>

View File

@ -16,10 +16,7 @@
</script>
<CardHeader documentation={`/docs/ftp`}>FTP Access</CardHeader>
<div class="mb-8">
Securely access your instance files via FTPS. Use your PocketHost account
login and password.
</div>
<div class="mb-8">Securely access your instance files via FTPS. Use your PocketHost account login and password.</div>
<div class="mb-12">
<CodeSample code={`ftp ${ftpUrl}`} language={bash} />
@ -36,10 +33,7 @@
<tbody>
<tr>
<th>pb_data</th>
<td
>The PocketBase data directory, including upload storage and database
backups</td
>
<td>The PocketBase data directory, including upload storage and database backups</td>
</tr>
<tr>
<th>pb_public</th>

View File

@ -1,21 +1,13 @@
<script lang="ts">
import { client } from '$src/pocketbase-client'
import { mkCleanup } from '$util/componentCleanup'
import {
StreamNames,
type Unsubscribe,
type InstanceLogFields,
} from 'pockethost/common'
import { StreamNames, type Unsubscribe, type InstanceLogFields } from 'pockethost/common'
import { onMount, tick } from 'svelte'
import { derived, writable } from 'svelte/store'
import { instance } from '../store'
import CardHeader from '$src/components/cards/CardHeader.svelte'
import Fa from 'svelte-fa'
import {
faArrowDown,
faClose,
faUpRightAndDownLeftFromCenter,
} from '@fortawesome/free-solid-svg-icons'
import { faArrowDown, faClose, faUpRightAndDownLeftFromCenter } from '@fortawesome/free-solid-svg-icons'
$: ({ id } = $instance)
@ -40,9 +32,7 @@
// This will open the full screen modal
const handleFullScreenModal = () => {
const modal = document.getElementById(
'loggingFullscreenModal',
) as HTMLDialogElement
const modal = document.getElementById('loggingFullscreenModal') as HTMLDialogElement
modal?.showModal()
}
@ -103,34 +93,23 @@
<CardHeader>Logs</CardHeader>
<div class="mb-4">
Instance logs appear here in realtime, including <code>console.log</code> from
JavaScript hooks.
Instance logs appear here in realtime, including <code>console.log</code> from JavaScript hooks.
</div>
<dialog id="loggingFullscreenModal" class="modal backdrop-blur">
<div class="modal-box max-w-[90vw] h-[90vh]">
<button
class="btn btn-sm absolute top-[6px] right-[6px]"
on:click={() => (autoScroll = !autoScroll)}
<button class="btn btn-sm absolute top-[6px] right-[6px]" on:click={() => (autoScroll = !autoScroll)}
>AutoScroll
<Fa icon={autoScroll ? faArrowDown : faClose} />
</button>
<h3 class="font-bold text-lg">Instance Logging</h3>
<div
class="py-4 h-[80vh] overflow-y-scroll flex flex-col"
bind:this={logElementPopup}
>
<div class="py-4 h-[80vh] overflow-y-scroll flex flex-col" bind:this={logElementPopup}>
{#each $logs as log}
<div
class="px-4 text-[16px] font-mono flex align-center"
data-prefix=">"
>
<div class="px-4 text-[16px] font-mono flex align-center" data-prefix=">">
<div>
<span class="mr-1 text-accent">{log.time}</span>
<span class="mr-1 text-base-content {logColor(log.stream)}"
>{@html logText(log)}</span
>
<span class="mr-1 text-base-content {logColor(log.stream)}">{@html logText(log)}</span>
</div>
</div>
{/each}
@ -158,9 +137,7 @@
<div>
<span class="mr-1 text-accent">{log.time}</span>
<span class="mr-1 text-base-content {logColor(log.stream)}"
>{@html logText(log)}</span
>
<span class="mr-1 text-base-content {logColor(log.stream)}">{@html logText(log)}</span>
</div>
</div>
{/each}

View File

@ -36,9 +36,7 @@
const instanceNameValidation = formSubdomain.trim().replace(/[0-9]/g, '')
// Prompt the user to confirm the version change
const confirmVersionChange = confirm(
`Are you sure you want to rename your instance to ${instanceNameValidation}?`,
)
const confirmVersionChange = confirm(`Are you sure you want to rename your instance to ${instanceNameValidation}?`)
// If they select yes, then update the version in pocketbase
if (confirmVersionChange) {
@ -62,24 +60,17 @@
</script>
<div class="max-w-lg">
<CardHeader documentation={`/docs/rename-instance`}
>Rename Instance</CardHeader
>
<CardHeader documentation={`/docs/rename-instance`}>Rename Instance</CardHeader>
<p class="mb-8">
Renaming your instance will cause it to become <strong class="text-error"
>inaccessible</strong
> by the old instance name. You also may not be able to change it back if someone
else choose it.
Renaming your instance will cause it to become <strong class="text-error">inaccessible</strong> by the old instance name.
You also may not be able to change it back if someone else choose it.
</p>
<AlertBar message={successMessage} type="success" flash />
<AlertBar message={errorMessage} type="error" />
<form
class="flex rename-instance-form-container-query gap-4"
on:submit={onRename}
>
<form class="flex rename-instance-form-container-query gap-4" on:submit={onRename}>
<input
title="Only letters and dashes are allowed"
required
@ -88,9 +79,7 @@
class="input input-bordered w-full"
/>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}
>Rename Instance</button
>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}>Rename Instance</button>
</form>
<style>

View File

@ -2,7 +2,7 @@
import { assertExists } from 'pockethost/common'
import { instance } from '../store'
import SecretsInner from './SecretsInner.svelte'
import CardHeader from '$src/components/cards/CardHeader.svelte';
import CardHeader from '$src/components/cards/CardHeader.svelte'
$: ({ status, version, id } = $instance)

View File

@ -2,10 +2,7 @@
import AlertBar from '$components/AlertBar.svelte'
import { client } from '$src/pocketbase-client/index.js'
import { reduce } from '@s-libs/micro-dash'
import {
SECRET_KEY_REGEX,
type UpdateInstancePayload,
} from 'pockethost/common'
import { SECRET_KEY_REGEX, type UpdateInstancePayload } from 'pockethost/common'
import { instance } from '../store.js'
import { items } from './stores.js'
import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons'
@ -58,7 +55,7 @@
c[name] = value
return c
},
{} as NonNullable<UpdateInstancePayload['fields']['secrets']>,
{} as NonNullable<UpdateInstancePayload['fields']['secrets']>
),
},
})
@ -87,16 +84,12 @@
<div class="mb-8">
{#if successfulSave}
<AlertBar
message="Your new secret has been saved."
type="success"
/>
<AlertBar message="Your new secret has been saved." type="success" />
{/if}
<AlertBar message={errorMessage} type="error" />
<form on:submit={handleSubmit} class="mb-4">
<div class="flex flex-row gap-4 mb-4">
<label class="flex-1 form-control">
<input
@ -104,7 +97,7 @@
type="text"
bind:value={secretKey}
placeholder="Key"
class={`input input-bordered ${!isKeyValid && secretKey.length > 0 ? "input-error text-error" : ""}`}
class={`input input-bordered ${!isKeyValid && secretKey.length > 0 ? 'input-error text-error' : ''}`}
/>
{#if !isKeyValid && secretKey.length > 0}
<div class="label">
@ -113,7 +106,6 @@
</span>
</div>
{/if}
</label>
<div class="flex-1 form-control">
@ -133,13 +125,11 @@
</div>
</div>
<!-- {#if !isKeyValid && secretKey.length > 0}
<AlertBar
message="All key names must be upper case, alphanumeric, and may include underscore (_)."
type="error"
/>
{/if} -->
</form>
</div>

View File

@ -22,7 +22,7 @@
c[name] = value
return c
},
{} as NonNullable<UpdateInstancePayload['fields']['secrets']>,
{} as NonNullable<UpdateInstancePayload['fields']['secrets']>
),
},
})
@ -42,17 +42,13 @@
{#each $items as item}
<tr transition:fade>
<th>{item.name}</th>
<td
>{item.value.slice(0, 2) +
item.value.slice(2).replaceAll(/./g, '*')}</td
>
<td>{item.value.slice(0, 2) + item.value.slice(2).replaceAll(/./g, '*')}</td>
<td class="text-right">
<button
aria-label="Delete"
on:click={handleDelete(item.name)}
type="button"
class="btn btn-sm btn-square btn-outline btn-warning"
><Fa icon={faTrash} /></button
class="btn btn-sm btn-square btn-outline btn-warning"><Fa icon={faTrash} /></button
>
</td>
</tr>

View File

@ -21,17 +21,14 @@
`// pb_hooks/env-test.pb.js\n\n` +
($items.length > 0
? $items
.map(
({ name, value }) =>
`const ${name} = process.env.${name}\nconsole.log("${name}: ", ${name})`,
)
.map(({ name, value }) => `const ${name} = process.env.${name}\nconsole.log("${name}: ", ${name})`)
.join('\n')
: `const YOUR_KEY = process.env.YOUR_KEY`)
</script>
<div class="mb-4">
These secrets are forwarded to your <code>pocketbase</code> as environment
variables, which are also accessible from any <code>pb_hooks</code> you have created.
These secrets are forwarded to your <code>pocketbase</code> as environment variables, which are also accessible from
any <code>pb_hooks</code> you have created.
</div>
<!-- If the user has any secrets, render them in a code block -->

View File

@ -47,10 +47,7 @@ function createItems(initialItems: SecretsArray) {
const { name, value } = sanitize(item)
return update((n) => {
return formatInput([
...n.filter((i) => i.name !== name),
{ name, value },
])
return formatInput([...n.filter((i) => i.name !== name), { name, value }])
})
},

View File

@ -48,9 +48,7 @@
isButtonDisabled = true
// Prompt the user to confirm the version change
const confirmVersionChange = confirm(
`Are you sure you want to change the version to ${selectedVersion}?`,
)
const confirmVersionChange = confirm(`Are you sure you want to change the version to ${selectedVersion}?`)
// If they select yes, then update the version in pocketbase
if (confirmVersionChange) {
@ -81,27 +79,19 @@
<div class="max-w-xl">
{#if power}
<AlertBar
message="Your instance must be powered off to change the version."
type="error"
/>
<AlertBar message="Your instance must be powered off to change the version." type="error" />
{/if}
<div class="mb-8">
We recommend you <strong>do a full backup</strong>
before making a change. We support the latest patch of
<a href="https://github.com/pocketbase/pocketbase/releases" class="link"
>every minor release</a
> of PocketBase.
<a href="https://github.com/pocketbase/pocketbase/releases" class="link">every minor release</a> of PocketBase.
</div>
{#if $is23Available || is23OrHigher}
<div class="mb-8 bg-info p-4 rounded text-info-content">
<p class="font-bold text-xl">Attention v0.23.* users:</p>
<p>
v0.22.* to v0.23.* is a major migration boundary and requires a manual
migration process.
</p>
<p>v0.22.* to v0.23.* is a major migration boundary and requires a manual migration process.</p>
<table class="table">
<thead class="text-info-content">
<tr>
@ -125,8 +115,7 @@
<td>0.23.*</td>
<td>&lt;=v0.22.*</td>
<td
>Create a new &lt;=v0.22.* instance and migrate your data
manually. Refer to the <a
>Create a new &lt;=v0.22.* instance and migrate your data manually. Refer to the <a
href="https://github.com/pocketbase/pocketbase/releases/tag/v0.23.0"
class="link">v0.23.* manual upgrade process</a
> and attempt to reverse it.</td
@ -140,17 +129,10 @@
<AlertBar message={successMessage} type="success" flash />
<AlertBar message={errorMessage} type="error" />
<form
class="flex change-version-form-container-query gap-4"
on:submit={handleSave}
>
<form class="flex change-version-form-container-query gap-4" on:submit={handleSave}>
<VersionPicker bind:selectedVersion bind:versions />
<button
type="submit"
class="btn btn-error"
disabled={power || isButtonDisabled}>Change Version</button
>
<button type="submit" class="btn btn-error" disabled={power || isButtonDisabled}>Change Version</button>
</form>
</div>

View File

@ -17,12 +17,7 @@
const dispatch = createEventDispatcher()
</script>
<select
class="select select-bordered w-full"
bind:value={selectedVersion}
on:change={handleSelect}
{disabled}
>
<select class="select select-bordered w-full" bind:value={selectedVersion} on:change={handleSelect} {disabled}>
<option value="" disabled>Select a version</option>
{#each versions as version}
<option value={version}>{version}</option>

View File

@ -1,10 +1,5 @@
<script lang="ts">
import {
globalInstancesStore,
userSubscriptionType,
versions,
userStore,
} from '$util/stores'
import { globalInstancesStore, userSubscriptionType, versions, userStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import Creator from './Creator.svelte'
import Paywall from './Paywall.svelte'
@ -24,9 +19,7 @@
<div class="flex items-center justify-center">
<div class="max-w-md">
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">
Create A New Instance
</h2>
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">Create A New Instance</h2>
{#if canCreate}
<Creator />

View File

@ -3,10 +3,7 @@
import CardHeader from '$components/cards/CardHeader.svelte'
import { client } from '$src/pocketbase-client'
import { handleCreateNewInstance } from '$util/database'
import {
faArrowsRotate,
faCircleExclamation,
} from '@fortawesome/free-solid-svg-icons'
import { faArrowsRotate, faCircleExclamation } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import { writable } from 'svelte/store'
import { slide } from 'svelte/transition'
@ -38,10 +35,7 @@
fetching: true,
}))
await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
await client().client.send(`/api/signup?name=${encodeURIComponent(name)}`, {})
instanceInfo.update((info) => ({
...info,
@ -68,8 +62,7 @@
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled =
$instanceInfo.name.length === 0 || !$instanceInfo.available
$: isFormButtonDisabled = $instanceInfo.name.length === 0 || !$instanceInfo.available
// Generate a unique name for the PocketHost instance
const handleInstanceNameRegeneration = () => {
@ -95,18 +88,13 @@
<CardHeader>Choose a name for your PocketBase instance.</CardHeader>
<div class="flex rename-instance-form-container-query gap-4">
<input
type="text"
bind:value={$instanceNameField}
class="input input-bordered w-full"
/>
<input type="text" bind:value={$instanceNameField} class="input input-bordered w-full" />
<button
type="button"
class="btn btn-outline btn-secondary"
aria-label="Regenerate Instance Name"
on:click={handleInstanceNameRegeneration}
><Fa icon={faArrowsRotate} /></button
on:click={handleInstanceNameRegeneration}><Fa icon={faArrowsRotate} /></button
>
</div>
@ -114,13 +102,9 @@
{#if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
<span class="text-success"> https://{$instanceInfo.name}.pockethost.io ✔︎</span>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
<span class="text-error"> https://{$instanceInfo.name}.pockethost.io ❌</span>
{/if}
</div>
@ -134,11 +118,7 @@
<div class="flex items-center justify-center gap-4">
<a href="/" class="btn">Cancel</a>
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
{#if isSubmitting}
<span class="loading loading-spinner loading-md"></span>
{:else}

View File

@ -7,11 +7,7 @@
onMount(async () => {
try {
stats.set(
(
(await client().client.collection('stats').getFullList()) || []
).pop() || {},
)
stats.set(((await client().client.collection('stats').getFullList()) || []).pop() || {})
} catch (e) {
console.error(e)
}

View File

@ -22,29 +22,20 @@
<h1 class="text-6xl font-bold mb-8">Amazingly Simple PocketBase Hosting</h1>
<p class="text-2xl mb-12">
Spend <b class="text-green-600 dark:text-green-400">less time</b> on
configuring your backend, and more time building
<b class="text-green-600 dark:text-green-400">new features</b> for your web
app.
Spend <b class="text-green-600 dark:text-green-400">less time</b> on configuring your backend, and more time
building
<b class="text-green-600 dark:text-green-400">new features</b> for your web app.
</p>
<AuthStateGuard>
<div slot="loading">
<PrimaryButton
text="Get Started"
url="/get-started"
icon={faArrowRight}
/>
<PrimaryButton text="Get Started" url="/get-started" icon={faArrowRight} />
</div>
<UserLoggedIn>
<PrimaryButton text="Dashboard" url="/dashboard" icon={faArrowRight} />
</UserLoggedIn>
<UserLoggedOut>
<PrimaryButton
text="Get Started"
url="/get-started"
icon={faArrowRight}
/>
<PrimaryButton text="Get Started" url="/get-started" icon={faArrowRight} />
</UserLoggedOut>
</AuthStateGuard>
</div>
@ -84,9 +75,7 @@
</div>
<div class="w-full">
<div
class="bg-zinc-900 p-[75px] border-zinc-700 border-t-2 rounded-[75px] flex flex-wrap justify-center"
>
<div class="bg-zinc-900 p-[75px] border-zinc-700 border-t-2 rounded-[75px] flex flex-wrap justify-center">
<SubFeatureBlock
icon={faServer}
title="Database"

View File

@ -39,10 +39,7 @@
<ul class="menu">
<li class="menu-title">Programming Guide</li>
<DocLink path="programming" title="Frontends and JS Hooks" />
<DocLink
path="server-side-pocketbase-antipattern"
title="Server-Side PocketBase is an Anti-Pattern"
/>
<DocLink path="server-side-pocketbase-antipattern" title="Server-Side PocketBase is an Anti-Pattern" />
</ul>
<ul class="menu">
<li class="menu-title">Appendix</li>

View File

@ -81,11 +81,7 @@
<AlertBar message={formError} type="error" />
<div class="card-actions justify-end">
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
{#if isButtonLoading}
<span class="loading loading-spinner"></span>
{:else}
@ -97,16 +93,12 @@
<div class="p-4 bg-zinc-800 text-center">
<div class="mb-4">
Need to Register? <button
type="button"
class="link font-bold"
on:click={handleRegisterClick}>Create A New Account</button
Need to Register? <button type="button" class="link font-bold" on:click={handleRegisterClick}
>Create A New Account</button
>
</div>
<div>
Forgot Your Password? <a href="login/password-reset" class="link font-bold"
>Reset Password</a
>
Forgot Your Password? <a href="login/password-reset" class="link font-bold">Reset Password</a>
</div>
</div>

View File

@ -1,7 +1,6 @@
<script lang="ts">
// Fun quotes when waiting for the instance to load. This could take up to 10 seconds
let processingQuote =
'Did you know it takes fourteen sentient robots to create each instance on PocketHost?'
let processingQuote = 'Did you know it takes fourteen sentient robots to create each instance on PocketHost?'
</script>
<div class="card-body">
@ -9,9 +8,7 @@
<span class="loading loading-spinner loading-lg mb-4"></span>
<div>
<h2 class="mb-12 font-bold text-white text-2xl">
Creating Your New Instance...
</h2>
<h2 class="mb-12 font-bold text-white text-2xl">Creating Your New Instance...</h2>
<p class="italic">{processingQuote}</p>
</div>

View File

@ -39,10 +39,7 @@
...info,
fetching: true,
}))
const res = await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
const res = await client().client.send(`/api/signup?name=${encodeURIComponent(name)}`, {})
instanceInfo.update((info) => ({
...info,
fetching: false,
@ -72,10 +69,7 @@
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled =
email.length === 0 ||
password.length === 0 ||
$instanceInfo.name.length === 0 ||
!$instanceInfo.available
email.length === 0 || password.length === 0 || $instanceInfo.name.length === 0 || !$instanceInfo.available
// Generate a unique name for the PocketHost instance
const handleInstanceNameRegeneration = () => {
@ -93,14 +87,9 @@
isFormButtonDisabled = true
isProcessing = true
await handleInstanceGeneratorWidget(
email,
password,
$instanceInfo.name,
(error) => {
formError = error
},
)
await handleInstanceGeneratorWidget(email, password, $instanceInfo.name, (error) => {
formError = error
})
isFormButtonDisabled = false
@ -135,10 +124,7 @@
class="input input-bordered w-full"
/>
<button
type="button"
class="btn btn-square"
on:click={handleInstanceNameRegeneration}
<button type="button" class="btn btn-square" on:click={handleInstanceNameRegeneration}
><Fa icon={faRotate} />
</button>
</div>
@ -149,13 +135,9 @@
{:else if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
<span class="text-success"> https://{$instanceInfo.name}.pockethost.io ✔︎</span>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
<span class="text-error"> https://{$instanceInfo.name}.pockethost.io ❌</span>
{/if}
</div>
</div>
@ -194,22 +176,14 @@
<AlertBar message={formError} type="error" />
<div class="card-actions justify-end">
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
Create <Fa icon={faArrowRight} />
</button>
</div>
</form>
<div class="p-4 bg-zinc-800 text-center">
Already have an account? <button
type="button"
class="link font-bold"
on:click={handleLoginClick}>Login</button
>
Already have an account? <button type="button" class="link font-bold" on:click={handleLoginClick}>Login</button>
</div>
</div>
{/if}

View File

@ -67,11 +67,7 @@
<AlertBar message={formError} type="error" />
<div class="mt-4 card-actions justify-end">
<button
type="submit"
class="btn btn-primary w-100"
disabled={isFormButtonDisabled}
>
<button type="submit" class="btn btn-primary w-100" disabled={isFormButtonDisabled}>
Reset Password <i class="bi bi-arrow-right-short" />
</button>
</div>

View File

@ -22,9 +22,7 @@
// Check for the token and block the request if it doesn't exist
if (!token) {
formErrors = [
'No token was found. Please check your email again for the link.',
]
formErrors = ['No token was found. Please check your email again for the link.']
return
}
@ -39,9 +37,7 @@
if (error instanceof Error) {
formErrors = client().parseError(error)
} else {
formErrors = [
'Something went wrong with confirming your password change.',
]
formErrors = ['Something went wrong with confirming your password change.']
}
}
@ -76,11 +72,7 @@
{/each}
<div class="mt-4 card-actions justify-end">
<button
type="submit"
class="btn btn-primary w-100"
disabled={isFormButtonDisabled}
>
<button type="submit" class="btn btn-primary w-100" disabled={isFormButtonDisabled}>
Save <i class="bi bi-arrow-right-short" />
</button>
</div>

View File

@ -8,19 +8,14 @@
<UserLoggedIn>
<a
class="btn btn-warning btn-lg {fixed
? ' btn-block rounded-none fixed bottom-0'
: ''}"
class="btn btn-warning btn-lg {fixed ? ' btn-block rounded-none fixed bottom-0' : ''}"
style="z-index: 1000"
href={`https://store.pockethost.io/buy/d4b2d062-429c-49b4-9cdc-853aaeb17e20?checkout[custom][user_id]=${$userStore?.id}&checkout[email]=${$userStore?.email}`}
>Unlock Access Now</a
>
</UserLoggedIn>
<UserLoggedOut>
<a
class="btn btn-warning btn-lg {fixed
? ' btn-block rounded-none fixed bottom-0'
: ''}"
href="/get-started">Get Started</a
<a class="btn btn-warning btn-lg {fixed ? ' btn-block rounded-none fixed bottom-0' : ''}" href="/get-started"
>Get Started</a
>
</UserLoggedOut>

View File

@ -5,24 +5,17 @@
export let feature: any
</script>
<div
class="flex flex-col items-center prose bg-neutral text-neutral-content rounded-lg relative w-full"
>
<div class="flex flex-col items-center prose bg-neutral text-neutral-content rounded-lg relative w-full">
<div class="text-2xl text-white pt-2 mb-5">
{feature.title}
</div>
{#if feature.img}
<div class="flex justify-center mr-5 ml-5 mb-5 not-prose">
<enhanced:img
src={feature.img}
class="rounded border border-1 border-gray-500 w-fit"
/>
<enhanced:img src={feature.img} class="rounded border border-1 border-gray-500 w-fit" />
</div>
{/if}
<div class="text-xl p-5 pt-0">{@html feature.description}</div>
<div
class="absolute top-0 left-0 text-primary-content text-2xl bg-primary p-2 rounded-br-lg rounded-tl-lg"
>
<div class="absolute top-0 left-0 text-primary-content text-2xl bg-primary p-2 rounded-br-lg rounded-tl-lg">
<Fa icon={faCheck} class="" />
</div>
</div>

View File

@ -19,19 +19,14 @@
<span class="badge badge-primary ml-2">new</span>
{/if}
{#if item.info}
<button
class="badge badge-secondary badge-sm ml-2"
on:click={handleInfoClick}
>
<button class="badge badge-secondary badge-sm ml-2" on:click={handleInfoClick}>
<Fa icon={faInfo} />
</button>
{/if}
</div>
{#if $showInfo}
<div
class={`text-neutral-content font-normal ${enlarge ? 'text-md' : 'text-sm'}`}
>
<div class={`text-neutral-content font-normal ${enlarge ? 'text-md' : 'text-sm'}`}>
{item.info}
</div>
{/if}

View File

@ -57,9 +57,7 @@
/>
</div>
<div
class="relative flex flex-col items-center mb-10 p-2 bg-neutral rounded-2xl w-fit mx-auto max-w-96"
>
<div class="relative flex flex-col items-center mb-10 p-2 bg-neutral rounded-2xl w-fit mx-auto max-w-96">
<a
href="https://www.youtube.com/watch?v=Xe0FrGzlcVM"
target="_blank"
@ -67,11 +65,7 @@
>
<Fa icon={faPlay} class="text-white text-4xl" />
</a>
<a
href="https://www.youtube.com/watch?v=Xe0FrGzlcVM"
target="_blank"
aria-label="PocketHost Demo"
>
<a href="https://www.youtube.com/watch?v=Xe0FrGzlcVM" target="_blank" aria-label="PocketHost Demo">
<enhanced:img src={thumb} alt="PocketHost Demo" class="rounded" />
</a>
</div>

View File

@ -6,7 +6,7 @@
export let selected: boolean = false
export let buttonText: string = 'Subscribe Now'
export let price: string
export let priceDetail: string=''
export let priceDetail: string = ''
export let bestDeal: boolean = false
export let title: string
export let cta: string
@ -38,7 +38,7 @@
{/each}
</div>
<div class="h-12 flex justify-start items-center flex-col">
<div class="text-3xl text-white text-center ">{price}</div>
<div class="text-3xl text-white text-center">{price}</div>
<div class="text-xs text-gray-400 text-center">{priceDetail}</div>
</div>
<div class="h-16 flex justify-center items-end">
@ -46,9 +46,7 @@
href={$isUserLoggedIn
? `https://store.pockethost.io/buy/d4b2d062-429c-49b4-9cdc-853aaeb17e20?checkout[custom][user_id]=${$userStore?.id}&checkout[email]=${$userStore?.email}`
: `javascript:alert('You must be logged in to subscribe')`}
class="btn {selected
? 'btn-warning'
: 'btn-neutral bg-neutral-700 text-white'} rounded-full"
class="btn {selected ? 'btn-warning' : 'btn-neutral bg-neutral-700 text-white'} rounded-full"
>
{buttonText}
</a>

View File

@ -33,8 +33,7 @@ export const features = [
},
{
title: 'Secure',
description:
'Infrastructure secured with RSA-2048 encryption and industry-standard security protocols.',
description: 'Infrastructure secured with RSA-2048 encryption and industry-standard security protocols.',
},
{
@ -47,13 +46,11 @@ export const features = [
},
{
title: 'Custom Domains',
description:
'Seamless custom domain integration for your PocketHost instances.',
description: 'Seamless custom domain integration for your PocketHost instances.',
},
{
title: 'Lifetime Option',
description:
'Limited-time opportunity for lifetime Pro tier access (up to 250 instances) with a single payment.',
description: 'Limited-time opportunity for lifetime Pro tier access (up to 250 instances) with a single payment.',
},
{
title: 'Early Access',
@ -61,13 +58,11 @@ export const features = [
},
{
title: 'Reliable',
description:
'Industry-leading 99.95% uptime guarantee ensures consistent application availability.',
description: 'Industry-leading 99.95% uptime guarantee ensures consistent application availability.',
},
{
title: 'Managed',
description:
'Comprehensive infrastructure management including scaling, backups, and maintenance operations.',
description: 'Comprehensive infrastructure management including scaling, backups, and maintenance operations.',
},
{
title: 'Affordable',

View File

@ -7,17 +7,14 @@
<h2 class="font-bold text-2xl mb-4 mt-10">Community Support</h2>
<p>
Community support is handled through <a
class="link"
href="https://github.com/pockethost/pockethost/discussions"
Community support is handled through <a class="link" href="https://github.com/pockethost/pockethost/discussions"
>Github Discussions</a
>.
</p>
<h2 class="font-bold text-2xl mb-4 mt-10">Discord</h2>
<p>
We also have a <a class="link" href={DISCORD_URL}>Discord server</a> with over
1,500 community members ready to help.
We also have a <a class="link" href={DISCORD_URL}>Discord server</a> with over 1,500 community members ready to help.
</p>
<h2 class="font-bold text-2xl mb-4 mt-10">Email Support</h2>

View File

@ -11,7 +11,7 @@
import PromoBanner from './PromoBanner.svelte'
import MothershipStatus from './MothershipStatus.svelte'
const currentYear = new Date().getFullYear();
const currentYear = new Date().getFullYear()
onMount(() => {
init()
@ -30,9 +30,7 @@
</div>
<slot />
</div>
<div
class="p-10 text-xs text-neutral-content flex flex-col text-center items-center"
>
<div class="p-10 text-xs text-neutral-content flex flex-col text-center items-center">
<div class="flex flex-row space-x-5">
<a href="/privacy">Privacy</a>
<a href="/terms">Terms</a>
@ -48,10 +46,7 @@
/>
<div>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-5Q6CM5HPCX"
></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-5Q6CM5HPCX"></script>
<script>
window.dataLayer = window.dataLayer || []
function gtag() {

View File

@ -7,8 +7,7 @@
<div class="text-center">
<h2 class="text-xl font-bold mb-2">Connection Lost</h2>
<p>
The PocketHost mothership is temporarily unreachable. Your instances are
safe and still reachable. Go to the <a
The PocketHost mothership is temporarily unreachable. Your instances are safe and still reachable. Go to the <a
href="https://status.pockethost.io"
class="link font-semibold underline">status page</a
> for more information.

View File

@ -17,9 +17,7 @@
// Convert the hash to a hex stringc
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
return hashHex
}

View File

@ -3,12 +3,6 @@
</script>
<div class="flex items-center justify-center gap-4">
<enhanced:img
src="./pockethost-cloud-logo.jpg"
class="mix-blend-lighten h-12 w-12"
alt="PocketHost Logo"
/>
<h1 class="text-white font-bold text-xl hidden md:block ml-1 {hideLogoText && 'sr-only'}">
PocketHost
</h1>
<enhanced:img src="./pockethost-cloud-logo.jpg" class="mix-blend-lighten h-12 w-12" alt="PocketHost Logo" />
<h1 class="text-white font-bold text-xl hidden md:block ml-1 {hideLogoText && 'sr-only'}">PocketHost</h1>
</div>

View File

@ -2,31 +2,15 @@
import Logo from '$src/routes/Navbar/Logo.svelte'
import Fa from 'svelte-fa'
import NavbarMenu from './NavbarMenu.svelte'
import {
faDiscord,
faGithub,
faProductHunt,
faYoutube,
} from '@fortawesome/free-brands-svg-icons'
import { faDiscord, faGithub, faProductHunt, faYoutube } from '@fortawesome/free-brands-svg-icons'
</script>
<div class="navbar bg-base-100 lg:pl-6">
<div class="flex-1 min-w-10">
<div class="dropdown mr-4">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<NavbarMenu isCollapsed />
@ -36,36 +20,16 @@
<!-- Redundant with the Logo component -->
<!-- <div class="hidden md:block ml-1 text-sm">PocketHost</div> -->
</a>
<a
href="https://github.com/pockethost/pockethost"
rel="noreferrer"
target="_blank"
class="m-2"
>
<a href="https://github.com/pockethost/pockethost" rel="noreferrer" target="_blank" class="m-2">
<Fa icon={faGithub} />
</a>
<a
href="https://www.producthunt.com/products/pockethost"
rel="noreferrer"
target="_blank"
class="m-2"
>
<a href="https://www.producthunt.com/products/pockethost" rel="noreferrer" target="_blank" class="m-2">
<Fa icon={faProductHunt} />
</a>
<a
href="https://discord.gg/nVTxCMEcGT"
rel="noreferrer"
target="_blank"
class="m-2"
>
<a href="https://discord.gg/nVTxCMEcGT" rel="noreferrer" target="_blank" class="m-2">
<Fa icon={faDiscord} />
</a>
<a
href="https://www.youtube.com/@pocketba5ed"
rel="noreferrer"
target="_blank"
class="m-2"
>
<a href="https://www.youtube.com/@pocketba5ed" rel="noreferrer" target="_blank" class="m-2">
<Fa icon={faYoutube} />
</a>
</div>

View File

@ -3,11 +3,7 @@
import UserHasRole from '$components/guards/UserHasRole.svelte'
import UserLoggedIn from '$components/guards/UserLoggedIn.svelte'
import UserLoggedOut from '$components/guards/UserLoggedOut.svelte'
import {
faDiscord,
faGithub,
faProductHunt,
} from '@fortawesome/free-brands-svg-icons'
import { faDiscord, faGithub, faProductHunt } from '@fortawesome/free-brands-svg-icons'
import Avatar from './Avatar.svelte'
import Fa from 'svelte-fa'
@ -66,10 +62,7 @@
<Avatar />
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-300 rounded-box z-[1] mt-36 md:mt-24 w-52 p-2 shadow"
>
<ul tabindex="0" class="dropdown-content menu bg-base-300 rounded-box z-[1] mt-36 md:mt-24 w-52 p-2 shadow">
<li><a href="/account">Settings</a></li>
<li>
<button on:click={handleLogoutAndRedirect}>Logout</button>

View File

@ -16,8 +16,7 @@
<div class="alert alert-info bg-yellow-300 rounded-none mb-10 relative">
<div class="text-info-content flex-1">
{latestPost.title}
<a href={latestPost.path} class="btn btn-sm btn-neutral m-2">Learn more</a
>
<a href={latestPost.path} class="btn btn-sm btn-neutral m-2">Learn more</a>
</div>
<button
class="btn btn-ghost btn-circle btn-xs absolute top-0 right-0"

View File

@ -14,10 +14,7 @@ export const handleFormError = (e: Error, setError?: FormErrorHandler) => {
}
}
export const handleCreateNewInstance = async (
instanceName: string,
setError?: FormErrorHandler,
) => {
export const handleCreateNewInstance = async (instanceName: string, setError?: FormErrorHandler) => {
const { user, createInstance } = client()
// Get the newly created user id
const { id } = user() || {}
@ -42,7 +39,7 @@ export const handleInstanceGeneratorWidget = async (
email: string,
password: string,
instanceName: string,
setError = (value: string) => {},
setError = (value: string) => {}
) => {
const { authViaEmail } = client()

View File

@ -17,7 +17,7 @@ try {
`logger`,
ConsoleLogger({
level: PUBLIC_DEBUG ? LogLevelName.Debug : LogLevelName.Info,
}),
})
)
} catch (e) {
console.warn(e)
@ -68,10 +68,7 @@ const continuouslyCheckMothershipReachability = () => {
}
async function fetchVersions(): Promise<string[]> {
const { versions } = await client().client.send<{ versions: string[] }>(
`/api/versions`,
{},
)
const { versions } = await client().client.send<{ versions: string[] }>(`/api/versions`, {})
return versions
}
@ -104,11 +101,9 @@ export const init = () => {
userStore.subscribe((user) => {
console.log(`userStore.subscribe`, { user })
const isPaid = [
SubscriptionType.Founder,
SubscriptionType.Premium,
SubscriptionType.Flounder,
].includes(user?.subscription || SubscriptionType.Free)
const isPaid = [SubscriptionType.Founder, SubscriptionType.Premium, SubscriptionType.Flounder].includes(
user?.subscription || SubscriptionType.Free
)
isUserPaid.set(isPaid)
isUserLegacy.set(!!user?.isLegacy)
userSubscriptionType.set(user?.subscription || SubscriptionType.Free)

View File

@ -28,10 +28,7 @@ export async function daemon() {
info(`Found ${containers.length} containers`)
await Promise.all(
containers.map(async (container) => {
if (
container.State === 'running' &&
container.Image === DOCKER_INSTANCE_IMAGE_NAME
) {
if (container.State === 'running' && container.Image === DOCKER_INSTANCE_IMAGE_NAME) {
try {
info(`Stopping ${container.Id}`)
await docker.getContainer(container.Id).stop()
@ -40,7 +37,7 @@ export async function daemon() {
warn(`Failed to stop ${container.Id}, but that's probably OK`, e)
}
}
}),
})
)
await PocketbaseService({})

View File

@ -7,11 +7,9 @@ type Options = {
}
export const ServeCommand = () => {
const cmd = new Command(`serve`)
.description(`Run an edge daemon server`)
.action(async (options: Options) => {
logger().context({ cli: 'edge:daemon:serve' })
await daemon()
})
const cmd = new Command(`serve`).description(`Run an edge daemon server`).action(async (options: Options) => {
logger().context({ cli: 'edge:daemon:serve' })
await daemon()
})
return cmd
}

View File

@ -1,23 +1,8 @@
import {
InstanceFields,
InstanceLogWriter,
InstanceLogWriterApi,
Logger,
PocketBase,
assert,
seqid,
} from '@'
import { InstanceFields, InstanceLogWriter, InstanceLogWriterApi, Logger, PocketBase, assert, seqid } from '@'
import { compact, forEach, map } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck'
import { spawn } from 'child_process'
import {
Mode,
constants,
createReadStream,
createWriteStream,
readFileSync,
writeFileSync,
} from 'fs'
import { Mode, constants, createReadStream, createWriteStream, readFileSync, writeFileSync } from 'fs'
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
import { dirname, isAbsolute, join, normalize, resolve, sep } from 'path'
import { DATA_ROOT } from '../../../../..'
@ -41,19 +26,12 @@ export type PathError = {
const UNIX_SEP_REGEX = /\//g
const WIN_SEP_REGEX = /\\/g
const checkBun = (
instance: InstanceFields,
virtualPath: string,
cwd: string,
) => {
const [subdomain, maybeImportant, ...rest] = virtualPath
.split('/')
.filter((p) => !!p)
const checkBun = (instance: InstanceFields, virtualPath: string, cwd: string) => {
const [subdomain, maybeImportant, ...rest] = virtualPath.split('/').filter((p) => !!p)
const isImportant =
maybeImportant === 'patches' ||
(rest.length === 0 &&
[`bun.lock`, `bun.lockb`, `package.json`].includes(maybeImportant || ''))
(rest.length === 0 && [`bun.lock`, `bun.lockb`, `package.json`].includes(maybeImportant || ''))
if (isImportant) {
const logger = InstanceLogWriter(instance.id, instance.volume, `exec`)
@ -96,14 +74,8 @@ const runBun = (() => {
new Promise<number | null>((resolve) => {
const proc = spawn(
'/root/.bun/bin/bun',
[
'install',
`--no-save`,
`--production`,
`--ignore-scripts`,
`--frozen-lockfile`,
],
{ cwd },
['install', `--no-save`, `--production`, `--ignore-scripts`, `--frozen-lockfile`],
{ cwd }
)
const tid = setTimeout(() => {
logger.error(`bun timeout after 10s`)
@ -120,7 +92,7 @@ const runBun = (() => {
clearTimeout(tid)
resolve(code)
})
}),
})
)
})()
@ -197,11 +169,7 @@ export class PhFs implements FileSystem {
throw new Error(`${subdomain} not found.`)
}
const instanceRootDir = restOfVirtualPath[0]
if (
instanceRootDir &&
POWERED_OFF_ONLY.includes(instanceRootDir) &&
instance.power
) {
if (instanceRootDir && POWERED_OFF_ONLY.includes(instanceRootDir) && instance.power) {
throw new Error(`Instance must be powered off first`)
}
if (instance.volume) {
@ -219,7 +187,7 @@ export class PhFs implements FileSystem {
const fsPath = resolve(
join(...fsPathParts, ...restOfVirtualPath)
.replace(UNIX_SEP_REGEX, sep)
.replace(WIN_SEP_REGEX, sep),
.replace(WIN_SEP_REGEX, sep)
)
// Create FTP client path using unix separator
@ -261,9 +229,7 @@ export class PhFs implements FileSystem {
}
async list(path = '.') {
const { dbg, error } = this.log
.create(`list`)
.breadcrumb({ cwd: this.cwd, path })
const { dbg, error } = this.log.create(`list`).breadcrumb({ cwd: this.cwd, path })
const { fsPath, instance } = await this._resolvePath(path)
@ -303,7 +269,7 @@ export class PhFs implements FileSystem {
})
})
.catch(() => null)
}),
})
)
})
.then(compact)
@ -312,9 +278,7 @@ export class PhFs implements FileSystem {
async get(fileName: string): Promise<FileStat> {
const { fsPath, instance, clientPath } = await this._resolvePath(fileName)
const { dbg, error } = this.log
.create(`get`)
.breadcrumb({ cwd: this.cwd, fileName, fsPath })
const { dbg, error } = this.log.create(`get`).breadcrumb({ cwd: this.cwd, fileName, fsPath })
dbg(`get`)
/*
@ -354,13 +318,8 @@ export class PhFs implements FileSystem {
})
}
async write(
fileName: string,
options?: { append?: boolean | undefined; start?: any } | undefined,
) {
const { dbg, error } = this.log
.create(`write`)
.breadcrumb({ cwd: this.cwd, fileName })
async write(fileName: string, options?: { append?: boolean | undefined; start?: any } | undefined) {
const { dbg, error } = this.log.create(`write`).breadcrumb({ cwd: this.cwd, fileName })
dbg(`write`)
const { fsPath, clientPath, instance } = await this._resolvePath(fileName)
@ -391,13 +350,8 @@ export class PhFs implements FileSystem {
}
}
async read(
fileName: string,
options: { start?: any } | undefined,
): Promise<any> {
const { dbg, error } = this.log
.create(`read`)
.breadcrumb({ cwd: this.cwd, fileName })
async read(fileName: string, options: { start?: any } | undefined): Promise<any> {
const { dbg, error } = this.log.create(`read`).breadcrumb({ cwd: this.cwd, fileName })
dbg(`read`)
const { fsPath, clientPath } = await this._resolvePath(fileName)
@ -419,9 +373,7 @@ export class PhFs implements FileSystem {
}
async delete(path: string) {
const { dbg, error } = this.log
.create(`delete`)
.breadcrumb({ cwd: this.cwd, path })
const { dbg, error } = this.log.create(`delete`).breadcrumb({ cwd: this.cwd, path })
dbg(`delete`)
const { fsPath, instance } = await this._resolvePath(path)
@ -436,9 +388,7 @@ export class PhFs implements FileSystem {
}
async mkdir(path: string) {
const { dbg, error } = this.log
.create(`mkdir`)
.breadcrumb({ cwd: this.cwd, path })
const { dbg, error } = this.log.create(`mkdir`).breadcrumb({ cwd: this.cwd, path })
dbg(`mkdir`)
const { fsPath } = await this._resolvePath(path)
@ -447,9 +397,7 @@ export class PhFs implements FileSystem {
}
async rename(from: string, to: string) {
const { dbg, error } = this.log
.create(`rename`)
.breadcrumb({ cwd: this.cwd, from, to })
const { dbg, error } = this.log.create(`rename`).breadcrumb({ cwd: this.cwd, from, to })
dbg(`rename`)
const { fsPath: fromPath, instance } = await this._resolvePath(from)
@ -462,9 +410,7 @@ export class PhFs implements FileSystem {
}
async chmod(path: string, mode: Mode) {
const { dbg, error } = this.log
.create(`chmod`)
.breadcrumb({ cwd: this.cwd, path, mode })
const { dbg, error } = this.log.create(`chmod`).breadcrumb({ cwd: this.cwd, path, mode })
dbg(`chmod`)
const { fsPath } = await this._resolvePath(path)

View File

@ -30,15 +30,11 @@ export const FolderNamesMap: {
export const INSTANCE_ROOT_VIRTUAL_FOLDER_NAMES = keys(FolderNamesMap)
export const INSTANCE_ROOT_PHYSICAL_FOLDER_NAMES = values(FolderNamesMap)
export function isInstanceRootVirtualFolder(
name: string,
): name is VirtualFolderNames {
export function isInstanceRootVirtualFolder(name: string): name is VirtualFolderNames {
return INSTANCE_ROOT_VIRTUAL_FOLDER_NAMES.includes(name as VirtualFolderNames)
}
export function virtualFolderGuard(
name: string,
): asserts name is VirtualFolderNames {
export function virtualFolderGuard(name: string): asserts name is VirtualFolderNames {
if (!isInstanceRootVirtualFolder(name)) {
// throw new Error(`Accessing ${name} is not allowed.`)
}

View File

@ -23,7 +23,7 @@ export const ftpService = mkSingleton((config: Partial<FtpConfig> = {}) => {
{
mothershipUrl: MOTHERSHIP_URL(),
},
config,
config
)
const tls = {
key: readFileSync(SSL_KEY()),
@ -42,34 +42,27 @@ export const ftpService = mkSingleton((config: Partial<FtpConfig> = {}) => {
pasv_min: PH_FTP_PASV_PORT_MIN(),
})
ftpServer.on(
'login',
async ({ connection, username, password }, resolve, reject) => {
dbg(`Got a connection with credentials ${username}:${password}`)
dbg(`Finding ${mothershipUrl}`)
const client = new PocketBase(mothershipUrl)
try {
if (username === `__auth__`) {
client.authStore.loadFromCookie(password)
if (!client.authStore.isValid) {
throw new Error(`Invalid cookie`)
}
} else {
await client.collection('users').authWithPassword(username, password)
ftpServer.on('login', async ({ connection, username, password }, resolve, reject) => {
dbg(`Got a connection with credentials ${username}:${password}`)
dbg(`Finding ${mothershipUrl}`)
const client = new PocketBase(mothershipUrl)
try {
if (username === `__auth__`) {
client.authStore.loadFromCookie(password)
if (!client.authStore.isValid) {
throw new Error(`Invalid cookie`)
}
dbg(`Logged in`)
const fs = new PhFs(
connection,
client,
_ftpServiceLogger.child(username).context({ ftpSession: Date.now() }),
)
resolve({ fs })
} catch (e) {
reject(new Error(`Invalid username or password`))
return
} else {
await client.collection('users').authWithPassword(username, password)
}
},
)
dbg(`Logged in`)
const fs = new PhFs(connection, client, _ftpServiceLogger.child(username).context({ ftpSession: Date.now() }))
resolve({ fs })
} catch (e) {
reject(new Error(`Invalid username or password`))
return
}
})
ftpServer.listen().then(() => {
dbg('Ftp server started...')

View File

@ -7,11 +7,9 @@ type Options = {
}
export const ServeCommand = () => {
const cmd = new Command(`serve`)
.description(`Run an edge FTP server`)
.action(async (options: Options) => {
logger().context({ cli: 'edge:ftp:serve' })
await ftp()
})
const cmd = new Command(`serve`).description(`Run an edge FTP server`).action(async (options: Options) => {
logger().context({ cli: 'edge:ftp:serve' })
await ftp()
})
return cmd
}

View File

@ -16,21 +16,14 @@ export const MigrateCommand = () => {
return new Command(`migrate`)
.description(`Migrate instance(s)`)
.option(`-i, --instance <instanceId>`, `The instance to migrate`)
.option(
`-m, --mount-point <mountPoint>`,
`The mount point`,
`cloud-storage`,
)
.option(`-m, --mount-point <mountPoint>`, `The mount point`, `cloud-storage`)
.action(async (options) => {
console.log({ options })
const { instance: instanceId, mountPoint } = options
const pb = new PocketBase(MOTHERSHIP_URL())
await pb.admins.authWithPassword(
MOTHERSHIP_ADMIN_USERNAME(),
MOTHERSHIP_ADMIN_PASSWORD(),
)
await pb.admins.authWithPassword(MOTHERSHIP_ADMIN_USERNAME(), MOTHERSHIP_ADMIN_PASSWORD())
const filter = [`status='idle'`, `volume=''`]
if (instanceId) {
@ -49,9 +42,7 @@ export const MigrateCommand = () => {
}
async function migrate(mountPoint: string, pb: PocketBase, filter: string) {
const instance = await pb
.collection<InstanceFields>('instances')
.getFirstListItem(filter)
const instance = await pb.collection<InstanceFields>('instances').getFirstListItem(filter)
const oldSuspension = instance.suspension
try {
@ -68,7 +59,7 @@ async function migrate(mountPoint: string, pb: PocketBase, filter: string) {
moveSync(instanceDataSrc, instanceDataDst)
} else {
console.warn(
`Skipping ${instanceDataSrc} to ${instanceDataDst} because source does not exist or destination already exists`,
`Skipping ${instanceDataSrc} to ${instanceDataDst} because source does not exist or destination already exists`
)
}
@ -81,9 +72,7 @@ async function migrate(mountPoint: string, pb: PocketBase, filter: string) {
} catch (e) {
console.error(`${e}`, JSON.stringify(e, null, 2))
} finally {
console.log(
`Restoring previous suspension value of instance ${instance.id}`,
)
console.log(`Restoring previous suspension value of instance ${instance.id}`)
await pb.collection<InstanceFields>('instances').update(instance.id, {
suspension: oldSuspension === 'migrating' ? '' : oldSuspension,
})

View File

@ -51,11 +51,7 @@ const cleanup = async (mountPoint: string) => {
}
}
const mount = async (
remoteName: string,
bucketName: string,
optionsIn: Partial<MountOptions> = {},
) => {
const mount = async (remoteName: string, bucketName: string, optionsIn: Partial<MountOptions> = {}) => {
const options: MountOptions = {
mountPoint: VOLUME_MOUNT_POINT(),
cacheDir: VOLUME_CACHE_DIR(),

View File

@ -5,11 +5,7 @@ import IPCIDR from 'ip-cidr'
export const createIpWhitelistMiddleware = (blockedCIDRs: string[]) => {
const blockedCIDRObjects = blockedCIDRs.map((cidr) => new IPCIDR(cidr))
return (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const ip = req.ip // or req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (
blockedCIDRs.length === 0 ||

View File

@ -3,11 +3,7 @@ import { Handler, Request } from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'
import vhost from 'vhost'
export function createVhostProxyMiddleware(
host: string,
target: string,
ws = false,
): Handler {
export function createVhostProxyMiddleware(host: string, target: string, ws = false): Handler {
const { dbg } = logger()
dbg(`Creating ${host}->${target}`)
const handler = createProxyMiddleware({ target, ws, changeOrigin: ws })

View File

@ -7,11 +7,9 @@ type Options = {
}
export const ServeCommand = () => {
const cmd = new Command(`serve`)
.description(`Serve the root firewall`)
.action(async (options: Options) => {
logger().context({ cli: 'firewall:serve' })
await firewall()
})
const cmd = new Command(`serve`).description(`Serve the root firewall`).action(async (options: Options) => {
logger().context({ cli: 'firewall:serve' })
await firewall()
})
return cmd
}

View File

@ -3,13 +3,7 @@ import { execSync } from 'child_process'
import fetch from 'node-fetch'
import { default as osu } from 'node-os-utils'
import { freemem } from 'os'
import {
DAEMON_PORT,
DISCORD_HEALTH_CHANNEL_URL,
LoggerService,
MOTHERSHIP_PORT,
stringify,
} from '../../..'
import { DAEMON_PORT, DISCORD_HEALTH_CHANNEL_URL, LoggerService, MOTHERSHIP_PORT, stringify } from '../../..'
export const checkHealth = async () => {
const { cpu, drive } = osu
@ -52,8 +46,7 @@ export const checkHealth = async () => {
Image: 'pockethost-instance',
Labels: '',
LocalVolumes: '0',
Mounts:
'/home/pocketho…,/home/pocketho…,/home/pocketho…,/home/pocketho…,/home/pocketho…',
Mounts: '/home/pocketho…,/home/pocketho…,/home/pocketho…,/home/pocketho…,/home/pocketho…',
Names: 'kekbase-1705984569777',
Networks: 'bridge',
Ports: '0.0.0.0:44447-\u003e8090/tcp, :::44447-\u003e8090/tcp',
@ -150,9 +143,9 @@ export const checkHealth = async () => {
.catch((e) => {
console.error({ e })
throw e
}),
),
),
})
)
)
)
const openFiles = _exec(`cat /proc/sys/fs/file-nr`)[0]?.trim() || `Unknown`
@ -164,9 +157,7 @@ export const checkHealth = async () => {
`CPU Usage: ${await cpu.usage()}%`,
`Free RAM: ${getFreeMemoryInGB()}GB`,
`Free Storage /: ${(await drive.info(`/`)).freeGb}GB`,
`Free Storage /mnt/sfo_data: ${
(await drive.info(`/mnt/sfo_data`)).freeGb
}GB`,
`Free Storage /mnt/sfo_data: ${(await drive.info(`/mnt/sfo_data`)).freeGb}GB`,
`Open files: ${openFiles}`,
`Containers: ${containers.length}`,
]
@ -210,7 +201,7 @@ export const checkHealth = async () => {
dbg(`${url}: ${e}`)
check.isHealthy = false
}
}),
})
)
dbg({ checks })
await send([
@ -229,13 +220,9 @@ export const checkHealth = async () => {
if (isInstance) {
return `${
isHealthy ? ':white_check_mark:' : ':face_vomiting: '
} \`${name.padStart(30)} ${(mem || '').padStart(10)} ${(
ago || ''
).padStart(20)}\``
} \`${name.padStart(30)} ${(mem || '').padStart(10)} ${(ago || '').padStart(20)}\``
} else {
return `${
isHealthy ? ':white_check_mark:' : ':face_vomiting: '
} ${name}`
return `${isHealthy ? ':white_check_mark:' : ':face_vomiting: '} ${name}`
}
}),
])

View File

@ -8,10 +8,8 @@ export const compact = async () => {
const files = [
...new Set(
[`data`, `logs`].flatMap((db) =>
globSync(`${DATA_ROOT()}/*/pb_data/${db}.db{-shm,-wal}`).map((f) =>
f.replace(/-(?:shm|wal)$/, ''),
),
),
globSync(`${DATA_ROOT()}/*/pb_data/${db}.db{-shm,-wal}`).map((f) => f.replace(/-(?:shm|wal)$/, ''))
)
),
]

View File

@ -16,20 +16,18 @@ export const HealthCommand = () => {
info(`Starting`)
const { checkHealth } = await import(`./checkHealth`)
await checkHealth()
}),
})
)
.addCommand(
new Command(`compact`)
.description(
`Compact SQLite databases by removing old SHM and WAL files`,
)
.description(`Compact SQLite databases by removing old SHM and WAL files`)
.action(async (options: Options) => {
logger().context({ cli: 'health:compact' })
const { dbg, error, info, warn } = logger()
info(`Starting`)
const { compact } = await import(`./compact`)
await compact()
}),
})
)
.action(() => {
cmd.help()

View File

@ -1,21 +1,13 @@
import {
GobotService,
LoggerService,
MOTHERSHIP_HOOKS_DIR,
PH_ALLOWED_POCKETBASE_SEMVER,
stringify,
} from '@'
import { GobotService, LoggerService, MOTHERSHIP_HOOKS_DIR, PH_ALLOWED_POCKETBASE_SEMVER, stringify } from '@'
import { uniq } from '@s-libs/micro-dash'
import { Command } from 'commander'
import { writeFileSync } from 'fs'
import { compare, parse, prerelease, satisfies } from 'semver'
export const UpdateVersionsCommand = () => {
const cmd = new Command(`update-versions`)
.description(`Update pocketbase versions`)
.action(async () => {
await freshenPocketbaseVersions()
})
const cmd = new Command(`update-versions`).description(`Update pocketbase versions`).action(async () => {
await freshenPocketbaseVersions()
})
return cmd
}
@ -30,14 +22,10 @@ export async function freshenPocketbaseVersions() {
(await bot.versions())
.map((v) => parse(v))
.filter((v) => !!v)
.filter(
(v) =>
satisfies(v, PH_ALLOWED_POCKETBASE_SEMVER()) &&
prerelease(v) === null,
)
.filter((v) => satisfies(v, PH_ALLOWED_POCKETBASE_SEMVER()) && prerelease(v) === null)
.sort(compare)
.reverse()
.map((v) => `${v.major}.${v.minor}.*`),
.map((v) => `${v.major}.${v.minor}.*`)
)
const cjs = `module.exports = ${stringify(versions, null, 2)}`

View File

@ -3,13 +3,11 @@ import { Command } from 'commander'
import { freshenPocketbaseVersions } from './freshenPocketbaseVersions'
export const UpdateCommand = () => {
const cmd = new Command(`update`)
.description(`Update PocketBase versions`)
.action(async (options) => {
logger().context({ cli: 'pocketbase:update' })
const { info } = logger()
await freshenPocketbaseVersions()
info('PocketBase versions updated')
})
const cmd = new Command(`update`).description(`Update PocketBase versions`).action(async (options) => {
logger().context({ cli: 'pocketbase:update' })
const { info } = logger()
await freshenPocketbaseVersions()
info('PocketBase versions updated')
})
return cmd
}

View File

@ -6,9 +6,7 @@ type Options = {
}
export const MailCommand = () => {
const cmd = new Command(`mail`)
.description(`PocketHost bulk mail`)
.addCommand(SendMailCommand())
const cmd = new Command(`mail`).description(`PocketHost bulk mail`).addCommand(SendMailCommand())
return cmd
}

View File

@ -33,10 +33,7 @@ export const SendMailCommand = () =>
logger().context({ cli: 'sendmail' })
const { dbg, info } = logger()
function interpolateString(
template: string,
dict: { [key: string]: string },
): string {
function interpolateString(template: string, dict: { [key: string]: string }): string {
return template.replace(/\{\$(\w+)\}/g, (match, key) => {
dbg({ match, key })
const lowerKey = key.toLowerCase()
@ -51,14 +48,9 @@ export const SendMailCommand = () =>
info(MOTHERSHIP_URL())
const client = new PocketBase(MOTHERSHIP_URL())
await client.admins.authWithPassword(
MOTHERSHIP_ADMIN_USERNAME(),
MOTHERSHIP_ADMIN_PASSWORD(),
)
await client.admins.authWithPassword(MOTHERSHIP_ADMIN_USERNAME(), MOTHERSHIP_ADMIN_PASSWORD())
const message = await client
.collection(`campaign_messages`)
.getOne(messageId, { expand: 'campaign' })
const message = await client.collection(`campaign_messages`).getOne(messageId, { expand: 'campaign' })
const { campaign } = message.expand || {}
dbg({ messageId, limit, message, campaign })
@ -69,7 +61,7 @@ export const SendMailCommand = () =>
map(campaign.vars, async (sql, k) => {
const result = db.prepare(sql).get() as { value: string }
vars[k.toLocaleLowerCase()] = result.value
}),
})
)
dbg({ vars })
@ -114,7 +106,7 @@ export const SendMailCommand = () =>
})
}
})
}),
})
)
db.close()

View File

@ -9,16 +9,14 @@ type Options = {
}
export const ServeCommand = () => {
const cmd = new Command(`serve`)
.description(`Run the entire PocketHost stack`)
.action(async (options: Options) => {
logger().context({ cli: 'serve' })
const { dbg, error, info, warn } = logger()
info(`Starting`)
const cmd = new Command(`serve`).description(`Run the entire PocketHost stack`).action(async (options: Options) => {
logger().context({ cli: 'serve' })
const { dbg, error, info, warn } = logger()
info(`Starting`)
await Promise.all([mothership(options), daemon(), firewall()])
await Promise.all([mothership(options), daemon(), firewall()])
await neverendingPromise()
})
await neverendingPromise()
})
return cmd
}

View File

@ -1,9 +1,4 @@
import {
GobotService,
ioc,
RegisterEnvSettingsService,
WinstonLoggerService,
} from '@'
import { GobotService, ioc, RegisterEnvSettingsService, WinstonLoggerService } from '@'
import { version } from '../../package.json'
export const initIoc = async () => {

View File

@ -52,10 +52,10 @@ export const createCleanupManager = (slug?: string) => {
(c, v) => {
return c.then(() => v())
},
Promise.resolve(),
Promise.resolve()
).catch((e) => {
error(
`Cleanup functions are failing. This should never happen, check all cleanup functions to make sure they are trapping their exceptions.`,
`Cleanup functions are failing. This should never happen, check all cleanup functions to make sure they are trapping their exceptions.`
)
throw e
})

View File

@ -10,9 +10,7 @@ const CONSOLE_METHODS = {
[LogLevelName.Abort]: console.error,
}
export function ConsoleLogger(
initialConfig: Partial<LoggerConfig> = {},
): Logger {
export function ConsoleLogger(initialConfig: Partial<LoggerConfig> = {}): Logger {
let config: LoggerConfig = {
level: LogLevelName.Info,
pfx: [],

View File

@ -5,10 +5,7 @@ export type Allocator<T> = {
alloc: () => [T, Deallocator]
}
export function ResourceAllocator<T>(
initialPoolSize: number,
initializer: () => T,
): Allocator<T> {
export function ResourceAllocator<T>(initialPoolSize: number, initializer: () => T): Allocator<T> {
// Available queue to hold resolved items of type T
const available: T[] = []

View File

@ -43,11 +43,7 @@ export const createTimerManager = (config?: Partial<TimeManagerConfig>) => {
dbg(`done`, cleanups)
}
const repeat = (
cb: RepeatableTimerCallback,
ms: UnixTimestampMs,
initial = true,
) => {
const repeat = (cb: RepeatableTimerCallback, ms: UnixTimestampMs, initial = true) => {
let _unsub: TimerCanceler | undefined = undefined
const _again = async () => {
const shouldRepeat = await cb()

View File

@ -1,25 +1,16 @@
export function assertExists<TType>(
v: TType,
message = `Value does not exist`,
): asserts v is NonNullable<TType> {
export function assertExists<TType>(v: TType, message = `Value does not exist`): asserts v is NonNullable<TType> {
if (typeof v === 'undefined') {
throw new Error(message)
}
}
export function assertTruthy<TType>(
v: unknown,
message = `Value should be truthy`,
): asserts v is NonNullable<TType> {
export function assertTruthy<TType>(v: unknown, message = `Value should be truthy`): asserts v is NonNullable<TType> {
if (!v) {
throw new Error(message)
}
}
export function assert<T>(
v: T | undefined | void | null,
msg?: string,
): asserts v is T {
export function assert<T>(v: T | undefined | void | null, msg?: string): asserts v is T {
if (!v) {
throw new Error(msg || `Assertion failure`)
}

View File

@ -2,25 +2,16 @@ import { values } from '@s-libs/micro-dash'
export type Unsubscribe = () => void
export type EventSubscriber<TPayload> = (
cb: EventHandler<TPayload>,
) => Unsubscribe
export type EventEmitter<TPayload> = (
payload: TPayload,
stopOnHandled?: boolean,
) => Promise<boolean>
export type EventHandler<TPayload> = (
payload: TPayload,
isHandled: boolean,
) => boolean | void | Promise<boolean | void>
export type EventSubscriber<TPayload> = (cb: EventHandler<TPayload>) => Unsubscribe
export type EventEmitter<TPayload> = (payload: TPayload, stopOnHandled?: boolean) => Promise<boolean>
export type EventHandler<TPayload> = (payload: TPayload, isHandled: boolean) => boolean | void | Promise<boolean | void>
/**
* @param defaultHandler Optional handler to call if no handler calls
* `handled()`
* @param defaultHandler Optional handler to call if no handler calls `handled()`
* @returns Void
*/
export const createEvent = <TPayload>(
defaultHandler?: EventHandler<TPayload>,
defaultHandler?: EventHandler<TPayload>
): [EventSubscriber<TPayload>, EventEmitter<TPayload>] => {
let i = 0
const callbacks: any = {}

View File

@ -11,7 +11,7 @@ export const ioc = <T = unknown>(serviceName: string, register?: T): T => {
if (register) {
if (serviceName in services) {
throw new Error(
`Service with key '${serviceName}' already registered. First registered: ${services[serviceName]!.stack}`,
`Service with key '${serviceName}' already registered. First registered: ${services[serviceName]!.stack}`
)
} else {
services[serviceName] = { service: register, stack: new Error().stack }

View File

@ -1,15 +1,11 @@
/**
* Merges a config object with defaults. Shallow copies root descendants,
* skipping`undefined` values.
* Merges a config object with defaults. Shallow copies root descendants, skipping`undefined` values.
*
* @param defaultConfig The default config values
* @param partialConfigs A partial config to merge with defaults
* @returns
*/
export function mergeConfig<T>(
defaultConfig: T,
...partialConfigs: Partial<T>[]
): T {
export function mergeConfig<T>(defaultConfig: T, ...partialConfigs: Partial<T>[]): T {
const result: T = { ...defaultConfig }
for (const partialConfig of partialConfigs) {

View File

@ -2,10 +2,9 @@ export type SingletonApi = Object
export type SingletonBaseConfig = {}
export function mkSingleton<
TConfig extends SingletonBaseConfig,
TInstance extends SingletonApi,
>(factory: (config: TConfig) => TInstance): (config?: TConfig) => TInstance {
export function mkSingleton<TConfig extends SingletonBaseConfig, TInstance extends SingletonApi>(
factory: (config: TConfig) => TInstance
): (config?: TConfig) => TInstance {
let instance: TInstance | undefined
return (config?: TConfig): TInstance => {

View File

@ -15,7 +15,7 @@ export const createRestHelper = (config: RestHelperConfig) => {
const mkRest = <TPayload extends JsonObject, TResult extends JsonObject>(
cmd: RestCommands,
method: RestMethods,
schema: JSONSchemaType<TPayload>,
schema: JSONSchemaType<TPayload>
) => {
const validator = new Ajv().compile(schema)
return async (payload: TPayload): Promise<TResult> => {
@ -28,9 +28,7 @@ export const createRestHelper = (config: RestHelperConfig) => {
}
const _payload = { ...payload }
const url = `/api/${cmd}${
method === RestMethods.Post ? '' : '/:id'
}`.replace(/:([a-zA-Z]+)/g, (_, key) => {
const url = `/api/${cmd}${method === RestMethods.Post ? '' : '/:id'}`.replace(/:([a-zA-Z]+)/g, (_, key) => {
if (!(key in _payload)) {
throw new Error(`Payload must include '${key}`)
}

View File

@ -1,14 +1,3 @@
import PocketBase, {
BaseAuthStore,
ClientResponseError,
type AuthModel,
type UnsubscribeFunc,
} from 'pocketbase'
import PocketBase, { BaseAuthStore, ClientResponseError, type AuthModel, type UnsubscribeFunc } from 'pocketbase'
export {
AuthModel,
BaseAuthStore,
ClientResponseError,
PocketBase,
UnsubscribeFunc,
}
export { AuthModel, BaseAuthStore, ClientResponseError, PocketBase, UnsubscribeFunc }

View File

@ -9,12 +9,11 @@ export type CreateInstanceResult = {
instance: InstanceFields
}
export const CreateInstancePayloadSchema: JSONSchemaType<CreateInstancePayload> =
{
type: 'object',
properties: {
subdomain: { type: 'string' },
},
required: ['subdomain'],
additionalProperties: false,
}
export const CreateInstancePayloadSchema: JSONSchemaType<CreateInstancePayload> = {
type: 'object',
properties: {
subdomain: { type: 'string' },
},
required: ['subdomain'],
additionalProperties: false,
}

View File

@ -10,12 +10,11 @@ export type DeleteInstanceResult = {
message?: string
}
export const DeleteInstancePayloadSchema: JSONSchemaType<DeleteInstancePayload> =
{
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
additionalProperties: false,
}
export const DeleteInstancePayloadSchema: JSONSchemaType<DeleteInstancePayload> = {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
additionalProperties: false,
}

View File

@ -3,18 +3,7 @@ import { InstanceFields, InstanceId } from '..'
export type UpdateInstancePayload = {
id: InstanceId
fields: Partial<
Pick<
InstanceFields,
| 'power'
| 'secrets'
| 'subdomain'
| 'syncAdmin'
| 'version'
| 'dev'
| 'cname'
>
>
fields: Partial<Pick<InstanceFields, 'power' | 'secrets' | 'subdomain' | 'syncAdmin' | 'version' | 'dev' | 'cname'>>
}
export const SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/
@ -24,36 +13,35 @@ export type UpdateInstanceResult = {
message?: string
}
export const UpdateInstancePayloadSchema: JSONSchemaType<UpdateInstancePayload> =
{
type: 'object',
properties: {
id: { type: 'string' },
fields: {
type: 'object',
properties: {
syncAdmin: { type: 'boolean', nullable: true },
subdomain: { type: 'string', nullable: true },
power: { type: 'boolean', nullable: true },
version: {
type: 'string',
nullable: true,
},
secrets: {
type: 'object',
nullable: true,
patternProperties: {
[SECRET_KEY_REGEX.source]: {
anyOf: [{ type: 'string' }],
},
},
required: [],
},
dev: { type: 'boolean', nullable: true },
cname: { type: 'string', nullable: true },
export const UpdateInstancePayloadSchema: JSONSchemaType<UpdateInstancePayload> = {
type: 'object',
properties: {
id: { type: 'string' },
fields: {
type: 'object',
properties: {
syncAdmin: { type: 'boolean', nullable: true },
subdomain: { type: 'string', nullable: true },
power: { type: 'boolean', nullable: true },
version: {
type: 'string',
nullable: true,
},
secrets: {
type: 'object',
nullable: true,
patternProperties: {
[SECRET_KEY_REGEX.source]: {
anyOf: [{ type: 'string' }],
},
},
required: [],
},
dev: { type: 'boolean', nullable: true },
cname: { type: 'string', nullable: true },
},
},
required: ['id', 'fields'],
additionalProperties: false,
}
},
required: ['id', 'fields'],
additionalProperties: false,
}

View File

@ -1,6 +1,6 @@
export * from './Instance'
export * from './InstanceLog'
export * from './Rest'
export * from './User'
export * from './types'
export * from './User'
export * from './util'

Some files were not shown because too many files have changed in this diff Show More