Add browser UI for todo testing

This commit is contained in:
Alexander Taborda Acosta
2026-08-07 19:54:35 -05:00
parent f04c496f95
commit 705b0ccda5
4 changed files with 128 additions and 6 deletions
+2
View File
@@ -2,6 +2,8 @@
Todo Back is a deliberately small Rails JSON API packaged as a native StartOS 0.4 service. Its purpose is to validate the complete local-build → `.s9pk` → sideload workflow before investing in a larger application.
The interface root also serves a dependency-free browser page for quickly exercising every todo operation.
## API
- `GET /up` — Rails and PostgreSQL health check
+1
View File
@@ -4,6 +4,7 @@ Rails.application.configure do
config.enable_reloading = false
config.eager_load = true
config.consider_all_requests_local = false
config.public_file_server.enabled = true
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
config.log_tags = [:request_id]
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
+119
View File
@@ -0,0 +1,119 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Todo Back</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { max-width: 680px; margin: 3rem auto; padding: 0 1rem; }
form, li { display: flex; gap: .6rem; align-items: center; }
input[type="text"] { flex: 1; padding: .7rem; }
button { padding: .6rem .8rem; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { padding: .7rem 0; border-bottom: 1px solid #8886; }
li span { flex: 1; }
li.completed span { text-decoration: line-through; opacity: .6; }
#status { min-height: 1.5rem; color: #d55; }
</style>
</head>
<body>
<h1>Todo Back</h1>
<form id="new-todo">
<input id="title" type="text" maxlength="255" placeholder="What needs doing?" required autofocus>
<button type="submit">Add</button>
</form>
<p id="status" role="status"></p>
<ul id="todos"></ul>
<script>
const list = document.querySelector('#todos')
const form = document.querySelector('#new-todo')
const title = document.querySelector('#title')
const status = document.querySelector('#status')
async function api(path = '', options = {}) {
const response = await fetch(`/todos${path}`, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options,
})
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)
return response.status === 204 ? null : response.json()
}
function render(todos) {
list.replaceChildren(...todos.map(todo => {
const item = document.createElement('li')
item.className = todo.completed ? 'completed' : ''
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.completed
checkbox.title = 'Mark complete'
checkbox.addEventListener('change', async () => {
await update(todo.id, { completed: checkbox.checked })
})
const text = document.createElement('span')
text.textContent = todo.title
const edit = document.createElement('button')
edit.textContent = 'Edit'
edit.addEventListener('click', async () => {
const nextTitle = prompt('Todo title', todo.title)
if (nextTitle?.trim()) await update(todo.id, { title: nextTitle.trim() })
})
const remove = document.createElement('button')
remove.textContent = 'Delete'
remove.addEventListener('click', async () => {
if (!confirm(`Delete “${todo.title}”?`)) return
await run(() => api(`/${todo.id}`, { method: 'DELETE' }))
})
item.append(checkbox, text, edit, remove)
return item
}))
}
async function load() {
render(await api())
}
async function run(action) {
status.textContent = ''
try {
await action()
await load()
} catch (error) {
status.textContent = `Request failed: ${error.message}`
}
}
async function update(id, attributes) {
await run(() => api(`/${id}`, {
method: 'PATCH',
body: JSON.stringify({ todo: attributes }),
}))
}
form.addEventListener('submit', async event => {
event.preventDefault()
const value = title.value.trim()
if (!value) return
await run(() => api('', {
method: 'POST',
body: JSON.stringify({ todo: { title: value, completed: false } }),
}))
title.value = ''
title.focus()
})
load().catch(error => {
status.textContent = `Request failed: ${error.message}`
})
</script>
</body>
</html>
+6 -6
View File
@@ -1,13 +1,13 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const current = VersionInfo.of({
version: '0.1.1:0',
version: '0.1.2:0',
releaseNotes: {
en_US: 'Fix PostgreSQL readiness checks and HTTP/1.1 proxying; health now verifies the database.',
es_ES: 'Corrige las comprobaciones de PostgreSQL y el proxy HTTP/1.1; la salud ahora verifica la base de datos.',
de_DE: 'Korrigiert PostgreSQL-Prüfungen und HTTP/1.1-Proxying; der Status prüft nun die Datenbank.',
pl_PL: 'Poprawia test PostgreSQL i proxy HTTP/1.1; kontrola stanu sprawdza teraz bazę danych.',
fr_FR: 'Corrige les contrôles PostgreSQL et le proxy HTTP/1.1 ; la santé vérifie désormais la base de données.',
en_US: 'Add a small browser interface for testing all todo operations.',
es_ES: 'Agrega una pequeña interfaz web para probar todas las operaciones de tareas.',
de_DE: 'Fügt eine kleine Browseroberfläche zum Testen aller Aufgabenoperationen hinzu.',
pl_PL: 'Dodaje mały interfejs przeglądarkowy do testowania wszystkich operacji na zadaniach.',
fr_FR: 'Ajoute une petite interface web pour tester toutes les opérations sur les tâches.',
},
migrations: {
up: async () => {},