backend: close P1 data model and create-flow issues
All checks were successful
docker / test (push) Successful in 13s
docker / build-and-push (push) Successful in 1m23s

This commit is contained in:
Space-Banane
2026-05-20 22:40:32 +02:00
parent 643785ad1e
commit 791126cdd0
2 changed files with 56 additions and 19 deletions

16
TODO.md
View File

@@ -26,14 +26,14 @@ Concrete follow-up work for Jellomator, prioritized by implementation risk and u
## P1 - Data Model and Backend Quality
- Replace string timestamps with DB-native datetime.
- Migrate `created_at`/`updated_at` columns from `varchar` to `datetime`.
- Use UTC consistently for writes and reads.
- Add display ordering support.
- Add `sort_order` column and stable ordering fallback by `name`.
- Update read query to order by `enabled desc`, `sort_order`, `name`.
- Remove duplicate connection pattern in create flow.
- Use one DB transaction/connection per request path where possible.
- [x] Replace string timestamps with DB-native datetime.
- [x] Migrate `created_at`/`updated_at` columns from `varchar` to `datetime`.
- [x] Use UTC consistently for writes and reads.
- [x] Add display ordering support.
- [x] Add `sort_order` column and stable ordering fallback by `name`.
- [x] Update read query to order by `enabled desc`, `sort_order`, `name`.
- [x] Remove duplicate connection pattern in create flow.
- [x] Use one DB transaction/connection per request path where possible.
- Add backup and restore flow in admin API/UI.
- Download full export.
- Upload validated import with explicit confirmation.

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import secrets
import os
import time
from datetime import datetime
from datetime import datetime, timezone
from contextlib import contextmanager
from pathlib import Path
from typing import Optional
@@ -122,14 +122,48 @@ def init_db():
url text not null,
description text,
category varchar(255),
sort_order int not null default 0,
icon_blob longblob,
icon_mime varchar(255),
icon_url text,
enabled tinyint(1) not null default 1,
created_at varchar(64) not null,
updated_at varchar(64) not null
created_at datetime not null,
updated_at datetime not null
) engine=InnoDB default charset=utf8mb4
""")
cur.execute("show columns from links like 'sort_order'")
if cur.fetchone() is None:
cur.execute("alter table links add column sort_order int not null default 0 after category")
cur.execute("show columns from links like 'created_at'")
created_col = cur.fetchone()
cur.execute("show columns from links like 'updated_at'")
updated_col = cur.fetchone()
created_is_varchar = bool(created_col and str(created_col.get("Type", "")).startswith("varchar"))
updated_is_varchar = bool(updated_col and str(updated_col.get("Type", "")).startswith("varchar"))
if created_is_varchar or updated_is_varchar:
cur.execute("alter table links add column created_at_dt datetime null, add column updated_at_dt datetime null")
cur.execute(
"""
update links
set created_at_dt=coalesce(
str_to_date(created_at, '%Y-%m-%dT%H:%i:%s.%f'),
str_to_date(created_at, '%Y-%m-%dT%H:%i:%s'),
str_to_date(created_at, '%Y-%m-%d %H:%i:%s'),
utc_timestamp()
),
updated_at_dt=coalesce(
str_to_date(updated_at, '%Y-%m-%dT%H:%i:%s.%f'),
str_to_date(updated_at, '%Y-%m-%dT%H:%i:%s'),
str_to_date(updated_at, '%Y-%m-%d %H:%i:%s'),
utc_timestamp()
)
"""
)
cur.execute("alter table links drop column created_at, drop column updated_at")
cur.execute(
"alter table links change column created_at_dt created_at datetime not null, "
"change column updated_at_dt updated_at datetime not null"
)
init_db()
@@ -156,6 +190,10 @@ def utc_now_iso() -> str:
return datetime.utcnow().isoformat()
def utc_now_db() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
def expires_at_iso() -> str:
now = datetime.utcnow().timestamp()
return datetime.utcfromtimestamp(now + SESSION_TTL_SECONDS).isoformat()
@@ -398,7 +436,7 @@ def logout(request: Request):
def links():
with db() as c:
with c.cursor() as cur:
cur.execute("select * from links order by enabled desc, category, name")
cur.execute("select * from links order by enabled desc, sort_order asc, name asc")
rows = cur.fetchall()
out = []
for r in rows:
@@ -437,17 +475,16 @@ def create_link(
require_admin(request)
require_csrf(request)
validate_link_payload(name, url, description, category, icon_url)
icon_blob, icon_mime = read_icon_blob(icon)
now = utc_now_db()
with db() as c:
if link_name_exists(c, name):
raise HTTPException(409, "Link name already exists")
icon_blob, icon_mime = read_icon_blob(icon)
now = datetime.utcnow().isoformat()
with db() as c:
with c.cursor() as cur:
cur.execute(
"""insert into links(name,url,description,category,icon_blob,icon_mime,icon_url,enabled,created_at,updated_at)
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
(name, url, description, category, icon_blob, icon_mime, icon_url, int(enabled), now, now),
"""insert into links(name,url,description,category,sort_order,icon_blob,icon_mime,icon_url,enabled,created_at,updated_at)
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
(name, url, description, category, 0, icon_blob, icon_mime, icon_url, int(enabled), now, now),
)
return {"ok": True}
@@ -489,7 +526,7 @@ def update_link(
require_csrf(request)
validate_link_payload(name, url, description, category, icon_url)
icon_blob, icon_mime = read_icon_blob(icon)
now = datetime.utcnow().isoformat()
now = utc_now_db()
with db() as c:
if link_name_exists(c, name, exclude_id=link_id):
raise HTTPException(409, "Link name already exists")