Add browser UI for todo testing
This commit is contained in:
@@ -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.
|
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
|
## API
|
||||||
|
|
||||||
- `GET /up` — Rails and PostgreSQL health check
|
- `GET /up` — Rails and PostgreSQL health check
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Rails.application.configure do
|
|||||||
config.enable_reloading = false
|
config.enable_reloading = false
|
||||||
config.eager_load = true
|
config.eager_load = true
|
||||||
config.consider_all_requests_local = false
|
config.consider_all_requests_local = false
|
||||||
|
config.public_file_server.enabled = true
|
||||||
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
||||||
config.log_tags = [:request_id]
|
config.log_tags = [:request_id]
|
||||||
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
|
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
export const current = VersionInfo.of({
|
export const current = VersionInfo.of({
|
||||||
version: '0.1.1:0',
|
version: '0.1.2:0',
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
en_US: 'Fix PostgreSQL readiness checks and HTTP/1.1 proxying; health now verifies the database.',
|
en_US: 'Add a small browser interface for testing all todo operations.',
|
||||||
es_ES: 'Corrige las comprobaciones de PostgreSQL y el proxy HTTP/1.1; la salud ahora verifica la base de datos.',
|
es_ES: 'Agrega una pequeña interfaz web para probar todas las operaciones de tareas.',
|
||||||
de_DE: 'Korrigiert PostgreSQL-Prüfungen und HTTP/1.1-Proxying; der Status prüft nun die Datenbank.',
|
de_DE: 'Fügt eine kleine Browseroberfläche zum Testen aller Aufgabenoperationen hinzu.',
|
||||||
pl_PL: 'Poprawia test PostgreSQL i proxy HTTP/1.1; kontrola stanu sprawdza teraz bazę danych.',
|
pl_PL: 'Dodaje mały interfejs przeglądarkowy do testowania wszystkich operacji na zadaniach.',
|
||||||
fr_FR: 'Corrige les contrôles PostgreSQL et le proxy HTTP/1.1 ; la santé vérifie désormais la base de données.',
|
fr_FR: 'Ajoute une petite interface web pour tester toutes les opérations sur les tâches.',
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
up: async () => {},
|
up: async () => {},
|
||||||
|
|||||||
Reference in New Issue
Block a user