@profullstack/coinpay 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -704,7 +704,7 @@ coinpay finances connections # institutions and their last sync
704
704
  coinpay finances sync # pull fresh balances (rate-limited by the bank bridge)
705
705
  ```
706
706
 
707
- The dashboard has seven screens (`1`–`7`, `Tab`): **Overview** (gross volume, crypto vs
707
+ The dashboard has seven screens (`1`–`7`, `←`/`→`): **Overview** (gross volume, crypto vs
708
708
  cards, commission paid, processor fees, refunds, net earnings, bank position, cashflow,
709
709
  invoices, escrow, payouts, a volume-vs-commission graph and a live feed), **Bank &
710
710
  Cards**, **Ledger**, **Crypto**, **Cards**, **Invoices & Escrow**, **Debt & Income**.
@@ -713,6 +713,13 @@ syncs the bank feed, `w` cycles the window, `p` pauses, `?` shows help, `q` quit
713
713
  refreshes every 30 seconds (`--interval`) and listens to the payments event stream, so
714
714
  a crypto payment shows up the moment it is detected.
715
715
 
716
+ Click **⧉ MD** on a summary pane to copy Markdown. The header copy button exports
717
+ status, including bank connection health and detailed source errors. Each pane
718
+ carries its displayed window, snapshot timestamp, stream status and partial-data
719
+ warnings. Transaction rows and the live event log are excluded. Tab / Shift+Tab
720
+ focus controls; Enter / Space copies the focused summary. Clipboard writes use
721
+ OSC 52 and require terminal support, including when connected over SSH.
722
+
716
723
  **Debt & Income** is the basic-accounting view, and it reads the same for a company or
717
724
  a person: income against spending per month, total owed split into revolving and
718
725
  instalment, months to clear each balance at its current payment rate, debt-to-income,
package/bin/coinpay.js CHANGED
@@ -173,6 +173,7 @@ const BOOLEAN_FLAGS = new Set([
173
173
  'no-model',
174
174
  'only-uncategorized',
175
175
  'no-attach',
176
+ 'estimate-gaps',
176
177
  'plain',
177
178
  'no-stream',
178
179
  'hidden',
@@ -477,7 +478,7 @@ ${colors.cyan}Commands:${colors.reset}
477
478
  coverage What part of a period was ever fetched (--period, --account)
478
479
  report Monthly/quarterly/custom activity report (--period 2026-08 |
479
480
  --from/--to; --account, --scope, --timezone, --format pdf|csv|html|json,
480
- --output <file>, --strict, --wait, --json)
481
+ --output <file>, --strict, --estimate-gaps, --wait, --json)
481
482
  reports [get|download|delete <id>] Report history and downloads
482
483
  statements import <pdf> Keep an original bank statement (--account, --period|--from/--to)
483
484
  statements list|get|download|reconcile|delete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -139,7 +139,7 @@
139
139
  "@inquirer/prompts": "^8.3.0",
140
140
  "@noble/curves": "^1.6.0",
141
141
  "@noble/hashes": "^1.5.0",
142
- "@profullstack/hqtui": "^0.1.11",
142
+ "@profullstack/hqtui": "^0.6.3",
143
143
  "@scure/bip32": "^1.5.0",
144
144
  "@scure/bip39": "^1.4.0"
145
145
  }
@@ -247,6 +247,7 @@ export async function runFinancesCommand(subcommand, args, flags, ctx) {
247
247
  includePendingAppendix: flags['no-pending'] ? false : true,
248
248
  formats: [format, 'json'],
249
249
  strict,
250
+ estimateGaps: flags['estimate-gaps'] === true,
250
251
  idempotencyKey: typeof flags['idempotency-key'] === 'string' ? flags['idempotency-key'] : undefined,
251
252
  });
252
253
  let report = created.report;
@@ -196,6 +196,8 @@ export function createFinanceReport(
196
196
  includePendingAppendix?: boolean;
197
197
  formats?: ReportFormat[];
198
198
  strict?: boolean;
199
+ /** Fill the days before the first observed posting with a labelled extrapolation. */
200
+ estimateGaps?: boolean;
199
201
  idempotencyKey?: string;
200
202
  },
201
203
  ): Promise<{ report: FinanceReport; job: FinanceJob }>;
@@ -156,12 +156,12 @@ export async function getFinanceCoverage(client, { period, from, to, timezone, a
156
156
  * use `waitForFinanceReport` to block until it is ready.
157
157
  */
158
158
  export async function createFinanceReport(client, {
159
- period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict, idempotencyKey,
159
+ period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict, estimateGaps, idempotencyKey,
160
160
  } = {}) {
161
161
  return call(client, '/finances/reports', {
162
162
  method: 'POST',
163
163
  headers: headersWith(idempotencyKey),
164
- body: JSON.stringify({ period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict }),
164
+ body: JSON.stringify({ period, from, to, timezone, accountIds, scope, includeHidden, includePendingAppendix, formats, strict, estimateGaps }),
165
165
  });
166
166
  }
167
167
 
@@ -95,7 +95,7 @@ function num(v) {
95
95
 
96
96
  // ── State ──
97
97
 
98
- function createState(days) {
98
+ export function createState(days) {
99
99
  return {
100
100
  tab: 0,
101
101
  days,
@@ -732,6 +732,111 @@ function positionScreen(ui, state, theme) {
732
732
 
733
733
  const SCREENS = [overviewScreen, bankScreen, ledgerScreen, cryptoScreen, cardsScreen, invoicesScreen, positionScreen];
734
734
 
735
+ /** Context exported with each pane; the displayed snapshot can lag the requested window. */
736
+ export function financeMarkdownContext(state) {
737
+ const s = state.snapshot;
738
+ return [
739
+ `CoinPay Finances · ${TABS[state.tab]}`,
740
+ `Payment business filter: ${state.businessId || 'all businesses'}; bank data is merchant-wide`,
741
+ `Displayed window: ${s?.windowDays ?? state.days} days`,
742
+ ...(s && s.windowDays !== state.days ? [`Requested window: ${state.days} days (refresh pending)`] : []),
743
+ `Snapshot: ${s?.generatedAt || 'not loaded'}`,
744
+ `Updates: ${state.loading ? 'refreshing' : state.paused ? 'paused' : 'live'}`,
745
+ `Payment stream: ${state.liveStatus}`,
746
+ ...(state.error ? [`Refresh failed: ${state.error}`] : []),
747
+ ...Object.entries(s?.errors || {}).map(([source, error]) => `Source unavailable — ${source}: ${error}`),
748
+ ...(s?.crypto.partial ? ['Crypto details cover a partial page.'] : []),
749
+ ...(s?.card.partial ? ['Card details, processor fees and refunds cover a partial page.'] : []),
750
+ ...(s?.position ? [`Debt & Income: ${s.position.observedDays ?? s.position.lookbackDays} days observed; independent of the dashboard window.`] : []),
751
+ ].join('\n');
752
+ }
753
+
754
+ /** Status can be copied without including any live payment or transaction rows. */
755
+ export function financeStatusMarkdown(state, markdownText) {
756
+ return [
757
+ '## CoinPay status',
758
+ markdownText(financeMarkdownContext(state)),
759
+ `Bank sync: ${state.syncing ? 'running' : 'idle'}`,
760
+ ...(state.notice ? [markdownText(state.notice)] : []),
761
+ ...(state.snapshot?.bank.connections || []).map((c) => markdownText(
762
+ `${c.label || c.provider || 'Bank connection'}: ${c.is_active ? 'active' : 'inactive'}; last sync ${c.last_synced_at || 'never'} (${c.last_sync_status || 'unknown'})${c.last_sync_error ? `; ${c.last_sync_error}` : ''}`,
763
+ )),
764
+ ].join('\n\n') + '\n';
765
+ }
766
+
767
+ /** Draw the same summary controls in production and headless interaction tests. */
768
+ export function renderFinancesTui(ui, state, t, { height, interval = 30, markdownText }) {
769
+ ui.row({ size: 1 }, (header) => {
770
+ header.text(' CoinPay ', { fg: t.title, bold: true, size: 10 });
771
+ header.tabs({
772
+ tabs: TABS.map((name, i) => `${i + 1} ${name}`),
773
+ active: state.tab,
774
+ onSelect: (index) => { state.tab = index; },
775
+ });
776
+ const right = [
777
+ state.paused ? 'paused' : state.loading ? 'loading…' : `${state.days}d`,
778
+ state.lastRefresh ? `updated ${ago(state.lastRefresh)}` : 'starting',
779
+ clock(),
780
+ ].join(' ');
781
+ header.text(`${right} `, { fg: state.paused ? t.warning : state.error ? t.danger : t.success, align: 'right' });
782
+ header.copyButton({ markdown: () => financeStatusMarkdown(state, markdownText), width: 6 });
783
+ });
784
+ ui.spacer(1);
785
+
786
+ ui.column({ size: height - 4 }, (body) => {
787
+ if (!state.snapshot) {
788
+ body.panel({ title: 'Finances' }, (p) => {
789
+ p.text(state.error ? `Could not load: ${state.error}` : 'Loading your numbers…', { fg: state.error ? t.danger : t.muted });
790
+ if (state.error) p.text('Press r to retry, q to quit.', { fg: t.muted });
791
+ });
792
+ return;
793
+ }
794
+ SCREENS[state.tab](body, state, t);
795
+ });
796
+
797
+ ui.spacer(1);
798
+ const errorCount = state.snapshot ? Object.keys(state.snapshot.errors).length : 0;
799
+ ui.statusBar({
800
+ items: [
801
+ { key: '1-7', label: 'Screen' },
802
+ { key: 'r', label: 'Refresh' },
803
+ { key: 's', label: state.syncing ? 'Syncing…' : 'Sync bank', active: state.syncing },
804
+ { key: 'w', label: `Window ${state.days}d` },
805
+ { key: 'p', label: state.paused ? 'Resume' : 'Pause', active: state.paused },
806
+ { key: '?', label: 'Help' },
807
+ { key: 'q', label: 'Quit' },
808
+ ],
809
+ right: [
810
+ ...(errorCount ? [{ label: `${errorCount} source${errorCount > 1 ? 's' : ''} unavailable`, color: t.warning }] : []),
811
+ { label: state.liveStatus, color: state.liveStatus === 'stream connected' ? t.success : t.muted },
812
+ ],
813
+ });
814
+
815
+ if (state.showHelp) {
816
+ ui.modal({
817
+ title: 'CoinPay Finances — Help',
818
+ width: 66,
819
+ height: 22,
820
+ message:
821
+ '1-7, ←/→ switch screens.\n' +
822
+ 'r refreshes now; refresh also runs every ' + Math.max(5, interval) + 's.\n' +
823
+ 's pulls fresh bank balances and transactions (rate-limited\n' +
824
+ ' by the bank bridge, so it is never automatic).\n' +
825
+ 'w cycles the window: 7 → 30 → 90 → 365 days.\n' +
826
+ 'p pauses the timer. ↑/↓ j/k, PgUp/PgDn, Home/End scroll.\n' +
827
+ 'Mouse: click tabs, scroll tables.\n\n' +
828
+ '⧉ MD copies a summary. Tab focuses, Enter copies.\n\n' +
829
+ 'Commission paid = platform fees on crypto + card payments.\n' +
830
+ 'Net earnings = gross − commission − processor fees − refunds.\n' +
831
+ 'Debt & Income covers 180 days regardless of w, and excludes\n' +
832
+ ' transfers and card payments from both sides.\n\n' +
833
+ 'Esc or Close dismisses help.',
834
+ buttons: [{ label: 'Close', onPress: () => { state.showHelp = false; } }],
835
+ onDismiss: () => { state.showHelp = false; },
836
+ });
837
+ }
838
+ }
839
+
735
840
  // ── App ──
736
841
 
737
842
  async function loadHqtui() {
@@ -755,8 +860,13 @@ async function loadHqtui() {
755
860
  */
756
861
  export async function runFinancesTui({ client, baseUrl, token, days = 30, interval = 30, businessId, limit = 100, theme } = {}) {
757
862
  const hqtui = await loadHqtui();
758
- const app = await hqtui.createApp({ fps: 30, theme: theme || 'dark', quitKeys: ['ctrl+c', 'q'] });
759
863
  const state = createState(days);
864
+ state.businessId = businessId;
865
+ const app = await hqtui.createApp({
866
+ fps: 30, theme: theme || 'dark', quitKeys: ['ctrl+c', 'q'],
867
+ copyMarkdown: true,
868
+ markdownContext: () => financeMarkdownContext(state),
869
+ });
760
870
 
761
871
  let refreshing = false;
762
872
  let refreshTimer = null;
@@ -867,7 +977,6 @@ export async function runFinancesTui({ client, baseUrl, token, days = 30, interv
867
977
  return;
868
978
  }
869
979
  switch (name) {
870
- case 'tab':
871
980
  case 'right':
872
981
  case 'l':
873
982
  state.tab = event.shift ? (state.tab + TABS.length - 1) % TABS.length : (state.tab + 1) % TABS.length;
@@ -925,74 +1034,7 @@ export async function runFinancesTui({ client, baseUrl, token, days = 30, interv
925
1034
  app.invalidate();
926
1035
  });
927
1036
 
928
- app.render(({ ui, theme: t, height }) => {
929
- ui.row({ size: 1 }, (header) => {
930
- header.text(' CoinPay ', { fg: t.title, bold: true, size: 10 });
931
- header.tabs({
932
- tabs: TABS.map((name, i) => `${i + 1} ${name}`),
933
- active: state.tab,
934
- onSelect: (index) => { state.tab = index; },
935
- });
936
- const right = [
937
- state.paused ? 'paused' : state.loading ? 'loading…' : `${state.days}d`,
938
- state.lastRefresh ? `updated ${ago(state.lastRefresh)}` : 'starting',
939
- clock(),
940
- ].join(' ');
941
- header.text(`${right} `, { fg: state.paused ? t.warning : state.error ? t.danger : t.success, align: 'right' });
942
- });
943
- ui.spacer(1);
944
-
945
- ui.column({ size: height - 4 }, (body) => {
946
- if (!state.snapshot) {
947
- body.panel({ title: 'Finances' }, (p) => {
948
- p.text(state.error ? `Could not load: ${state.error}` : 'Loading your numbers…', { fg: state.error ? t.danger : t.muted });
949
- if (state.error) p.text('Press r to retry, q to quit.', { fg: t.muted });
950
- });
951
- return;
952
- }
953
- SCREENS[state.tab](body, state, t);
954
- });
955
-
956
- ui.spacer(1);
957
- const errorCount = state.snapshot ? Object.keys(state.snapshot.errors).length : 0;
958
- ui.statusBar({
959
- items: [
960
- { key: '1-7', label: 'Screen' },
961
- { key: 'r', label: 'Refresh' },
962
- { key: 's', label: state.syncing ? 'Syncing…' : 'Sync bank', active: state.syncing },
963
- { key: 'w', label: `Window ${state.days}d` },
964
- { key: 'p', label: state.paused ? 'Resume' : 'Pause', active: state.paused },
965
- { key: '?', label: 'Help' },
966
- { key: 'q', label: 'Quit' },
967
- ],
968
- right: [
969
- ...(errorCount ? [{ label: `${errorCount} source${errorCount > 1 ? 's' : ''} unavailable`, color: t.warning }] : []),
970
- { label: state.liveStatus, color: state.liveStatus === 'stream connected' ? t.success : t.muted },
971
- ],
972
- });
973
-
974
- if (state.showHelp) {
975
- ui.modal({
976
- title: 'CoinPay Finances — Help',
977
- width: 66,
978
- height: 20,
979
- message:
980
- '1-7, Tab, ←/→ switch screens.\n' +
981
- 'r refreshes now; refresh also runs every ' + Math.max(5, interval) + 's.\n' +
982
- 's pulls fresh bank balances and transactions (rate-limited\n' +
983
- ' by the bank bridge, so it is never automatic).\n' +
984
- 'w cycles the window: 7 → 30 → 90 → 365 days.\n' +
985
- 'p pauses the timer. ↑/↓ j/k, PgUp/PgDn, Home/End scroll.\n' +
986
- 'Mouse: click tabs, scroll tables.\n\n' +
987
- 'Commission paid = platform fees on crypto + card payments.\n' +
988
- 'Net earnings = gross − commission − processor fees − refunds.\n' +
989
- 'Debt & Income covers 180 days regardless of w, and excludes\n' +
990
- ' transfers and card payments from both sides.\n\n' +
991
- 'Press any key to close.',
992
- buttons: [{ label: 'Close', focused: true }],
993
- });
994
- }
995
- });
1037
+ app.render(({ ui, theme: t, height }) => renderFinancesTui(ui, state, t, { height, interval, markdownText: hqtui.markdownText }));
996
1038
 
997
1039
  app.on('exit', () => {
998
1040
  clearInterval(poll);