payment-skill 1.1.1 → 1.1.2

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/dashboard.html CHANGED
@@ -290,7 +290,7 @@
290
290
  </head>
291
291
  <body>
292
292
  <div class="header">
293
- <button class="emergency-btn">🔴 Emergency Stop</button>
293
+ <button class="emergency-btn" id="emergency-btn">🔴 Emergency Stop</button>
294
294
  <img src="logo.png" alt="Logo" class="header-logo">
295
295
  </div>
296
296
 
@@ -354,18 +354,16 @@
354
354
  </div>
355
355
  <div class="section-body">
356
356
  <div class="form-row">
357
- <div class="form-group"><label>Per Transaction</label><input type="text" value="5.0"></div>
358
- <div class="form-group"><label>Per Hour</label><input type="text" value="50.0"></div>
359
- <div class="form-group"><label>Per Day</label><input type="text" value="500.0"></div>
360
- <div class="form-group"><label>Per Week</label><input type="text" value="2,500.0"></div>
357
+ <div class="form-group"><label>Per Transaction</label><input type="number" id="per-transaction-limit" value="10000"></div>
358
+ <div class="form-group"><label>Per Hour</label><input type="number" id="hourly-limit" value="10"></div>
359
+ <div class="form-group"><label>Per Day</label><input type="number" id="daily-limit" value="50000"></div>
360
+ <div class="form-group"><label>Per Week</label><input type="number" id="weekly-limit" value="200000"></div>
361
361
  </div>
362
362
  <div class="form-row">
363
- <div class="form-group"><label>Per Month</label><input type="text" value="10,000.0"></div>
364
- <div class="form-group"><label>Per Year</label><input type="text" value="100,000.0"></div>
365
- <div class="form-group"><label>Currency</label><select><option>USD</option><option>EUR</option></select></div>
366
- <div class="form-group"><label>Max Tx/Hour</label><input type="text" value="20"></div>
363
+ <div class="form-group"><label>Per Month</label><input type="number" id="monthly-limit" value="500000"></div>
364
+ <div class="form-group"><label>Currency</label><select id="limit-currency"><option>USD</option><option>EUR</option></select></div>
367
365
  </div>
368
- <button class="save-section-btn">Save Account Limits</button>
366
+ <button class="save-section-btn" onclick="saveAccountLimits()">Save Account Limits</button>
369
367
  <div class="clearfix"></div>
370
368
  </div>
371
369
  </div>
@@ -450,18 +448,15 @@
450
448
  </div>
451
449
  <div class="section-body">
452
450
  <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
453
- <div style="width: 36px; height: 18px; background: var(--accent-green); border-radius: 9px; position: relative; cursor: pointer;">
454
- <div style="position: absolute; width: 14px; height: 14px; background: white; border-radius: 50%; top: 2px; right: 2px;"></div>
455
- </div>
451
+ <input type="checkbox" id="time-window-enabled" style="width: 18px; height: 18px; cursor: pointer;">
456
452
  <span style="font-size: 11px;">Enable time window enforcement</span>
457
453
  </div>
458
454
  <div class="form-row">
459
- <div class="form-group"><label>Start Time</label><input type="time" value="08:00"></div>
460
- <div class="form-group"><label>End Time</label><input type="time" value="22:00"></div>
461
- <div class="form-group"><label>Timezone</label><select><option>Europe/Bucharest</option><option>UTC</option></select></div>
462
- <div class="form-group"><label>Blocked Days</label><select><option>None</option><option>Weekends</option></select></div>
455
+ <div class="form-group"><label>Start Time</label><input type="time" id="start-time" value="08:00"></div>
456
+ <div class="form-group"><label>End Time</label><input type="time" id="end-time" value="22:00"></div>
457
+ <div class="form-group"><label>Timezone</label><select id="timezone"><option>Europe/Bucharest</option><option>UTC</option></select></div>
463
458
  </div>
464
- <button class="save-section-btn">Save Time Policy</button>
459
+ <button class="save-section-btn" onclick="saveTimeWindow()">Save Time Policy</button>
465
460
  <div class="clearfix"></div>
466
461
  </div>
467
462
  </div>
@@ -665,5 +660,6 @@
665
660
  arrow.textContent = body.classList.contains('collapsed') ? '▶' : '▼';
666
661
  }
667
662
  </script>
663
+ <script src="dashboard.js"></script>
668
664
  </body>
