From a739218fb0c413c1427c864996b3e789b8bcbe7a Mon Sep 17 00:00:00 2001 From: Ignace Date: Thu, 14 May 2026 21:31:33 +0200 Subject: [PATCH] chatgpt added offline items update --- items.py | 45 ++++++++++++++ static/item_update_queue.js | 117 ++++++++++++++++++++++++++++++++++++ templates/base.html | 1 + templates/items_append.html | 20 ++++-- templates/items_show.html | 4 +- 5 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 static/item_update_queue.js diff --git a/items.py b/items.py index 7702fd3..2ca0019 100644 --- a/items.py +++ b/items.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from models import db from models import Item, ListOfItems from sqlalchemy import desc, text +from datetime import datetime import os from log import Log from auth import create_cookie @@ -23,6 +24,28 @@ def require_item_access(itemid): abort(403) return item +def client_update_time(): + client_updated_at = request.args.get("client_updated_at") + if not client_updated_at: + return None + + try: + return datetime.fromtimestamp(int(client_updated_at) / 1000) + except ValueError: + return None + +def item_update_is_current(item): + updated_at = client_update_time() + if updated_at is None: + return True + + return item.updated_at is None or updated_at >= item.updated_at + +def stamp_item_update(item): + updated_at = client_update_time() + if updated_at is not None: + item.updated_at = updated_at + # home screen for items @items_bp.route("/items/") @login_required @@ -42,7 +65,10 @@ def items(listid): @login_required def item_update(itemid, itemchecked): i = require_item_access(itemid) + if not item_update_is_current(i): + return '', 204 i.is_checked = True if itemchecked == 'true' else False + stamp_item_update(i) db.session.commit() return '', 204 @@ -170,6 +196,8 @@ def items_multiappend(listid): @login_required def item_delete(itemid): i = require_item_access(itemid) + if not item_update_is_current(i): + return redirect(url_for("items.items_append", listid=i.listofitems_id)) listid = i.listofitems_id db.session.delete(i) db.session.commit() @@ -195,6 +223,12 @@ def items_clean(listid): @login_required def item_addone(itemid): i = require_item_access(itemid) + if not item_update_is_current(i): + resp = make_response(render_template("items_append.html", user=current_user, + items=Item.query.filter_by(listofitems_id=i.listofitems_id).order_by(Item.label).all(), + logo_url=url_for("items.items", listid=i.listofitems_id))) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp listid = i.listofitems_id # if it is checked or not on the list yet, set quantity to one if i.is_suggestion or i.is_checked: @@ -208,6 +242,7 @@ def item_addone(itemid): # if no unit given, make it 'x' if not i.unit: i.unit = 'x' + stamp_item_update(i) db.session.commit() resp = make_response(render_template("items_append.html", user=current_user, @@ -221,6 +256,12 @@ def item_addone(itemid): @login_required def item_addquantity(itemid, quantity): i = require_item_access(itemid) + if not item_update_is_current(i): + resp = make_response(render_template("items_append.html", user=current_user, + items=Item.query.filter_by(listofitems_id=i.listofitems_id).order_by(Item.label).all(), + logo_url=url_for("items.items", listid=i.listofitems_id))) + resp.set_cookie('username', create_cookie(current_user.id)) + return resp listid = i.listofitems_id # always add the item to the active todo if i.is_suggestion or i.is_checked: @@ -229,6 +270,7 @@ def item_addquantity(itemid, quantity): i.is_checked = False else: i.quantity += int(quantity) + stamp_item_update(i) db.session.commit() resp = make_response(render_template("items_append.html", user=current_user, @@ -242,6 +284,9 @@ def item_addquantity(itemid, quantity): @login_required def item_upitem_update_categorydate(itemid, category): i = require_item_access(itemid) + if not item_update_is_current(i): + return '', 204 i.category = category + stamp_item_update(i) db.session.commit() return '', 204 diff --git a/static/item_update_queue.js b/static/item_update_queue.js new file mode 100644 index 0000000..f09a1fd --- /dev/null +++ b/static/item_update_queue.js @@ -0,0 +1,117 @@ +(function () { + const STORAGE_KEY = "lapp.pendingItemUpdates"; + + function readQueue() { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); + } catch (error) { + return []; + } + } + + function writeQueue(queue) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(queue)); + } + + function queueRequest(request) { + const queue = readQueue(); + const queuedRequest = { + url: request.url, + method: request.method || "GET", + coalesceKey: request.coalesceKey || null, + clientUpdatedAt: request.clientUpdatedAt || Date.now() + }; + + if (queuedRequest.coalesceKey) { + const existingIndex = queue.findIndex(function (item) { + return item.coalesceKey === queuedRequest.coalesceKey; + }); + if (existingIndex >= 0) { + queue[existingIndex] = queuedRequest; + } else { + queue.push(queuedRequest); + } + } else { + queue.push(queuedRequest); + } + + writeQueue(queue); + } + + async function sendRequest(request) { + const url = new URL(request.url, window.location.origin); + if (request.clientUpdatedAt) { + url.searchParams.set("client_updated_at", request.clientUpdatedAt); + } + + const response = await fetch(url.toString(), { + method: request.method || "GET", + cache: "no-store", + credentials: "same-origin" + }); + + if (response.redirected && new URL(response.url).pathname === "/login") { + const error = new Error("Login required"); + error.retryable = true; + throw error; + } + + if (!response.ok) { + const error = new Error("Request failed: " + response.status); + error.retryable = response.status >= 500 || response.status === 408 || response.status === 429; + throw error; + } + return response; + } + + async function queueableItemRequest(url, options) { + const request = Object.assign({}, options || {}, { + url: url, + clientUpdatedAt: Date.now() + }); + try { + await sendRequest(request); + flushItemUpdateQueue(); + return true; + } catch (error) { + if (error.retryable !== false) { + queueRequest(request); + } + return false; + } + } + + async function flushItemUpdateQueue() { + if (flushItemUpdateQueue.running) { + return; + } + flushItemUpdateQueue.running = true; + + const queue = readQueue(); + const remaining = []; + + for (const request of queue) { + try { + await sendRequest(request); + } catch (error) { + if (error.retryable !== false) { + remaining.push(request); + } + } + } + + writeQueue(remaining); + flushItemUpdateQueue.running = false; + } + + window.lappQueueItemRequest = queueableItemRequest; + window.lappFlushItemUpdateQueue = flushItemUpdateQueue; + + window.addEventListener("online", flushItemUpdateQueue); + document.addEventListener("visibilitychange", function () { + if (!document.hidden) { + flushItemUpdateQueue(); + } + }); + document.addEventListener("DOMContentLoaded", flushItemUpdateQueue); +})(); diff --git a/templates/base.html b/templates/base.html index 26341af..2272dc8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,6 +17,7 @@ +