Author: sebb
Date: Sat Feb  7 17:34:22 2026
New Revision: 1931747

Log:
Replace Whimsy with BAT

Added:
   comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/bat.py   
(contents, props changed)
Deleted:
   comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/whimsy.py
Modified:
   comdev/reporter.apache.org/branches/tooling-project/DOCKER.md
   comdev/reporter.apache.org/branches/tooling-project/compose.yml
   comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/drafts.py
   comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/overview.py
   comdev/reporter.apache.org/branches/tooling-project/scripts/wsgi.py
   
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/drafts.js
   
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/primer.js
   
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/unified.js
   comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/wizard.js

Modified: comdev/reporter.apache.org/branches/tooling-project/DOCKER.md
==============================================================================
--- comdev/reporter.apache.org/branches/tooling-project/DOCKER.md       Sat Feb 
 7 17:33:40 2026        (r1931746)
+++ comdev/reporter.apache.org/branches/tooling-project/DOCKER.md       Sat Feb 
 7 17:34:22 2026        (r1931747)
@@ -29,7 +29,7 @@ The `.env` file should not be readable o
 e.g. use `chmod 600 .env` on macOS/Linux
 Also be careful that it does not get added to any repository.
 
-Browse to ```http://localhost/```
+Browse to ```http://localhost/:81```
 
 You will be prompted to login.
 The credentials are not validated initially, but if they are incorrect, then 
no data will be displayed.

Modified: comdev/reporter.apache.org/branches/tooling-project/compose.yml
==============================================================================
--- comdev/reporter.apache.org/branches/tooling-project/compose.yml     Sat Feb 
 7 17:33:40 2026        (r1931746)
+++ comdev/reporter.apache.org/branches/tooling-project/compose.yml     Sat Feb 
 7 17:34:22 2026        (r1931747)
@@ -10,4 +10,4 @@ services:
       REPORTER_JIRA_USER: ${REPORTER_JIRA_USER:-githubbot}
       REPORTER_JIRA_PASSWORD: ${REPORTER_JIRA_PASSWORD:?}
     ports:
-      - 80:80
+      - 81:80

