← Commit history
README.md+1
@@ -159,6 +159,7 @@ inbox. A failed board post never sweeps the email; it retries next pass. | `meName` | `You` | your first name, used in the involvement line ("Drew opened this") | | `fullName` | auto (To header) | your wiki author name (e.g. "Drew Owens"), used to detect pages you authored | | `weeklySummary` | `true` | Sunday waiting-on-others digest at purgeHour |+| `delegated` | `{}` | pages you own but handed off, `{"owner/slug": "Name"}`: their open items triage as WAITING attributed to the delegate (mentions still come to you). Set via `config set delegated '{"adom/molecule-publish": "Aditya"}'` |  ## Staying alive and staying current (zero maintenance) 
bin/__pycache__/wiki-notify-gchatcpython-312.pyc
⋯ 1 unchanged line ⋯
bin/wiki-notify-gchat+42−18
@@ -48,6 +48,7 @@ DEFAULT_CONFIG = {     "meName": "You",     "fullName": "",     "weeklySummary": True,+    "delegated": {}, }  OPEN_COLOR, CLOSED_COLOR = "#22c55e", "#64abff"  # CLOSED = Adom blue (text-safe --blue)@@ -58,13 +59,20 @@ OPEN_COLOR, CLOSED_COLOR = "#22c55e", "#64abff"  # CLOSED = Adom blue (text-safe NEEDS_COLOR, WAITING_COLOR = "#8c6bf7", "#00b8b1"  # Adom purple = your move, Adom teal = waiting  -def triage(it):+def triage(it, delegated=None):     """closed | needs_you | waiting. 'needs_you' = your page (you're the     maintainer who closes it) OR someone @mentioned you (they want your call).-    Otherwise it's on another member's page and the ball is in their court."""+    A page in the `delegated` config map ({"owner/slug": "Name"}) triages its+    open items as WAITING attributed to the delegate, even though you own the+    page - the work is handed off. A mention still outranks delegation:+    whoever @mentioned you wants YOUR call, not the delegate's."""     if it.get("status") != "open":         return "closed"-    if it.get("ownsPage") or it.get("lastEvent") == "mention":+    if it.get("lastEvent") == "mention":+        return "needs_you"+    if (delegated or {}).get(it.get("page")):+        return "waiting"+    if it.get("ownsPage"):         return "needs_you"     return "waiting" @@ -538,7 +546,8 @@ def is_hidden_waiting(it, cfg, tz, now):     activity, like closed items - but they stay in state (never deleted), so     the moment the other party acts, the new lastAt makes them visible again     and they keep feeding the weekly summary while hidden."""-    return triage(it) == "waiting" and now >= hide_deadline(it["lastAt"], cfg, tz)+    return (triage(it, cfg.get("delegated")) == "waiting"+            and now >= hide_deadline(it["lastAt"], cfg, tz))   def weekly_summary(state, cfg, force=False):@@ -556,7 +565,9 @@ def weekly_summary(state, cfg, force=False):         if state.get("lastWeeklySummary") == today:             return False     state["lastWeeklySummary"] = today-    waiting = [it for it in state.get("items", {}).values() if triage(it) == "waiting"]+    delegated = cfg.get("delegated") or {}+    waiting = [it for it in state.get("items", {}).values()+               if triage(it, delegated) == "waiting"]     if not waiting:         save_state(state)         log_line("OK weekly summary: nothing waiting, skipped post")@@ -566,7 +577,7 @@ def weekly_summary(state, cfg, force=False):     for it in waiting:         item = it["page"] + (f" #{it['num']}" if it["num"] else "")         ref = f'<a href="{it["url"]}">{item}</a>' if it.get("url") else item-        who = it.get("pageAuthor") or "-"+        who = delegated.get(it.get("page")) or it.get("pageAuthor") or "-"         days = max(0, int((now - datetime.fromtimestamp(it["lastAt"] / 1000, tz)).days))         lines.append(f"{ref} · waiting on {who} · {days}d")     card = {"cardsV2": [{"cardId": "wiki-notify-weekly", "card": {@@ -656,22 +667,24 @@ EVENT_LABEL = {"opened": "opened", "comment": "new comment", "mention": "mention def render_card(items, cfg, max_rows=None, max_comments=None):     tz = tzinfo_of(cfg)     # NEEDS YOU floats to the top, then WAITING, then CLOSED; newest-first within each.-    rows = sorted(items.values(), key=lambda it: (TRIAGE_RANK[triage(it)], -it["lastAt"]))+    delegated = cfg.get("delegated") or {}+    rows = sorted(items.values(),+                  key=lambda it: (TRIAGE_RANK[triage(it, delegated)], -it["lastAt"]))     now = datetime.now(tz)     n_hidden = sum(1 for it in rows if is_hidden_waiting(it, cfg, tz, now))     rows = [it for it in rows if not is_hidden_waiting(it, cfg, tz, now)]-    n_needs = sum(1 for it in rows if triage(it) == "needs_you")-    n_wait = sum(1 for it in rows if triage(it) == "waiting")-    n_closed = sum(1 for it in rows if triage(it) == "closed")+    n_needs = sum(1 for it in rows if triage(it, delegated) == "needs_you")+    n_wait = sum(1 for it in rows if triage(it, delegated) == "waiting")+    n_closed = sum(1 for it in rows if triage(it, delegated) == "closed")     if max_rows is None:         max_rows = int(cfg.get("maxRows", 40))     shown, overflow = rows[:max_rows], max(0, len(rows) - max_rows)     style = cfg.get("style", "table")     if style == "blocks":-        widgets = render_block_widgets(shown, tz)+        widgets = render_block_widgets(shown, tz, delegated=delegated)     else:         widgets = render_table_widgets(shown, tz, me=cfg.get("meName", "You"),-                                       max_comments=max_comments)+                                       max_comments=max_comments, delegated=delegated)     if overflow:         widgets.append({"textParagraph": {"text": f"<i>+{overflow} older item(s) not shown</i>"}})     if not shown:@@ -690,7 +703,7 @@ EVENT_CAP = {"opened": "Opened", "comment": "Commented", "mention": "Mentioned",              "closed": "Closed", "reopened": "Reopened", "activity": "Activity"}  -def render_table_widgets(shown, tz, me="You", max_comments=None):+def render_table_widgets(shown, tz, me="You", max_comments=None, delegated=None):     """Wiki-digest-style rows (the same skeleton the wiki digest posts use):     left column = the page's hero image (clickable, opens the page); right     column = a decoratedText with the colored OPEN/CLOSED status in the@@ -702,12 +715,14 @@ def render_table_widgets(shown, tz, me="You", max_comments=None):     for i, it in enumerate(shown):         if i:             widgets.append({"divider": {}})-        t = triage(it)+        t = triage(it, delegated)         if t == "needs_you":             status = f'<font color="{NEEDS_COLOR}"><b>NEEDS YOU</b></font>'         elif t == "waiting":-            # name who the ball is on: the page owner (who can close it), else last actor-            on = it.get("pageAuthor") or it.get("actor") or "another member"+            # name who the ball is on: the delegate if the page is handed off,+            # else the page owner (who can close it), else last actor+            on = ((delegated or {}).get(it.get("page"))+                  or it.get("pageAuthor") or it.get("actor") or "another member")             status = f'<font color="{WAITING_COLOR}"><b>WAITING</b></font> · {html_mod.escape(str(on), quote=False)}'         else:             status = f'<font color="{CLOSED_COLOR}"><b>CLOSED</b></font>'@@ -781,10 +796,10 @@ def render_table_widgets(shown, tz, me="You", max_comments=None):     return widgets  -def render_block_widgets(shown, tz):+def render_block_widgets(shown, tz, delegated=None):     widgets = []     for it in shown:-        t = triage(it)+        t = triage(it, delegated)         status_html = {             "needs_you": f'<font color="{NEEDS_COLOR}"><b>NEEDS YOU</b></font>',             "waiting": f'<font color="{WAITING_COLOR}"><b>WAITING</b></font>',@@ -1285,6 +1300,7 @@ def cmd_uninstall_cron(as_json):  INT_KEYS = {"pollSeconds", "purgeHour", "maxRows"} BOOL_KEYS = {"weeklySummary"}+DICT_KEYS = {"delegated"}   def mask_webhook(url):@@ -1315,6 +1331,14 @@ def cmd_config(as_json, args):         key, value = args[1], " ".join(args[2:])         if key not in DEFAULT_CONFIG:             die(f"unknown key '{key}'", [f"keys: {', '.join(DEFAULT_CONFIG)}"], as_json)+        if key in DICT_KEYS:+            try:+                value = json.loads(value)+                assert isinstance(value, dict) and all(+                    isinstance(k, str) and isinstance(v, str) for k, v in value.items())+            except Exception:+                die(f"{key} needs a JSON object of page -> name",+                    ['e.g. `config set delegated \'{"adom/molecule-publish": "Aditya"}\'`'], as_json)         if key in BOOL_KEYS:             if str(value).lower() not in ("true", "false"):                 die(f"{key} needs true or false", [f"e.g. `config set {key} true`"], as_json)
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "wiki-notify-gchat",   "title": "Wiki Notify for Google Chat",   "type": "app",-  "version": "1.8.4",+  "version": "1.8.5",   "description": "Turn wiki.adom.inc notification emails into a live Google Chat board. One card, one row per issue - newest first, OPEN/CLOSED status, origination date, actor, and a View link - continuously updated in your own Chat space via your own webhook. Emails are swept out of your inbox (trash, archive, or label). Closed items linger until 6am the next day, then drop off. Delegates all auth to adom-google; the only secret is your webhook URL, kept in your private config.",   "tags": [     "cli",
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "wiki-notify-gchat",   "title": "Wiki Notify for Google Chat",   "type": "app",-  "version": "1.8.4",+  "version": "1.8.5",   "description": "Turn wiki.adom.inc notification emails into a live Google Chat board. One card, one row per issue - newest first, OPEN/CLOSED status, origination date, actor, and a View link - continuously updated in your own Chat space via your own webhook. Emails are swept out of your inbox (trash, archive, or label). Closed items linger until 6am the next day, then drop off. Delegates all auth to adom-google; the only secret is your webhook URL, kept in your private config.",   "tags": [     "cli",