Skip to content

Commit fbd24be

Browse files
committed
feat: Link exported pages back to the tracked project
1 parent ed9e913 commit fbd24be

11 files changed

Lines changed: 208 additions & 65 deletions

File tree

README.org

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -181,21 +181,24 @@ exactly one of the three type keys:
181181
| =:alias= | Email alias | =X-Original-To=, =Envelope-To=, =X-Envelope-To=, or =Delivered-To= |
182182
| =:to= | Mailbox | =Delivered-To= header |
183183

184-
| Field | Required | Description |
185-
|------------------------+----------+----------------------------------------------------------------------|
186-
| =:name= | yes | Unique source identifier |
187-
| =:list= / =:alias= / =:to= | yes | Exactly one; determines source type and matched header |
188-
| =:commands= | no | Command keyword overrides (per action) |
189-
| =:labels= | no | Report label overrides (per report type) |
190-
| =:maintainers= | no | Initial maintainers; first entry is the lead (see below) |
191-
| =:list-archive= | no | URL of the list archive (shown in HTML reports) |
192-
| =:archive-format-string= | no | URL template for message-ids (=%s= is replaced) |
193-
| =:base-url= | no | Base URL where this source's public directory is served |
194-
| =:report-types= | no | Restrict detected and exported types, e.g. =#{:bug :patch}= |
195-
| =:export-formats= | no | Output formats (default: =["json" "org" "rss"]=) |
196-
| =:expiry= | no | Auto-close delays in days per type |
197-
| =:notifications= | no | ={:enabled false}= silences notifications for this source |
198-
| =:awaiting-delay= | no | Duration before "awaiting reply" flag, e.g. ="14d"= (default: 14 days) |
184+
| Field | Required | Description |
185+
|------------------------+----------+-----------------------------------------------------------------------|
186+
| =:name= | yes | Unique source identifier |
187+
| =:list= / =:alias= / =:to= | yes | Exactly one; determines source type and matched header |
188+
| =:commands= | no | Command keyword overrides (per action) |
189+
| =:labels= | no | Report label overrides (per report type) |
190+
| =:maintainers= | no | Initial maintainers; first entry is the lead (see below) |
191+
| =:list-archive= | no | URL of the list archive (shown in HTML reports) |
192+
| =:archive-format-string= | no | URL template for message-ids (=%s= is replaced) |
193+
| =:base-url= | no | Base URL where this source's public directory is served |
194+
| =:website= | no | Project website, linked from page titles and footers |
195+
| =:contribute-url= | no | Contribution guide; drives the "Create new" button and a footer link |
196+
| =:post-address= | no | Posting address shown on =docs.html= (mailbox sources fall back to =:to=) |
197+
| =:report-types= | no | Restrict detected and exported types, e.g. =#{:bug :patch}= |
198+
| =:export-formats= | no | Output formats (default: =["json" "org" "rss"]=) |
199+
| =:expiry= | no | Auto-close delays in days per type |
200+
| =:notifications= | no | ={:enabled false}= silences notifications for this source |
201+
| =:awaiting-delay= | no | Duration before "awaiting reply" flag, e.g. ="14d"= (default: 14 days) |
199202

200203
Send all state-changing actions (commands, role controls) through
201204
the source's public channel. Anyone running BONE on the same source

config.edn.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@
5555
:list-archive "https://list.example.org"
5656
:archive-format-string "https://list.example.org/%s"
5757
;;
58+
;; Project links surfaced on the exported pages (all
59+
;; optional): :website links the source name in the page
60+
;; titles and the footer; :contribute-url drives the
61+
;; "Create new" button on the reports page and a footer
62+
;; link; :post-address is the address shown on docs.html
63+
;; for creating reports (mailbox sources fall back to :to).
64+
;;
65+
;; :website "https://project.example.org"
66+
;; :contribute-url "https://project.example.org/contribute.html"
67+
;; :post-address "bugs@example.org"
68+
;;
5869
;; Maintainers seed initial tenures (plain list of email
5970
;; strings). The FIRST entry is the lead (can remove others,
6071
;; cannot self-remove). After startup, "Add/Remove maintainer:"

docs/bone-manual.org

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,8 @@ copy of the list, download it, replace the placeholder =:mailboxes= with
896896
account), and run =bb export=.
897897

