Use manifest names for new services and document release acceptance
Demo images / Build & push demo images (push) Failing after 38s

This commit is contained in:
archipelago
2026-09-30 12:20:29 -04:00
parent 5ab65f7581
commit 439b55a236
6 changed files with 41 additions and 11 deletions
+9
View File
@@ -2,6 +2,15 @@
## Unreleased ## Unreleased
- Kept installed apps visible through restarts and hard refreshes, and delayed app launches until their web interface is ready.
- Made Bitcoin version selection readable and usable in the ThinkPad kiosk, above the pruning settings.
- Restored GitWorkshop build files in installation/update payloads and made slow image-pull progress clearer.
- Fixed same-node Gitea access from Portainer, with persistent runtime migration, state backups and recovery after failed restarts.
- Preserved Gitea configuration and SSH operation during fresh setup and upgrades.
- Improved paid-file delivery, saved-file permissions and repeat-download compatibility; verified Tor-only payment with change, rejection refunds and free repeat downloads.
- Added a headless Angor Indexer service using the existing Mempool/ElectrumX stack, and an optional separate Angor relay.
- Prevented manifest command arguments containing apostrophes from being corrupted in generated services.
## v1.8.21-alpha (2026-09-30) ## v1.8.21-alpha (2026-09-30)
- Fixed Bitcoin and other containers being forcibly stopped after ten seconds during managed updates and restarts. - Fixed Bitcoin and other containers being forcibly stopped after ten seconds during managed updates and restarts.
+8 -1
View File
@@ -35,7 +35,7 @@ See the Framework incident and 1.8.21 execution records for evidence/limits.
| App disappearance/readiness | Durable inventory and safe lifecycle repair; delayed HTTP and desktop/mobile hard-refresh checks passed | Final lifecycle/reboot gate on candidate | | App disappearance/readiness | Durable inventory and safe lifecycle repair; delayed HTTP and desktop/mobile hard-refresh checks passed | Final lifecycle/reboot gate on candidate |
| X250 GitWorkshop/Nginx | Missing build contexts restored, dependency/build checks and live UI passed; Nginx slow pull diagnosed; truthful progress label | Verify both artifact payloads contain all build contexts | | X250 GitWorkshop/Nginx | Missing build contexts restored, dependency/build checks and live UI passed; Nginx slow pull diagnosed; truthful progress label | Verify both artifact payloads contain all build contexts |
| PRs 161/162 | Reviewed, repaired, merged/closed normally; combined regression suite passed | Funded Tor-only candidate purchase, retained change, refund, Files bytes and cached repeat passed; include in signed artifacts | | PRs 161/162 | Reviewed, repaired, merged/closed normally; combined regression suite passed | Funded Tor-only candidate purchase, retained change, refund, Files bytes and cached repeat passed; include in signed artifacts |
| Gitea/Portainer | Root cause confirmed; source network/backup/retry/catalog changes; real X250 routing repair and restart verified; private Git, SSH, LFS, registry and browser fixture checks passed | Automatic migration, scratch restore, failed-start recovery and reverse installation order passed. Operator confirms production site works through Portainer; still need final candidate reboot convergence and signed delivery | | Gitea/Portainer | Root cause confirmed; source network/backup/retry/catalog changes; real X250 routing repair and restart verified; private Git, SSH, LFS, registry and browser fixture checks passed | Automatic migration, scratch restore, failed-start recovery and reverse installation order passed. Operator confirms production site works through Portainer; production host reboot also preserved network/Git/Compose access; final candidate delivery and release checks remain |
| Angor headless store service | Implemented standard Mempool adapter and separate optional relay, official logo, headless store entries and declarative dependency guard. API security/outage/DNS tests and five relay lifecycle cycles passed | Final candidate prerequisite/install acceptance, management restart/reboot checks and signed catalog delivery; real indexing on dev waits for Bitcoin sync | | Angor headless store service | Implemented standard Mempool adapter and separate optional relay, official logo, headless store entries and declarative dependency guard. API security/outage/DNS tests and five relay lifecycle cycles passed | Final candidate prerequisite/install acceptance, management restart/reboot checks and signed catalog delivery; real indexing on dev waits for Bitcoin sync |
Durable payment receipts after a lost seller response remain a separately Durable payment receipts after a lost seller response remain a separately
@@ -113,3 +113,10 @@ whitespace-free command arguments containing apostrophes lost those characters
in the generated service. The renderer now quotes these arguments and in the generated service. The renderer now quotes these arguments and
environment values; the updated isolated backend suite passed (1,607 passed, four opt-in tests environment values; the updated isolated backend suite passed (1,607 passed, four opt-in tests
ignored), and the final candidate rebuild is in progress. Do not tag a release before this live regression is verified. ignored), and the final candidate rebuild is in progress. Do not tag a release before this live regression is verified.
The production Portainer host subsequently rebooted after the routing repair.
A post-boot probe from the actual Portainer namespace again verified the Git
smart-HTTP response type, current branch ref and Compose contents. Its
slirp4netns route and all production app containers survived. The temporary
Portainer fixture was absent. This verifies the repaired production route
across reboot; it does not substitute for final new-runtime delivery checks.
+2 -5
View File
@@ -214,7 +214,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { PackageDataEntry } from '@/types/api' import type { PackageDataEntry } from '@/types/api'
import { import {
isWebOnlyApp, opensInTab, canLaunch, launchBlockedReason, resolveAppIcon, isWebOnlyApp, opensInTab, canLaunch, launchBlockedReason, resolveAppIcon, resolveAppTitle,
getStatusClass, getStatusLabel, handleImageError, getStatusClass, getStatusLabel, handleImageError,
} from './appsConfig' } from './appsConfig'
import { getCuratedAppList } from '../discover/curatedApps' import { getCuratedAppList } from '../discover/curatedApps'
@@ -255,10 +255,7 @@ const isWebOnly = computed(() => isWebOnlyApp(props.id))
// Enrich from marketplace when backend data is sparse (e.g. during install) // Enrich from marketplace when backend data is sparse (e.g. during install)
const curated = computed(() => curatedMap.get(props.id)) const curated = computed(() => curatedMap.get(props.id))
const title = computed(() => { const title = computed(() => resolveAppTitle(props.id, props.pkg, curated.value?.title))
const t = props.pkg.manifest?.title
return (t && t !== props.id) ? t : (curated.value?.title || t || props.id)
})
const description = computed(() => { const description = computed(() => {
const d = props.pkg.manifest?.description?.short const d = props.pkg.manifest?.description?.short
return (d && d !== 'Installing...') ? d : (curated.value?.description || d || '') return (d && d !== 'Installing...') ? d : (curated.value?.description || d || '')
+2 -4
View File
@@ -132,7 +132,7 @@ import type { AppCredential, AppCredentialsResponse, PackageDataEntry } from '@/
import { rpcClient } from '@/api/rpc-client' import { rpcClient } from '@/api/rpc-client'
import { resolveAppUrl } from '@/views/appSession/appSessionConfig' import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
import { resolveAppCredentials } from './appCredentials' import { resolveAppCredentials } from './appCredentials'
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig' import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveAppTitle, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig'
import { getCuratedAppList } from '../discover/curatedApps' import { getCuratedAppList } from '../discover/curatedApps'
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
@@ -194,9 +194,7 @@ const pages = computed(() => {
}) })
function getTitle(id: string, pkg: PackageDataEntry): string { function getTitle(id: string, pkg: PackageDataEntry): string {
const t = pkg.manifest?.title return resolveAppTitle(id, pkg, curatedMap.get(id)?.title)
if (t && t !== id) return t
return curatedMap.get(id)?.title || t || id
} }
function getIcon(id: string, pkg: PackageDataEntry): string { function getIcon(id: string, pkg: PackageDataEntry): string {
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { ref } from 'vue' import { ref } from 'vue'
import { PackageState, type PackageDataEntry } from '@/types/api' import { PackageState, type PackageDataEntry } from '@/types/api'
import { APP_CATEGORY_MAP, canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, isAppReadyForLaunch, launchBlockedReason, resolveAppIcon, useCategoriesWithApps, DEFAULT_APP_ICON } from '../appsConfig' import { APP_CATEGORY_MAP, canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, isAppReadyForLaunch, launchBlockedReason, resolveAppIcon, resolveAppTitle, useCategoriesWithApps, DEFAULT_APP_ICON } from '../appsConfig'
function makePkg(id: string, title: string, category: string): PackageDataEntry { function makePkg(id: string, title: string, category: string): PackageDataEntry {
return { return {
@@ -214,3 +214,13 @@ describe('HTTP readiness independent of container health', () => {
expect(isAppReadyForLaunch(pkg)).toBe(true) expect(isAppReadyForLaunch(pkg)).toBe(true)
}) })
}) })
describe('manifest-generated service names', () => {
it('gives a new headless service its declared name without a hardcoded curated entry', () => {
const pkg = makePkg('angor-relay', 'angor-relay', 'money')
expect(resolveAppTitle('angor-relay', pkg)).toBe('Angor Relay')
pkg.manifest.title = 'My project relay'
expect(resolveAppTitle('angor-relay', pkg)).toBe('My project relay')
expect(resolveAppTitle('unknown-service', makePkg('unknown-service', 'unknown-service', ''))).toBe('unknown-service')
})
})
+9
View File
@@ -4,6 +4,7 @@ import type { Ref } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import { PackageState, type PackageDataEntry } from '@/types/api' import { PackageState, type PackageDataEntry } from '@/types/api'
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig' import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
import { GENERATED_APP_TITLES } from '../appSession/generatedAppSessionConfig'
import { portIsGateFronted } from '../discover/curatedApps' import { portIsGateFronted } from '../discover/curatedApps'
import { isAutoTabApp } from '@/utils/autoTabApps' import { isAutoTabApp } from '@/utils/autoTabApps'
import { import {
@@ -13,6 +14,14 @@ import {
export type AppsTab = 'apps' | 'websites' | 'services' export type AppsTab = 'apps' | 'websites' | 'services'
/** Use manifest-generated names when a new service has only its ID in runtime state. */
export function resolveAppTitle(id: string, pkg: PackageDataEntry, curatedTitle?: string): string {
const title = pkg.manifest?.title
if (title && title !== id) return title
return curatedTitle || GENERATED_APP_TITLES[id] || title || id
}
// Re-exported for every existing caller — the canon moved to serviceNames.ts // Re-exported for every existing caller — the canon moved to serviceNames.ts
// so the App Store's catalog merge can share it without a circular import. // so the App Store's catalog merge can share it without a circular import.
export const SERVICE_NAMES = SHARED_SERVICE_NAMES export const SERVICE_NAMES = SHARED_SERVICE_NAMES