Initial commit: Premier Gunner tracker + StartOS 0.4.0 s9pk package
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { store } from '../fileModels/store'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
password: Value.text({
|
||||
name: i18n('Password'),
|
||||
description: i18n(
|
||||
'The password Gunner types on the login screen (at least 4 characters)',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 4,
|
||||
}),
|
||||
})
|
||||
|
||||
export const setPassword = sdk.Action.withInput(
|
||||
'set-password',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Set Login Password'),
|
||||
description: i18n('Set the password Gunner uses to log in to Premier Gunner'),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const password = await store.read((s) => s.password).const(effects)
|
||||
return { password: password ?? undefined }
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
await store.merge(effects, { password: input.password })
|
||||
},
|
||||
)
|
||||
|
||||
export const actions = sdk.Actions.of().addAction(setPassword)
|
||||
@@ -0,0 +1,5 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
export const { createBackup, restoreInit } = sdk.setupBackups(
|
||||
async ({ effects }) => sdk.Backups.ofVolumes('main'),
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
export const setDependencies = sdk.setupDependencies(
|
||||
async ({ effects }) => ({}),
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
Use the `/fileModels` directory to create separate `.ts` files to represent underlying files used by you package. The exported `FileModels` afford a convenient and type safe way to read amd write to the underlying files, as well as react to changes.
|
||||
|
||||
Supported file formats are `.yaml`, `.toml`, `.json`, `.env`, `.ini`, and `.txt`. For alternative file formats, you can use the `raw` method and provide custom serialization and parser functions.
|
||||
|
||||
It is common for packages to use a `store.json.ts` FileModel as a convenient place to persist arbitrary data that are needed by the package but are _not_ persisted by the upstream service. For example, you might use store.json to persist startup flags or login credentials.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FileHelper, z } from '@start9labs/start-sdk'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* Persists package data that the upstream app does not store itself.
|
||||
* Here: the login password Premier Gunner injects into the app as PG_PASSWORD.
|
||||
*/
|
||||
export const store = FileHelper.json(
|
||||
{ base: sdk.volumes.main, subpath: 'store.json' },
|
||||
z.object({
|
||||
password: z.string(),
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
export const DEFAULT_LANG = 'en_US'
|
||||
|
||||
const dict = {
|
||||
// main.ts
|
||||
'Starting Premier Gunner!': 0,
|
||||
'Web Interface': 1,
|
||||
'The web interface is ready': 2,
|
||||
'The web interface is not ready': 3,
|
||||
|
||||
// interfaces.ts
|
||||
'Premier Gunner': 4,
|
||||
'The Premier Gunner training tracker web app': 5,
|
||||
|
||||
// actions
|
||||
'Set Login Password': 6,
|
||||
'Set the password Gunner uses to log in to Premier Gunner': 7,
|
||||
'Password': 8,
|
||||
'The password Gunner types on the login screen (at least 4 characters)': 9,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*/
|
||||
export type I18nKey = keyof typeof dict
|
||||
export type LangDict = Record<(typeof dict)[I18nKey], string>
|
||||
export default dict
|
||||
@@ -0,0 +1,52 @@
|
||||
import { LangDict } from './default'
|
||||
|
||||
export default {
|
||||
es_ES: {
|
||||
0: '¡Iniciando Premier Gunner!',
|
||||
1: 'Interfaz web',
|
||||
2: 'La interfaz web está lista',
|
||||
3: 'La interfaz web no está lista',
|
||||
4: 'Premier Gunner',
|
||||
5: 'La aplicación web de seguimiento de entrenamiento Premier Gunner',
|
||||
6: 'Establecer contraseña de acceso',
|
||||
7: 'Establece la contraseña que Gunner usa para iniciar sesión en Premier Gunner',
|
||||
8: 'Contraseña',
|
||||
9: 'La contraseña que Gunner escribe en la pantalla de inicio de sesión (al menos 4 caracteres)',
|
||||
},
|
||||
de_DE: {
|
||||
0: 'Starte Premier Gunner!',
|
||||
1: 'Weboberfläche',
|
||||
2: 'Die Weboberfläche ist bereit',
|
||||
3: 'Die Weboberfläche ist nicht bereit',
|
||||
4: 'Premier Gunner',
|
||||
5: 'Die Premier Gunner Trainings-Tracker-Web-App',
|
||||
6: 'Anmeldepasswort festlegen',
|
||||
7: 'Lege das Passwort fest, mit dem Gunner sich bei Premier Gunner anmeldet',
|
||||
8: 'Passwort',
|
||||
9: 'Das Passwort, das Gunner auf dem Anmeldebildschirm eingibt (mindestens 4 Zeichen)',
|
||||
},
|
||||
pl_PL: {
|
||||
0: 'Uruchamianie Premier Gunner!',
|
||||
1: 'Interfejs webowy',
|
||||
2: 'Interfejs webowy jest gotowy',
|
||||
3: 'Interfejs webowy nie jest gotowy',
|
||||
4: 'Premier Gunner',
|
||||
5: 'Aplikacja webowa do śledzenia treningów Premier Gunner',
|
||||
6: 'Ustaw hasło logowania',
|
||||
7: 'Ustaw hasło, którego Gunner używa do logowania w Premier Gunner',
|
||||
8: 'Hasło',
|
||||
9: 'Hasło, które Gunner wpisuje na ekranie logowania (co najmniej 4 znaki)',
|
||||
},
|
||||
fr_FR: {
|
||||
0: 'Démarrage de Premier Gunner !',
|
||||
1: 'Interface web',
|
||||
2: "L'interface web est prête",
|
||||
3: "L'interface web n'est pas prête",
|
||||
4: 'Premier Gunner',
|
||||
5: "L'application web de suivi d'entraînement Premier Gunner",
|
||||
6: 'Définir le mot de passe de connexion',
|
||||
7: 'Définissez le mot de passe que Gunner utilise pour se connecter à Premier Gunner',
|
||||
8: 'Mot de passe',
|
||||
9: "Le mot de passe que Gunner saisit sur l'écran de connexion (au moins 4 caractères)",
|
||||
},
|
||||
} satisfies Record<string, LangDict>
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT this file.
|
||||
*/
|
||||
import { setupI18n } from '@start9labs/start-sdk'
|
||||
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
|
||||
import translations from './dictionaries/translations'
|
||||
|
||||
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*/
|
||||
export { createBackup } from './backups'
|
||||
export { main } from './main'
|
||||
export { init, uninit } from './init'
|
||||
export { actions } from './actions'
|
||||
import { buildManifest } from '@start9labs/start-sdk'
|
||||
import { manifest as sdkManifest } from './manifest'
|
||||
import { versionGraph } from './versions'
|
||||
export const manifest = buildManifest(versionGraph, sdkManifest)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { setDependencies } from '../dependencies'
|
||||
import { setInterfaces } from '../interfaces'
|
||||
import { versionGraph } from '../versions'
|
||||
import { actions } from '../actions'
|
||||
import { restoreInit } from '../backups'
|
||||
|
||||
export const init = sdk.setupInit(
|
||||
restoreInit,
|
||||
versionGraph,
|
||||
setInterfaces,
|
||||
setDependencies,
|
||||
actions,
|
||||
)
|
||||
|
||||
export const uninit = sdk.setupUninit(versionGraph)
|
||||
@@ -0,0 +1,25 @@
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
|
||||
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
|
||||
const uiMultiOrigin = await uiMulti.bindPort(uiPort, {
|
||||
protocol: 'http',
|
||||
})
|
||||
const ui = sdk.createInterface(effects, {
|
||||
name: i18n('Premier Gunner'),
|
||||
id: 'ui',
|
||||
description: i18n('The Premier Gunner training tracker web app'),
|
||||
type: 'ui',
|
||||
masked: false,
|
||||
schemeOverride: null,
|
||||
username: null,
|
||||
path: '',
|
||||
query: {},
|
||||
})
|
||||
|
||||
const uiReceipt = await uiMultiOrigin.export([ui])
|
||||
|
||||
return [uiReceipt]
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { store } from './fileModels/store'
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
export const main = sdk.setupMain(async ({ effects }) => {
|
||||
console.info(i18n('Starting Premier Gunner!'))
|
||||
|
||||
// The login password lives in store.json. Reading it with `.const` makes the
|
||||
// daemon restart whenever it changes (e.g. via the "Set Login Password" action),
|
||||
// so PG_PASSWORD stays authoritative on every boot.
|
||||
const password = await store.read((s) => s.password).const(effects)
|
||||
|
||||
return sdk.Daemons.of(effects).addDaemon('primary', {
|
||||
subcontainer: await sdk.SubContainer.of(
|
||||
effects,
|
||||
{ imageId: 'premier-gunner' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: '/data',
|
||||
readonly: false,
|
||||
}),
|
||||
'premier-gunner-sub',
|
||||
),
|
||||
exec: {
|
||||
command: ['node', 'src/server.js'],
|
||||
cwd: '/app',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PG_HOST: '0.0.0.0',
|
||||
PG_PORT: String(uiPort),
|
||||
PG_DATA_DIR: '/data',
|
||||
...(password ? { PG_PASSWORD: password } : {}),
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Web Interface'),
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||
successMessage: i18n('The web interface is ready'),
|
||||
errorMessage: i18n('The web interface is not ready'),
|
||||
}),
|
||||
},
|
||||
requires: [],
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
export const short = {
|
||||
en_US: 'A kid-friendly soccer training tracker',
|
||||
es_ES: 'Un rastreador de entrenamiento de fútbol para niños',
|
||||
de_DE: 'Ein kinderfreundlicher Fußball-Trainings-Tracker',
|
||||
pl_PL: 'Przyjazny dzieciom tracker treningu piłkarskiego',
|
||||
fr_FR: "Un suivi d'entraînement de football adapté aux enfants",
|
||||
}
|
||||
|
||||
export const long = {
|
||||
en_US:
|
||||
'Premier Gunner is a kid-friendly, mobile-friendly soccer training tracker. Log daily training across categories, plan future sessions, set goals, and watch progress climb toward the big reward.',
|
||||
es_ES:
|
||||
'Premier Gunner es un rastreador de entrenamiento de fútbol adaptado a niños y móviles. Registra el entrenamiento diario por categorías, planifica sesiones futuras, fija metas y observa el progreso hacia la gran recompensa.',
|
||||
de_DE:
|
||||
'Premier Gunner ist ein kinder- und mobilfreundlicher Fußball-Trainings-Tracker. Erfasse das tägliche Training nach Kategorien, plane zukünftige Einheiten, setze Ziele und verfolge den Fortschritt zur großen Belohnung.',
|
||||
pl_PL:
|
||||
'Premier Gunner to przyjazny dzieciom i urządzeniom mobilnym tracker treningu piłkarskiego. Zapisuj codzienny trening w kategoriach, planuj przyszłe sesje, ustawiaj cele i obserwuj postępy w drodze do wielkiej nagrody.',
|
||||
fr_FR:
|
||||
"Premier Gunner est un suivi d'entraînement de football adapté aux enfants et aux mobiles. Enregistrez l'entraînement quotidien par catégories, planifiez les séances futures, fixez des objectifs et suivez les progrès vers la grande récompense.",
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { setupManifest } from '@start9labs/start-sdk'
|
||||
import { long, short } from './i18n'
|
||||
|
||||
export const manifest = setupManifest({
|
||||
id: 'premier-gunner',
|
||||
title: 'Premier Gunner',
|
||||
license: 'MIT',
|
||||
packageRepo: 'https://github.com/ten31/premier-gunner',
|
||||
upstreamRepo: 'https://github.com/ten31/premier-gunner',
|
||||
marketingUrl: 'https://github.com/ten31/premier-gunner',
|
||||
donationUrl: null,
|
||||
description: { short, long },
|
||||
volumes: ['main'],
|
||||
images: {
|
||||
'premier-gunner': {
|
||||
source: { dockerBuild: { dockerfile: 'Dockerfile', workdir: '.' } },
|
||||
arch: ['x86_64', 'aarch64'],
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
install: null,
|
||||
update: null,
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { StartSdk } from '@start9labs/start-sdk'
|
||||
import { manifest } from './manifest'
|
||||
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*
|
||||
* The exported "sdk" const is used throughout this package codebase.
|
||||
*/
|
||||
export const sdk = StartSdk.of().withManifest(manifest).build(true)
|
||||
@@ -0,0 +1,5 @@
|
||||
// Here we define any constants or functions that are shared by multiple components
|
||||
// throughout the package codebase.
|
||||
|
||||
// The port the Premier Gunner Node server listens on inside the container.
|
||||
export const uiPort = 3000
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IMPOSSIBLE, utils, VersionInfo } from '@start9labs/start-sdk'
|
||||
import { store } from '../fileModels/store'
|
||||
|
||||
export const current = VersionInfo.of({
|
||||
version: '0.1.0:0',
|
||||
releaseNotes: {
|
||||
en_US: 'Initial release of Premier Gunner for StartOS.',
|
||||
es_ES: 'Versión inicial de Premier Gunner para StartOS.',
|
||||
de_DE: 'Erste Veröffentlichung von Premier Gunner für StartOS.',
|
||||
pl_PL: 'Pierwsze wydanie Premier Gunner dla StartOS.',
|
||||
fr_FR: 'Première version de Premier Gunner pour StartOS.',
|
||||
},
|
||||
migrations: {
|
||||
up: async ({ effects }) => {
|
||||
// Generate a random login password on first install so the app is never
|
||||
// left on a known default. The user can change it via "Set Login Password".
|
||||
const existing = await store.read().once()
|
||||
if (!existing) {
|
||||
const password = utils.getDefaultString({
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 16,
|
||||
})
|
||||
await store.write(effects, { password })
|
||||
}
|
||||
},
|
||||
down: IMPOSSIBLE,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { VersionGraph } from '@start9labs/start-sdk'
|
||||
import { current } from './current'
|
||||
|
||||
export const versionGraph = VersionGraph.of({
|
||||
current,
|
||||
other: [],
|
||||
})
|
||||
Reference in New Issue
Block a user