← Commit history
README.md+6−2
@@ -114,8 +114,12 @@ inbox. A failed board post never sweeps the email; it retries next pass. ## Board lifecycle  - **New issue / reopened** - row appears (or returns) as OPEN, jumps to the top.-- **Comment / mention** - row bumps to the top with `last: new comment` /-  `mentioned you`.+- **Comment / mention** - row bumps to the top, and the **comment text itself+  renders under the row** - commenter name, timestamp, then the comment -+  one after another as a thread (up to the last 8; older ones collapse into+  a `+N earlier` note). Once a day at `purgeHour` - the same moment closed+  items are cleared - every item's thread is trimmed to just its most recent+  comment so the board never bloats. - **Closed** - row shows CLOSED (gray) and sticks around so you can enjoy it,   then is removed at `purgeHour` (default 6am, your `timezone`) the next day. - The board caps at `maxRows` (default 40); older items collapse into a
bin/__pycache__/wiki-notify-gchatcpython-312.pyc
⋯ 1 unchanged line ⋯
bin/wiki-notify-gchat+51−1
@@ -259,11 +259,26 @@ def parse_notification(msg):     rm = re.search(r"You're receiving this because (.+?)[.\n]", body)     if rm:         reason = rm.group(1).strip()+    comment = ""+    if event in ("comment", "mention"):+        lines = body.splitlines()+        for i, ln in enumerate(lines):+            if ln.startswith("Hi ") and ln.rstrip().endswith(":"):+                for ln2 in lines[i + 1:]:+                    stripped = ln2.strip()+                    if not stripped:+                        if comment:+                            break+                        continue+                    if stripped.startswith("View ") or "You're receiving this" in stripped:+                        break+                    comment += (" " if comment else "") + stripped+                break     return {         "key": f"{page}#{num or title[:40]}",         "page": page, "num": num, "title": title.strip(),         "event": event, "status": status, "actor": actor, "url": url, "ts": ts,-        "reason": reason,+        "reason": reason, "comment": comment[:400],     }  @@ -315,6 +330,11 @@ def apply_event(items, ev):             it["closedAt"] = ev["ts"] if ev["status"] == "closed" else None     if ev["url"] and not it["url"]:         it["url"] = ev["url"]+    if ev.get("comment"):+        comments = it.setdefault("comments", [])+        if not any(c["ts"] == ev["ts"] and c["by"] == (ev["actor"] or "-") for c in comments):+            comments.append({"by": ev["actor"] or "-", "ts": ev["ts"], "text": ev["comment"]})+            comments.sort(key=lambda c: c["ts"])   def purge_items(items, cfg):@@ -333,6 +353,25 @@ def purge_items(items, cfg):     return dropped  +def trim_comments_daily(state, cfg):+    """Once a day at purgeHour (same moment closed items are cleared), collapse+    every item's comment thread to just its most recent comment so boards do+    not grow without bound. Returns number of comments removed."""+    tz = tzinfo_of(cfg)+    now = datetime.now(tz)+    today = now.strftime("%Y-%m-%d")+    if now.hour < int(cfg.get("purgeHour", 6)) or state.get("lastCommentTrim") == today:+        return 0+    removed = 0+    for it in state.get("items", {}).values():+        comments = it.get("comments", [])+        if len(comments) > 1:+            removed += len(comments) - 1+            it["comments"] = comments[-1:]+    state["lastCommentTrim"] = today+    return removed++ # ---------- card rendering ----------  def fmt_date(ts, tz):@@ -409,6 +448,13 @@ def render_table_widgets(shown, tz, me="You"):             inv = "owns this page"  # page authorship beats mention/comment/watch, not opened/assigned         if inv:             body.append(dim(f"{me} {inv}"))+        comments = it.get("comments", [])+        shown_c = comments[-8:]+        if len(comments) > len(shown_c):+            body.append(dim(f"<i>+{len(comments) - len(shown_c)} earlier comment(s)</i>"))+        for c in shown_c:+            body.append(dim(f"<b>{c['by']}</b> ยท {fmt_datetime(c['ts'], tz)}"))+            body.append(html_mod.escape(c["text"], quote=False))         left_widgets = []         if it.get("heroUrl"):             img = {"image": {"imageUrl": it["heroUrl"], "altText": it["page"]}}@@ -637,6 +683,10 @@ def cmd_run(as_json, dry_run=False, quiet=False):     if dropped:         save_state(state)         log_line(f"OK purged {len(dropped)} closed item(s): {', '.join(dropped)}")+    trimmed = 0 if dry_run else trim_comments_daily(state, cfg)+    if trimmed:+        save_state(state)+        log_line(f"OK daily comment trim: removed {trimmed} older comment(s)")     board_changed = False     if not dry_run:         try:
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "wiki-notify-gchat",   "title": "Wiki Notify for Google Chat",   "type": "app",-  "version": "1.5.2",+  "version": "1.6.0",   "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.5.2",+  "version": "1.6.0",   "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",