Added: comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/bat.py
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/bat.py     
Sat Feb  7 17:34:22 2026        (r1931747)
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# -*- coding: UTF-8 -*-
+""" script for publishing a report to BAT """
+# This is part of a gunicorn app (not intended for standalone use)
+
+import os
+import json
+import requests
+import re
+import pdata
+import committee_info
+
+# BAT_BASE = 'https://agenda.apache.org'
+BAT_BASE = 'https://agenda-dev.apache.org'
+
+# read-only URLs
+BAT_CALENDAR = BAT_BASE + '/integration/reporter/calendar.json'
+BAT_COMMENTS = BAT_BASE + '/integration/reporter/historical-comments'
+BAT_AGENDA_IP = BAT_BASE + '/integration/reporter/agenda/%s.json'
+
+# Submit the report
+BAT_SUBMIT = BAT_BASE + '/integration/reporter/post'
+
+def get_bat(url, env, ttl = 14400):
+    cached = True
+    xurl = re.sub(r"[^a-z0-9]+", "-", url.replace('.json', ''))
+    wanted_file = '/tmp/%s.json' % xurl
+    if pdata.has_cache(wanted_file, ttl = ttl):
+        js = json.load(open(wanted_file))
+    else:
+        try:
+            print("Fetching %s => %s..." % (url, wanted_file))
+            rv = requests.get(url, headers = {'Authorization': 
env.get('HTTP_AUTHORIZATION')}, timeout = 5)
+            rv.raise_for_status()
+            js = rv.json()
+            with open(wanted_file, "w") as f:
+                json.dump(js, f)
+                f.close()
+                cached = False
+        except Exception as e: # fall back to cache on failure!
+            print(f"Failed to fetch {url}: {e}")
+            if os.path.exists(wanted_file):
+                print(f"Using stale cache for {wanted_file}")
+                js = json.load(open(wanted_file))
+            else:
+                print(f"No cache for {wanted_file}")
+                raise
+
+    return js, cached
+
+def latest_agenda(environ):
+    calendar, _cached = get_bat(BAT_CALENDAR, environ)
+    latest = calendar['agendas'][-1]
+    ymd = re.match(r"board_agenda_(\d\d\d\d_\d\d_\d\d)\.txt", latest)
+    if ymd:
+        return latest, BAT_AGENDA_IP % ymd.group(1).replace('_', '-')
+    else:
+        return latest, BAT_AGENDA_IP % 'latest'
+
+def has_access(user, project):
+    member = pdata.isASFMember(user)
+    pmc = project in pdata.getPMCs(user)
+    return (member or pmc)
+
+def guess_title(project):
+    """ Guess the whimsy name of a project """
+    pmcSummary = committee_info.PMCsummary()
+
+    # Figure out the name as written in whimsy..
+    pname = project
+    if project in pmcSummary:
+        pname = pmcSummary[project]['name'].replace('Apache ', '')
+
+    return pname
+
+# /api/bat/agenda_refresh
+def agenda_forced(environ, user):
+    """ Force BAT agenda refresh... """
+    _txtfile, url = latest_agenda(environ)
+    get_bat(url, environ, ttl = 0)
+    return agenda(environ, user)
+
+# /api/bat/agenda
+def agenda(environ, user, fproject = None):
+    """ Returns data on the board report for a project, IF present and/or 
filed in the current agenda """
+    project = fproject or environ.get('QUERY_STRING')
+    # Kludge for webservices
+    project = pdata.ldapmap.get(project, project)
+    report = None
+    _txtfile, url = latest_agenda(environ)
+    if has_access(user, project):
+        agenda, _cached = get_bat(url, environ, ttl = 3600)
+        titleguess = guess_title(project)
+        for entry in agenda:
+            if titleguess == entry.get('title'):
+                report = entry
+                break
+
+        comments, _cached = get_bat(BAT_COMMENTS, environ)
+        title = report and report.get('title', titleguess)
+        if title in comments:
+            comments = comments[title]
+        else:
+            comments = {}
+
+        # Allow for empty agenda response
+        if len(agenda) > 0:
+            timestamp = agenda[0].get('timestamp', 0)
+        else:
+            timestamp = 0
+        return {
+            'can_access': True,
+            'found': report and True or False,
+            'filed': report and report.get('report') and True or False,
+            'report': report,
+            'comments': comments,
+            'timestamp': timestamp,
+            }
+
+    return {
+        'can_access': False,
+        'found': False,
+        }
+
+
+# /api/bat/comments
+def comments(environ, user):
+    """ Display board feedback from previous reports ... """
+    project = environ.get('QUERY_STRING')
+    comments, cached = get_bat(BAT_COMMENTS, environ)
+
+    # Figure out the name as written in whimsy..
+    pname = guess_title(project)
+
+    # If we can access, fetch comments
+    if comments and pname in comments and has_access(user, project):
+        comments = comments[pname]
+    else:
+        comments = {}
+
+    js = {
+        "pid": project,
+        "pname": pname,
+        "comments": comments,
+        "used_cache": cached,
+    }
+    return js
+
+
+# /api/bat/publish
+def publish(environ, _user):
+    try:
+        request_body_size = int(environ.get('CONTENT_LENGTH', 0))
+    except (ValueError):
+        request_body_size = 0
+    if request_body_size:
+        request_body = environ['wsgi.input'].read(request_body_size)
+        try:
+            js = json.loads(request_body.decode('utf-8'))
+        except:
+            js = {}
+    if js:
+        agenda = js.get('agenda')
+        project = js.get('project')
+        report = js.get('report')
+        digest = js.get('digest')
+        attach = js.get('attach')
+        print(project, agenda)
+        # Kludge for webservices
+        project = pdata.ldapmap.get(project, project)
+        if agenda and project and report:
+            message = "Publishing report for %s via Reporter" % project
+            payload = {
+             'agenda': agenda,
+             'project': project,
+             'report': report,
+             'message': message,
+            }
+            if digest and attach:
+                del payload['project']
+                payload['attach'] = attach
+                payload['message'] = "Updating report for %s via Reporter." % 
project
+                payload['digest'] = digest
+            try:
+                rv = requests.post(BAT_SUBMIT, headers = {
+                    'Authorization': environ.get('HTTP_AUTHORIZATION'),
+                    "Content-Type": "application/json"
+                    }, json = payload, timeout = 10)
+                rv.raise_for_status()
+                return {'okay': True, 'message': "Posted to board agenda!"}
+            except:
+                pass
+    return {}

Modified: 
comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/drafts.py
==============================================================================
--- comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/drafts.py  
Sat Feb  7 17:33:40 2026        (r1931746)
+++ comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/drafts.py  
Sat Feb  7 17:34:22 2026        (r1931747)
@@ -7,13 +7,10 @@ import os
 import time
 import json
 import re
-import requests
 import pdata
-import rapp.whimsy
 
 DRAFTS_DIR = '/var/lib/rapp/drafts'
 EDITOR_TYPE = 'unified'
-WHIMSY_NOTIFY = 'https://whimsy.apache.org/board/agenda/json/reporter'
 
 if not os.path.isdir(DRAFTS_DIR):
     os.makedirs(DRAFTS_DIR, exist_ok = True)
