Collapsible dashboard sections and clean up BoA audit log (#22)

* Make dashboard sections collapsible and clean up BoA audit log

- Scheduled Checks, Scheduled ACH, and Outstanding Checks sections
  default to collapsed with summary line; Expand/Collapse button
  toggles detail view via private_metadata state
- Filter backfill failure records from BoA submissions display
- Limit Recent BoA Submissions to 5 most recent entries

* Share Slack home publish pipeline

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
This commit is contained in:
Adam Moussa 2026-05-08 11:24:32 -04:00 • committed by GitHub
parent 491d29de1e
commit b7feb4a914
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -134,7 +134,7 @@ export const handler = async (event) => {
return { statusCode: 200, body: body.challenge }; return { statusCode: 200, body: body.challenge };
} }
// Handle button clicks → open modal // Handle button clicks → toggle sections or open modal
if (body.type === "block_actions") { if (body.type === "block_actions") {
return handleBlockAction(body); return handleBlockAction(body);
} }
@ -146,6 +146,10 @@ export const handler = async (event) => {
const userId = body.event.user; const userId = body.event.user;
return publishHomeView(userId, []);
};
async function publishHomeView(userId, expanded) {
const { Item: metadata } = await ddb.send( const { Item: metadata } = await ddb.send(
new GetCommand({ TableName: TABLE_NAME, Key: { pk: "metadata" } }) new GetCommand({ TableName: TABLE_NAME, Key: { pk: "metadata" } })
); );
@ -159,7 +163,7 @@ export const handler = async (event) => {
return { statusCode: 200, body: JSON.stringify({ error: "No payment data" }) }; return { statusCode: 200, body: JSON.stringify({ error: "No payment data" }) };
} }
const view = buildHomeView(payments, metadata, boaTransactions); const view = buildHomeView(payments, metadata, boaTransactions, expanded);
const slackToken = await getSlackToken(); const slackToken = await getSlackToken();
const res = await fetch("https://slack.com/api/views.publish", { const res = await fetch("https://slack.com/api/views.publish", {
@ -179,7 +183,7 @@ export const handler = async (event) => {
} }
return { statusCode: 200, body: JSON.stringify({ ok: true }) }; return { statusCode: 200, body: JSON.stringify({ ok: true }) };
}; }
// --- Block action handler (button clicks) --- // --- Block action handler (button clicks) ---
@ -190,6 +194,19 @@ async function handleBlockAction(body) {
const triggerId = body.trigger_id; const triggerId = body.trigger_id;
const actionId = action.action_id; const actionId = action.action_id;
// Toggle section expand/collapse → re-publish home view
if (actionId.startsWith("toggle_section_")) {
const section = actionId.replace("toggle_section_", "");
const meta = JSON.parse(body.view?.private_metadata || "{}");
const expanded = Array.isArray(meta.expanded) ? [...meta.expanded] : [];
const idx = expanded.indexOf(section);
if (idx >= 0) expanded.splice(idx, 1);
else expanded.push(section);
const userId = body.user?.id;
return publishHomeView(userId, expanded);
}
const payments = await scanPayments(); const payments = await scanPayments();
const { today, daysSince, scheduledChecks, scheduledACH, bucketedOutstanding } = categorizePayments(payments); const { today, daysSince, scheduledChecks, scheduledACH, bucketedOutstanding } = categorizePayments(payments);
@ -302,8 +319,9 @@ async function scanBoATransactions() {
items.push(...result.Items); items.push(...result.Items);
lastKey = result.LastEvaluatedKey; lastKey = result.LastEvaluatedKey;
} while (lastKey); } while (lastKey);
items.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")); const filtered = items.filter((t) => !(t.backfill && !t.success));
return items.slice(0, 10); filtered.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
return filtered.slice(0, 5);
} }
// --- Scan all payments from DynamoDB --- // --- Scan all payments from DynamoDB ---
@ -354,7 +372,7 @@ function buildBoABlocks(transactions) {
{ {
type: "mrkdwn", type: "mrkdwn",
text: transactions.length text: transactions.length
? `Last ${transactions.length} submission${transactions.length !== 1 ? "s" : ""} · retained 90 days` ? `Last ${transactions.length} submission${transactions.length !== 1 ? "s" : ""} · 90-day retention`
: "No submissions in the last 90 days", : "No submissions in the last 90 days",
}, },
], ],
@ -393,28 +411,34 @@ function buildBoABlocks(transactions) {
return blocks; return blocks;
} }
function buildHomeView(payments, metadata, boaTransactions = []) { function buildHomeView(payments, metadata, boaTransactions = [], expanded = []) {
const { today, daysSince, scheduledChecks, scheduledACH, outstandingChecks, bucketedOutstanding } = categorizePayments(payments); const { today, daysSince, scheduledChecks, scheduledACH, outstandingChecks, bucketedOutstanding } = categorizePayments(payments);
const isExpanded = (key) => expanded.includes(key);
const buildScheduledBlocks = (title, emoji, items, methodKey) => { const buildScheduledBlocks = (title, emoji, items, sectionKey, methodKey) => {
const total = items.reduce((sum, p) => sum + p.amount_usd, 0); const total = items.reduce((sum, p) => sum + p.amount_usd, 0);
const open = isExpanded(sectionKey);
const blocks = [ const blocks = [
{ {
type: "header", type: "section",
text: { type: "plain_text", text: `${emoji} ${title}` }, text: {
},
{
type: "context",
elements: [
{
type: "mrkdwn", type: "mrkdwn",
text: `${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)} total`, text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} ${emoji} *${title}* · ${items.length} payment${items.length !== 1 ? "s" : ""} · ${formatCurrency(total)}`,
},
accessory: {
type: "button",
text: { type: "plain_text", text: open ? "Collapse" : "Expand" },
action_id: `toggle_section_${sectionKey}`,
}, },
],
}, },
]; ];
if (!open) {
blocks.push({ type: "divider" });
return blocks;
}
if (!items.length) { if (!items.length) {
blocks.push({ blocks.push({
type: "section", type: "section",
@ -460,23 +484,28 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
const buildOutstandingBlocks = () => { const buildOutstandingBlocks = () => {
const totalAll = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0); const totalAll = outstandingChecks.reduce((sum, p) => sum + p.amount_usd, 0);
const open = isExpanded("outstanding");
const blocks = [ const blocks = [
{ {
type: "header", type: "section",
text: { type: "plain_text", text: ":warning: Outstanding Checks" }, text: {
},
{
type: "context",
elements: [
{
type: "mrkdwn", type: "mrkdwn",
text: `${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)} total`, text: `${open ? ":large_orange_diamond:" : ":small_orange_diamond:"} :warning: *Outstanding Checks* · ${outstandingChecks.length} check${outstandingChecks.length !== 1 ? "s" : ""} · ${formatCurrency(totalAll)}`,
},
accessory: {
type: "button",
text: { type: "plain_text", text: open ? "Collapse" : "Expand" },
action_id: "toggle_section_outstanding",
}, },
],
}, },
]; ];
if (!open) {
blocks.push({ type: "divider" });
return blocks;
}
if (!outstandingChecks.length) { if (!outstandingChecks.length) {
blocks.push({ blocks.push({
type: "section", type: "section",
@ -517,6 +546,7 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
return { return {
type: "home", type: "home",
private_metadata: JSON.stringify({ expanded }),
blocks: [ blocks: [
{ {
type: "header", type: "header",
@ -555,8 +585,8 @@ function buildHomeView(payments, metadata, boaTransactions = []) {
], ],
}, },
{ type: "divider" }, { type: "divider" },
...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "checks"), ...buildScheduledBlocks("Scheduled Checks", ":ledger:", scheduledChecks, "scheduled_checks", "checks"),
...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "ach"), ...buildScheduledBlocks("Scheduled ACH", ":electric_plug:", scheduledACH, "scheduled_ach", "ach"),
...buildOutstandingBlocks(), ...buildOutstandingBlocks(),
...buildBoABlocks(boaTransactions), ...buildBoABlocks(boaTransactions),
], ],