Major JQL bugfixes

This commit is contained in:
2026-09-01 16:30:54 -05:00
parent bf5566e911
commit 6655b02f4a
4 changed files with 109 additions and 8 deletions
+15
View File
@@ -210,6 +210,21 @@ summary. Company is resolved from the SKU prefix (`SH`/`OK`) via
usually blank, the app falls back to the joined deliverable
descriptions for the Summary column when there's nothing else there.
## Resetting the local database
**Reset Local Database** in the toolbar clears every cached order (with
a confirmation first). This is always safe - JIRA is the real source of
truth, this is just a rebuildable cache - but it's worth knowing when
you'd actually need it: if a ticket transitioned status under an older,
buggy version of this app, its `fulfilled_at`/`cancelled_at` timestamp
can get stuck wrong, since those are only recalculated **at the moment
of a transition** - re-importing an already-transitioned ticket finds
"no status change" and never touches that timestamp again. A reset
clears the stale value entirely; the next Import from JIRA then stamps
everything correctly from scratch. Follow a reset with **Import from
JIRA**, and **Pull Tracking Numbers** if you rely on today's
already-pulled tracking data.
## Moving storage to your MariaDB LXC later
In Settings (or directly in `.env`), change:
+43 -7
View File
@@ -21,8 +21,12 @@ from app.queries import get_open_ticket_numbers
from app.services.base import OrderService, NormalizedOrder
from app.status_rules import get_active_statuses
SEARCH_PAGE_SIZE = 50
SEARCH_PAGE_SIZE = 100
REQUEST_TIMEOUT_SECONDS = 30
# Hard ceiling on pages per fetch, purely as a backstop against the new
# search endpoint's documented pagination flakiness (tokens that repeat
# or never advance) - normal usage should never come close to this.
MAX_SEARCH_PAGES = 200
# Deliverable values come back like "SH011: Shipping - Return Label iPad -
# Physical in Box" - the part before the colon is the actual SKU code;
@@ -138,7 +142,23 @@ class JiraService(OrderService):
return combined_where + order_by_clause
def _search_all_issues(self, jql: str) -> List[dict]:
url = f"{self.base_url}/rest/api/3/search"
# /rest/api/3/search (with startAt/total pagination) has been
# deprecated and removed by Atlassian - /rest/api/3/search/jql is
# the replacement, using nextPageToken instead. That migration is
# NOT optional: hitting the old endpoint either errors outright or,
# worse, silently stops returning `total`, which made the old
# startAt-based loop here think page 1 was the whole result set -
# exactly the "only pulled tickets from this afternoon onward" bug,
# since with newest-first sorting, page 1 is just the most recent
# slice.
#
# The new endpoint's token pagination has its own documented
# flakiness in the wild (tokens that repeat or never advance), so
# this loop defends against that directly: it stops on an empty
# batch, an explicit isLast, a missing token, OR a token identical
# to one already seen - plus a hard page-count ceiling as a last
# resort so a pagination bug can never turn into an infinite loop.
url = f"{self.base_url}/rest/api/3/search/jql"
auth = (self.email, self.api_token)
headers = {"Accept": "application/json"}
@@ -152,15 +172,19 @@ class JiraService(OrderService):
fields += "," + ",".join(contact_field_ids)
all_issues: List[dict] = []
start_at = 0
next_page_token: str | None = None
seen_tokens: set[str] = set()
page_count = 0
while True:
params = {
"jql": jql,
"startAt": start_at,
"maxResults": SEARCH_PAGE_SIZE,
"fields": fields,
}
if next_page_token:
params["nextPageToken"] = next_page_token
try:
response = requests.get(
url,
@@ -193,12 +217,24 @@ class JiraService(OrderService):
batch = data.get("issues", [])
all_issues.extend(batch)
page_count += 1
total = data.get("total", len(all_issues))
start_at += len(batch)
if start_at >= total or not batch:
new_token = data.get("nextPageToken")
is_last = bool(data.get("isLast")) or not new_token
if (
not batch
or is_last
or not new_token
or new_token == next_page_token
or new_token in seen_tokens
or page_count >= MAX_SEARCH_PAGES
):
break
seen_tokens.add(new_token)
next_page_token = new_token
return all_issues
def _raw_values_from_field(self, fields: dict, field_id: str) -> List[str]:
+27
View File
@@ -39,6 +39,7 @@ from app.workers import (
load_orders_by_view,
get_dashboard_stats,
mark_shipstation_sent,
reset_local_database,
)
@@ -115,6 +116,11 @@ class MainWindow(QMainWindow):
refresh_action.triggered.connect(self._refresh_everything)
toolbar.addAction(refresh_action)
reset_action = QAction("Reset Local Database", self)
reset_action.setToolTip("Wipes the local cache - re-import from JIRA afterward")
reset_action.triggered.connect(self._on_reset_database_clicked)
toolbar.addAction(reset_action)
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_label = QLabel("Ready.")
@@ -333,6 +339,27 @@ class MainWindow(QMainWindow):
"Upload this into ShipStation's Import Orders wizard.",
)
def _on_reset_database_clicked(self) -> None:
confirm = QMessageBox.warning(
self,
"Reset Local Database",
"This clears every locally cached order (JIRA is still the real source of "
"truth, so nothing there is affected). You'll need to click Import from JIRA "
"afterward to repopulate, and Pull Tracking Numbers again if you rely on "
"today's already-pulled tracking data.\n\n"
"Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if confirm != QMessageBox.StandardButton.Yes:
return
count = reset_local_database()
self._refresh_everything()
self.status_label.setText(
f"Local database reset - {count} order(s) cleared. Run Import from JIRA to repopulate."
)
def _refresh_everything(self) -> None:
active_orders, cancelled_orders, done_orders = load_orders_by_view()
self.orders_table.set_orders(active_orders)
+24 -1
View File
@@ -12,7 +12,7 @@ import datetime as dt
from typing import List, Tuple, TypedDict
from PyQt6.QtCore import QThread, pyqtSignal
from sqlalchemy import select
from sqlalchemy import select, delete
from app.database import get_session
from app.models import Order
@@ -216,6 +216,29 @@ def save_orders(orders: List[NormalizedOrder]) -> SaveResult:
)
def reset_local_database() -> int:
"""
Wipes every locally cached order. This is a rebuildable cache, not a
system of record - JIRA is - so this is always safe, just requires
re-running Import from JIRA (and Pull Tracking Numbers) afterward.
Also the real fix for one specific situation: a ticket that
transitioned status under an older, buggy version of this app can end
up with a permanently-wrong fulfilled_at/cancelled_at timestamp, since
those are only recalculated ON a transition - re-importing an
already-transitioned ticket finds "no change" and never touches it
again. A reset clears that stale timestamp entirely; the fresh
re-import then stamps everything correctly from scratch.
"""
session = get_session()
try:
result = session.execute(delete(Order))
session.commit()
return result.rowcount or 0
finally:
session.close()
def load_all_orders() -> List[Order]:
session = get_session()
try: