merchant landing: recent activity split GOA/CHF, 5 each
Stats script emits recent_activity_by_currency (limit 5); UI shows side-by-side columns instead of one long mixed list.
This commit is contained in:
parent
f22158e369
commit
458472e177
2 changed files with 183 additions and 82 deletions
|
|
@ -181,21 +181,47 @@
|
||||||
}
|
}
|
||||||
.cur-panel .amt-secondary strong { color: var(--val); font-weight: 700; }
|
.cur-panel .amt-secondary strong { color: var(--val); font-weight: 700; }
|
||||||
|
|
||||||
/* Recent activity */
|
/* Recent activity — split by currency */
|
||||||
|
.act-pair {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.act-pair { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.act-col {
|
||||||
|
background: rgba(24, 18, 40, 0.45);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.22);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.55rem 0.55rem 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.act-col-h {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--goa);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.act-col.chf .act-col-h { color: var(--chf); }
|
||||||
.act-list { list-style: none; margin: 0; padding: 0; }
|
.act-list { list-style: none; margin: 0; padding: 0; }
|
||||||
.act-item {
|
.act-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
gap: 0.35rem 0.65rem;
|
gap: 0.25rem 0.45rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.55rem 0.35rem;
|
padding: 0.4rem 0.2rem;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
font-size: 0.88rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
.act-item:last-child { border-bottom: 0; }
|
.act-item:last-child { border-bottom: 0; }
|
||||||
.act-kind {
|
.act-kind {
|
||||||
font-size: 0.65rem; font-weight: 800; letter-spacing: 0.06em;
|
font-size: 0.58rem; font-weight: 800; letter-spacing: 0.05em;
|
||||||
text-transform: uppercase; padding: 0.18rem 0.4rem; border-radius: 6px;
|
text-transform: uppercase; padding: 0.14rem 0.32rem; border-radius: 6px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
.act-kind.payment { color: #14532d; background: rgba(134, 239, 172, 0.85); }
|
.act-kind.payment { color: #14532d; background: rgba(134, 239, 172, 0.85); }
|
||||||
|
|
@ -204,17 +230,17 @@
|
||||||
.act-main { min-width: 0; }
|
.act-main { min-width: 0; }
|
||||||
.act-amt {
|
.act-amt {
|
||||||
font-weight: 800; font-variant-numeric: tabular-nums; color: var(--val);
|
font-weight: 800; font-variant-numeric: tabular-nums; color: var(--val);
|
||||||
font-size: 0.95rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
.act-sum {
|
.act-sum {
|
||||||
color: var(--muted); font-size: 0.78rem; white-space: nowrap;
|
color: var(--muted); font-size: 0.72rem; white-space: nowrap;
|
||||||
overflow: hidden; text-overflow: ellipsis; max-width: 100%;
|
overflow: hidden; text-overflow: ellipsis; max-width: 100%;
|
||||||
}
|
}
|
||||||
.act-meta {
|
.act-meta {
|
||||||
text-align: right; font-size: 0.68rem; color: var(--muted);
|
text-align: right; font-size: 0.62rem; color: var(--muted);
|
||||||
font-variant-numeric: tabular-nums; white-space: nowrap;
|
font-variant-numeric: tabular-nums; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.empty { text-align: center; color: var(--muted); font-size: 0.85rem; padding: 0.5rem; }
|
.empty { text-align: center; color: var(--muted); font-size: 0.8rem; padding: 0.45rem 0.2rem; }
|
||||||
|
|
||||||
.stats-foot {
|
.stats-foot {
|
||||||
margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center;
|
margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center;
|
||||||
|
|
@ -509,10 +535,21 @@
|
||||||
|
|
||||||
<section class="stats" id="activity" aria-labelledby="act-title">
|
<section class="stats" id="activity" aria-labelledby="act-title">
|
||||||
<h2 id="act-title">Recent activity</h2>
|
<h2 id="act-title">Recent activity</h2>
|
||||||
<p class="note">Latest payments & refunds across instances</p>
|
<p class="note">Latest 5 payments & refunds per currency</p>
|
||||||
<ul class="act-list" id="st-activity">
|
<div class="act-pair" id="st-activity-pair">
|
||||||
|
<div class="act-col goa" aria-labelledby="act-goa-h">
|
||||||
|
<h3 class="act-col-h" id="act-goa-h">GOA</h3>
|
||||||
|
<ul class="act-list" id="st-activity-goa">
|
||||||
<li class="empty">Loading…</li>
|
<li class="empty">Loading…</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="act-col chf" aria-labelledby="act-chf-h">
|
||||||
|
<h3 class="act-col-h" id="act-chf-h">CHF</h3>
|
||||||
|
<ul class="act-list" id="st-activity-chf">
|
||||||
|
<li class="empty">Loading…</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="step-card">
|
<section class="step-card">
|
||||||
|
|
@ -637,12 +674,14 @@
|
||||||
fillCurrency("goa", by.GOA);
|
fillCurrency("goa", by.GOA);
|
||||||
fillCurrency("chf", by.CHF);
|
fillCurrency("chf", by.CHF);
|
||||||
|
|
||||||
var list = document.getElementById("st-activity");
|
function fillActivityList(el, acts) {
|
||||||
list.innerHTML = "";
|
if (!el) return;
|
||||||
var acts = d.recent_activity || [];
|
el.innerHTML = "";
|
||||||
|
acts = acts || [];
|
||||||
if (!acts.length) {
|
if (!acts.length) {
|
||||||
list.innerHTML = '<li class="empty">No recent payments or refunds</li>';
|
el.innerHTML = '<li class="empty">No recent payments or refunds</li>';
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
acts.forEach(function (a) {
|
acts.forEach(function (a) {
|
||||||
var kind = (a.kind || "other").toLowerCase();
|
var kind = (a.kind || "other").toLowerCase();
|
||||||
var li = document.createElement("li");
|
var li = document.createElement("li");
|
||||||
|
|
@ -659,9 +698,27 @@
|
||||||
'<div class="act-meta">' +
|
'<div class="act-meta">' +
|
||||||
"<span>" + esc(a.ts_human || a.ts || "") + "</span>" +
|
"<span>" + esc(a.ts_human || a.ts || "") + "</span>" +
|
||||||
"</div>";
|
"</div>";
|
||||||
list.appendChild(li);
|
el.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Prefer split-by-currency (≤5 each); fall back to flat list split by amount prefix
|
||||||
|
var byCur = {};
|
||||||
|
(d.recent_activity_by_currency || []).forEach(function (block) {
|
||||||
|
byCur[(block.currency || "").toUpperCase()] = block.items || [];
|
||||||
|
});
|
||||||
|
if (!Object.keys(byCur).length && (d.recent_activity || []).length) {
|
||||||
|
byCur.GOA = [];
|
||||||
|
byCur.CHF = [];
|
||||||
|
(d.recent_activity || []).forEach(function (a) {
|
||||||
|
var cur = String(a.amount || "").split(":")[0].toUpperCase();
|
||||||
|
if (cur === "GOA") byCur.GOA.push(a);
|
||||||
|
else if (cur === "CHF") byCur.CHF.push(a);
|
||||||
|
});
|
||||||
|
byCur.GOA = byCur.GOA.slice(0, 5);
|
||||||
|
byCur.CHF = byCur.CHF.slice(0, 5);
|
||||||
|
}
|
||||||
|
fillActivityList(document.getElementById("st-activity-goa"), byCur.GOA);
|
||||||
|
fillActivityList(document.getElementById("st-activity-chf"), byCur.CHF);
|
||||||
|
|
||||||
if (foot) {
|
if (foot) {
|
||||||
foot.textContent = (d.dual_currency ? "Dual-currency · " : "") +
|
foot.textContent = (d.dual_currency ? "Dual-currency · " : "") +
|
||||||
|
|
@ -725,9 +782,11 @@
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (foot) foot.innerHTML = 'Stats offline · <a href="https://bank.hacktivism.ch/intro/" style="color:#e8c878">bank.hacktivism.ch</a>';
|
if (foot) foot.innerHTML = 'Stats offline · <a href="https://bank.hacktivism.ch/intro/" style="color:#e8c878">bank.hacktivism.ch</a>';
|
||||||
var list = document.getElementById("st-activity");
|
["st-activity-goa", "st-activity-chf"].forEach(function (id) {
|
||||||
|
var list = document.getElementById(id);
|
||||||
if (list) list.innerHTML = '<li class="empty">Stats offline</li>';
|
if (list) list.innerHTML = '<li class="empty">Stats offline</li>';
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -206,12 +206,16 @@ for item in $SCHEMAS; do
|
||||||
REFUNDS=$((REFUNDS + r))
|
REFUNDS=$((REFUNDS + r))
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- recent activity ---
|
# --- recent activity: 5 latest events per currency (GOA + CHF), payments + refunds ---
|
||||||
PAY_LIMIT=10
|
ACT_PER_CURRENCY="${ACT_PER_CURRENCY:-5}"
|
||||||
REF_LIMIT=5
|
|
||||||
{
|
# Query latest ACT_PER_CURRENCY events for one currency code (payments ∪ refunds).
|
||||||
|
# Writes TSV rows: ts kind order_id amount summary status
|
||||||
|
query_activity_for_currency() {
|
||||||
|
local cur="$1"
|
||||||
|
local out="$2"
|
||||||
|
{
|
||||||
echo "SELECT * FROM ("
|
echo "SELECT * FROM ("
|
||||||
echo "("
|
|
||||||
echo "SELECT * FROM ("
|
echo "SELECT * FROM ("
|
||||||
first=1
|
first=1
|
||||||
for item in $SCHEMAS; do
|
for item in $SCHEMAS; do
|
||||||
|
|
@ -226,16 +230,13 @@ SELECT creation_time AS ts,
|
||||||
CASE WHEN wired THEN 'wired' ELSE 'paid' END AS status
|
CASE WHEN wired THEN 'wired' ELSE 'paid' END AS status
|
||||||
FROM ${sch}.merchant_contract_terms
|
FROM ${sch}.merchant_contract_terms
|
||||||
WHERE paid
|
WHERE paid
|
||||||
|
AND upper(split_part(coalesce(contract_terms->>'amount',''), ':', 1)) = upper('${cur}')
|
||||||
SQL
|
SQL
|
||||||
done
|
done
|
||||||
if [ "$first" = 1 ]; then
|
if [ "$first" = 1 ]; then
|
||||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||||
fi
|
fi
|
||||||
echo ") pay ORDER BY ts DESC LIMIT ${PAY_LIMIT}"
|
echo " UNION ALL "
|
||||||
echo ")"
|
|
||||||
echo "UNION ALL"
|
|
||||||
echo "("
|
|
||||||
echo "SELECT * FROM ("
|
|
||||||
first=1
|
first=1
|
||||||
for item in $SCHEMAS; do
|
for item in $SCHEMAS; do
|
||||||
sch=${item%%:*}
|
sch=${item%%:*}
|
||||||
|
|
@ -255,17 +256,80 @@ SELECT r.refund_timestamp AS ts,
|
||||||
'refunded'::text AS status
|
'refunded'::text AS status
|
||||||
FROM ${sch}.merchant_refunds r
|
FROM ${sch}.merchant_refunds r
|
||||||
JOIN ${sch}.merchant_contract_terms ct ON ct.order_serial = r.order_serial
|
JOIN ${sch}.merchant_contract_terms ct ON ct.order_serial = r.order_serial
|
||||||
|
WHERE upper((r.refund_amount).curr) = upper('${cur}')
|
||||||
SQL
|
SQL
|
||||||
done
|
done
|
||||||
if [ "$first" = 1 ]; then
|
if [ "$first" = 1 ]; then
|
||||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||||
fi
|
fi
|
||||||
echo ") ref ORDER BY ts DESC LIMIT ${REF_LIMIT}"
|
echo ") u ORDER BY ts DESC LIMIT ${ACT_PER_CURRENCY}"
|
||||||
echo ")"
|
echo ") act;"
|
||||||
echo ") act ORDER BY ts DESC;"
|
} | psql_pipe >"$out" || abort "activity query failed for $cur"
|
||||||
} | psql_pipe >"/tmp/merch_act_$$.tsv" || abort "activity query failed"
|
}
|
||||||
ACT_TSV=$(cat "/tmp/merch_act_$$.tsv" 2>/dev/null || true)
|
|
||||||
rm -f "/tmp/merch_act_$$.tsv"
|
# Build JSON array of activity items from a TSV file
|
||||||
|
activity_tsv_to_json() {
|
||||||
|
local tsv_file="$1"
|
||||||
|
local json="["
|
||||||
|
local af=1
|
||||||
|
local ts kind oid amt sum st sec human_fmt iso
|
||||||
|
while IFS=$'\t' read -r ts kind oid amt sum st; do
|
||||||
|
[ -z "${ts:-}" ] && continue
|
||||||
|
[ "$ts" = "0" ] && [ -z "$kind" ] && continue
|
||||||
|
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||||
|
human_fmt=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||||
|
iso=$(date -d "@${sec}" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' || true)
|
||||||
|
if [ "$af" = 1 ]; then af=0; else json="${json},"; fi
|
||||||
|
json="${json}
|
||||||
|
{
|
||||||
|
\"ts_us\": ${ts:-0},
|
||||||
|
\"ts\": $(json_str "$iso"),
|
||||||
|
\"ts_human\": $(json_str "$human_fmt"),
|
||||||
|
\"kind\": $(json_str "$kind"),
|
||||||
|
\"order_id\": $(json_str "$oid"),
|
||||||
|
\"amount\": $(json_str "$amt"),
|
||||||
|
\"summary\": $(json_str "$sum"),
|
||||||
|
\"status\": $(json_str "$st")
|
||||||
|
}"
|
||||||
|
done <"$tsv_file"
|
||||||
|
json="${json}
|
||||||
|
]"
|
||||||
|
printf '%s' "$json"
|
||||||
|
}
|
||||||
|
|
||||||
|
query_activity_for_currency GOA "/tmp/merch_act_goa_$$.tsv"
|
||||||
|
query_activity_for_currency CHF "/tmp/merch_act_chf_$$.tsv"
|
||||||
|
ACT_GOA_JSON=$(activity_tsv_to_json "/tmp/merch_act_goa_$$.tsv")
|
||||||
|
ACT_CHF_JSON=$(activity_tsv_to_json "/tmp/merch_act_chf_$$.tsv")
|
||||||
|
rm -f "/tmp/merch_act_goa_$$.tsv" "/tmp/merch_act_chf_$$.tsv"
|
||||||
|
|
||||||
|
# Flat recent_activity: GOA then CHF (compat; landing prefers by_currency)
|
||||||
|
ACT_JSON="["
|
||||||
|
af=1
|
||||||
|
for block in "$ACT_GOA_JSON" "$ACT_CHF_JSON"; do
|
||||||
|
# strip outer [ ] and inject if non-empty
|
||||||
|
inner=$(printf '%s' "$block" | sed '1s/^\s*\[//; $s/\]\s*$//')
|
||||||
|
# empty array → skip
|
||||||
|
if printf '%s' "$inner" | grep -q '"kind"'; then
|
||||||
|
if [ "$af" = 1 ]; then af=0; else ACT_JSON="${ACT_JSON},"; fi
|
||||||
|
ACT_JSON="${ACT_JSON}${inner}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ACT_JSON="${ACT_JSON}
|
||||||
|
]"
|
||||||
|
|
||||||
|
ACT_BY_CUR_JSON="[
|
||||||
|
{
|
||||||
|
\"currency\": \"GOA\",
|
||||||
|
\"limit\": ${ACT_PER_CURRENCY},
|
||||||
|
\"items\": ${ACT_GOA_JSON}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\"currency\": \"CHF\",
|
||||||
|
\"limit\": ${ACT_PER_CURRENCY},
|
||||||
|
\"items\": ${ACT_CHF_JSON}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
|
||||||
# by_currency JSON — GOA then CHF then rest
|
# by_currency JSON — GOA then CHF then rest
|
||||||
CUR_JSON="["
|
CUR_JSON="["
|
||||||
|
|
@ -297,30 +361,6 @@ done
|
||||||
CUR_JSON="${CUR_JSON}
|
CUR_JSON="${CUR_JSON}
|
||||||
]"
|
]"
|
||||||
|
|
||||||
ACT_JSON="["
|
|
||||||
af=1
|
|
||||||
while IFS=$'\t' read -r ts kind oid amt sum st; do
|
|
||||||
[ -z "${ts:-}" ] && continue
|
|
||||||
[ "$ts" = "0" ] && [ -z "$kind" ] && continue
|
|
||||||
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
|
||||||
human_fmt=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
|
||||||
iso=$(date -d "@${sec}" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' || true)
|
|
||||||
if [ "$af" = 1 ]; then af=0; else ACT_JSON="${ACT_JSON},"; fi
|
|
||||||
ACT_JSON="${ACT_JSON}
|
|
||||||
{
|
|
||||||
\"ts_us\": ${ts:-0},
|
|
||||||
\"ts\": $(json_str "$iso"),
|
|
||||||
\"ts_human\": $(json_str "$human_fmt"),
|
|
||||||
\"kind\": $(json_str "$kind"),
|
|
||||||
\"order_id\": $(json_str "$oid"),
|
|
||||||
\"amount\": $(json_str "$amt"),
|
|
||||||
\"summary\": $(json_str "$sum"),
|
|
||||||
\"status\": $(json_str "$st")
|
|
||||||
}"
|
|
||||||
done <<<"$ACT_TSV"
|
|
||||||
ACT_JSON="${ACT_JSON}
|
|
||||||
]"
|
|
||||||
|
|
||||||
GEN=$(now_iso)
|
GEN=$(now_iso)
|
||||||
HUMAN=$(now_human)
|
HUMAN=$(now_human)
|
||||||
|
|
||||||
|
|
@ -374,6 +414,8 @@ cat >"$TMP" <<EOF
|
||||||
"deposits": ${DEPOSITS:-0},
|
"deposits": ${DEPOSITS:-0},
|
||||||
"refunds": ${REFUNDS:-0},
|
"refunds": ${REFUNDS:-0},
|
||||||
"by_currency": $CUR_JSON,
|
"by_currency": $CUR_JSON,
|
||||||
|
"recent_activity_limit_per_currency": ${ACT_PER_CURRENCY},
|
||||||
|
"recent_activity_by_currency": $ACT_BY_CUR_JSON,
|
||||||
"recent_activity": $ACT_JSON,
|
"recent_activity": $ACT_JSON,
|
||||||
"performance": {
|
"performance": {
|
||||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue