pipey 0.2.1

A lightweight HTTP-to-WebSocket event delivery service.
Documentation
{% 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>&nbsp; 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>&nbsp; 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 %}