Dorian e8a0e1af19 feat: add Ollama proxy timeouts, SSH key migration, polish skills, and demo content
- Update all skill SSH commands from sshpass to key-based auth (~/.ssh/archipelago-deploy)
- Add proxy_connect_timeout 120s to nginx Ollama location blocks
- Add new polish/sweep skills for overnight automation
- Add demo content (documents, photos) for demo stack
- Add .ssh/ to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 08:06:52 +00:00

3.3 KiB

Skill: Polish Form Validation

Improve all form inputs to have real-time validation feedback, proper trimming, disabled states during submission, and consistent error messaging.

Forms to Polish

1. Login.vue — Password Setup

  • Real-time validation as user types (debounced 300ms):
    • Length >= 8 chars (show checkmark/X)
    • Passwords match (show match indicator)
  • Trim input on submit
  • Disable submit button while isSubmitting
  • Clear error on new input

2. Login.vue — TOTP Verification

  • inputmode="numeric" + pattern="[0-9]*"
  • Auto-submit when 6 digits entered
  • Show session timeout countdown if applicable
  • Trim and strip non-numeric characters on paste

3. Settings.vue — Password Change

  • Real-time strength validation:
    • 12+ characters
    • Has uppercase, lowercase, digit, special char
    • New password matches confirmation
  • Show strength meter (weak/medium/strong)
  • Disable button during submission
  • Show spinner in button during async operation

4. Any other form inputs found across views

Validation Pattern

const password = ref('')
const confirmPassword = ref('')
const isSubmitting = ref(false)

const passwordErrors = computed(() => {
  const errors: string[] = []
  if (password.value.length > 0 && password.value.length < 8)
    errors.push('Must be at least 8 characters')
  return errors
})

const passwordsMatch = computed(() =>
  confirmPassword.value.length > 0 && password.value === confirmPassword.value
)

async function submit() {
  if (isSubmitting.value) return
  isSubmitting.value = true
  try {
    await rpcClient.call(...)
  } catch (err) {
    errorMessage.value = formatError(err)
  } finally {
    isSubmitting.value = false
  }
}

Template Pattern

<input v-model="password" type="password" class="glass-input" />
<ul v-if="passwordErrors.length" class="text-red-400 text-xs mt-1 space-y-0.5">
  <li v-for="err in passwordErrors" :key="err">{{ err }}</li>
</ul>

<button
  class="glass-button"
  :disabled="isSubmitting || passwordErrors.length > 0"
  @click="submit"
>
  <span v-if="isSubmitting">Saving...</span>
  <span v-else>Save</span>
</button>

Input Trimming

All text inputs should be trimmed before submission:

const trimmed = password.value.trim()

Error Message Consistency

Create or use a formatError utility:

function formatError(err: unknown): string {
  if (err instanceof Error) {
    if (err.message.includes('fetch') || err.message.includes('network'))
      return 'Unable to reach server. Check your connection.'
    if (err.message.includes('401') || err.message.includes('unauthorized'))
      return 'Session expired. Please log in again.'
    return err.message
  }
  return 'Something went wrong. Please try again.'
}

Verification

For each form:

  • Real-time validation shows feedback as user types
  • Submit button disabled during operation
  • Submit button disabled when validation fails
  • Inputs trimmed before submission
  • Error messages are user-friendly (no raw error strings)
  • Success feedback shown after completion

Deploy After Fixes

./scripts/deploy-to-target.sh --live

Test each form with: valid input, invalid input, empty input, whitespace-only input, rapid double-click on submit.