sova-devtools 0.1.10

In-app DevTools bar for Sova (HTML inject, SSE timeline, request snapshots)
Documentation
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { Send, Zap } from "@lucide/vue";
import DataTable from "../components/DataTable.vue";
import EmptyState from "../components/EmptyState.vue";
import Pane from "../components/Pane.vue";
import SearchField from "../components/SearchField.vue";
import { fetchCustomEvents, postAction } from "../api";
import { useDevToolsStore } from "../stores/devtools";
import type { CustomEvent } from "../types";

const store = useDevToolsStore();
const events = ref<CustomEvent[]>([]);
const q = ref("");
const kind = ref("demo.event");
const payload = ref('{"hello":true}');
const loading = ref(false);
const emitError = ref<string | null>(null);

const hasEvents = computed(() => {
  const cfg = store.config as { features?: string[] } | null;
  return cfg?.features?.includes("console-events") ?? false;
});

async function load() {
  events.value = await fetchCustomEvents(store.mount.api);
}

onMounted(() => {
  void load();
  store.onCustomEvent = (e) => {
    events.value = [e, ...events.value].slice(0, 100);
  };
});

async function emitEvent() {
  loading.value = true;
  emitError.value = null;
  let pl: unknown = {};
  try {
    pl = JSON.parse(payload.value || "{}");
  } catch {
    emitError.value = "Payload must be valid JSON";
    loading.value = false;
    return;
  }
  try {
    const res = await postAction(store.api, "events", {
      kind: kind.value,
      payload: pl,
    });
    if (!res.ok) emitError.value = res.error ?? "emit failed";
  } catch (e) {
    emitError.value = e instanceof Error ? e.message : String(e);
  } finally {
    loading.value = false;
  }
}

const filtered = computed(() => {
  const s = q.value.trim().toLowerCase();
  if (!s) return events.value;
  return events.value.filter(
    (e) =>
      e.kind.toLowerCase().includes(s) ||
      JSON.stringify(e.payload).toLowerCase().includes(s),
  );
});

const headers = ["kind", "payload", "time"];
const rows = computed(() =>
  filtered.value.map((e) => [
    e.kind,
    JSON.stringify(e.payload),
    new Date(e.ts_ms).toLocaleTimeString(),
  ]),
);
</script>

<template>
  <div class="flex flex-col gap-3">
    <Pane v-if="hasEvents" title="Emit event" :icon="Zap">
      <div class="flex flex-wrap items-end gap-2">
        <input
          v-model="kind"
          type="text"
          placeholder="kind"
          class="w-40 rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 font-mono text-[11px]"
        />
        <input
          v-model="payload"
          type="text"
          placeholder='{"key":"value"}'
          class="min-w-0 flex-1 rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 font-mono text-[11px]"
        />
        <button
          type="button"
          class="inline-flex items-center gap-1 rounded bg-[var(--dt-accent)] px-3 py-1.5 text-[11px] font-semibold text-white disabled:opacity-50"
          :disabled="loading"
          @click="emitEvent"
        >
          <Send class="size-3.5" /> Emit
        </button>
      </div>
      <p v-if="emitError" class="mt-2 text-[11px] text-[var(--dt-err)]">
        {{ emitError }}
      </p>
    </Pane>

    <EmptyState
      v-if="!events.length"
      title="No custom events"
      hint="hub.emit(kind, payload) or auth/mail EventBus forwards appear here."
      :icon="Zap"
    />
    <div v-else class="flex flex-col gap-3">
      <SearchField v-model="q" placeholder="Filter kind / payload…" />
      <Pane title="Events" :icon="Zap" :hint="`${filtered.length}`">
        <DataTable :headers="headers" :rows="rows" :mono-cols="[1]" />
      </Pane>
    </div>
  </div>
</template>