modify checkbox text in notes

This commit is contained in:
2026-11-01 20:04:33 +01:00
parent 7675acd81e
commit e55af30644
7 changed files with 77 additions and 8 deletions
+6 -1
View File
@@ -167,7 +167,8 @@ login.
Logged-in users manage their own private short notes at `/notes`. Each note has 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; 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 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 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 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 listens on `127.0.0.1` by default and is open to every local user without
authentication. 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 ### Development
```bash ```bash
+14
View File
@@ -174,6 +174,20 @@ def create_app():
user_db.session.commit() user_db.session.commit()
return _note_redirect(note.id) return _note_redirect(note.id)
@app.route(
"/notes/<int:note_id>/items/<int:item_id>", 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( @app.route(
"/notes/<int:note_id>/items/<int:item_id>/toggle", methods=["POST"] "/notes/<int:note_id>/items/<int:item_id>/toggle", methods=["POST"]
) )
+10
View File
@@ -38,6 +38,16 @@ esac
PORT="${PORT:-$DEFAULT_PORT}" PORT="${PORT:-$DEFAULT_PORT}"
HOST="${HOST:-$DEFAULT_HOST}" 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 if [ "$APP" = "main" ]; then
SECRET_FILE=".secret_key" SECRET_FILE=".secret_key"
if [ -z "${SECRET_KEY:-}" ]; then if [ -z "${SECRET_KEY:-}" ]; then
+10
View File
@@ -38,6 +38,16 @@ esac
PORT="${PORT:-$DEFAULT_PORT}" PORT="${PORT:-$DEFAULT_PORT}"
HOST="${HOST:-$DEFAULT_HOST}" 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 if [ -x ".venv/bin/python" ]; then
PYTHON=".venv/bin/python" PYTHON=".venv/bin/python"
else else
+19 -3
View File
@@ -343,7 +343,7 @@ textarea {
padding: 0.15em 0; padding: 0.15em 0;
} }
.check-form { .check-form {
flex: 1; flex: 0 0 auto;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 0.6em; gap: 0.6em;
@@ -354,8 +354,24 @@ textarea {
margin: 0; margin: 0;
accent-color: var(--mc-yellow); accent-color: var(--mc-yellow);
} }
.item-text { flex: 1; overflow-wrap: anywhere; } .item-edit-form {
.checklist li.done .item-text { 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; text-decoration: line-through;
color: var(--mc-dim); color: var(--mc-dim);
} }
+7 -1
View File
@@ -61,9 +61,15 @@
{% if item.is_done %}checked{% endif %} {% if item.is_done %}checked{% endif %}
onchange="this.form.submit()" onchange="this.form.submit()"
aria-label="Mark '{{ item.text }}' as done"> aria-label="Mark '{{ item.text }}' as done">
<span class="item-text">{{ item.text }}</span>
<noscript><button type="submit">[ Update ]</button></noscript> <noscript><button type="submit">[ Update ]</button></noscript>
</form> </form>
<form method="post"
action="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}"
class="item-edit-form">
<input type="text" name="text" value="{{ item.text }}"
maxlength="255" required aria-label="Checklist item text">
<button type="submit">[ Save ]</button>
</form>
<form method="post" <form method="post"
action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}" action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}"
class="item-delete-form"> class="item-delete-form">
+11 -3
View File
@@ -235,11 +235,18 @@ class MainAppTest(unittest.TestCase):
) )
with self.main_app.app_context(): with self.main_app.app_context():
first, second = ( first, _second = (
NoteItem.query.filter_by(note_id=note_id).order_by(NoteItem.id).all() NoteItem.query.filter_by(note_id=note_id).order_by(NoteItem.id).all()
) )
first_id = first.id 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.assertEqual(
self.client.post( self.client.post(
f"/notes/{note_id}/items/{first_id}/toggle", f"/notes/{note_id}/items/{first_id}/toggle",
@@ -253,7 +260,8 @@ class MainAppTest(unittest.TestCase):
self.assertEqual(page.status_code, 200) self.assertEqual(page.status_code, 200)
self.assertIn('class="done"', html) self.assertIn('class="done"', html)
self.assertIn('value="Test title"', 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(): with self.main_app.app_context():
user_db.session.expire_all() user_db.session.expire_all()
@@ -264,7 +272,7 @@ class MainAppTest(unittest.TestCase):
self.assertFalse(items[1].is_done) self.assertFalse(items[1].is_done)
self.assertEqual( self.assertEqual(
[item.text for item in items[0].note.ordered_items], [item.text for item in items[0].note.ordered_items],
["second item", "first item"], ["second item", "renamed first item"],
) )