diff --git a/server/README.md b/server/README.md index 07e609e..c763636 100644 --- a/server/README.md +++ b/server/README.md @@ -167,7 +167,8 @@ login. Logged-in users manage their own private short notes at `/notes`. Each note has a title, a text body and an optional checklist. Unchecked items are shown first; when an item is checked it is saved as done, moves to the bottom of the checklist -and is displayed with a line through it. +and is displayed with a line through it. Checklist item text can be edited at any +time. Notes are stored in `main_app.db` in the `note` and `note_item` tables. Apply the users database migration once (the production launcher does this @@ -184,6 +185,10 @@ The main app (`:8000`) listens on all interfaces; the sensors API (`:8001`) listens on `127.0.0.1` by default and is open to every local user without authentication. +Do not run the main app on the sensors port (`8001`) while `SENSORS_API_URL` is +unset: it defaults to `http://localhost:8001`, so the main app would proxy API +requests to itself. Both launchers refuse to start in that configuration. + ### Development ```bash diff --git a/server/app.py b/server/app.py index 8d6c7da..eb9954f 100644 --- a/server/app.py +++ b/server/app.py @@ -174,6 +174,20 @@ def create_app(): user_db.session.commit() return _note_redirect(note.id) + @app.route( + "/notes//items/", methods=["POST"] + ) + @login_required + def notes_update_item(note_id, item_id): + note = _note_or_404(note_id) + item = NoteItem.query.filter_by(id=item_id, note_id=note.id).first_or_404() + text = request.form.get("text", "").strip() + if text: + item.text = text[:255] + note.touch() + user_db.session.commit() + return _note_redirect(note.id) + @app.route( "/notes//items//toggle", methods=["POST"] ) diff --git a/server/run-prod.sh b/server/run-prod.sh index 8dd99ed..9dbd57f 100755 --- a/server/run-prod.sh +++ b/server/run-prod.sh @@ -38,6 +38,16 @@ esac PORT="${PORT:-$DEFAULT_PORT}" HOST="${HOST:-$DEFAULT_HOST}" +# Prevent the main app from proxying to itself. Without an explicit +# SENSORS_API_URL the client targets localhost:8001, so main must not listen +# there as well. +if [ "$APP" = "main" ] && [ -z "${SENSORS_API_URL:-}" ] && [ "$PORT" = "8001" ]; then + echo "Refusing to start main on port 8001: SENSORS_API_URL would default to" >&2 + echo "http://localhost:8001 and the app would call itself recursively." >&2 + echo "Use another port (for example 8000) or set SENSORS_API_URL explicitly." >&2 + exit 2 +fi + if [ "$APP" = "main" ]; then SECRET_FILE=".secret_key" if [ -z "${SECRET_KEY:-}" ]; then diff --git a/server/run.sh b/server/run.sh index 3a2c0b4..b1faf08 100755 --- a/server/run.sh +++ b/server/run.sh @@ -38,6 +38,16 @@ esac PORT="${PORT:-$DEFAULT_PORT}" HOST="${HOST:-$DEFAULT_HOST}" +# Prevent the main app from proxying to itself. Without an explicit +# SENSORS_API_URL the client targets localhost:8001, so main must not listen +# there as well. +if [ "$APP" = "main" ] && [ -z "${SENSORS_API_URL:-}" ] && [ "$PORT" = "8001" ]; then + echo "Refusing to start main on port 8001: SENSORS_API_URL would default to" >&2 + echo "http://localhost:8001 and the app would call itself recursively." >&2 + echo "Use another port (for example 8000) or set SENSORS_API_URL explicitly." >&2 + exit 2 +fi + if [ -x ".venv/bin/python" ]; then PYTHON=".venv/bin/python" else diff --git a/server/static/retro.css b/server/static/retro.css index fc62fa3..6cdf284 100644 --- a/server/static/retro.css +++ b/server/static/retro.css @@ -343,7 +343,7 @@ textarea { padding: 0.15em 0; } .check-form { - flex: 1; + flex: 0 0 auto; flex-direction: row; align-items: center; gap: 0.6em; @@ -354,8 +354,24 @@ textarea { margin: 0; accent-color: var(--mc-yellow); } -.item-text { flex: 1; overflow-wrap: anywhere; } -.checklist li.done .item-text { +.item-edit-form { + flex: 1; + flex-direction: row; + align-items: center; + gap: 0.4em; + margin: 0; +} +.item-edit-form input { flex: 1; min-width: 0; } +.item-edit-form button { + background: var(--mc-dim); + color: var(--mc-black); + box-shadow: none; + padding: 0.1em 0.4em; + font-size: 0.85em; + letter-spacing: 0; +} +.item-edit-form button:hover { background: var(--mc-cyan); } +.checklist li.done .item-edit-form input { text-decoration: line-through; color: var(--mc-dim); } diff --git a/server/templates/notes.html b/server/templates/notes.html index a490862..119dbad 100644 --- a/server/templates/notes.html +++ b/server/templates/notes.html @@ -61,9 +61,15 @@ {% if item.is_done %}checked{% endif %} onchange="this.form.submit()" aria-label="Mark '{{ item.text }}' as done"> - {{ item.text }} +
+ + +
diff --git a/server/tests/test_split_apps.py b/server/tests/test_split_apps.py index ec7a94d..439830d 100644 --- a/server/tests/test_split_apps.py +++ b/server/tests/test_split_apps.py @@ -235,11 +235,18 @@ class MainAppTest(unittest.TestCase): ) with self.main_app.app_context(): - first, second = ( + first, _second = ( NoteItem.query.filter_by(note_id=note_id).order_by(NoteItem.id).all() ) first_id = first.id + self.assertEqual( + self.client.post( + f"/notes/{note_id}/items/{first_id}", + data={"text": "renamed first item"}, + ).status_code, + 302, + ) self.assertEqual( self.client.post( f"/notes/{note_id}/items/{first_id}/toggle", @@ -253,7 +260,8 @@ class MainAppTest(unittest.TestCase): self.assertEqual(page.status_code, 200) self.assertIn('class="done"', html) self.assertIn('value="Test title"', html) - self.assertLess(html.index("second item"), html.index("first item")) + self.assertIn('value="renamed first item"', html) + self.assertLess(html.index("second item"), html.index("renamed first item")) with self.main_app.app_context(): user_db.session.expire_all() @@ -264,7 +272,7 @@ class MainAppTest(unittest.TestCase): self.assertFalse(items[1].is_done) self.assertEqual( [item.text for item in items[0].note.ordered_items], - ["second item", "first item"], + ["second item", "renamed first item"], )