Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: logo and favicon

Hugo cb7d80c4 3e9749cc

+146 -60
+5
app/components/Layout/Header/index.tsx
··· 6 6 <header class={s.header}> 7 7 <div class={s.inner}> 8 8 <a href="/" class={s.brand}> 9 + <svg class={s.logo} viewBox="0 0 64 64" fill="none" aria-hidden="true"> 10 + <circle cx="32" cy="32" r="10" fill="#E8923A" /> 11 + <circle cx="32" cy="32" r="20" stroke="#E8923A" stroke-width="3.5" opacity="0.45" /> 12 + <circle cx="32" cy="32" r="29" stroke="#E8923A" stroke-width="2.5" opacity="0.2" /> 13 + </svg> 9 14 Airglow 10 15 </a> 11 16 <div class={s.nav}>
+8
app/components/Layout/Header/styles.css.ts
··· 20 20 }); 21 21 22 22 export const brand = style({ 23 + display: "inline-flex", 24 + alignItems: "center", 25 + gap: space[2], 23 26 fontSize: fontSize.lg, 24 27 fontWeight: fontWeight.semibold, 25 28 color: vars.color.heading, ··· 27 30 ":hover": { 28 31 color: vars.color.heading, 29 32 }, 33 + }); 34 + 35 + export const logo = style({ 36 + inlineSize: "24px", 37 + blockSize: "24px", 30 38 }); 31 39 32 40 export const nav = style({
+57
app/icons.ts
··· 1 + // Re-export lucide-preact icons with hono/jsx-compatible types. 2 + // Deep imports avoid pulling in all 3000+ icon modules during dev. 3 + // At runtime the preact Vite alias makes them work; this file fixes the TS type mismatch. 4 + import type { FC } from "hono/jsx"; 5 + 6 + // @ts-expect-error deep import 7 + import ActivityIcon from "lucide-preact/icons/activity.js"; 8 + // @ts-expect-error deep import 9 + import ArrowLeftIcon from "lucide-preact/icons/arrow-left.js"; 10 + // @ts-expect-error deep import 11 + import DatabaseIcon from "lucide-preact/icons/database.js"; 12 + // @ts-expect-error deep import 13 + import EyeIcon from "lucide-preact/icons/eye.js"; 14 + // @ts-expect-error deep import 15 + import FilePlus2Icon from "lucide-preact/icons/file-plus-corner.js"; 16 + // @ts-expect-error deep import 17 + import FilterIcon from "lucide-preact/icons/funnel.js"; 18 + // @ts-expect-error deep import 19 + import FlaskConicalIcon from "lucide-preact/icons/flask-conical.js"; 20 + // @ts-expect-error deep import 21 + import MoonIcon from "lucide-preact/icons/moon.js"; 22 + // @ts-expect-error deep import 23 + import PencilIcon from "lucide-preact/icons/pencil.js"; 24 + // @ts-expect-error deep import 25 + import PlusIcon from "lucide-preact/icons/plus.js"; 26 + // @ts-expect-error deep import 27 + import PowerIcon from "lucide-preact/icons/power.js"; 28 + // @ts-expect-error deep import 29 + import RefreshCwIcon from "lucide-preact/icons/refresh-cw.js"; 30 + // @ts-expect-error deep import 31 + import SunIcon from "lucide-preact/icons/sun.js"; 32 + // @ts-expect-error deep import 33 + import Trash2Icon from "lucide-preact/icons/trash-2.js"; 34 + // @ts-expect-error deep import 35 + import WebhookIcon from "lucide-preact/icons/webhook.js"; 36 + // @ts-expect-error deep import 37 + import ZapIcon from "lucide-preact/icons/zap.js"; 38 + 39 + type IconProps = { size?: number; class?: string; color?: string; "stroke-width"?: number }; 40 + const cast = (icon: unknown) => icon as FC<IconProps>; 41 + 42 + export const Activity = cast(ActivityIcon); 43 + export const ArrowLeft = cast(ArrowLeftIcon); 44 + export const Database = cast(DatabaseIcon); 45 + export const Eye = cast(EyeIcon); 46 + export const FilePlus2 = cast(FilePlus2Icon); 47 + export const Filter = cast(FilterIcon); 48 + export const FlaskConical = cast(FlaskConicalIcon); 49 + export const Moon = cast(MoonIcon); 50 + export const Pencil = cast(PencilIcon); 51 + export const Plus = cast(PlusIcon); 52 + export const Power = cast(PowerIcon); 53 + export const RefreshCw = cast(RefreshCwIcon); 54 + export const Sun = cast(SunIcon); 55 + export const Trash2 = cast(Trash2Icon); 56 + export const Webhook = cast(WebhookIcon); 57 + export const Zap = cast(ZapIcon);
+1
app/islands/DeliveryLog.css.ts
··· 21 21 display: "inline-flex", 22 22 alignItems: "center", 23 23 justifyContent: "center", 24 + gap: space[1], 24 25 paddingBlock: space[1], 25 26 paddingInline: space[3], 26 27 fontSize: fontSize.sm,
+5 -4
app/islands/DeliveryLog.tsx
··· 1 1 import { useState, useCallback, useRef } from "hono/jsx"; 2 + import { Power, FlaskConical, Trash2, RefreshCw } from "../icons.js"; 2 3 import * as s from "./DeliveryLog.css.ts"; 3 4 import { variant as badgeVariant } from "../components/Badge/styles.css.ts"; 4 5 ··· 157 158 <div class={s.wrapper}> 158 159 <div class={s.actions}> 159 160 <button type="button" class={s.toggleBtn} onClick={toggleActive} disabled={loading}> 160 - {isActive ? "Deactivate" : "Activate"} 161 + <Power size={14} /> {isActive ? "Deactivate" : "Activate"} 161 162 </button> 162 163 <button type="button" class={s.toggleBtn} onClick={toggleDryRun} disabled={loading}> 163 - {isDryRun ? "Disable Dry Run" : "Enable Dry Run"} 164 + <FlaskConical size={14} /> {isDryRun ? "Disable Dry Run" : "Enable Dry Run"} 164 165 </button> 165 166 <button type="button" class={s.deleteBtn} onClick={handleDelete} disabled={loading}> 166 - Delete 167 + <Trash2 size={14} /> Delete 167 168 </button> 168 169 </div> 169 170 ··· 172 173 <div class={s.logsHeader}> 173 174 <h3>Delivery Logs</h3> 174 175 <button type="button" class={s.refreshBtn} onClick={refreshLogs}> 175 - Refresh 176 + <RefreshCw size={14} /> Refresh 176 177 </button> 177 178 </div> 178 179
+2 -43
app/islands/ThemeToggle.tsx
··· 1 1 import { useState, useCallback } from "hono/jsx"; 2 + import { Sun, Moon } from "../icons.js"; 2 3 import * as s from "./ThemeToggle.css.ts"; 3 4 4 5 function getInitialTheme(): "light" | "dark" { ··· 6 7 return (document.documentElement.dataset.theme as "light" | "dark") || "light"; 7 8 } 8 9 9 - // Sun icon for dark mode (click to switch to light) 10 - function SunIcon() { 11 - return ( 12 - <svg 13 - class={s.icon} 14 - viewBox="0 0 24 24" 15 - fill="none" 16 - stroke="currentColor" 17 - stroke-width="2" 18 - stroke-linecap="round" 19 - stroke-linejoin="round" 20 - > 21 - <circle cx="12" cy="12" r="5" /> 22 - <line x1="12" y1="1" x2="12" y2="3" /> 23 - <line x1="12" y1="21" x2="12" y2="23" /> 24 - <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /> 25 - <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" /> 26 - <line x1="1" y1="12" x2="3" y2="12" /> 27 - <line x1="21" y1="12" x2="23" y2="12" /> 28 - <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /> 29 - <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" /> 30 - </svg> 31 - ); 32 - } 33 - 34 - // Moon icon for light mode (click to switch to dark) 35 - function MoonIcon() { 36 - return ( 37 - <svg 38 - class={s.icon} 39 - viewBox="0 0 24 24" 40 - fill="none" 41 - stroke="currentColor" 42 - stroke-width="2" 43 - stroke-linecap="round" 44 - stroke-linejoin="round" 45 - > 46 - <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /> 47 - </svg> 48 - ); 49 - } 50 - 51 10 export default function ThemeToggle() { 52 11 const [theme, setTheme] = useState(getInitialTheme); 53 12 ··· 66 25 aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} 67 26 title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} 68 27 > 69 - {theme === "dark" ? <SunIcon /> : <MoonIcon />} 28 + {theme === "dark" ? <Sun class={s.icon} size={20} /> : <Moon class={s.icon} size={20} />} 70 29 </button> 71 30 ); 72 31 }
+1
app/routes/_renderer.tsx
··· 56 56 {headInline} 57 57 {!import.meta.env.PROD && <link rel="stylesheet" href="/__dev.css" />} 58 58 <CssLinks /> 59 + <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> 59 60 <title>{title ?? "Airglow"}</title> 60 61 <Script src="/app/client.ts" async /> 61 62 </head>
+13 -6
app/routes/dashboard/automations/[rkey].tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 2 import { eq, and, desc } from "drizzle-orm"; 3 + import { ArrowLeft, Pencil, Filter, Database, Zap } from "../../../icons.js"; 3 4 import { db } from "@/db/index.js"; 4 5 import { automations, deliveryLogs } from "@/db/schema.js"; 5 6 import { AppShell } from "../../../components/Layout/AppShell/index.js"; ··· 32 33 title="Not Found" 33 34 actions={ 34 35 <Button href="/dashboard" variant="ghost" size="sm"> 35 - &larr; Back 36 + <ArrowLeft size={14} /> Back 36 37 </Button> 37 38 } 38 39 /> ··· 65 66 </Badge> 66 67 </span> 67 68 <Button href={`/dashboard/automations/${rkey}/edit`} variant="secondary" size="sm"> 68 - Edit 69 + <Pencil size={14} /> Edit 69 70 </Button> 70 71 <Button href="/dashboard" variant="ghost" size="sm"> 71 - &larr; Back 72 + <ArrowLeft size={14} /> Back 72 73 </Button> 73 74 </div> 74 75 } ··· 103 104 {auto.conditions.length > 0 && ( 104 105 <Card variant="flat"> 105 106 <Stack gap={3}> 106 - <h3>Conditions</h3> 107 + <h3 class={inlineCluster}> 108 + <Filter size={18} /> Conditions 109 + </h3> 107 110 <ul class={plainList}> 108 111 {auto.conditions.map((cond, i) => { 109 112 const opLabels: Record<string, string> = { ··· 129 132 {auto.fetches.length > 0 && ( 130 133 <Card variant="flat"> 131 134 <Stack gap={3}> 132 - <h3>Data Sources</h3> 135 + <h3 class={inlineCluster}> 136 + <Database size={18} /> Data Sources 137 + </h3> 133 138 <ul class={plainList}> 134 139 {auto.fetches.map((f, i) => ( 135 140 <li key={i}> ··· 143 148 )} 144 149 145 150 <Stack gap={3}> 146 - <h3>Actions ({auto.actions.length})</h3> 151 + <h3 class={inlineCluster}> 152 + <Zap size={18} /> Actions ({auto.actions.length}) 153 + </h3> 147 154 {auto.actions.map((action, i) => ( 148 155 <Card key={i} variant="flat"> 149 156 <Stack gap={2}>
+3 -2
app/routes/dashboard/automations/[rkey]/edit.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 2 import { eq, and } from "drizzle-orm"; 3 + import { ArrowLeft } from "../../../../icons.js"; 3 4 import { db } from "@/db/index.js"; 4 5 import { automations } from "@/db/schema.js"; 5 6 import { AppShell } from "../../../../components/Layout/AppShell/index.js"; ··· 28 29 title="Not Found" 29 30 actions={ 30 31 <Button href="/dashboard" variant="ghost" size="sm"> 31 - &larr; Back 32 + <ArrowLeft size={14} /> Back 32 33 </Button> 33 34 } 34 35 /> ··· 46 47 title={`Edit: ${auto.name}`} 47 48 actions={ 48 49 <Button href={`/dashboard/automations/${rkey}`} variant="ghost" size="sm"> 49 - &larr; Back 50 + <ArrowLeft size={14} /> Back 50 51 </Button> 51 52 } 52 53 />
+2 -1
app/routes/dashboard/automations/new.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 + import { ArrowLeft } from "../../../icons.js"; 2 3 import { AppShell } from "../../../components/Layout/AppShell/index.js"; 3 4 import { Header } from "../../../components/Layout/Header/index.js"; 4 5 import { Container } from "../../../components/Layout/Container/index.js"; ··· 18 19 title="New Automation" 19 20 actions={ 20 21 <Button href="/dashboard" variant="ghost" size="sm"> 21 - &larr; Back 22 + <ArrowLeft size={14} /> Back 22 23 </Button> 23 24 } 24 25 />
+3 -2
app/routes/dashboard/index.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 2 import { eq } from "drizzle-orm"; 3 + import { Plus, Eye } from "../../icons.js"; 3 4 import { db } from "@/db/index.js"; 4 5 import { automations } from "@/db/schema.js"; 5 6 import { AppShell } from "../../components/Layout/AppShell/index.js"; ··· 28 29 description={`${autos.length} automation${autos.length !== 1 ? "s" : ""}`} 29 30 actions={ 30 31 <Button href="/dashboard/automations/new" size="sm"> 31 - New Automation 32 + <Plus size={16} /> New Automation 32 33 </Button> 33 34 } 34 35 /> ··· 78 79 </td> 79 80 <td> 80 81 <Button href={`/dashboard/automations/${auto.rkey}`} variant="ghost" size="sm"> 81 - View 82 + <Eye size={14} /> View 82 83 </Button> 83 84 </td> 84 85 </tr>
+13
app/routes/index.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 + import { Webhook, FilePlus2, Filter, Activity } from "../icons.js"; 2 3 import { getSessionUser } from "@/auth/middleware.js"; 3 4 import { AppShell } from "../components/Layout/AppShell/index.js"; 4 5 import { Header } from "../components/Layout/Header/index.js"; ··· 32 33 33 34 <section class={s.features}> 34 35 <div class={s.featureCard}> 36 + <div class={s.featureIcon}> 37 + <Webhook size={28} /> 38 + </div> 35 39 <h3 class={s.featureTitle}>Webhook Delivery</h3> 36 40 <p class={s.featureDesc}> 37 41 Receive HTTP POST callbacks instantly when matching events occur on the AT Protocol ··· 39 43 </p> 40 44 </div> 41 45 <div class={s.featureCard}> 46 + <div class={s.featureIcon}> 47 + <FilePlus2 size={28} /> 48 + </div> 42 49 <h3 class={s.featureTitle}>Record Creation</h3> 43 50 <p class={s.featureDesc}> 44 51 Automatically create records on your PDS when events match. Use templates with ··· 46 53 </p> 47 54 </div> 48 55 <div class={s.featureCard}> 56 + <div class={s.featureIcon}> 57 + <Filter size={28} /> 58 + </div> 49 59 <h3 class={s.featureTitle}>Smart Filtering</h3> 50 60 <p class={s.featureDesc}> 51 61 Listen to specific record types by NSID. Add field-level conditions with operators ··· 53 63 </p> 54 64 </div> 55 65 <div class={s.featureCard}> 66 + <div class={s.featureIcon}> 67 + <Activity size={28} /> 68 + </div> 56 69 <h3 class={s.featureTitle}>Delivery Tracking</h3> 57 70 <p class={s.featureDesc}> 58 71 Full delivery log with status codes, retry attempts, and error details. Know exactly
+5
app/styles/pages/landing.css.ts
··· 66 66 backgroundColor: vars.color.surface, 67 67 }); 68 68 69 + export const featureIcon = style({ 70 + color: vars.color.accent, 71 + marginBlockEnd: space[3], 72 + }); 73 + 69 74 export const featureTitle = style({ 70 75 fontSize: fontSize.base, 71 76 fontWeight: fontWeight.semibold,
+5
bun.lock
··· 12 12 "drizzle-orm": "^0.45.2", 13 13 "hono": "^4.12.12", 14 14 "honox": "^0.1.55", 15 + "lucide-preact": "^1.8.0", 15 16 "nanoid": "^5.1.7", 16 17 "vite": "npm:@voidzero-dev/vite-plus-core@latest", 17 18 "vitest": "^0.1.16", ··· 542 543 543 544 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 544 545 546 + "lucide-preact": ["lucide-preact@1.8.0", "", { "peerDependencies": { "preact": "^10.27.2" } }, "sha512-va4a8kofUvE324fKPgBIewfpm9IiGm1TwMN8NoveK2wwywuZIQxsXX9IJVB0+rM1I79XBMnfWNuLjIF2LwAXiQ=="], 547 + 545 548 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 546 549 547 550 "media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w=="], ··· 607 610 "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], 608 611 609 612 "postcss-values-parser": ["postcss-values-parser@6.0.2", "", { "dependencies": { "color-name": "^1.1.4", "is-url-superb": "^4.0.0", "quote-unquote": "^1.0.0" }, "peerDependencies": { "postcss": "^8.2.9" } }, "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw=="], 613 + 614 + "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], 610 615 611 616 "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], 612 617
+2
lib/shims/preact-hooks.ts
··· 1 + // Preact hooks shim — re-exports hono/jsx hooks for lucide-preact compatibility. 2 + export { useContext, useMemo } from "hono/jsx";
+9
lib/shims/preact.ts
··· 1 + // Preact compatibility shim — maps preact APIs to hono/jsx equivalents 2 + // so that lucide-preact works in this hono/jsx project. 3 + export { createElement as h, createContext, Fragment } from "hono/jsx"; 4 + 5 + export function toChildArray(children: unknown): unknown[] { 6 + if (children == null || children === false) return []; 7 + if (Array.isArray(children)) return children.flat(Infinity); 8 + return [children]; 9 + }
+1
package.json
··· 17 17 "drizzle-orm": "^0.45.2", 18 18 "hono": "^4.12.12", 19 19 "honox": "^0.1.55", 20 + "lucide-preact": "^1.8.0", 20 21 "nanoid": "^5.1.7", 21 22 "vite": "npm:@voidzero-dev/vite-plus-core@latest", 22 23 "vitest": "^0.1.16"
+5
public/favicon.svg
··· 1 + <svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <circle cx="32" cy="32" r="10" fill="#E8923A"/> 3 + <circle cx="32" cy="32" r="20" stroke="#E8923A" stroke-width="3.5" opacity="0.45"/> 4 + <circle cx="32" cy="32" r="29" stroke="#E8923A" stroke-width="2.5" opacity="0.2"/> 5 + </svg>
+6 -2
vite.config.ts
··· 1 1 import { execSync } from "node:child_process"; 2 2 import { request as httpRequest } from "node:http"; 3 + import { resolve } from "node:path"; 3 4 import honox from "honox/vite"; 4 5 import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; 5 6 import { defineConfig } from "vite-plus"; ··· 178 179 test: { 179 180 silent: "passed-only", 180 181 }, 181 - fmt: {}, 182 - lint: { options: { typeAware: true, typeCheck: true } }, 182 + fmt: { ignorePatterns: ["lib/db/migrations"] }, 183 + lint: { ignorePatterns: ["lib/db/migrations"], options: { typeAware: true, typeCheck: true } }, 183 184 server: { 184 185 port: 5175, 185 186 host: true, ··· 191 192 "bun:sqlite": "/lib/db/sqlite-compat.ts", 192 193 "better-sqlite3": "/lib/db/sqlite-compat.ts", 193 194 "drizzle-orm/bun-sqlite": "drizzle-orm/better-sqlite3", 195 + "lucide-preact/icons": resolve(import.meta.dirname, "node_modules/lucide-preact/dist/esm/icons"), 196 + "preact/hooks": resolve(import.meta.dirname, "lib/shims/preact-hooks.ts"), 197 + preact: resolve(import.meta.dirname, "lib/shims/preact.ts"), 194 198 }, 195 199 }, 196 200 });