feat: TASK-49 container reliability — tests, orchestration, MASTER_PLAN
- Add orchestration_tests.rs + mock_podman.rs (container unit tests) - Add container-tests.yml CI workflow - Add dev-container-test.sh for local testing - MASTER_PLAN.md: add TASK-49 (P0) with 6-phase plan - Login.vue: minor fixes from user testing - AppCard.vue: enter key handler fix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="text-2xl font-semibold text-white/96 text-center mb-8 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
|
||||
<span v-if="isSetupMode && !isSetup">{{ t('login.setupTitle') }}</span>
|
||||
<span v-if="isCheckingSetup"> </span>
|
||||
<span v-else-if="isSetupMode && !isSetup">{{ t('login.setupTitle') }}</span>
|
||||
<span v-else>{{ t('login.title') }}</span>
|
||||
</h1>
|
||||
|
||||
@@ -38,8 +39,16 @@
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Checking setup state -->
|
||||
<div v-if="isCheckingSetup" class="flex items-center justify-center py-8">
|
||||
<svg class="animate-spin h-6 w-6 text-white/40" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Setup Mode: Password Setup -->
|
||||
<template v-if="isSetupMode && !isSetup">
|
||||
<template v-else-if="isSetupMode && !isSetup">
|
||||
<div class="mb-4 p-4 bg-white/5 border border-white/10 rounded-lg text-white/80 text-sm">
|
||||
<p class="mb-2">Create a password to secure your Archipelago node.</p>
|
||||
<p class="text-white/60 text-xs">This password will be required to access your node.</p>
|
||||
@@ -53,7 +62,8 @@
|
||||
id="setup-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
autocomplete="new-password"
|
||||
data-form-type="other"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
:placeholder="t('login.enterPasswordSetup')"
|
||||
@keydown.enter="handleSetupWithSound"
|
||||
@@ -69,7 +79,8 @@
|
||||
id="setup-confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
autocomplete="new-password"
|
||||
data-form-type="other"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
:placeholder="t('login.confirmPasswordPlaceholder')"
|
||||
@keydown.enter="handleSetupWithSound"
|
||||
@@ -153,7 +164,8 @@
|
||||
id="login-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
autocomplete="current-password"
|
||||
data-form-type="other"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
:placeholder="t('login.enterPasswordPlaceholder')"
|
||||
@keydown.enter="handleLoginWithSound"
|
||||
@@ -250,6 +262,9 @@ let startupProgressInterval: ReturnType<typeof setInterval> | null = null
|
||||
// Whether we're in setup mode (no password created yet)
|
||||
const isSetupMode = ref(false)
|
||||
|
||||
// Whether we're still checking the setup state (prevents flash of wrong form)
|
||||
const isCheckingSetup = ref(true)
|
||||
|
||||
// Whether the login form should be disabled (server not ready)
|
||||
const formDisabled = computed(() => !serverReady.value)
|
||||
|
||||
@@ -348,6 +363,8 @@ onMounted(async () => {
|
||||
} catch {
|
||||
isSetup.value = false
|
||||
isSetupMode.value = true
|
||||
} finally {
|
||||
isCheckingSetup.value = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -379,11 +396,19 @@ async function handleSetup() {
|
||||
params: { password: password.value.trim() }
|
||||
})
|
||||
|
||||
await store.login(password.value.trim())
|
||||
// Verify session cookie works before navigating (prevents connection lost on first login)
|
||||
try {
|
||||
await rpcClient.call({ method: 'server.echo', params: { message: 'session-check' } })
|
||||
} catch {
|
||||
error.value = 'Setup succeeded but session could not be established. Try refreshing.'
|
||||
store.logout()
|
||||
return
|
||||
}
|
||||
stopSynthwave()
|
||||
whooshAway.value = true
|
||||
playLoginSuccessWhoosh()
|
||||
loginTransition.setJustLoggedIn(true)
|
||||
await store.login(password.value.trim())
|
||||
await new Promise(r => setTimeout(r, 520))
|
||||
await router.replace(loginRedirectTo.value).catch(() => {
|
||||
window.location.href = loginRedirectTo.value
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:class="{ 'card-stagger': showStagger }"
|
||||
:style="{ '--stagger-index': index }"
|
||||
@click="$emit('goToApp', id)"
|
||||
@keydown.enter="$emit('goToApp', id)"
|
||||
@keydown.enter="handleEnter"
|
||||
>
|
||||
<!-- Installing overlay -->
|
||||
<div
|
||||
@@ -188,7 +188,7 @@ const props = defineProps<{
|
||||
isUninstalling: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
goToApp: [id: string]
|
||||
launch: [id: string]
|
||||
start: [id: string]
|
||||
@@ -197,6 +197,12 @@ defineEmits<{
|
||||
showUninstall: [id: string, pkg: PackageDataEntry]
|
||||
}>()
|
||||
|
||||
function handleEnter(e: KeyboardEvent) {
|
||||
// Controller nav already handled this Enter (preventDefault was called) — skip to avoid double navigation
|
||||
if (e.defaultPrevented) return
|
||||
emit('goToApp', props.id)
|
||||
}
|
||||
|
||||
const isWebOnly = computed(() => isWebOnlyApp(props.id))
|
||||
|
||||
// Enrich from marketplace when backend data is sparse (e.g. during install)
|
||||
|
||||
Reference in New Issue
Block a user