@@ -98,15 +95,6 @@ def save(environ, user):
                 with open(os.path.join(DRAFTS_DIR, filename), "w") as f:
                     f.write(report)
                     f.close()
-                # Notify whimsy - this may fail, or not, we don't care :)
-                try:
-                    forgot = forgotten(environ, user)
-                    requests.post(WHIMSY_NOTIFY, headers = {
-                        'Authorization': environ.get('HTTP_AUTHORIZATION'),
-                        "Content-Type": "application/json"
-                        }, json = forgot, timeout = 2)
-                except:
-                    pass
                 return {
                     'okay': True,
                     'filename': filename,

Modified: 
comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/overview.py
==============================================================================
--- 
comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/overview.py    
    Sat Feb  7 17:33:40 2026        (r1931746)
+++ 
comdev/reporter.apache.org/branches/tooling-project/scripts/rapp/overview.py    
    Sat Feb  7 17:34:22 2026        (r1931747)
@@ -7,7 +7,7 @@ import pdata
 import time
 import re
 import committee_info
-import rapp.whimsy
+import rapp.bat
 from datetime import datetime
 
 CACHE_TIMEOUT = 14400
@@ -54,7 +54,7 @@ def run(environ, user):
         # Check for filed reports
         dumps['filed'] = {}
         for k in allpmcs:
-            f = rapp.whimsy.agenda(environ, user, k)
+            f = rapp.bat.agenda(environ, user, k)
             dumps['filed'][k] = f.get('filed', False)
         
         dumps['you'] = committers[user]

Modified: comdev/reporter.apache.org/branches/tooling-project/scripts/wsgi.py
==============================================================================
--- comdev/reporter.apache.org/branches/tooling-project/scripts/wsgi.py Sat Feb 
 7 17:33:40 2026        (r1931746)
+++ comdev/reporter.apache.org/branches/tooling-project/scripts/wsgi.py Sat Feb 
 7 17:34:22 2026        (r1931747)
@@ -9,15 +9,15 @@ import re
 CACHE_TIMEOUT = 14400
 
 import rapp.overview
-import rapp.whimsy
+import rapp.bat
 import rapp.drafts
 
 webmap = {
     '/api/overview': rapp.overview.run,
-    '/api/whimsy/comments': rapp.whimsy.comments,
-    '/api/whimsy/agenda': rapp.whimsy.agenda,
-    '/api/whimsy/agenda/refresh': rapp.whimsy.agenda_forced,
-    '/api/whimsy/publish': rapp.whimsy.publish,
+    '/api/bat/comments': rapp.bat.comments,
+    '/api/bat/agenda': rapp.bat.agenda,
+    '/api/bat/agenda/refresh': rapp.bat.agenda_forced,
+    '/api/bat/publish': rapp.bat.publish,
     '/api/drafts/index': rapp.drafts.index,
     '/api/drafts/save': rapp.drafts.save,
     '/api/drafts/fetch': rapp.drafts.fetch,

Modified: 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/drafts.js
==============================================================================
--- 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/drafts.js
 Sat Feb  7 17:33:40 2026        (r1931746)
+++ 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/drafts.js
 Sat Feb  7 17:34:22 2026        (r1931747)
@@ -157,7 +157,7 @@ function publish_report() {
     document.getElementById('wrapper').style.display = 'none';
     document.getElementById("pname").style.display = 'none';
     
-    POST('/api/whimsy/publish', report_published, {}, formdata);
+    POST('/api/bat/publish', report_published, {}, formdata);
 }
 
 function report_published(state, json) {
@@ -174,7 +174,7 @@ function report_published(state, json) {
   }
   
   // Force whimsy reload of report meta data
-  GET("/api/whimsy/agenda/refresh?%s".format(project), prime_meta, {noreset: 
true});
+  GET("/api/bat/agenda/refresh?%s".format(project), prime_meta, {noreset: 
true});
     
 }
 

Modified: 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/primer.js
==============================================================================
--- 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/primer.js
 Sat Feb  7 17:33:40 2026        (r1931746)
+++ 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/primer.js
 Sat Feb  7 17:34:22 2026        (r1931747)
@@ -27,7 +27,7 @@ function prime_wizard(state, json) {
     if (statsonly) {
         GET("/reportingcycles.json", prime_cycles, {});
     } else {
-        GET("/api/whimsy/agenda?%s".format(project), prime_meta, {})
+        GET("/api/bat/agenda?%s".format(project), prime_meta, {})
     }
 }
 

Modified: 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/unified.js
==============================================================================
--- 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/unified.js
        Sat Feb  7 17:33:40 2026        (r1931746)
+++ 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/source/unified.js
        Sat Feb  7 17:34:22 2026        (r1931747)
@@ -275,7 +275,7 @@ function UnifiedEditor_compile() {
     }
     text += "<br/><button class='btn btn-warning' onclick='save_draft();'>Save 
as draft</button>"
     if (!meta_data.found) {
-        text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Your project is not listed in the current agenda!'>Publish via 
Whimsy</button>";
+        text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Your project is not listed in the current agenda!'>Publish via the 
BAT</button>";
         // Is there a timestamp to check?
         let ts = 'timestamp' in meta_data ? meta_data.timestamp : 0
         if (ts > 0 && ts < new Date().valueOf()) {
@@ -284,8 +284,8 @@ function UnifiedEditor_compile() {
             text += "<br/><span style='color: maroon;'>Your project is not 
expected to report this month. You may save drafts but you cannot publish 
yet.</span>";        
         }
     }
-    else if (this.compiles) text += " &nbsp; &nbsp; <button 
onclick='publish_report();' class='btn btn-success'>Publish via Whimsy</button>"
-    else text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Please fix the above issues before you can publish'>Publish via 
Whimsy</button>"
+    else if (this.compiles) text += " &nbsp; &nbsp; <button 
onclick='publish_report();' class='btn btn-success'>Publish via the 
BAT</button>"
+    else text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Please fix the above issues before you can publish'>Publish via the 
BAT</button>"
     
     if (this.compiles) {
         text += "<hr/><h5>PLEASE MAKE SURE YOUR REPORT ADHERES TO THE <a 
target='_blank' 
href='https://www.apache.org/foundation/board/reporting#guidelines'>ASF 
REPORTING GUIDELINES</a> BEFORE SUBMITTING!</h5>";

Modified: 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/wizard.js
==============================================================================
--- 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/wizard.js    
    Sat Feb  7 17:33:40 2026        (r1931746)
+++ 
comdev/reporter.apache.org/branches/tooling-project/site/wizard/js/wizard.js    
    Sat Feb  7 17:34:22 2026        (r1931747)
@@ -1170,7 +1170,7 @@ function publish_report() {
     document.getElementById('wrapper').style.display = 'none';
     document.getElementById("pname").style.display = 'none';
     
-    POST('/api/whimsy/publish', report_published, {}, formdata);
+    POST('/api/bat/publish', report_published, {}, formdata);
 }
 
 function report_published(state, json) {
@@ -1187,7 +1187,7 @@ function report_published(state, json) {
   }
   
   // Force whimsy reload of report meta data
-  GET("/api/whimsy/agenda/refresh?%s".format(project), prime_meta, {noreset: 
true});
+  GET("/api/bat/agenda/refresh?%s".format(project), prime_meta, {noreset: 
true});
     
 }
 
@@ -1662,7 +1662,7 @@ function prime_wizard(state, json) {
     if (statsonly) {
         GET("/reportingcycles.json", prime_cycles, {});
     } else {
-        GET("/api/whimsy/agenda?%s".format(project), prime_meta, {})
+        GET("/api/bat/agenda?%s".format(project), prime_meta, {})
     }
 }
 
@@ -2695,7 +2695,7 @@ function UnifiedEditor_compile() {
     }
     text += "<br/><button class='btn btn-warning' onclick='save_draft();'>Save 
as draft</button>"
     if (!meta_data.found) {
-        text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Your project is not listed in the current agenda!'>Publish via 
Whimsy</button>";
+        text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Your project is not listed in the current agenda!'>Publish via the 
BAT</button>";
         // Is there a timestamp to check?
         let ts = 'timestamp' in meta_data ? meta_data.timestamp : 0
         if (ts > 0 && ts < new Date().valueOf()) {
@@ -2704,8 +2704,8 @@ function UnifiedEditor_compile() {
             text += "<br/><span style='color: maroon;'>Your project is not 
expected to report this month. You may save drafts but you cannot publish 
yet.</span>";        
         }
     }
-    else if (this.compiles) text += " &nbsp; &nbsp; <button 
onclick='publish_report();' class='btn btn-success'>Publish via Whimsy</button>"
-    else text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Please fix the above issues before you can publish'>Publish via 
Whimsy</button>"
+    else if (this.compiles) text += " &nbsp; &nbsp; <button 
onclick='publish_report();' class='btn btn-success'>Publish via the 
BAT</button>"
+    else text += " &nbsp; &nbsp; <button class='btn btn-secondary' disabled 
title='Please fix the above issues before you can publish'>Publish via the 
BAT</button>"
     
     if (this.compiles) {
         text += "<hr/><h5>PLEASE MAKE SURE YOUR REPORT ADHERES TO THE <a 
target='_blank' 
href='https://www.apache.org/foundation/board/reporting#guidelines'>ASF 
REPORTING GUIDELINES</a> BEFORE SUBMITTING!</h5>";

Reply via email to