898898
The companion =reports/meta.json= carries the source /identity/ -- =:list= /
899-
=:alias= / =:to=, =:list-archive=, =:archive-format-string= and =:base-url=.
899+
=:alias= / =:to=, =:list-archive=, =:archive-format-string= and =:base-url= --
900+
plus the project links (=:website=, =:contribute-url=) when configured.
900901
Tools such as [[https://codeberg.org/bzg/gnaw][gnaw]] use it to bind a published source to a local
901902
mailbox: the path lives in your local tool config, =meta.json=
902903
supplies the stable key (the =List-Id=) to match against.
@@ -1048,6 +1049,9 @@ Emails that match no source are discarded at ingestion.
10481049
| =:archive-format-string= | URL template for message-ids (=%s= is replaced), used when the |
10491050
| | =Archived-At= header is absent |
10501051
| =:base-url= | Base URL where this source's public directory is served |
1052+
| =:website= | Project website: links the source name in page titles and the footer |
1053+
| =:contribute-url= | Contribution guide: "Create new" button on the reports page, plus a footer link |
1054+
| =:post-address= | Posting address shown on =docs.html= (mailbox sources fall back to =:to=) |
10511055
| =:labels= | Per-source label overrides (see [[#customising-subject-labels][customising subject labels]]) |
10521056
| =:commands= | Per-source command overrides (see [[#customising-commands][customising commands]]) |
10531057
| =:report-types= | Restrict detected and exported types, e.g. =#{:bug :patch}= |

resources/bone-index.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -877,9 +877,31 @@ function replaceURL() { history.replaceState(null, '', buildURL()); }
877877

878878
/* ── Button handlers ─────────────────────────────────────────── */
879879

880+
// Mirror the visual 'outline' (inactive) class into aria-pressed so
881+
// the toggle buttons expose their state to assistive tech.
882+
function syncPressed(btn) {
883+
btn.setAttribute('aria-pressed', String(!btn.classList.contains('outline')));
884+
}
885+
886+
// Set a toolbar toggle's active state (visual class + ARIA) by id.
887+
function setToggleState(id, active) {
888+
var btn = document.getElementById(id);
889+
btn.classList.toggle('outline', !active);
890+
syncPressed(btn);
891+
}
892+
893+
// Keyboard activation for the sortable column headers (tabindex=0).
894+
function thSortKey(e) {
895+
if (e.key === 'Enter' || e.key === ' ') {
896+
e.preventDefault();
897+
e.currentTarget.click();
898+
}
899+
}
900+
880901
function toggleType(type, btn) {
881902
activeTypes[type] = !activeTypes[type];
882903
btn.classList.toggle('outline');
904+
syncPressed(btn);
883905
currentPage = 1;
884906
filterReports();
885907
pushURL();
@@ -888,6 +910,7 @@ function toggleType(type, btn) {
888910
function syncToolbarButtons() {
889911
document.querySelectorAll('.filters button[data-type]').forEach(function(btn) {
890912
btn.classList.toggle('outline', !activeTypes[btn.getAttribute('data-type')]);
913+
syncPressed(btn);
891914
});
892915
}
893916

@@ -903,6 +926,7 @@ function buildTypeFilters() {
903926
btn.setAttribute('data-type', t);
904927
btn.textContent = _typeLabels[t] || t;
905928
btn.addEventListener('click', function() { toggleType(t, btn); });
929+
syncPressed(btn);
906930
container.appendChild(btn);
907931
});
908932
}
@@ -923,6 +947,7 @@ function isolateType(type) {
923947
function toggleAcked(btn) {
924948
onlyAcked = !onlyAcked;
925949
btn.classList.toggle('outline');
950+
syncPressed(btn);
926951
currentPage = 1;
927952
filterReports();
928953
pushURL();
@@ -931,6 +956,7 @@ function toggleAcked(btn) {
931956
function toggleOwned(btn) {
932957
onlyOwned = !onlyOwned;
933958
btn.classList.toggle('outline');
959+
syncPressed(btn);
934960
currentPage = 1;
935961
filterReports();
936962
pushURL();
@@ -939,6 +965,7 @@ function toggleOwned(btn) {
939965
function toggleAwaiting(btn) {
940966
onlyAwaiting = !onlyAwaiting;
941967
btn.classList.toggle('outline');
968+
syncPressed(btn);
942969
currentPage = 1;
943970
filterReports();
944971
pushURL();
@@ -947,6 +974,7 @@ function toggleAwaiting(btn) {
947974
function toggleOpen(btn) {
948975
onlyOpen = !onlyOpen;
949976
btn.classList.toggle('outline');
977+
syncPressed(btn);
950978
currentPage = 1;
951979
if (!onlyOpen && !closedLoaded) {
952980
loadClosedReports(function() { filterReports(); pushURL(); });
@@ -994,8 +1022,11 @@ function sortTable(colIdx, key) {
9941022
sortState[key] = dir;
9951023
document.querySelectorAll('th[data-sort]').forEach(function(th) {
9961024
th.classList.remove('asc', 'desc');
1025+
th.removeAttribute('aria-sort');
9971026
});
998-
document.querySelector('th[data-sort="' + key + '"]').classList.add(dir);
1027+
var th = document.querySelector('th[data-sort="' + key + '"]');
1028+
th.classList.add(dir);
1029+
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
9991030
sortReports(key, dir);
10001031
buildDisplayList();
10011032
currentPage = 1;
@@ -1034,6 +1065,7 @@ function restoreFromURL() {
10341065
}
10351066
document.querySelectorAll('.filters button[data-type]').forEach(function(btn) {
10361067
btn.classList.toggle('outline', !activeTypes[btn.dataset.type]);
1068+
syncPressed(btn);
10371069
});
10381070

10391071
// If the search query bypasses the Open filter via 'closed:true', any
@@ -1042,19 +1074,20 @@ function restoreFromURL() {
10421074
// Open-only filtering automatically.
10431075
var queryHasClosed = queryIncludesClosed(qVal);
10441076
onlyOpen = queryHasClosed ? true : (params.get('open') !== '0');
1045-
document.getElementById('btn-open').classList.toggle('outline', !onlyOpen);
1077+
setToggleState('btn-open', onlyOpen);
10461078

10471079
onlyAcked = params.get('acked') === '1';
1048-
document.getElementById('btn-acked').classList.toggle('outline', !onlyAcked);
1080+
setToggleState('btn-acked', onlyAcked);
10491081

10501082
onlyOwned = params.get('owned') === '1';
1051-
document.getElementById('btn-owned').classList.toggle('outline', !onlyOwned);
1083+
setToggleState('btn-owned', onlyOwned);
10521084

10531085
onlyAwaiting = params.get('awaiting') === '1';
1054-
document.getElementById('btn-awaiting').classList.toggle('outline', !onlyAwaiting);
1086+
setToggleState('btn-awaiting', onlyAwaiting);
10551087

10561088
document.querySelectorAll('th[data-sort]').forEach(function(th) {
10571089
th.classList.remove('asc', 'desc');
1090+
th.removeAttribute('aria-sort');
10581091
});
10591092
sortState = {};
10601093
if (params.has('sort') && params.has('dir')) {
@@ -1064,6 +1097,7 @@ function restoreFromURL() {
10641097
if (th && (dir === 'asc' || dir === 'desc')) {
10651098
sortState[key] = dir;
10661099
th.classList.add(dir);
1100+
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
10671101
}
10681102
} else if (boneConfig.columnsSort) {
10691103
// Default sort from --html-columns-sort (date -> desc, else asc).
@@ -1073,6 +1107,7 @@ function restoreFromURL() {
10731107
if (dth) {
10741108
sortState[dkey] = ddir;
10751109
dth.classList.add(ddir);
1110+
dth.setAttribute('aria-sort', ddir === 'asc' ? 'ascending' : 'descending');
10761111
}
10771112
}
10781113

@@ -1108,6 +1143,10 @@ var _setupToggles, _showTogglesIfNeeded;
11081143
toggle.className = 'unfold';
11091144
toggle.textContent = '\u2026';
11101145
toggle.style.display = 'none';
1146+
// Focusable and key-activatable: the ellipsis acts as a button.
1147+
toggle.setAttribute('role', 'button');
1148+
toggle.setAttribute('tabindex', '0');
1149+
toggle.setAttribute('aria-label', 'Show full subject');
11111150
toggle.onmousedown = function(e) {
11121151
e.preventDefault();
11131152
e.stopPropagation();
@@ -1118,6 +1157,9 @@ var _setupToggles, _showTogglesIfNeeded;
11181157
td.classList.add('expanded');
11191158
toggle.remove();
11201159
};
1160+
toggle.onkeydown = function(e) {
1161+
if (e.key === 'Enter' || e.key === ' ') toggle.onclick(e);
1162+
};
11211163
td.appendChild(toggle);
11221164
});
11231165
};

resources/docs-tpl.org

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Replies in the same thread update each report through a small fixed
77
vocabulary of commands. Maintainers can also write to the inbox
88
directly to manage roles and receive periodic digests.
99

10+
[source-links]
11+
1012
BONE comes with two companions to help contributors handle reports: a
1113
command line interface [[https://codeberg.org/bzg/gnaw][gnaw]] and a GNU Emacs library [[https://codeberg.org/bzg/gnaw.el][gnaw.el]].
1214

scripts/bone-docs.clj

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,22 @@
189189
(str/replace org-text "[version]" version)
190190
(str/replace org-text " [version]" "")))
191191

192+
(defn- substitute-source-links
193+
"Replace the [source-links] placeholder with the per-source contact
194+
paragraph: the posting address (:post-address, falling back to :to
195+
for mailbox sources) and the :contribute-url page. Dropped when the
196+
source configures neither."
197+
[org-text {:keys [post-address to contribute-url]}]
198+
(let [addr (or post-address to)
199+
parts (cond-> []
200+
addr
201+
(conj (str "Reports are created and updated by emailing "
202+
"[[mailto:" addr "][" addr "]]."))
203+
contribute-url
204+
(conj (str "New to the project? See [["
205+
contribute-url "][how to contribute]].")))]
206+
(str/replace org-text "[source-links]" (str/join " " parts))))
207+
192208
;; ---------------------------------------------------------------------------
193209
;; Minimal org -> HTML conversion
194210
;; ---------------------------------------------------------------------------
@@ -291,8 +307,8 @@
291307
.meta { font-size: 0.78rem; color: var(--pico-muted-color); margin-bottom: 2rem; }
292308
" footer-css))
293309

294-
(defn docs-page [body-html {:keys [ical]}]
295-
(let [title "BONE -- Docs"
310+
(defn docs-page [body-html {:keys [ical website contribute-url]}]
311+
(let [title "BONE - Docs"
296312
generated-at (str (java.util.Date.))]
297313
(str
298314
"<!DOCTYPE html>\n"
@@ -319,7 +335,9 @@
319335
[:p.meta (str "Generated " generated-at)]
320336
(h/raw body-html)
321337
[:script (h/raw (wrap-js theme-toggle-js))]]
322-
(bone-footer {:ical ical})]]))))
338+
(bone-footer {:ical ical
339+
:website website
340+
:contribute-url contribute-url})]]))))
323341

324342
;; ---------------------------------------------------------------------------
325343
;; Filter feed links in "Getting the data" table
@@ -488,14 +506,17 @@
488506
config-html (build-configuration-html config source-name)
489507
org-text (-> (slurp "resources/docs-tpl.org")
490508
(substitute-version (bone-version))
509+
(substitute-source-links source-cfg)
491510
(substitute-template labels cmds prefix)
492511
(filter-feed-links effective-dir))
493512
body-html (cond-> (org->html org-text)
494513
maint-html (str "\n" maint-html)
495514
config-html (str "\n" config-html))
496515
has-ical? (.exists (io/file effective-dir "events"
497516
"announcements.ics"))]
498-
(docs-page body-html {:ical has-ical?}))
517+
(docs-page body-html {:ical has-ical?
518+
:website (:website source-cfg)
519+
:contribute-url (:contribute-url source-cfg)}))
499520
(finally
500521
((resolve 'pod.huahaiy.datalevin/close) conn)))]
501522
(io/make-parents out-file)

scripts/bone-export.clj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,9 @@
801801
(:to cfg) (assoc :to (:to cfg))
802802
(:list-archive cfg) (assoc :list-archive (:list-archive cfg))
803803
(:archive-format-string cfg) (assoc :archive-format-string (:archive-format-string cfg))
804-
(:base-url cfg) (assoc :base-url (:base-url cfg)))))
804+
(:base-url cfg) (assoc :base-url (:base-url cfg))
805+
(:website cfg) (assoc :website (:website cfg))
806+
(:contribute-url cfg) (assoc :contribute-url (:contribute-url cfg)))))
805807

806808
;; ---------------------------------------------------------------------------
807809
;; XML helpers
@@ -1596,7 +1598,7 @@
15961598
page
15971599
(str
15981600
"<!DOCTYPE html>\n<html lang=\"en\">\n"
1599-
(html-head {:title "BONE -- Sources"
1601+
(html-head {:title "BONE - Sources"
16001602
:css (str "table{margin-top:1.5rem}"
16011603
"td.num,th.num{text-align:right}"
16021604
"a.archive{font-size:0.82rem;margin-left:0.4rem;opacity:0.7}"
@@ -1605,7 +1607,7 @@
16051607
footer-css)})
16061608
"<body>\n<main class=\"container\">\n"
16071609
(h/html (nav-bar "BONE" nil))
1608-
"\n<table role=\"grid\">\n"
1610+
"\n<table>\n"
16091611
"<thead><tr>"
16101612
"<th>Source</th>"
16111613
"<th class=\"num\">Open</th>"

0 commit comments

Comments
 (0)