669
665
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-skill",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Self-hosted payment orchestration app for OpenClaw - pay from your bank account to merchants via API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Payment Skill Dashboard - JavaScript API Client
3
+ *
4
+ * Connects dashboard UI to backend API
5
+ */
6
+
7
+ const API_BASE = '';
8
+
9
+ // Utility functions
10
+ function showNotification(message, type = 'success') {
11
+ const notification = document.createElement('div');
12
+ notification.style.cssText = `
13
+ position: fixed;
14
+ top: 20px;
15
+ right: 20px;
16
+ padding: 12px 20px;
17
+ border-radius: 6px;
18
+ color: white;
19
+ font-size: 12px;
20
+ z-index: 10000;
21
+ background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#dc2626' : '#3b82f6'};
22
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
23
+ `;
24
+ notification.textContent = message;
25
+ document.body.appendChild(notification);
26
+ setTimeout(() => notification.remove(), 3000);
27
+ }
28
+
29
+ async function apiGet(endpoint) {
30
+ try {
31
+ const response = await fetch(`${API_BASE}${endpoint}`);
32
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
33
+ return await response.json();
34
+ } catch (error) {
35
+ console.error('API Error:', error);
36
+ showNotification(`Error: ${error.message}`, 'error');
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async function apiPost(endpoint, data) {
42
+ try {
43
+ const response = await fetch(`${API_BASE}${endpoint}`, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(data)
47
+ });
48
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
49
+ const result = await response.json();
50
+ showNotification(result.message || 'Success');
51
+ return result;
52
+ } catch (error) {
53
+ console.error('API Error:', error);
54
+ showNotification(`Error: ${error.message}`, 'error');
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ // Load all limits and controls
60
+ async function loadLimits() {
61
+ try {
62
+ const data = await apiGet('/api/limits');
63
+
64
+ // Update Account Limits
65
+ if (data.limits) {
66
+ document.getElementById('per-transaction-limit').value = data.limits.perTransaction || '';
67
+ document.getElementById('daily-limit').value = data.limits.daily || '';
68
+ document.getElementById('weekly-limit').value = data.limits.weekly || '';
69
+ document.getElementById('monthly-limit').value = data.limits.monthly || '';
70
+ document.getElementById('hourly-limit').value = data.limits.maxTransactionsPerHour || '';
71
+ }
72
+
73
+ // Update Time Window
74
+ if (data.timeWindow) {
75
+ document.getElementById('time-window-enabled').checked = data.timeWindow.enabled;
76
+ document.getElementById('start-time').value = data.timeWindow.start || '08:00';
77
+ document.getElementById('end-time').value = data.timeWindow.end || '22:00';
78
+ document.getElementById('timezone').value = data.timeWindow.timezone || 'Europe/Bucharest';
79
+ }
80
+
81
+ // Update Cumulative Budgets
82
+ if (data.cumulativeBudgets) {
83
+ updateBudgetsList(data.cumulativeBudgets);
84
+ }
85
+
86
+ // Update Domain Controls
87
+ if (data.domainControls) {
88
+ document.getElementById('domain-mode').value = data.domainControls.mode || 'blacklist';
89
+ updateDomainsList(data.domainControls.domains || []);
90
+ }
91
+
92
+ // Update Geography Controls
93
+ if (data.geographyControls) {
94
+ document.getElementById('geo-enabled').checked = data.geographyControls.enabled;
95
+ document.getElementById('geo-mode').value = data.geographyControls.mode || 'allow';
96
+ updateCountriesList(data.geographyControls.countries || []);
97
+ }
98
+
99
+ // Update Category Controls
100
+ if (data.categoryControls) {
101
+ updateBlockedCategories(data.categoryControls.blockedCategories || []);
102
+ updateAllowedCategories(data.categoryControls.allowedCategories || []);
103
+ }
104
+
105
+ } catch (error) {
106
+ console.error('Failed to load limits:', error);
107
+ }
108
+ }
109
+
110
+ // Save Account Limits
111
+ async function saveAccountLimits() {
112
+ const limits = {
113
+ perTransaction: parseInt(document.getElementById('per-transaction-limit').value) || 0,
114
+ daily: parseInt(document.getElementById('daily-limit').value) || 0,
115
+ weekly: parseInt(document.getElementById('weekly-limit').value) || 0,
116
+ monthly: parseInt(document.getElementById('monthly-limit').value) || 0,
117
+ maxTransactionsPerHour: parseInt(document.getElementById('hourly-limit').value) || 0
118
+ };
119
+
120
+ await apiPost('/api/limits', limits);
121
+ }
122
+
123
+ // Save Time Window
124
+ async function saveTimeWindow() {
125
+ const timeWindow = {
126
+ enabled: document.getElementById('time-window-enabled').checked,
127
+ start: document.getElementById('start-time').value,
128
+ end: document.getElementById('end-time').value,
129
+ timezone: document.getElementById('timezone').value
130
+ };
131
+
132
+ await apiPost('/api/limits/time-window', timeWindow);
133
+ }
134
+
135
+ // Budget Management
136
+ let budgets = [];
137
+
138
+ function updateBudgetsList(budgetsList) {
139
+ budgets = budgetsList;
140
+ const container = document.getElementById('budgets-list');
141
+ if (!container) return;
142
+
143
+ container.innerHTML = budgets.map((b, i) => `
144
+ <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: var(--bg-primary); border-radius: 4px; margin-bottom: 5px;">
145
+ <span>${b.amount} ${b.currency || ''} per ${b.period}</span>
146
+ <button onclick="removeBudget(${i})" style="background: #dc2626; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 10px;">Remove</button>
147
+ </div>
148
+ `).join('');
149
+ }
150
+
151
+ async function addBudget() {
152
+ const amount = parseFloat(document.getElementById('budget-amount').value);
153
+ const currency = document.getElementById('budget-currency').value || 'EUR';
154
+ const period = document.getElementById('budget-period').value;
155
+
156
+ if (!amount || !period) {
157
+ showNotification('Please fill in all fields', 'error');
158
+ return;
159
+ }
160
+
161
+ await apiPost('/api/limits/budgets', { amount, currency, period, enabled: true });
162
+ document.getElementById('budget-amount').value = '';
163
+ await loadLimits();
164
+ }
165
+
166
+ async function removeBudget(index) {
167
+ try {
168
+ const response = await fetch(`${API_BASE}/api/limits/budgets/${index}`, { method: 'DELETE' });
169
+ if (!response.ok) throw new Error('Failed to remove budget');
170
+ showNotification('Budget removed');
171
+ await loadLimits();
172
+ } catch (error) {
173
+ showNotification('Error removing budget', 'error');
174
+ }
175
+ }
176
+
177
+ // Domain Management
178
+ let domains = [];
179
+
180
+ function updateDomainsList(domainList) {
181
+ domains = domainList;
182
+ const container = document.getElementById('domains-list');
183
+ if (!container) return;
184
+
185
+ container.innerHTML = domains.map(d => `
186
+ <span style="background: rgba(59, 130, 246, 0.2); color: #3b82f6; padding: 3px 8px; border-radius: 3px; font-size: 10px; display: inline-flex; align-items: center; gap: 5px; margin: 2px;">
187
+ ${d}
188
+ <button onclick="removeDomain('${d}')" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 12px; padding: 0;">×</button>
189
+ </span>
190
+ `).join('');
191
+ }
192
+
193
+ async function addDomain() {
194
+ const domain = document.getElementById('new-domain').value.trim();
195
+ if (!domain) return;
196
+
197
+ await apiPost('/api/limits/domains', { domain });
198
+ document.getElementById('new-domain').value = '';
199
+ await loadLimits();
200
+ }
201
+
202
+ async function removeDomain(domain) {
203
+ try {
204
+ const response = await fetch(`${API_BASE}/api/limits/domains/${encodeURIComponent(domain)}`, { method: 'DELETE' });
205
+ if (!response.ok) throw new Error('Failed to remove domain');
206
+ showNotification('Domain removed');
207
+ await loadLimits();
208
+ } catch (error) {
209
+ showNotification('Error removing domain', 'error');
210
+ }
211
+ }
212
+
213
+ async function saveDomainMode() {
214
+ const mode = document.getElementById('domain-mode').value;
215
+ await apiPost('/api/limits/domains', { mode });
216
+ }
217
+
218
+ // Geography Management
219
+ let countries = [];
220
+
221
+ function updateCountriesList(countryList) {
222
+ countries = countryList;
223
+ const container = document.getElementById('countries-list');
224
+ if (!container) return;
225
+
226
+ container.innerHTML = countries.map(c => `
227
+ <span style="background: rgba(34, 197, 94, 0.2); color: #22c55e; padding: 3px 8px; border-radius: 3px; font-size: 10px; display: inline-flex; align-items: center; gap: 5px; margin: 2px;">
228
+ ${c}
229
+ <button onclick="removeCountry('${c}')" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 12px; padding: 0;">×</button>
230
+ </span>
231
+ `).join('');
232
+ }
233
+
234
+ async function addCountry() {
235
+ const country = document.getElementById('new-country').value.trim().toUpperCase();
236
+ if (!country) return;
237
+
238
+ await apiPost('/api/limits/geo', { country });
239
+ document.getElementById('new-country').value = '';
240
+ await loadLimits();
241
+ }
242
+
243
+ async function removeCountry(country) {
244
+ try {
245
+ const response = await fetch(`${API_BASE}/api/limits/geo/${country}`, { method: 'DELETE' });
246
+ if (!response.ok) throw new Error('Failed to remove country');
247
+ showNotification('Country removed');
248
+ await loadLimits();
249
+ } catch (error) {
250
+ showNotification('Error removing country', 'error');
251
+ }
252
+ }
253
+
254
+ async function saveGeoSettings() {
255
+ const enabled = document.getElementById('geo-enabled').checked;
256
+ const mode = document.getElementById('geo-mode').value;
257
+ await apiPost('/api/limits/geo', { enabled, mode });
258
+ }
259
+
260
+ // Category Management
261
+ function updateBlockedCategories(categories) {
262
+ const container = document.getElementById('blocked-categories-list');
263
+ if (!container) return;
264
+
265
+ container.innerHTML = categories.map(c => `
266
+ <span style="background: rgba(220, 38, 38, 0.2); color: #ef4444; padding: 3px 8px; border-radius: 3px; font-size: 10px; display: inline-flex; align-items: center; gap: 5px; margin: 2px;">
267
+ ${c}
268
+ <button onclick="unblockCategory('${c}')" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 12px; padding: 0;">×</button>
269
+ </span>
270
+ `).join('');
271
+ }
272
+
273
+ function updateAllowedCategories(categories) {
274
+ const container = document.getElementById('allowed-categories-list');
275
+ if (!container) return;
276
+
277
+ container.innerHTML = categories.map(c => `
278
+ <span style="background: rgba(34, 197, 94, 0.2); color: #22c55e; padding: 3px 8px; border-radius: 3px; font-size: 10px; display: inline-flex; align-items: center; gap: 5px; margin: 2px;">
279
+ ${c}
280
+ <button onclick="disallowCategory('${c}')" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 12px; padding: 0;">×</button>
281
+ </span>
282
+ `).join('');
283
+ }
284
+
285
+ async function blockCategory() {
286
+ const category = document.getElementById('new-blocked-category').value.trim();
287
+ if (!category) return;
288
+
289
+ await apiPost('/api/limits/categories/block', { category });
290
+ document.getElementById('new-blocked-category').value = '';
291
+ await loadLimits();
292
+ }
293
+
294
+ async function unblockCategory(category) {
295
+ await apiPost('/api/limits/categories/unblock', { category });
296
+ await loadLimits();
297
+ }
298
+
299
+ async function allowCategory() {
300
+ const category = document.getElementById('new-allowed-category').value.trim();
301
+ if (!category) return;
302
+
303
+ await apiPost('/api/limits/categories/allow', { category });
304
+ document.getElementById('new-allowed-category').value = '';
305
+ await loadLimits();
306
+ }
307
+
308
+ async function disallowCategory(category) {
309
+ await apiPost('/api/limits/categories/disallow', { category });
310
+ await loadLimits();
311
+ }
312
+
313
+ // Emergency Stop
314
+ async function toggleEmergencyStop() {
315
+ const btn = document.getElementById('emergency-btn');
316
+ const isActive = btn.classList.contains('active');
317
+
318
+ if (!isActive) {
319
+ // Activate
320
+ if (!confirm('Are you sure you want to activate EMERGENCY STOP? This will halt all transactions.')) {
321
+ return;
322
+ }
323
+ await apiPost('/api/emergency/stop', { reason: 'Dashboard activation' });
324
+ btn.classList.add('active');
325
+ btn.textContent = '🔴 Emergency Stop ACTIVE - Click to Resume';
326
+ showNotification('Emergency stop activated', 'error');
327
+ } else {
328
+ // Deactivate
329
+ await apiPost('/api/emergency/resume', {});
330
+ btn.classList.remove('active');
331
+ btn.textContent = '🔴 Emergency Stop';
332
+ showNotification('Emergency stop deactivated');
333
+ }
334
+ }
335
+
336
+ // Load emergency stop status
337
+ async function loadEmergencyStatus() {
338
+ try {
339
+ const data = await apiGet('/api/emergency');
340
+ const btn = document.getElementById('emergency-btn');
341
+ if (data.active) {
342
+ btn.classList.add('active');
343
+ btn.textContent = '🔴 Emergency Stop ACTIVE - Click to Resume';
344
+ }
345
+ } catch (error) {
346
+ console.error('Failed to load emergency status:', error);
347
+ }
348
+ }
349
+
350
+ // Initialize dashboard
351
+ document.addEventListener('DOMContentLoaded', () => {
352
+ loadLimits();
353
+ loadEmergencyStatus();
354
+
355
+ // Set up emergency button
356
+ const emergencyBtn = document.getElementById('emergency-btn');
357
+ if (emergencyBtn) {
358
+ emergencyBtn.addEventListener('click', toggleEmergencyStop);
359
+ }
360
+ });