Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
Adds a `credentials` entity to the back office (never mirrored to Sheets, gated by the CRM token even to read) so client logins like the auto-generated Easy!Appointments provider password can be viewed and copied from the dashboard instead of getting lost — the actual cause of the happynails password going missing. Onboarding now saves that generated password instead of discarding it. Also adds Documentation.md, brings README/TODO in line with the current Postgres-first architecture, and tidies the backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,7 @@
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-e="leads">Leads<span class="n" id="n-leads"></span></button>
|
||||
<button class="tab" data-e="clients">Clients<span class="n" id="n-clients"></span></button>
|
||||
<button class="tab" data-e="credentials">Credentials<span class="n" id="n-credentials"></span></button>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<input id="search" placeholder="Filtern …" />
|
||||
@@ -112,20 +113,25 @@
|
||||
const COLS = {
|
||||
leads: ['lead_id','received_at','client_id','source','name','contact','service_interest','message','status','notified'],
|
||||
clients: ['client_id','business_name','owner_name','email','phone','niche','tier','status','billing_cycle','monthly_fee_eur','renewal_date','created_at'],
|
||||
credentials: ['cred_id','client_id','label','username','secret','created_at'],
|
||||
};
|
||||
const PK = { leads: 'lead_id', clients: 'client_id' };
|
||||
const PK = { leads: 'lead_id', clients: 'client_id', credentials: 'cred_id' };
|
||||
let current = 'leads';
|
||||
let cache = {};
|
||||
let revealed = new Set(); // cred_ids currently shown in plaintext (resets on tab switch/reload)
|
||||
|
||||
const esc = s => (s==null?'':String(s)).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
function showErr(t){ const m=document.getElementById('msg'); m.textContent=t; m.className='msg err'; }
|
||||
function clearErr(){ document.getElementById('msg').className='msg'; }
|
||||
function showOk(t){ const m=document.getElementById('msg'); m.textContent=t; m.className='msg'; m.style.cssText='display:block;background:#e3f6ec;color:#0c6b3c;border:1px solid #b8e6cc;padding:10px 14px;border-radius:9px;font-size:.85rem;margin-bottom:12px;'; setTimeout(clearErr, 1800); }
|
||||
function clearErr(){ const m=document.getElementById('msg'); m.className='msg'; m.style.cssText=''; }
|
||||
|
||||
async function load(entity){
|
||||
clearErr();
|
||||
revealed.clear();
|
||||
document.getElementById('table').innerHTML = '<div class="empty">Lädt …</div>';
|
||||
try {
|
||||
const r = await fetch(`${API}/${entity}`);
|
||||
// Credentials require the token to read (see app.py); harmless to send it always.
|
||||
const r = await fetch(`${API}/${entity}`, { headers: { 'X-CRM-Token': TOKEN } });
|
||||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||||
const d = await r.json();
|
||||
cache[entity] = d.rows;
|
||||
@@ -150,6 +156,13 @@
|
||||
let v = r[c];
|
||||
if(c==='status' || c==='tier') return `<td><span class="pillv">${esc(v)}</span></td>`;
|
||||
if((c==='received_at'||c==='created_at') && v) v = String(v).replace('T',' ').slice(0,16);
|
||||
if(c==='secret') {
|
||||
const shown = revealed.has(id);
|
||||
const display = shown ? esc(v) : '••••••••';
|
||||
return `<td><code>${display}</code> ` +
|
||||
`<button class="act" title="${shown?'Verbergen':'Anzeigen'}" onclick="toggleSecret('${esc(id)}')">${shown?'🙈':'👁'}</button>` +
|
||||
`<button class="act" title="Kopieren" onclick="copySecret('${esc(id)}')">📋</button></td>`;
|
||||
}
|
||||
return `<td title="${esc(v)}">${esc(v)}</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}<td class="actcol"><button class="act" title="Bearbeiten" onclick="openForm('${esc(id)}')">Bearbeiten</button><button class="del" title="Löschen" onclick="del('${esc(id)}')">Löschen</button></td></tr>`;
|
||||
@@ -157,6 +170,19 @@
|
||||
document.getElementById('table').innerHTML = `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
|
||||
function toggleSecret(id){
|
||||
if(revealed.has(id)) revealed.delete(id); else revealed.add(id);
|
||||
render();
|
||||
}
|
||||
async function copySecret(id){
|
||||
const row = (cache.credentials||[]).find(r => r.cred_id === id);
|
||||
if(!row) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(row.secret || '');
|
||||
showOk('✓ Passwort kopiert (' + (row.label || row.cred_id) + ')');
|
||||
} catch(e){ showErr('Kopieren fehlgeschlagen: '+e.message); }
|
||||
}
|
||||
|
||||
async function del(id){
|
||||
if(!confirm(`${current.slice(0,-1).toUpperCase()} „${id}" wirklich löschen?`)) return;
|
||||
clearErr();
|
||||
@@ -189,6 +215,10 @@
|
||||
{k:'services',wide:true},{k:'stack_notes',t:'textarea',wide:true},
|
||||
{k:'vault_ref'},{k:'notes',t:'textarea',wide:true},
|
||||
],
|
||||
credentials: [
|
||||
{k:'client_id'},{k:'label'},{k:'username'},{k:'secret'},
|
||||
{k:'notes',t:'textarea',wide:true},
|
||||
],
|
||||
};
|
||||
let editId = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user