{% extends "base.html" %}
{% block title %}Pipey | Test client{% endblock %}
{% block styles %}
<style>
h1.page-title{ font-size:27px; font-weight:700; margin-bottom:6px; }
p.subtitle{ color:var(--muted); font-size:14.5px; margin-bottom:0; }
h2.card-title{ font-size:15px; font-weight:700; margin-bottom:16px; }
label{
display:block;
margin-bottom:8px;
font-size:12px;
font-weight:700;
color:var(--muted);
text-transform:uppercase;
letter-spacing:0.04em;
}
input, textarea{
width:100%;
padding:12px 14px;
border:1px solid var(--border-2);
border-radius:var(--radius-sm);
outline:none;
font-size:14.5px;
background:var(--surface-2);
color:var(--ink);
font-family:var(--font-mono);
transition:border-color .15s ease, background .15s ease;
}
input:focus, textarea:focus{ border-color:var(--accent); background:var(--surface); }
textarea{ min-height:170px; resize:vertical; line-height:1.6; }
button{
margin-top:16px;
padding:11px 22px;
border:none;
border-radius:999px;
background:var(--accent);
color:var(--accent-ink);
cursor:pointer;
font-size:14.5px;
font-weight:700;
font-family:var(--font-body);
box-shadow:0 6px 16px -6px #ffab2e66;
transition:transform .12s ease, background .12s ease;
}
button:hover{ background:var(--accent-dark); transform:translateY(-1px); }
.status-line{
margin-top:18px;
display:flex; align-items:center; gap:10px;
font-size:14px;
color:var(--muted);
}
.pill{
display:inline-flex; align-items:center; gap:7px;
padding:5px 13px;
border-radius:999px;
font-size:13px;
font-weight:700;
font-family:var(--font-mono);
}
.pill-dot{ width:7px; height:7px; border-radius:50%; }
.pill.connected{ background:var(--live-soft); color:var(--live); }
.pill.connected .pill-dot{ background:var(--live); box-shadow:0 0 0 3px var(--live-soft); }
.pill.disconnected{ background:var(--error-soft); color:var(--error); }
.pill.disconnected .pill-dot{ background:var(--error); }
#messages{
background:#08090a;
border:1px solid var(--border);
color:#e9e9f0;
border-radius:var(--radius-sm);
padding:16px 18px;
min-height:260px;
max-height:340px;
overflow:auto;
white-space:pre-wrap;
font-size:13.5px;
font-family:var(--font-mono);
line-height:1.7;
}
#messages:empty::before{ content:"Waiting for activity…"; color:var(--muted-2); }
.divider{ color:var(--border-2); }
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="eyebrow">Test client</div>
<h1 class="page-title">Try Pipey live</h1>
<p class="subtitle">Open a WebSocket connection, then publish an event and watch it arrive.</p>
</div>
<div class="card">
<h2 class="card-title">1 · Connect</h2>
<label for="user">User ID</label>
<input id="user" value="550e8400-e29b-41d4-a716-446655440000">
<button onclick="connect()"><i class="fa-solid fa-plug"></i> Connect</button>
<div class="status-line">
Status:
<span id="status" class="pill disconnected"><span class="pill-dot"></span>Disconnected</span>
</div>
</div>
<div class="card">
<h2 class="card-title">2 · Publish event</h2>
<label for="payload">Payload (JSON)</label>
<textarea id="payload">{
"message": "Hello Pipey!",
"count": 1
}</textarea>
<button onclick="publishEvent()"><i class="fa-solid fa-paper-plane"></i> Publish event</button>
</div>
<div class="card">
<h2 class="card-title">3 · Incoming events</h2>
<div id="messages"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
let socket = null;
const status = document.getElementById("status");
const messages = document.getElementById("messages");
function setStatus(connected) {
status.className = "pill " + (connected ? "connected" : "disconnected");
status.innerHTML =
'<span class="pill-dot"></span>' +
(connected ? "Connected" : "Disconnected");
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function log(message) {
const divider = messages.innerHTML
? '<div class="divider">────────────────────────────</div>'
: "";
messages.innerHTML += `
${divider}
<pre style="margin:0;white-space:pre-wrap;font-family:inherit;background:none;padding:0;color:inherit;">${escapeHtml(message)}</pre>
`;
messages.scrollTop = messages.scrollHeight;
}
function getWebSocketUrl(user) {
const protocol = window.location.protocol === "https:"
? "wss:"
: "ws:";
return `${protocol}//${window.location.host}/api/v1/ws/pipey?user_id=${encodeURIComponent(user)}`;
}
function connect() {
if (socket) {
socket.close();
}
const user = document.getElementById("user").value.trim();
if (!user) {
alert("User ID is required.");
return;
}
const wsUrl = getWebSocketUrl(user);
console.log("Connecting WebSocket:", wsUrl);
socket = new WebSocket(wsUrl);
socket.onopen = () => {
setStatus(true);
log("WebSocket connected");
};
socket.onclose = () => {
setStatus(false);
log("WebSocket disconnected");
};
socket.onerror = (err) => {
console.error("WebSocket error:", err);
log("WebSocket error");
};
socket.onmessage = ({ data }) => {
try {
const json = JSON.parse(data);
log(JSON.stringify(json, null, 2));
} catch {
log(data);
}
};
}
async function publishEvent() {
const user = document.getElementById("user").value.trim();
let payload;
try {
payload = JSON.parse(document.getElementById("payload").value);
} catch {
alert("Payload must be valid JSON.");
return;
}
try {
const response = await fetch("/api/v1/pipey/publish", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
recipient: user,
payload,
}),
});
if (!response.ok) {
const text = await response.text();
alert(`Publish failed:\n${text}`);
}
} catch (err) {
console.error("Publish error:", err);
alert("Failed to publish event.");
}
}
</script>
{% endblock %}