[object Object]

← back to Ticket System

Ticket desktop bar: show priority rank/score + created date/time on dropdown rows

59e3c395ba2c0845172f5947e11314748e39c12c · 2026-09-03 10:34:25 -0700 · Steve Abrams

Each status dropdown row now renders 'TK-xxxx  #rank  p<priority> · title  🕓 <created
date/time>', sorted highest-priority-first. Priority/rank come best-effort from the
viewer's /api/tickets (server-computed, mirrors the board); created date/time is the
create-event ts formatted 'MMM d, h:mm a' (admin-chip standing rule). If the viewer is
unreachable, counts + date/time still render and priority drops off (never blank).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XNcHvXXepEQar7Qsb7GGy

Files touched

Diff

commit 59e3c395ba2c0845172f5947e11314748e39c12c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 10:34:25 2026 -0700

    Ticket desktop bar: show priority rank/score + created date/time on dropdown rows
    
    Each status dropdown row now renders 'TK-xxxx  #rank  p<priority> · title  🕓 <created
    date/time>', sorted highest-priority-first. Priority/rank come best-effort from the
    viewer's /api/tickets (server-computed, mirrors the board); created date/time is the
    create-event ts formatted 'MMM d, h:mm a' (admin-chip standing rule). If the viewer is
    unreachable, counts + date/time still render and priority drops off (never blank).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015XNcHvXXepEQar7Qsb7GGy
---
 desktop-bar/TicketBar.swift | 62 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 57 insertions(+), 5 deletions(-)

diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index 097a0259..d84402a0 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -10,6 +10,7 @@ private struct TicketState {
   var title = ""
   var status = "open"
   var updated = ""
+  var created = ""
 }
 
 private final class BarTile: NSButton {
@@ -444,6 +445,41 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
     } else { completionHandler(.performDefaultHandling, nil) }
   }
 
+  // ── priority + created-time enrichment ──────────────────────────────────────
+  private let isoParser: ISO8601DateFormatter = {
+    let f = ISO8601DateFormatter(); f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]; return f
+  }()
+  private let isoParserPlain = ISO8601DateFormatter()
+  private let stampFmt: DateFormatter = {
+    let f = DateFormatter(); f.dateFormat = "MMM d, h:mm a"; return f
+  }()
+
+  private func stamp(_ iso: String) -> String {
+    guard !iso.isEmpty else { return "" }
+    guard let d = isoParser.date(from: iso) ?? isoParserPlain.date(from: iso) else { return "" }
+    return "🕓 " + stampFmt.string(from: d)
+  }
+
+  // Best-effort pull of server-computed rank/priority from the viewer API.
+  // Empty (no priority shown) if the viewer is unreachable — counts still work.
+  private func fetchPriorities() -> [String: (rank: Int?, priority: Double)] {
+    guard let url = URL(string: "http://127.0.0.1:9794/api/tickets") else { return [:] }
+    var req = URLRequest(url: url, timeoutInterval: 2.5)
+    req.setValue("Basic " + Data("admin:DW2024!".utf8).base64EncodedString(), forHTTPHeaderField: "Authorization")
+    var result: [String: (Int?, Double)] = [:]
+    let sem = DispatchSemaphore(value: 0)
+    URLSession.shared.dataTask(with: req) { data, _, _ in
+      defer { sem.signal() }
+      guard let data, let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return }
+      for t in arr {
+        guard let id = t["id"] as? String else { continue }
+        result[id] = (t["rank"] as? Int, (t["priority"] as? NSNumber)?.doubleValue ?? 0)
+      }
+    }.resume()
+    _ = sem.wait(timeout: .now() + 3)
+    return result
+  }
+
   private func refresh() {
     DispatchQueue.global(qos: .utility).async { [weak self] in
       guard let self else { return }
@@ -458,7 +494,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
           let type = event["type"] as? String ?? ""
           let timestamp = event["ts"] as? String ?? ""
           if type == "create" {
-            states[id] = TicketState(id: id, title: event["title"] as? String ?? id, status: "open", updated: timestamp)
+            states[id] = TicketState(id: id, title: event["title"] as? String ?? id, status: "open", updated: timestamp, created: timestamp)
           } else if var ticket = states[id] {
             if type == "status", let status = event["status"] as? String { ticket.status = status }
             ticket.updated = timestamp
@@ -467,12 +503,28 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
           if type == "action" || type == "comment" || type == "status" { newest = event }
         }
         let active = states.values.filter { $0.status != "done" && $0.status != "stopped" }
-        let openList = active.filter { $0.status == "open" }.sorted { $0.updated > $1.updated }
-        let blockedList = active.filter { $0.status == "blocked" }.sorted { $0.updated > $1.updated }
-        let doing = active.filter { $0.status == "doing" }.sorted { $0.updated > $1.updated }
+        let prios = self.fetchPriorities()   // best-effort; empty if viewer down
+        // Highest priority first (lowest rank #); un-ranked fall to the bottom, newest first.
+        let byPrio: (TicketState, TicketState) -> Bool = { a, b in
+          let ra = prios[a.id]?.rank ?? Int.max, rb = prios[b.id]?.rank ?? Int.max
+          if ra != rb { return ra < rb }
+          return a.updated > b.updated
+        }
+        let openList = active.filter { $0.status == "open" }.sorted(by: byPrio)
+        let blockedList = active.filter { $0.status == "blocked" }.sorted(by: byPrio)
+        let doing = active.filter { $0.status == "doing" }.sorted(by: byPrio)
         let workText = doing.prefix(3).map { "\(self.shortID($0.id)) \(self.fiveWords($0.title))" }.joined(separator: "  •  ")
         let latestText = self.describe(newest)
-        let row: (TicketState) -> String = { "\(self.shortID($0.id)) · \($0.title)" }
+        // Row: TK-xxxx  #rank  p<priority>  · title   🕓 created date/time
+        let row: (TicketState) -> String = { t in
+          var prefix = self.shortID(t.id)
+          if let p = prios[t.id] {
+            if let r = p.rank { prefix += "  #\(r)" }
+            if p.priority > 0 { prefix += String(format: "  p%.1f", p.priority) }
+          }
+          let when = self.stamp(t.created)
+          return when.isEmpty ? "\(prefix) · \(t.title)" : "\(prefix) · \(t.title)   \(when)"
+        }
         DispatchQueue.main.async {
           self.openLabel.stringValue    = "OPEN \(openList.count)"
           self.blockedLabel.stringValue = "BLOCKED \(blockedList.count)"

← 01ba92d4 record TK-11183 bounded monitoring proof  ·  back to Ticket System  ·  Ticket desktop bar: dock-position toggle — top/left/right/of 3c6638a4 →