chatgpt added offline items update

This commit is contained in:
2026-05-14 21:31:33 +02:00
parent 7e64c5b4cf
commit a739218fb0
5 changed files with 182 additions and 5 deletions
+45
View File
@@ -3,6 +3,7 @@ from flask_login import login_required, current_user
from models import db from models import db
from models import Item, ListOfItems from models import Item, ListOfItems
from sqlalchemy import desc, text from sqlalchemy import desc, text
from datetime import datetime
import os import os
from log import Log from log import Log
from auth import create_cookie from auth import create_cookie
@@ -23,6 +24,28 @@ def require_item_access(itemid):
abort(403) abort(403)
return item 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 # home screen for items
@items_bp.route("/items/<listid>") @items_bp.route("/items/<listid>")
@login_required @login_required
@@ -42,7 +65,10 @@ def items(listid):
@login_required @login_required
def item_update(itemid, itemchecked): def item_update(itemid, itemchecked):
i = require_item_access(itemid) i = require_item_access(itemid)
if not item_update_is_current(i):
return '', 204
i.is_checked = True if itemchecked == 'true' else False i.is_checked = True if itemchecked == 'true' else False
stamp_item_update(i)
db.session.commit() db.session.commit()
return '', 204 return '', 204
@@ -170,6 +196,8 @@ def items_multiappend(listid):
@login_required @login_required
def item_delete(itemid): def item_delete(itemid):
i = require_item_access(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 listid = i.listofitems_id
db.session.delete(i) db.session.delete(i)
db.session.commit() db.session.commit()
@@ -195,6 +223,12 @@ def items_clean(listid):
@login_required @login_required
def item_addone(itemid): def item_addone(itemid):
i = require_item_access(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 listid = i.listofitems_id
# if it is checked or not on the list yet, set quantity to one # if it is checked or not on the list yet, set quantity to one
if i.is_suggestion or i.is_checked: if i.is_suggestion or i.is_checked:
@@ -208,6 +242,7 @@ def item_addone(itemid):
# if no unit given, make it 'x' # if no unit given, make it 'x'
if not i.unit: if not i.unit:
i.unit = 'x' i.unit = 'x'
stamp_item_update(i)
db.session.commit() db.session.commit()
resp = make_response(render_template("items_append.html", user=current_user, resp = make_response(render_template("items_append.html", user=current_user,
@@ -221,6 +256,12 @@ def item_addone(itemid):
@login_required @login_required
def item_addquantity(itemid, quantity): def item_addquantity(itemid, quantity):
i = require_item_access(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 listid = i.listofitems_id
# always add the item to the active todo # always add the item to the active todo
if i.is_suggestion or i.is_checked: if i.is_suggestion or i.is_checked:
@@ -229,6 +270,7 @@ def item_addquantity(itemid, quantity):
i.is_checked = False i.is_checked = False
else: else:
i.quantity += int(quantity) i.quantity += int(quantity)
stamp_item_update(i)
db.session.commit() db.session.commit()
resp = make_response(render_template("items_append.html", user=current_user, resp = make_response(render_template("items_append.html", user=current_user,
@@ -242,6 +284,9 @@ def item_addquantity(itemid, quantity):
@login_required @login_required
def item_upitem_update_categorydate(itemid, category): def item_upitem_update_categorydate(itemid, category):
i = require_item_access(itemid) i = require_item_access(itemid)
if not item_update_is_current(i):
return '', 204
i.category = category i.category = category
stamp_item_update(i)
db.session.commit() db.session.commit()
return '', 204 return '', 204
+117
View File
@@ -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);
})();
+1
View File
@@ -17,6 +17,7 @@
<link href="/static/bootstrap.min.css" rel="stylesheet"> <link href="/static/bootstrap.min.css" rel="stylesheet">
<script src="/static/jquery.min.js"></script> <script src="/static/jquery.min.js"></script>
<script src="/static/bootstrap.min.js"></script> <script src="/static/bootstrap.min.js"></script>
<script src="/static/item_update_queue.js"></script>
<style> <style>
body { body {
+16 -4
View File
@@ -6,17 +6,27 @@
// handle Delete // handle Delete
function handleDelete(button) { function handleDelete(button) {
window.location.href = '/item_delete/'+button.name ; lappQueueItemRequest('/item_delete/'+button.name, {
coalesceKey: 'item:' + button.name
}).then(function (sent) {
if (sent) {
window.location.href = '/items_append/{{ listid }}';
}
});
} }
// handle AddOne // handle AddOne
function handleAddOne(button) { function handleAddOne(button) {
const response = fetch('/item_addone/'+button.name); lappQueueItemRequest('/item_addone/'+button.name, {
coalesceKey: 'item:' + button.name
});
} }
// handle Add with quantity // handle Add with quantity
function handleAddWithQuantity(button, q) { function handleAddWithQuantity(button, q) {
const response = fetch('/item_addquantity/'+button.name+'/'+q); lappQueueItemRequest('/item_addquantity/'+button.name+'/'+q, {
coalesceKey: 'item:' + button.name
});
} }
//trigger change category button //trigger change category button
@@ -27,7 +37,9 @@
// selected the category // selected the category
function clickedUpdateCategory4Item(icon) { function clickedUpdateCategory4Item(icon) {
image_to_update.src="{{ iconpath }}" + icon.name + ".svg" image_to_update.src="{{ iconpath }}" + icon.name + ".svg"
const response = fetch('/item_update_category/'+image_to_update.name+'/'+icon.name); lappQueueItemRequest('/item_update_category/'+image_to_update.name+'/'+icon.name, {
coalesceKey: 'category:' + image_to_update.name
});
$('#staticBackdrop').modal('hide'); $('#staticBackdrop').modal('hide');
} }
+3 -1
View File
@@ -35,7 +35,9 @@
// handle Checboc clicks // handle Checboc clicks
function handleCBClick(cb) { function handleCBClick(cb) {
const response = fetch('/item_update/'+cb.name+'/'+cb.checked); lappQueueItemRequest('/item_update/'+cb.name+'/'+cb.checked, {
coalesceKey: 'checked:' + cb.name
});
} }
</script> </script>