From 1ea5ac60ccc70fa180109330efef935093721140 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 13 Mar 2026 15:12:51 +0000 Subject: [PATCH] feat: Postgres CRUD backend for card text field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - schema.sql: card_text table (single-row, id=1) with content + updated_at - server.js: Express + pg server with CRUD API: GET /api/text β€” read current text PUT /api/text β€” create / update text DELETE /api/text β€” reset to default lorem ipsum - package.json: express + pg dependencies (converts project to Node type) - index.html: card now loads text from DB, inline Edit / Save / Cancel controls, Reset button with confirm dialog, last-updated timestamp DB: testapp (appuser) on localhost:5432 Closes #8 --- index.html | 150 ++++++++++++++++++++++++++++++++++++++++++++++++--- package.json | 12 +++++ schema.sql | 13 +++++ server.js | 92 +++++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 package.json create mode 100644 schema.sql create mode 100644 server.js diff --git a/index.html b/index.html index fc33bdc..d6904b7 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,8 @@ --btn-bg: #111; --btn-fg: #fff; --btn-hover: #333; + --input-bg: #f9f9f9; + --meta: #888; } :root[data-theme="dark"] { --bg: #0d0d0d; @@ -23,6 +25,8 @@ --btn-bg: #2a2a2a; --btn-fg: #e0e0e0; --btn-hover: #3a3a3a; + --input-bg: #0d0d0d; + --meta: #555; } *, *::before, *::after { box-sizing: border-box; } body { @@ -57,13 +61,14 @@ transition: background .15s; } #theme-toggle:hover { background: var(--btn-hover); } + + /* Card */ .card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 1.4rem 1.8rem; - max-width: 520px; - text-align: center; + width: min(520px, 96vw); line-height: 1.6; box-shadow: 0 0 0 1px rgba(34, 197, 94, .15), 0 0 18px 4px rgba(34, 197, 94, .25), @@ -75,32 +80,165 @@ 0 0 20px 5px rgba(34, 197, 94, .3), 0 0 50px 10px rgba(34, 197, 94, .12); } + #card-text { margin: 0 0 .9rem; } + #card-meta { color: var(--meta); font-size: .72rem; margin-bottom: .7rem; min-height: 1rem; } + .card-actions { display: flex; gap: .5rem; justify-content: flex-end; } + .card-btn { + background: var(--btn-bg); + color: var(--btn-fg); + border: 1px solid var(--border); + border-radius: 7px; + padding: .35rem .75rem; + font-size: .8rem; + cursor: pointer; + font-family: inherit; + transition: background .15s; + } + .card-btn:hover { background: var(--btn-hover); } + .card-btn:disabled { opacity: .4; cursor: default; } + .card-btn-danger { color: #f57c7c; border-color: rgba(245,124,124,.3); } + .card-btn-danger:hover { background: rgba(245,124,124,.1); } + #edit-form { display: none; flex-direction: column; gap: .6rem; margin-top: .5rem; } + #edit-form.open { display: flex; } + #edit-input { + width: 100%; min-height: 90px; background: var(--input-bg); color: var(--fg); + border: 1px solid var(--border); border-radius: 8px; padding: .55rem .7rem; + font-family: inherit; font-size: .9rem; resize: vertical; outline: none; + transition: border-color .15s; + } + #edit-input:focus { border-color: var(--accent); } + #card-status { font-size: .75rem; color: var(--meta); min-height: 1rem; text-align: right; }

πŸš€ Deploy verified βœ…

-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ +
+

Loading…

+
+
+ + +
+
+ +
+ + + +
+
+
+

Committed by OpenClaw at 2026-03-13 10:11:00 UTC

+ + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3abba1b --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^5.2.1", + "pg": "^8.13.3" + } +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..ac0e0dd --- /dev/null +++ b/schema.sql @@ -0,0 +1,13 @@ +-- schema.sql β€” test project +-- Run against the "testapp" Postgres database + +CREATE TABLE IF NOT EXISTS card_text ( + id INT PRIMARY KEY DEFAULT 1, + content TEXT NOT NULL DEFAULT '', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Seed default row (idempotent) +INSERT INTO card_text (id, content) +VALUES (1, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.') +ON CONFLICT (id) DO NOTHING; diff --git a/server.js b/server.js new file mode 100644 index 0000000..93b01cc --- /dev/null +++ b/server.js @@ -0,0 +1,92 @@ +const express = require('express'); +const { Pool } = require('pg'); +const path = require('path'); +const fs = require('fs'); + +const app = express(); +app.use(express.json()); + +const PORT = parseInt(process.env.PORT || '3070'); +const BASE = (process.env.BASE_PATH || '/test').replace(/\/$/, ''); + +const DEFAULT_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'; + +// ── Postgres ────────────────────────────────────────────────────────────── +const pgPass = process.env.PG_PASS || + (fs.existsSync('/root/pg-appuser.env') + ? fs.readFileSync('/root/pg-appuser.env', 'utf8').match(/APP_PASS=(.*)/)?.[1]?.trim() + : null); + +const pool = new Pool({ + host: '127.0.0.1', + port: 5432, + database: process.env.PG_DB || 'testapp', + user: process.env.PG_USER || 'appuser', + password: pgPass, +}); + +async function initDb() { + await pool.query(` + CREATE TABLE IF NOT EXISTS card_text ( + id INT PRIMARY KEY DEFAULT 1, + content TEXT NOT NULL DEFAULT '', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + INSERT INTO card_text (id, content) VALUES (1, $1) + ON CONFLICT (id) DO NOTHING; + `, [DEFAULT_TEXT]); +} + +// ── CORS ────────────────────────────────────────────────────────────────── +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + res.header('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS'); + if (req.method === 'OPTIONS') return res.sendStatus(200); + next(); +}); + +// ── GET /api/text ───────────────────────────────────────────────────────── +app.get(['/api/text', BASE + '/api/text'], async (req, res) => { + try { + const { rows } = await pool.query('SELECT content, updated_at FROM card_text WHERE id = 1'); + res.json(rows[0] || { content: DEFAULT_TEXT, updated_at: null }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// ── PUT /api/text β€” create / update ────────────────────────────────────── +app.put(['/api/text', BASE + '/api/text'], async (req, res) => { + const { content } = req.body; + if (!content?.trim()) return res.status(400).json({ error: 'content required' }); + try { + const { rows } = await pool.query( + `INSERT INTO card_text (id, content, updated_at) VALUES (1, $1, NOW()) + ON CONFLICT (id) DO UPDATE SET content = $1, updated_at = NOW() + RETURNING content, updated_at`, + [content.trim()] + ); + res.json(rows[0]); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// ── DELETE /api/text β€” reset to default ────────────────────────────────── +app.delete(['/api/text', BASE + '/api/text'], async (req, res) => { + try { + const { rows } = await pool.query( + `UPDATE card_text SET content = $1, updated_at = NOW() WHERE id = 1 + RETURNING content, updated_at`, + [DEFAULT_TEXT] + ); + res.json(rows[0] || { content: DEFAULT_TEXT }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + +// ── Serve index.html ────────────────────────────────────────────────────── +app.get([BASE, BASE + '/', BASE + '/index.html'], (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +// ── Boot ────────────────────────────────────────────────────────────────── +initDb() + .then(() => app.listen(PORT, '127.0.0.1', () => console.log(`test on :${PORT} at ${BASE}`))) + .catch(err => { console.error('DB init failed:', err); process.exit(1); });