Add NeuralNet private messaging MVP
mindflux-ci / build (push) Successful in 23s

This commit is contained in:
AliceDarkest
2026-07-14 15:53:07 +02:00
parent a484416ea0
commit 49d588b609
17 changed files with 260 additions and 47 deletions
@@ -0,0 +1,28 @@
CREATE TABLE neuralnet_privacy (
username VARCHAR(100) PRIMARY KEY REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE neuralnet_connections (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
requester VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
addressee VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP NOT NULL,
responded_at TIMESTAMP,
CONSTRAINT uq_neuralnet_connection UNIQUE (requester, addressee),
CONSTRAINT chk_neuralnet_no_self CHECK (requester <> addressee)
);
CREATE INDEX idx_neuralnet_connection_addressee ON neuralnet_connections(addressee, status);
CREATE TABLE neuralnet_messages (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sender VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
recipient VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
body VARCHAR(2000) NOT NULL,
sent_at TIMESTAMP NOT NULL,
read_at TIMESTAMP,
reported BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX idx_neuralnet_message_pair ON neuralnet_messages(sender, recipient, sent_at);
CREATE INDEX idx_neuralnet_message_unread ON neuralnet_messages(recipient, read_at);
@@ -0,0 +1 @@
.nn-head{margin:1.3rem 0}.nn-head>span,.nn-chat small{color:var(--accent);font:700 .72rem/1 monospace;letter-spacing:.14em}.nn-head h1{margin:.4rem 0;color:var(--heading)}.nn-head p{color:var(--muted)}.nn-optin{max-width:700px}.nn-optin p{color:var(--muted);line-height:1.6}.nn-app{display:grid;grid-template-columns:minmax(270px,340px) 1fr;gap:1rem}.nn-sidebar{display:grid;align-content:start;gap:1rem}.nn-request div{display:flex;gap:.45rem}.nn-request .mf-input{min-width:0}.nn-section{border-top:1px solid var(--border);padding-top:.85rem}.nn-section h2{font-size:.82rem;color:var(--heading);margin:0 0 .5rem}.nn-list{display:grid;gap:.45rem}.nn-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--border);background:var(--surface-2);padding:.55rem .65rem;border-radius:8px;color:var(--text);font-size:.84rem}.nn-item button{border:0;background:none;color:var(--secondary);font:inherit;cursor:pointer}.nn-item button:hover,.nn-item button.active{color:var(--accent)}.nn-actions{display:flex;gap:.35rem}.nn-actions button{border:1px solid var(--border);padding:.22rem .38rem;border-radius:5px}.nn-chat{min-height:530px}.nn-empty{height:100%;min-height:430px;display:grid;place-content:center;text-align:center;gap:.5rem;color:var(--muted)}.nn-empty strong{color:var(--heading);font-size:1.1rem}.nn-chat header{border-bottom:1px solid var(--border);padding-bottom:.9rem}.nn-chat h2{margin:.25rem 0 0;color:var(--heading)}.nn-messages{display:flex;flex-direction:column;gap:.6rem;height:380px;overflow:auto;padding:1rem 0}.nn-message{align-self:flex-start;max-width:min(80%,550px);padding:.65rem .8rem;border-radius:10px;background:var(--surface-2);border:1px solid var(--border);color:var(--text);line-height:1.45}.nn-message.mine{align-self:flex-end;border-color:var(--border-accent);background:color-mix(in srgb,var(--accent) 12%,var(--surface))}.nn-message small{display:block;margin-top:.35rem;color:var(--muted);font-size:.66rem}.nn-compose{display:grid;grid-template-columns:1fr auto;gap:.6rem;border-top:1px solid var(--border);padding-top:.85rem}.nn-compose textarea{min-height:52px;resize:vertical;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.65rem;font:inherit}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:800px){.nn-app{grid-template-columns:1fr}.nn-chat{min-height:400px}.nn-messages{height:300px}.nn-request div{flex-wrap:wrap}.nn-request .mf-input{flex:1}}
@@ -0,0 +1 @@
(function(){'use strict';var selected=null,me='',poll=null;function q(s){return document.querySelector(s)}function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}async function api(url,method,body){var r=await fetch('/api/neuralnet'+url,{method:method||'GET',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok){var e=await r.json().catch(()=>({message:'Anfrage fehlgeschlagen.'}));throw new Error(e.message)}return r.status===204?null:r.json()}function item(name,actions){return '<div class="nn-item"><span>'+esc(name)+'</span><span class="nn-actions">'+actions+'</span></div>'}function render(data){q('[data-nn-incoming]').innerHTML=data.incoming.length?data.incoming.map(n=>item(n,'<button data-action="accept" data-user="'+esc(n)+'">Annehmen</button><button data-action="decline" data-user="'+esc(n)+'">Ablehnen</button>')).join(''):'<span class="mf-small-hint">Keine offenen Anfragen.</span>';q('[data-nn-outgoing]').innerHTML=data.outgoing.length?data.outgoing.map(n=>item(n,'<button data-action="cancel" data-user="'+esc(n)+'">Zurückziehen</button>')).join(''):'<span class="mf-small-hint">Keine gesendeten Anfragen.</span>';q('[data-nn-connections]').innerHTML=data.connections.length?data.connections.map(n=>item(n,'<button data-action="chat" data-user="'+esc(n)+'" class="'+(selected===n?'active':'')+'">Chat</button>')).join(''):'<span class="mf-small-hint">Noch keine Verbindungen.</span>'}async function refresh(){var data=await api('/overview');me=data.username;q('#nnOptIn').hidden=data.enabled;q('#nnApp').hidden=!data.enabled;if(data.enabled)render(data)}async function loadChat(){if(!selected)return;var rows=await api('/messages/'+encodeURIComponent(selected));q('[data-nn-empty]').hidden=true;q('[data-nn-conversation]').hidden=false;q('[data-nn-partner]').textContent=selected;q('[data-nn-messages]').innerHTML=rows.map(m=>'<article class="nn-message '+(m.sender===me?'mine':'')+'">'+esc(m.body)+'<small>'+esc(m.sender)+' · '+new Date(m.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'})+'</small></article>').join('');var box=q('[data-nn-messages]');box.scrollTop=box.scrollHeight}function message(text){var old=q('.nn-head p');old.textContent=text;setTimeout(()=>old.textContent='Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.',3500)}function init(){q('[data-nn-enable]').onclick=async()=>{await api('/enable','POST');refresh()};q('[data-nn-request]').onsubmit=async e=>{e.preventDefault();try{await api('/requests','POST',{username:q('#nn-user').value});q('#nn-user').value='';message('Kontaktanfrage gesendet.')}catch(x){message(x.message)}refresh()};q('.nn-sidebar').onclick=async e=>{var b=e.target.closest('[data-action]');if(!b)return;var u=b.dataset.user,a=b.dataset.action;try{if(a==='chat'){selected=u;await refresh();await loadChat();return}if(a==='accept')await api('/requests/'+encodeURIComponent(u)+'/accept','POST');if(a==='decline')await api('/requests/'+encodeURIComponent(u)+'/decline','POST');if(a==='cancel')await api('/requests/'+encodeURIComponent(u),'DELETE');await refresh()}catch(x){message(x.message)}};q('[data-nn-compose]').onsubmit=async e=>{e.preventDefault();try{await api('/messages','POST',{recipient:selected,body:q('#nn-message').value});q('#nn-message').value='';loadChat()}catch(x){message(x.message)}};refresh().catch(x=>message(x.message));poll=setInterval(()=>{refresh();if(selected)loadChat()},5000)}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init()})();
+1 -1
View File
@@ -64,7 +64,7 @@
</svg>
<span class="ft-trigger-dot" aria-hidden="true"></span>
</button>
<button class="mf-iconbtn" type="button" aria-label="Benachrichtigungen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></button>
<a class="mf-iconbtn" href="/soziales" aria-label="NeuralNet öffnen" title="NeuralNet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></a>
<div class="mf-search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg><input placeholder="Suche..."></div>
<button class="mf-theme"></button>
<form th:action="@{/logout}" method="post" style="display:inline">
+24 -45
View File
@@ -1,51 +1,30 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SysnaptiK Neural Soziales</title>
<link rel="stylesheet" href="/css/theme.css?v=3">
<script src="/js/theme.js?v=3"></script>
<style>
.mf-soziales { text-align:center; padding:2.5rem 1.5rem; }
.mf-soziales h1 { color:var(--heading); font-size:1.6rem; line-height:1.4; margin-bottom:1rem; }
.mf-soziales p { color:var(--text); line-height:1.6; }
.mf-time-machine { font-size:3rem; margin:1.6rem 0; filter:drop-shadow(0 0 12px var(--glow-s)); }
.mf-small-hint { color:var(--muted); font-size:.86rem; margin-top:1rem; }
</style>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeuralNet – SysnaptiK Neural</title>
<link rel="stylesheet" href="/css/theme.css?v=3"><link rel="stylesheet" href="/css/neuralnet.css?v=1">
<script src="/js/theme.js?v=3"></script><script src="/js/neuralnet.js?v=1" defer></script>
</head>
<body>
<div class="mf-theme-float"><button class="mf-theme"></button></div>
<div class="mf-container">
<div class="mf-page-head">
<a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a>
</div>
<nav th:replace="~{fragments/layout :: profileTabs('soziales')}"></nav>
<div class="mf-panel accent mf-soziales">
<h1>„Straßen? Wo wir hingehen, brauchen wir keine Straßen!“ 🚀</h1>
<p>Das soziale Netzwerk für SysnaptiK Neural-User befindet sich noch in Entwicklung.<br>
Bald kannst du dich hier mit anderen Neural-Mitgliedern verbinden!</p>
<div class="mf-time-machine">
🚗💨💥
</div>
<p class="mf-small-hint">Zurück in die Zukunft... und bald auch ins SysnaptiK Neural-Netzwerk!</p>
</div>
</div>
<footer class="mf-footer">
<a href="#">Support</a>
<a href="#">Feedback</a>
<a href="/nutzungsbedingungen">Nutzungsbedingungen</a>
<a href="/richtlinien">Richtlinien</a>
<a href="/datenschutz">Datenschutz</a>
<a href="/impressum">Impressum</a>
</footer>
</body>
</html>
<main class="mf-container">
<div class="mf-page-head"><a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a></div>
<nav th:replace="~{fragments/layout :: profileTabs('soziales')}"></nav>
<section class="nn-head"><span>NEURALNET</span><h1>Private Lernverbindungen</h1><p>Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.</p></section>
<section id="nnOptIn" class="mf-panel nn-optin" hidden><h2>NeuralNet aktivieren</h2><p>Mit der Aktivierung wirst du für andere Mitglieder auffindbar und kannst Kontaktanfragen senden oder erhalten. Private Nachrichten sind erst nach gegenseitiger Verbindung möglich.</p><button class="mf-btn" data-nn-enable>NeuralNet aktivieren</button></section>
<section id="nnApp" class="nn-app" hidden>
<aside class="mf-panel nn-sidebar">
<form class="nn-request" data-nn-request><label class="mf-label" for="nn-user">Verbindung anfragen</label><div><input class="mf-input" id="nn-user" maxlength="100" placeholder="Benutzername" required><button class="mf-btn" type="submit">Senden</button></div></form>
<div class="nn-section"><h2>Offene Anfragen</h2><div data-nn-incoming class="nn-list"></div></div>
<div class="nn-section"><h2>Gesendet</h2><div data-nn-outgoing class="nn-list"></div></div>
<div class="nn-section"><h2>Verbindungen</h2><div data-nn-connections class="nn-list"></div></div>
</aside>
<section class="mf-panel nn-chat">
<div data-nn-empty class="nn-empty"><strong>Wähle eine Verbindung</strong><span>Private Nachrichten werden erst nach einer angenommenen Anfrage freigeschaltet.</span></div>
<div data-nn-conversation hidden><header><div><small>PRIVATE VERBINDUNG</small><h2 data-nn-partner></h2></div></header><div class="nn-messages" data-nn-messages aria-live="polite"></div><form class="nn-compose" data-nn-compose><label class="sr-only" for="nn-message">Nachricht</label><textarea id="nn-message" maxlength="2000" placeholder="Nachricht schreiben …" required></textarea><button class="mf-btn" type="submit">Senden</button></form></div>
</section>
</section>
</main>
<footer class="mf-footer"><a href="#">Support</a><a href="#">Feedback</a><a href="/nutzungsbedingungen">Nutzungsbedingungen</a><a href="/richtlinien">Richtlinien</a><a href="/datenschutz">Datenschutz</a><a href="/impressum">Impressum</a></footer>
</body></html>