Rebrand to Proof of Work; multi-user 0.4 package with curated library sync
Repo cleanup - Add top-level .gitignore (was missing; node_modules, .next, *.s9pk, image.tar, seed/data/*.db, log files, etc.) and a root README. - Delete legacy start9/0.3.5/ package (StartOS 0.3.5 wrapper, no longer the deploy target). - Delete start9-example-packaging/ (template from another project). - Delete planning docs (START9_PACKAGING_LOG.md, VERSIONING.md, STARTOS_0.4_UPGRADE_PROMPT.md, ICON_FILES_INDEX.md, etc.) — info now lives in the deploy guide and code comments. - Drop the standalone Dockerfile, docker-compose.yml, ICON_*, and dev log/build artifacts from the app dir. - Drop the v0.1.0:18/19/20 version files (they belonged to the legacy workout-log package and don't apply to the new id). Rename + new package - Rename app dir workout-planner/ -> proof-of-work/. - Rename StartOS package id workout-log -> proof-of-work; the new id makes this a brand new StartOS service (clean cutover from the old one rather than in-place upgrade). - Reset version graph; v1.0.0:1 is the seeded cutover release. The Dockerfile bakes a one-time /data snapshot and docker_entrypoint.sh copies it into the new volume on truly-fresh first boot only (both /data/app.db missing AND /data/.seeded absent). - Move start9/0.4-migration/ -> start9/0.4/; the old start9/0.4/ stub is gone. Curated exercise library (multi-user-aware) - proof-of-work/prisma/exercises.seed.json is the canonical library shipped to every install (164 exercises today, dumped from the live snapshot). - proof-of-work/scripts/sync-library.cjs (npm run sync-library) refreshes the JSON from start9/0.4/seed/data/app.db after refresh_seed.sh. - proof-of-work/prisma/seed.ts now reads from the JSON instead of a hardcoded 52-exercise array; runs at Docker build time to seed the fallback DB and on first boot for fresh installs. - proof-of-work/prisma/ensureExerciseLibrary.cjs runs on every container boot (from docker_entrypoint.sh) and INSERT OR IGNOREs every library entry for every user, keyed on (userId, name). Library updates flow to existing installs on package upgrade; user-custom exercises (isCustom=true) and any colliding names are never overwritten; removed exercises stay on existing installs (additive-only). Deploy guide (start9/0.4/DEPLOY_040.md) - Rewritten end-to-end for the workout-log -> proof-of-work cutover: refresh_seed, sync-library, build, sideload, verify, rotate creds, stop the old service, then post-cutover cleanup release v1.0.0:2.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import bcryptjs from 'bcryptjs'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* change-admin-credentials — StartOS Package Action.
|
||||
*
|
||||
* Lets the user rotate the admin email and password for Proof of Work directly
|
||||
* from the StartOS UI, without dropping to a shell. Replaces the manual CLI
|
||||
* fallback documented in DEPLOY_040.md \u00a75b.
|
||||
*
|
||||
* Design notes:
|
||||
*
|
||||
* - allowedStatuses: 'only-stopped'. StartOS forces a Stop before the action
|
||||
* runs, so there is zero risk of two writers (the running Next.js server +
|
||||
* the action's sqlite3 UPDATE) racing on /data/app.db. As a side effect,
|
||||
* any previously-issued session cookies are implicitly invalidated when
|
||||
* the service restarts and re-reads the User row.
|
||||
*
|
||||
* - Bcrypt salt rounds = 10. This MUST match
|
||||
* proof-of-work/lib/auth.ts::hashPassword, which uses bcryptjs.genSalt(10)
|
||||
* followed by bcryptjs.hash. If the app ever changes its rounds, change them
|
||||
* here too \u2014 otherwise login will fail.
|
||||
*
|
||||
* - We compute the bcrypt hash in the action's own JS runtime (bcryptjs is
|
||||
* bundled via package.json), then push only the finished hash into the
|
||||
* subcontainer. The plaintext password never lands in /proc, the SQL log,
|
||||
* or anywhere persistent.
|
||||
*
|
||||
* - The UPDATE is keyed on `id = (SELECT id FROM User ORDER BY createdAt ASC
|
||||
* LIMIT 1)` rather than `WHERE email = 'admin@local'` (the original 0.3.5
|
||||
* default). That makes the action safe to re-run after a previous rotation.
|
||||
* The app is single-user by design, so this targets the only User row.
|
||||
*
|
||||
* - We assert exactly 1 row was updated (`changes() == 1`). Anything else
|
||||
* means the schema/data is in an unexpected state and we abort without
|
||||
* reporting success, so the user is forced to investigate before assuming
|
||||
* credentials rotated successfully.
|
||||
*
|
||||
* Available from package version 0.1.0:20 onward.
|
||||
*/
|
||||
|
||||
const EMAIL_PATTERN = '^[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}$'
|
||||
|
||||
/** Escape a string for safe inclusion inside SQLite single-quoted literal. */
|
||||
const sqlQuote = (s: string): string => `'${s.replace(/'/g, "''")}'`
|
||||
|
||||
export const changeAdminCredentials = sdk.Action.withInput(
|
||||
'change-admin-credentials',
|
||||
// ---------------------------------------------------------------------------
|
||||
// metadata
|
||||
// ---------------------------------------------------------------------------
|
||||
async () => ({
|
||||
name: 'Change admin credentials',
|
||||
description:
|
||||
'Rotate the admin email and password stored in /data/app.db. The service must be stopped first; you will log in with the new credentials after starting it again.',
|
||||
warning:
|
||||
'This permanently overwrites the existing User row. Any browser sessions issued under the old credentials will stop working as soon as the service restarts. Make sure you can receive the new email at the address you enter.',
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'only-stopped',
|
||||
group: null,
|
||||
}),
|
||||
// ---------------------------------------------------------------------------
|
||||
// input form
|
||||
// ---------------------------------------------------------------------------
|
||||
sdk.InputSpec.of({
|
||||
email: sdk.Value.text({
|
||||
name: 'New email address',
|
||||
description: 'The email you will use to log in.',
|
||||
required: true,
|
||||
default: null,
|
||||
inputmode: 'email',
|
||||
placeholder: 'you@example.com',
|
||||
patterns: [
|
||||
{
|
||||
regex: EMAIL_PATTERN,
|
||||
description: 'Must be a valid email address (e.g. you@example.com).',
|
||||
},
|
||||
],
|
||||
}),
|
||||
password: sdk.Value.text({
|
||||
name: 'New password',
|
||||
description: 'Minimum 8 characters. No other complexity rules.',
|
||||
required: true,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 8,
|
||||
placeholder: 'At least 8 characters',
|
||||
}),
|
||||
passwordConfirm: sdk.Value.text({
|
||||
name: 'Confirm new password',
|
||||
description: 'Retype the new password to guard against typos.',
|
||||
required: true,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 8,
|
||||
placeholder: 'Retype new password',
|
||||
}),
|
||||
}),
|
||||
// ---------------------------------------------------------------------------
|
||||
// getInput (prefill) \u2014 we deliberately don't prefill the email. Prefilling
|
||||
// would require spinning up a subcontainer just to read the current row,
|
||||
// which is overkill for what is a once-in-a-blue-moon action.
|
||||
// ---------------------------------------------------------------------------
|
||||
async () => null,
|
||||
// ---------------------------------------------------------------------------
|
||||
// run
|
||||
// ---------------------------------------------------------------------------
|
||||
async ({ effects, input }) => {
|
||||
if (input.password !== input.passwordConfirm) {
|
||||
throw new Error(
|
||||
'New password and confirmation do not match. Re-enter both fields.',
|
||||
)
|
||||
}
|
||||
|
||||
// Compute the bcrypt hash in the action's runtime. Salt rounds 10 to
|
||||
// match proof-of-work/lib/auth.ts::hashPassword.
|
||||
const passwordHash = await bcryptjs.hash(input.password, 10)
|
||||
|
||||
// Run the UPDATE inside a temp subcontainer with /data mounted. The
|
||||
// subcontainer uses the same image as the main service, so sqlite3 is
|
||||
// available (the runner image apks it in for the entrypoint's compat
|
||||
// ALTERs).
|
||||
await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: '/data',
|
||||
readonly: false,
|
||||
}),
|
||||
'change-admin-credentials',
|
||||
async (sc) => {
|
||||
const sql = [
|
||||
'BEGIN IMMEDIATE;',
|
||||
`UPDATE User`,
|
||||
`SET email = ${sqlQuote(input.email)},`,
|
||||
` passwordHash = ${sqlQuote(passwordHash)},`,
|
||||
` updatedAt = (strftime('%s','now') * 1000)`,
|
||||
`WHERE id = (SELECT id FROM User ORDER BY createdAt ASC LIMIT 1);`,
|
||||
'SELECT changes();',
|
||||
'COMMIT;',
|
||||
].join('\n')
|
||||
|
||||
const res = await sc.execFail(
|
||||
['sqlite3', '/data/app.db'],
|
||||
{ input: sql },
|
||||
30_000,
|
||||
)
|
||||
|
||||
// sqlite3 prints `changes()` on its own line. Be defensive about
|
||||
// trailing whitespace / multiple lines.
|
||||
const changes = parseInt(
|
||||
res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.pop() ?? '0',
|
||||
10,
|
||||
)
|
||||
if (changes !== 1) {
|
||||
throw new Error(
|
||||
`Aborting: expected exactly 1 user row updated, but sqlite3 reported changes()=${changes}. The User table may be empty or contain unexpected rows. Inspect /data/app.db before retrying.`,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Credentials updated',
|
||||
message:
|
||||
'The admin email and password have been rotated. Start the service and log in with your new credentials.',
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: 'New login email',
|
||||
description: 'What you will enter on the Proof of Work login page.',
|
||||
value: input.email,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: 'Password',
|
||||
description:
|
||||
'Stored as a bcrypt hash (salt rounds 10). Not displayed.',
|
||||
value: '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022',
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { changeAdminCredentials } from './changeAdminCredentials'
|
||||
|
||||
/**
|
||||
* Package actions registered with StartOS.
|
||||
*
|
||||
* - change-admin-credentials (added v0.1.0:20): rotate the admin email +
|
||||
* password from the StartOS UI without dropping to a shell. See
|
||||
* ./changeAdminCredentials.ts for full design notes.
|
||||
*/
|
||||
export const actions = sdk.Actions.of().addAction(changeAdminCredentials)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
/**
|
||||
* Back up the entire `main` volume. StartOS handles snapshotting everything
|
||||
* under /data (app.db, sidecar WAL/SHM files, future additions), matching
|
||||
* the 0.3.5 backup semantics.
|
||||
*/
|
||||
export const { createBackup, restoreInit } = sdk.setupBackups(
|
||||
async () => sdk.Backups.ofVolumes('main'),
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
/** Proof of Work is fully self-contained and has no StartOS dependencies. */
|
||||
export const setDependencies = sdk.setupDependencies(async () => ({}))
|
||||
@@ -0,0 +1,14 @@
|
||||
export const DEFAULT_LANG = 'en_US'
|
||||
|
||||
const dict = {
|
||||
'Starting Proof of Work': 0,
|
||||
'Web Interface': 1,
|
||||
'The web interface is ready': 2,
|
||||
'The web interface is not ready': 3,
|
||||
'Web UI': 4,
|
||||
'The browser interface for Proof of Work': 5,
|
||||
} as const
|
||||
|
||||
export type I18nKey = keyof typeof dict
|
||||
export type LangDict = Record<(typeof dict)[I18nKey], string>
|
||||
export default dict
|
||||
@@ -0,0 +1,36 @@
|
||||
import { LangDict } from './default'
|
||||
|
||||
export default {
|
||||
es_ES: {
|
||||
0: 'Iniciando Proof of Work',
|
||||
1: 'Interfaz web',
|
||||
2: 'La interfaz web esta lista',
|
||||
3: 'La interfaz web no esta lista',
|
||||
4: 'Interfaz web',
|
||||
5: 'La interfaz del navegador para Proof of Work',
|
||||
},
|
||||
de_DE: {
|
||||
0: 'Proof of Work wird gestartet',
|
||||
1: 'Weboberflaeche',
|
||||
2: 'Die Weboberflaeche ist bereit',
|
||||
3: 'Die Weboberflaeche ist nicht bereit',
|
||||
4: 'Weboberflaeche',
|
||||
5: 'Die Browseroberflaeche fuer Proof of Work',
|
||||
},
|
||||
pl_PL: {
|
||||
0: 'Uruchamianie Proof of Work',
|
||||
1: 'Interfejs webowy',
|
||||
2: 'Interfejs webowy jest gotowy',
|
||||
3: 'Interfejs webowy nie jest gotowy',
|
||||
4: 'Interfejs webowy',
|
||||
5: 'Interfejs przegladarkowy dla Proof of Work',
|
||||
},
|
||||
fr_FR: {
|
||||
0: 'Demarrage de Proof of Work',
|
||||
1: 'Interface web',
|
||||
2: "L'interface web est prete",
|
||||
3: "L'interface web n'est pas prete",
|
||||
4: 'Interface web',
|
||||
5: "L'interface navigateur pour Proof of Work",
|
||||
},
|
||||
} 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,30 @@
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
/**
|
||||
* Expose the Next.js UI over StartOS's standard HTTP multi-host interface.
|
||||
* The UI is unmasked (no shared secret in the URL) because the app has its
|
||||
* own password-protected login screen.
|
||||
*/
|
||||
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('Web UI'),
|
||||
id: 'ui',
|
||||
description: i18n('The browser interface for Proof of Work'),
|
||||
type: 'ui',
|
||||
masked: false,
|
||||
schemeOverride: null,
|
||||
username: null,
|
||||
path: '',
|
||||
query: {},
|
||||
})
|
||||
|
||||
const uiReceipt = await uiMultiOrigin.export([ui])
|
||||
|
||||
return [uiReceipt]
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
/**
|
||||
* Main daemon definition.
|
||||
*
|
||||
* Mounts the `main` volume at /data (same contract as 0.3.5) and runs the
|
||||
* container's ENTRYPOINT (docker_entrypoint.sh -> Next.js standalone).
|
||||
*
|
||||
* Readiness: we check the listening port rather than hitting /api/health
|
||||
* because the Next.js server starts listening before Prisma has warmed up;
|
||||
* the entrypoint's own DB seeding logic is responsible for DB readiness.
|
||||
* If you want a stricter gate later, switch to `sdk.healthCheck.checkWebUrl`
|
||||
* pointed at `http://localhost:3000/api/health` and look for `status: ok`.
|
||||
*/
|
||||
export const main = sdk.setupMain(async ({ effects }) => {
|
||||
console.info(i18n('Starting Proof of Work'))
|
||||
|
||||
return sdk.Daemons.of(effects).addDaemon('main', {
|
||||
subcontainer: await sdk.SubContainer.of(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: '/data',
|
||||
readonly: false,
|
||||
}),
|
||||
'proof-of-work-main',
|
||||
),
|
||||
exec: { command: sdk.useEntrypoint() },
|
||||
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,46 @@
|
||||
export const short = {
|
||||
en_US: 'Self-hosted workout planning and training log app.',
|
||||
es_ES: 'Aplicacion autoalojada para planificar y registrar entrenamientos.',
|
||||
de_DE: 'Selbst gehostete App fuer Trainingsplanung und Trainingsprotokolle.',
|
||||
pl_PL: 'Samodzielnie hostowana aplikacja do planowania i rejestrowania treningow.',
|
||||
fr_FR: "Application auto-hebergee pour planifier et enregistrer les entrainements.",
|
||||
}
|
||||
|
||||
export const long = {
|
||||
en_US:
|
||||
'Proof of Work is a private workout planning and logging app. This StartOS 0.4 package preserves the existing /data volume contract from 0.3.5: the SQLite database at /data/app.db, workout history, exercise library, and imported records all survive the 0.3.5 \u2192 0.4 migration. The first 0.4 release ships with a one-time data seed baked into the image; subsequent releases use the live /data volume as the sole source of truth.',
|
||||
es_ES:
|
||||
'Proof of Work es una aplicacion privada para planificar y registrar entrenamientos. Este paquete de StartOS 0.4 conserva el contrato del volumen /data de 0.3.5: la base SQLite en /data/app.db, el historial de entrenamientos, la biblioteca de ejercicios y los registros importados sobreviven a la migracion 0.3.5 \u2192 0.4. La primera version 0.4 incluye una semilla unica; las siguientes usan el volumen /data vivo como unica fuente de verdad.',
|
||||
de_DE:
|
||||
'Proof of Work ist eine private App fuer Trainingsplanung und Trainingsprotokolle. Dieses StartOS-0.4-Paket behaelt den /data-Vertrag von 0.3.5 bei: die SQLite-Datenbank unter /data/app.db, der Trainingsverlauf, die Uebungsbibliothek und importierte Daten ueberstehen die Migration 0.3.5 \u2192 0.4. Das erste 0.4-Release liefert einen einmaligen Daten-Seed; spaetere Releases nutzen /data als einzige Wahrheit.',
|
||||
pl_PL:
|
||||
'Proof of Work to prywatna aplikacja do planowania i zapisywania treningow. Ten pakiet StartOS 0.4 zachowuje kontrakt /data z 0.3.5: baza SQLite w /data/app.db, historia treningow, biblioteka cwiczen i zaimportowane dane przetrwaja migracje 0.3.5 \u2192 0.4. Pierwsze wydanie 0.4 zawiera jednorazowy seed danych; kolejne wydania uzywaja /data jako jedynego zrodla prawdy.',
|
||||
fr_FR:
|
||||
"Proof of Work est une application privee de planification et de suivi d'entrainement. Ce paquet StartOS 0.4 conserve le contrat /data de la 0.3.5 : la base SQLite dans /data/app.db, l'historique, la bibliotheque d'exercices et les donnees importees survivent a la migration 0.3.5 \u2192 0.4. La premiere 0.4 inclut un seed unique ; les suivantes utilisent /data comme seule source de verite.",
|
||||
}
|
||||
|
||||
export const alertInstall = {
|
||||
en_US:
|
||||
'This package bakes a one-time snapshot of your 0.3.5 /data volume into the image. On first boot, it will populate /data only if it is empty. After first boot, /data is the sole source of truth; do NOT Uninstall (that destroys /data).',
|
||||
es_ES:
|
||||
'Este paquete incluye una instantanea unica del volumen /data de 0.3.5. En el primer arranque poblara /data solo si esta vacio. Despues, /data es la unica fuente de verdad; NO desinstale (eso destruye /data).',
|
||||
de_DE:
|
||||
'Dieses Paket enthaelt eine einmalige Momentaufnahme Ihres 0.3.5 /data-Volumes. Beim ersten Start wird /data nur befuellt, wenn es leer ist. Danach ist /data die einzige Wahrheit; NICHT deinstallieren (das zerstoert /data).',
|
||||
pl_PL:
|
||||
'Ten pakiet zawiera jednorazowa migawke Twojego woluminu /data z 0.3.5. Przy pierwszym starcie wypelni /data tylko, jesli jest pusty. Pozniej /data jest jedynym zrodlem prawdy; NIE odinstalowuj (to zniszczy /data).',
|
||||
fr_FR:
|
||||
"Ce paquet integre un instantane unique de votre volume /data 0.3.5. Au premier demarrage, /data ne sera rempli que s'il est vide. Ensuite, /data est la seule source de verite ; NE PAS desinstaller (cela detruit /data).",
|
||||
}
|
||||
|
||||
export const alertUpdate = {
|
||||
en_US:
|
||||
'Updating is safe: the entrypoint never overwrites an existing /data. NEVER choose Uninstall to troubleshoot \u2014 Uninstall destroys /data. If you need to disable the service, use Stop. If you need to roll back, run a StartOS Backup first.',
|
||||
es_ES:
|
||||
'Actualizar es seguro: el entrypoint nunca sobrescribe /data existente. NUNCA desinstale para solucionar problemas \u2014 Desinstalar destruye /data. Para desactivar el servicio use Detener. Para revertir, haga primero una copia de seguridad de StartOS.',
|
||||
de_DE:
|
||||
'Updaten ist sicher: der Entrypoint ueberschreibt niemals ein vorhandenes /data. NIE zur Fehlersuche deinstallieren \u2014 Deinstallieren zerstoert /data. Dienst deaktivieren: Stop. Zurueckrollen: erst StartOS-Backup.',
|
||||
pl_PL:
|
||||
'Aktualizacja jest bezpieczna: entrypoint nigdy nie nadpisze istniejacego /data. NIGDY nie odinstalowuj w ramach diagnostyki \u2014 Odinstalowanie niszczy /data. Aby wylaczyc usluge, uzyj Stop. Aby wycofac zmiany, najpierw wykonaj StartOS Backup.',
|
||||
fr_FR:
|
||||
"La mise a jour est sure : l'entrypoint ne remplace jamais un /data existant. NE PAS desinstaller pour depanner \u2014 Desinstaller detruit /data. Pour arreter le service, utilisez Stop. Pour revenir en arriere, faites d'abord une sauvegarde StartOS.",
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { setupManifest } from '@start9labs/start-sdk'
|
||||
import { alertInstall, alertUpdate, long, short } from './i18n'
|
||||
|
||||
/**
|
||||
* Proof of Work (proof-of-work) — StartOS 0.4 manifest.
|
||||
*
|
||||
* Contract invariants that MUST stay stable across all future releases so the
|
||||
* 0.3.5 \u2192 0.4 data migration and subsequent upgrades remain safe:
|
||||
* - package identifier = 'proof-of-work' (matches the 0.3.5 package)
|
||||
* - volume name = 'main' (matches the 0.3.5 volume)
|
||||
* - mount path = '/data' (matches the 0.3.5 mount)
|
||||
*
|
||||
* NOTE: do NOT write the literal two-character token "i"+"d" followed by ":"
|
||||
* anywhere else in this file. s9pk.mk extracts PACKAGE_ID with a naive awk
|
||||
* that matches on that substring and will concatenate multiple hits into a
|
||||
* broken BASE_NAME (symptom: make warning "overriding commands for target
|
||||
* 'proof-of-work'" and "No rule to make target .git/HEAD").
|
||||
*/
|
||||
export const manifest = setupManifest({
|
||||
id: 'proof-of-work',
|
||||
title: 'Proof of Work',
|
||||
license: 'Proprietary',
|
||||
packageRepo: 'https://github.com/your-org/proof-of-work-startos',
|
||||
upstreamRepo: 'https://github.com/your-org/proof-of-work',
|
||||
marketingUrl: 'https://github.com/your-org/proof-of-work',
|
||||
donationUrl: null,
|
||||
docsUrls: ['https://docs.start9.com/packaging/0.4.0.x/'],
|
||||
description: { short, long },
|
||||
volumes: ['main'],
|
||||
images: {
|
||||
main: {
|
||||
source: {
|
||||
dockerBuild: {
|
||||
// Both `workdir` and `dockerfile` are resolved by start-cli
|
||||
// relative to the PACKAGE directory (where this manifest
|
||||
// lives), per the 0.4.0.x packaging docs. That means:
|
||||
// workdir: '../..' -> Docker build context = repo root
|
||||
// (two levels up from
|
||||
// start9/0.4/). The
|
||||
// Dockerfile's COPY paths such as
|
||||
// 'proof-of-work/...' and
|
||||
// 'start9/0.4/...' are
|
||||
// resolved from there.
|
||||
// dockerfile: './Dockerfile'
|
||||
// -> <package-dir>/Dockerfile. That's
|
||||
// where ours actually lives; the
|
||||
// Dockerfile sitting OUTSIDE the
|
||||
// workdir is fine -- Docker
|
||||
// supports it via `-f <path>
|
||||
// <context>` and buildkit resolves
|
||||
// COPY paths against the context
|
||||
// (workdir), not the Dockerfile's
|
||||
// directory.
|
||||
// DO NOT set dockerfile to './start9/0.4/Dockerfile'.
|
||||
// That would resolve to
|
||||
// start9/0.4/start9/0.4/Dockerfile
|
||||
// (nonexistent), producing
|
||||
// `resolve : lstat start9: no such file or directory`
|
||||
// from buildkit before any layer runs.
|
||||
workdir: '../..',
|
||||
dockerfile: './Dockerfile',
|
||||
},
|
||||
},
|
||||
// 0.4 beta is x86_64-only; expand to ['x86_64', 'aarch64'] post-beta.
|
||||
arch: ['x86_64'],
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
install: alertInstall,
|
||||
update: alertUpdate,
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { StartSdk } from '@start9labs/start-sdk'
|
||||
import { manifest } from './manifest'
|
||||
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*/
|
||||
export const sdk = StartSdk.of().withManifest(manifest).build(true)
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Shared constants for the Proof of Work StartOS 0.4 package.
|
||||
*
|
||||
* Keep these in lockstep with the manifest (`./manifest/index.ts`) and with
|
||||
* the Dockerfile environment variables. Changing any of these is effectively
|
||||
* a breaking change for the on-disk contract with the `/data` volume.
|
||||
*/
|
||||
|
||||
export const PACKAGE_ID = 'proof-of-work'
|
||||
export const PACKAGE_TITLE = 'Proof of Work'
|
||||
export const IMAGE_ID = 'main'
|
||||
export const VOLUME_ID = 'main'
|
||||
export const DATA_MOUNT_PATH = '/data'
|
||||
|
||||
/** Internal HTTP port the Next.js standalone server listens on. */
|
||||
export const uiPort = 3000
|
||||
|
||||
/** Path the health route is mounted at inside the app. */
|
||||
export const HEALTH_PATH = '/api/health'
|
||||
@@ -0,0 +1,18 @@
|
||||
import { VersionGraph } from '@start9labs/start-sdk'
|
||||
import { v_1_0_0_1 } from './v1.0.0.1'
|
||||
|
||||
/**
|
||||
* Version graph for the `proof-of-work` package.
|
||||
*
|
||||
* v1.0.0:1 — initial release, seeded cutover from the legacy `workout-log`
|
||||
* package. No prior version to upgrade from.
|
||||
*
|
||||
* StartOS picks `current` as the install target; `other` lists every node
|
||||
* that can upgrade into `current`. Fresh sideloads land directly on
|
||||
* `current`. Once we ship the post-cutover cleanup release, it goes here as
|
||||
* the new `current` and v1.0.0:1 moves into `other`.
|
||||
*/
|
||||
export const versionGraph = VersionGraph.of({
|
||||
current: v_1_0_0_1,
|
||||
other: [],
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
/**
|
||||
* v1.0.0:1 — initial Proof of Work release.
|
||||
*
|
||||
* Upstream version: 1.0.0
|
||||
* Wrapper rev: 1
|
||||
*
|
||||
* This is a one-shot "seeded cutover" release for users migrating from the
|
||||
* old `workout-log` StartOS package. The Docker image bakes in a snapshot of
|
||||
* the maintainer's live /data volume under /app/seed/data; the entrypoint
|
||||
* copies that snapshot into the new StartOS-managed /data volume only on a
|
||||
* truly-fresh first boot (both /data/app.db missing AND /data/.seeded
|
||||
* absent). Every subsequent boot leaves /data untouched.
|
||||
*
|
||||
* Because StartOS treats `proof-of-work` as a brand new service (different
|
||||
* package id from `workout-log`), the old install stays running until the
|
||||
* operator confirms the cutover and stops it manually. There is no
|
||||
* downgrade path; `down` is IMPOSSIBLE.
|
||||
*
|
||||
* The post-cutover cleanup release (v1.0.0:2) will strip the baked seed and
|
||||
* the seed-copy branch from docker_entrypoint.sh.
|
||||
*/
|
||||
export const v_1_0_0_1 = VersionInfo.of({
|
||||
version: '1.0.0:1',
|
||||
releaseNotes: {
|
||||
en_US:
|
||||
'Initial Proof of Work release. Replaces the legacy `workout-log` package with multi-user support and a curated exercise library shared across all users on the instance. Bakes a one-time seed of /data into the image and copies it into the new volume only on truly-fresh first boot, so an operator migrating from `workout-log` keeps every workout, exercise, and preference.',
|
||||
},
|
||||
migrations: {
|
||||
up: async () => {},
|
||||
down: IMPOSSIBLE,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user