Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
301 lines
9.9 KiB
JavaScript
301 lines
9.9 KiB
JavaScript
// OpenFang Channels Page — OpenClaw-style setup UX with QR code support
|
|
'use strict';
|
|
|
|
function channelsPage() {
|
|
return {
|
|
allChannels: [],
|
|
categoryFilter: 'all',
|
|
searchQuery: '',
|
|
setupModal: null,
|
|
configuring: false,
|
|
testing: {},
|
|
formValues: {},
|
|
showAdvanced: false,
|
|
showBusinessApi: false,
|
|
loading: true,
|
|
loadError: '',
|
|
pollTimer: null,
|
|
|
|
// Setup flow step tracking
|
|
setupStep: 1, // 1=Configure, 2=Verify, 3=Ready
|
|
testPassed: false,
|
|
|
|
// WhatsApp QR state
|
|
qr: {
|
|
loading: false,
|
|
available: false,
|
|
dataUrl: '',
|
|
sessionId: '',
|
|
message: '',
|
|
help: '',
|
|
connected: false,
|
|
expired: false,
|
|
error: ''
|
|
},
|
|
qrPollTimer: null,
|
|
|
|
categories: [
|
|
{ key: 'all', label: 'All' },
|
|
{ key: 'messaging', label: 'Messaging' },
|
|
{ key: 'social', label: 'Social' },
|
|
{ key: 'enterprise', label: 'Enterprise' },
|
|
{ key: 'developer', label: 'Developer' },
|
|
{ key: 'notifications', label: 'Notifications' }
|
|
],
|
|
|
|
get filteredChannels() {
|
|
var self = this;
|
|
return this.allChannels.filter(function(ch) {
|
|
if (self.categoryFilter !== 'all' && ch.category !== self.categoryFilter) return false;
|
|
if (self.searchQuery) {
|
|
var q = self.searchQuery.toLowerCase();
|
|
return ch.name.toLowerCase().indexOf(q) !== -1 ||
|
|
ch.display_name.toLowerCase().indexOf(q) !== -1 ||
|
|
ch.description.toLowerCase().indexOf(q) !== -1;
|
|
}
|
|
return true;
|
|
});
|
|
},
|
|
|
|
get configuredCount() {
|
|
return this.allChannels.filter(function(ch) { return ch.configured; }).length;
|
|
},
|
|
|
|
categoryCount(cat) {
|
|
var all = this.allChannels.filter(function(ch) { return cat === 'all' || ch.category === cat; });
|
|
var configured = all.filter(function(ch) { return ch.configured; });
|
|
return configured.length + '/' + all.length;
|
|
},
|
|
|
|
basicFields() {
|
|
if (!this.setupModal || !this.setupModal.fields) return [];
|
|
return this.setupModal.fields.filter(function(f) { return !f.advanced; });
|
|
},
|
|
|
|
advancedFields() {
|
|
if (!this.setupModal || !this.setupModal.fields) return [];
|
|
return this.setupModal.fields.filter(function(f) { return f.advanced; });
|
|
},
|
|
|
|
hasAdvanced() {
|
|
return this.advancedFields().length > 0;
|
|
},
|
|
|
|
isQrChannel() {
|
|
return this.setupModal && this.setupModal.setup_type === 'qr';
|
|
},
|
|
|
|
async loadChannels() {
|
|
this.loading = true;
|
|
this.loadError = '';
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/channels');
|
|
this.allChannels = (data.channels || []).map(function(ch) {
|
|
ch.connected = ch.configured && ch.has_token;
|
|
return ch;
|
|
});
|
|
} catch(e) {
|
|
this.loadError = e.message || 'Could not load channels.';
|
|
}
|
|
this.loading = false;
|
|
this.startPolling();
|
|
},
|
|
|
|
async loadData() { return this.loadChannels(); },
|
|
|
|
startPolling() {
|
|
var self = this;
|
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
this.pollTimer = setInterval(function() { self.refreshStatus(); }, 15000);
|
|
},
|
|
|
|
async refreshStatus() {
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/channels');
|
|
var byName = {};
|
|
(data.channels || []).forEach(function(ch) { byName[ch.name] = ch; });
|
|
this.allChannels.forEach(function(c) {
|
|
var fresh = byName[c.name];
|
|
if (fresh) {
|
|
c.configured = fresh.configured;
|
|
c.has_token = fresh.has_token;
|
|
c.connected = fresh.configured && fresh.has_token;
|
|
c.fields = fresh.fields;
|
|
}
|
|
});
|
|
} catch(e) { console.warn('Channel refresh failed:', e.message); }
|
|
},
|
|
|
|
statusBadge(ch) {
|
|
if (!ch.configured) return { text: 'Not Configured', cls: 'badge-muted' };
|
|
if (!ch.has_token) return { text: 'Missing Token', cls: 'badge-warn' };
|
|
if (ch.connected) return { text: 'Ready', cls: 'badge-success' };
|
|
return { text: 'Configured', cls: 'badge-info' };
|
|
},
|
|
|
|
difficultyClass(d) {
|
|
if (d === 'Easy') return 'difficulty-easy';
|
|
if (d === 'Hard') return 'difficulty-hard';
|
|
return 'difficulty-medium';
|
|
},
|
|
|
|
openSetup(ch) {
|
|
this.setupModal = ch;
|
|
this.formValues = {};
|
|
this.showAdvanced = false;
|
|
this.showBusinessApi = false;
|
|
this.setupStep = ch.configured ? 3 : 1;
|
|
this.testPassed = !!ch.configured;
|
|
this.resetQR();
|
|
// Auto-start QR flow for QR-type channels
|
|
if (ch.setup_type === 'qr') {
|
|
this.startQR();
|
|
}
|
|
},
|
|
|
|
// ── QR Code Flow (WhatsApp Web style) ──────────────────────────
|
|
|
|
resetQR() {
|
|
this.qr = {
|
|
loading: false, available: false, dataUrl: '', sessionId: '',
|
|
message: '', help: '', connected: false, expired: false, error: ''
|
|
};
|
|
if (this.qrPollTimer) { clearInterval(this.qrPollTimer); this.qrPollTimer = null; }
|
|
},
|
|
|
|
async startQR() {
|
|
this.qr.loading = true;
|
|
this.qr.error = '';
|
|
this.qr.connected = false;
|
|
this.qr.expired = false;
|
|
try {
|
|
var result = await OpenFangAPI.post('/api/channels/whatsapp/qr/start', {});
|
|
this.qr.available = result.available || false;
|
|
this.qr.dataUrl = result.qr_data_url || '';
|
|
this.qr.sessionId = result.session_id || '';
|
|
this.qr.message = result.message || '';
|
|
this.qr.help = result.help || '';
|
|
this.qr.connected = result.connected || false;
|
|
if (this.qr.available && this.qr.dataUrl && !this.qr.connected) {
|
|
this.pollQR();
|
|
}
|
|
if (this.qr.connected) {
|
|
OpenFangToast.success('WhatsApp connected!');
|
|
await this.refreshStatus();
|
|
}
|
|
} catch(e) {
|
|
this.qr.error = e.message || 'Could not start QR login';
|
|
}
|
|
this.qr.loading = false;
|
|
},
|
|
|
|
pollQR() {
|
|
var self = this;
|
|
if (this.qrPollTimer) clearInterval(this.qrPollTimer);
|
|
this.qrPollTimer = setInterval(async function() {
|
|
try {
|
|
var result = await OpenFangAPI.get('/api/channels/whatsapp/qr/status?session_id=' + encodeURIComponent(self.qr.sessionId));
|
|
if (result.connected) {
|
|
clearInterval(self.qrPollTimer);
|
|
self.qrPollTimer = null;
|
|
self.qr.connected = true;
|
|
self.qr.message = result.message || 'Connected!';
|
|
OpenFangToast.success('WhatsApp linked successfully!');
|
|
await self.refreshStatus();
|
|
} else if (result.expired) {
|
|
clearInterval(self.qrPollTimer);
|
|
self.qrPollTimer = null;
|
|
self.qr.expired = true;
|
|
self.qr.message = 'QR code expired. Click to generate a new one.';
|
|
} else {
|
|
self.qr.message = result.message || 'Waiting for scan...';
|
|
}
|
|
} catch(e) { /* silent retry */ }
|
|
}, 3000);
|
|
},
|
|
|
|
// ── Standard Form Flow ─────────────────────────────────────────
|
|
|
|
async saveChannel() {
|
|
if (!this.setupModal) return;
|
|
var name = this.setupModal.name;
|
|
this.configuring = true;
|
|
try {
|
|
await OpenFangAPI.post('/api/channels/' + name + '/configure', {
|
|
fields: this.formValues
|
|
});
|
|
this.setupStep = 2;
|
|
// Auto-test after save
|
|
try {
|
|
var testResult = await OpenFangAPI.post('/api/channels/' + name + '/test', {});
|
|
if (testResult.status === 'ok') {
|
|
this.testPassed = true;
|
|
this.setupStep = 3;
|
|
OpenFangToast.success(this.setupModal.display_name + ' activated!');
|
|
} else {
|
|
OpenFangToast.success(this.setupModal.display_name + ' saved. ' + (testResult.message || ''));
|
|
}
|
|
} catch(te) {
|
|
OpenFangToast.success(this.setupModal.display_name + ' saved. Test to verify connection.');
|
|
}
|
|
await this.refreshStatus();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed: ' + (e.message || 'Unknown error'));
|
|
}
|
|
this.configuring = false;
|
|
},
|
|
|
|
async removeChannel() {
|
|
if (!this.setupModal) return;
|
|
var name = this.setupModal.name;
|
|
var displayName = this.setupModal.display_name;
|
|
var self = this;
|
|
OpenFangToast.confirm('Remove Channel', 'Remove ' + displayName + ' configuration? This will deactivate the channel.', async function() {
|
|
try {
|
|
await OpenFangAPI.delete('/api/channels/' + name + '/configure');
|
|
OpenFangToast.success(displayName + ' removed and deactivated.');
|
|
await self.refreshStatus();
|
|
self.setupModal = null;
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed: ' + (e.message || 'Unknown error'));
|
|
}
|
|
});
|
|
},
|
|
|
|
async testChannel() {
|
|
if (!this.setupModal) return;
|
|
var name = this.setupModal.name;
|
|
this.testing[name] = true;
|
|
try {
|
|
var result = await OpenFangAPI.post('/api/channels/' + name + '/test', {});
|
|
if (result.status === 'ok') {
|
|
this.testPassed = true;
|
|
this.setupStep = 3;
|
|
OpenFangToast.success(result.message);
|
|
} else {
|
|
OpenFangToast.error(result.message);
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Test failed: ' + (e.message || 'Unknown error'));
|
|
}
|
|
this.testing[name] = false;
|
|
},
|
|
|
|
async copyConfig(ch) {
|
|
var tpl = ch ? ch.config_template : (this.setupModal ? this.setupModal.config_template : '');
|
|
if (!tpl) return;
|
|
try {
|
|
await navigator.clipboard.writeText(tpl);
|
|
OpenFangToast.success('Copied to clipboard');
|
|
} catch(e) {
|
|
OpenFangToast.error('Copy failed');
|
|
}
|
|
},
|
|
|
|
destroy() {
|
|
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
if (this.qrPollTimer) { clearInterval(this.qrPollTimer); this.qrPollTimer = null; }
|
|
}
|
|
};
|
|
}
|