fmt: use biome

This commit is contained in:
2025-12-06 22:26:28 +08:00
parent 8be7af6815
commit 9a82ecedac
22 changed files with 400 additions and 429 deletions

View File

@@ -35,6 +35,7 @@
pkg-config
sqlite
gemini-cli
biome
];
buildInputs = [ ];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs);

41
frontend/biome.json Normal file
View File

@@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -1,13 +1,13 @@
import { Component, createSignal, onMount, Show } from 'solid-js';
import RulesList from './components/RulesList';
import CommandQueue from './components/CommandQueue';
import Login from './components/Login';
import ChangePassword from './components/ChangePassword';
import RequestLog from './components/RequestLog';
import { authStore } from './api/auth';
import { Component, createSignal, onMount, Show } from "solid-js";
import RulesList from "./components/RulesList";
import CommandQueue from "./components/CommandQueue";
import Login from "./components/Login";
import ChangePassword from "./components/ChangePassword";
import RequestLog from "./components/RequestLog";
import { authStore } from "./api/auth";
const App: Component = () => {
const [activeTab, setActiveTab] = createSignal<'rules' | 'commands' | 'logs'>('rules');
const [activeTab, setActiveTab] = createSignal<"rules" | "commands" | "logs">("rules");
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
const [showChangePassword, setShowChangePassword] = createSignal(false);
@@ -31,9 +31,7 @@ const App: Component = () => {
<div class="min-h-screen bg-gray-100">
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-6 flex justify-between items-center">
<h1 class="text-3xl font-bold text-gray-900">
My Linspirer Control Panel
</h1>
<h1 class="text-3xl font-bold text-gray-900">My Linspirer Control Panel</h1>
<div class="flex space-x-3">
<button
onClick={() => setShowChangePassword(true)}
@@ -55,31 +53,31 @@ const App: Component = () => {
<div class="max-w-7xl mx-auto px-4">
<div class="flex space-x-8">
<button
onClick={() => setActiveTab('rules')}
onClick={() => setActiveTab("rules")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'rules'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
activeTab() === "rules"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Interception Rules
</button>
<button
onClick={() => setActiveTab('commands')}
onClick={() => setActiveTab("commands")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'commands'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
activeTab() === "commands"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Command Queue
</button>
<button
onClick={() => setActiveTab('logs')}
onClick={() => setActiveTab("logs")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'logs'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
activeTab() === "logs"
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Request Log
@@ -89,16 +87,13 @@ const App: Component = () => {
</nav>
<main class="max-w-7xl mx-auto px-4 py-6">
{activeTab() === 'rules' && <RulesList />}
{activeTab() === 'commands' && <CommandQueue />}
{activeTab() === 'logs' && <RequestLog />}
{activeTab() === "rules" && <RulesList />}
{activeTab() === "commands" && <CommandQueue />}
{activeTab() === "logs" && <RequestLog />}
</main>
<Show when={showChangePassword()}>
<ChangePassword
onClose={() => setShowChangePassword(false)}
onLogout={handleLogout}
/>
<ChangePassword onClose={() => setShowChangePassword(false)} onLogout={handleLogout} />
</Show>
</div>
</Show>

View File

@@ -1,4 +1,4 @@
const TOKEN_KEY = 'admin_token';
const TOKEN_KEY = "admin_token";
export const authStore = {
getToken(): string | null {

View File

@@ -1,18 +1,22 @@
import type { InterceptionRule, Command, CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest, RequestLog } from '../types';
import { authStore } from './auth';
import type {
InterceptionRule,
Command,
CreateRuleRequest,
UpdateRuleRequest,
UpdateCommandRequest,
RequestLog,
} from "../types";
import { authStore } from "./auth";
const API_BASE = '/admin/api';
const API_BASE = "/admin/api";
async function request<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const token = authStore.getToken();
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options?.headers,
},
...options,
@@ -22,7 +26,7 @@ async function request<T>(
// Unauthorized - clear token and trigger re-authentication
authStore.clearToken();
window.location.reload();
throw new Error('Unauthorized');
throw new Error("Unauthorized");
}
if (!response.ok) {
@@ -32,14 +36,14 @@ async function request<T>(
if (response.status === 204 || response.status === 200) {
// Check if response has content
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
return undefined as T;
}
// Check if there's actually content to parse
const text = await response.text();
if (!text || text.trim() === '') {
if (!text || text.trim() === "") {
return undefined as T;
}
@@ -51,43 +55,40 @@ async function request<T>(
// Rules API
export const rulesApi = {
list: () =>
request<InterceptionRule[]>('/rules'),
list: () => request<InterceptionRule[]>("/rules"),
create: (rule: CreateRuleRequest) =>
request<InterceptionRule>('/rules', {
method: 'POST',
request<InterceptionRule>("/rules", {
method: "POST",
body: JSON.stringify(rule),
}),
update: (id: number, rule: UpdateRuleRequest) =>
request<InterceptionRule>(`/rules/${id}`, {
method: 'PUT',
method: "PUT",
body: JSON.stringify(rule),
}),
delete: (id: number) =>
request<void>(`/rules/${id}`, { method: 'DELETE' }),
delete: (id: number) => request<void>(`/rules/${id}`, { method: "DELETE" }),
};
// Commands API
export const commandsApi = {
list: (status?: string) =>
request<Command[]>(`/commands${status ? `?status=${status}` : ''}`),
list: (status?: string) => request<Command[]>(`/commands${status ? `?status=${status}` : ""}`),
updateStatus: (id: number, req: UpdateCommandRequest) =>
request<Command>(`/commands/${id}`, {
method: 'POST',
method: "POST",
body: JSON.stringify(req),
}),
};
// Config API
export const configApi = {
get: () => request<Record<string, string>>('/config'),
get: () => request<Record<string, string>>("/config"),
update: (config: Record<string, string>) =>
request<void>('/config', {
method: 'PUT',
request<void>("/config", {
method: "PUT",
body: JSON.stringify(config),
}),
};
@@ -95,8 +96,8 @@ export const configApi = {
// Auth API
export const authApi = {
changePassword: (oldPassword: string, newPassword: string) =>
request<void>('/password', {
method: 'PUT',
request<void>("/password", {
method: "PUT",
body: JSON.stringify({
old_password: oldPassword,
new_password: newPassword,
@@ -109,12 +110,12 @@ export const logsApi = {
list: (params?: { method?: string; search?: string }) => {
const query = new URLSearchParams();
if (params?.method) {
query.set('method', params.method);
query.set("method", params.method);
}
if (params?.search) {
query.set('search', params.search);
query.set("search", params.search);
}
const queryString = query.toString();
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ''}`);
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ""}`);
},
};

View File

@@ -1,10 +1,10 @@
import { Component, createSignal } from 'solid-js';
import { authApi } from '../api/client';
import { authStore } from '../api/auth';
import { Button } from './ui/Button';
import { CardContent, CardFooter, CardHeader } from './ui/Card';
import { Input } from './ui/Input';
import { Modal, ModalContent } from './ui/Modal';
import { Component, createSignal } from "solid-js";
import { authApi } from "../api/client";
import { authStore } from "../api/auth";
import { Button } from "./ui/Button";
import { CardContent, CardFooter, CardHeader } from "./ui/Card";
import { Input } from "./ui/Input";
import { Modal, ModalContent } from "./ui/Modal";
interface ChangePasswordProps {
onClose: () => void;
@@ -12,9 +12,9 @@ interface ChangePasswordProps {
}
const ChangePassword: Component<ChangePasswordProps> = (props) => {
const [oldPassword, setOldPassword] = createSignal('');
const [newPassword, setNewPassword] = createSignal('');
const [confirmPassword, setConfirmPassword] = createSignal('');
const [oldPassword, setOldPassword] = createSignal("");
const [newPassword, setNewPassword] = createSignal("");
const [confirmPassword, setConfirmPassword] = createSignal("");
const [error, setError] = createSignal<string | null>(null);
const [success, setSuccess] = createSignal(false);
const [loading, setLoading] = createSignal(false);
@@ -26,17 +26,17 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
// Validation
if (!oldPassword() || !newPassword() || !confirmPassword()) {
setError('Please fill in all fields');
setError("Please fill in all fields");
return;
}
if (newPassword() !== confirmPassword()) {
setError('New passwords do not match');
setError("New passwords do not match");
return;
}
if (newPassword().length < 6) {
setError('New password must be at least 6 characters');
setError("New password must be at least 6 characters");
return;
}
@@ -45,9 +45,9 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
try {
await authApi.changePassword(oldPassword(), newPassword());
setSuccess(true);
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
setOldPassword("");
setNewPassword("");
setConfirmPassword("");
// Close modal and logout after 2 seconds
setTimeout(() => {
@@ -56,7 +56,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
props.onLogout();
}, 2000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to change password');
setError(err instanceof Error ? err.message : "Failed to change password");
} finally {
setLoading(false);
}
@@ -72,9 +72,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
<form onSubmit={handleSubmit}>
<CardContent>
{error() && (
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error()}
</div>
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">{error()}</div>
)}
{success() && (
@@ -84,9 +82,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
)}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Old Password
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">Old Password</label>
<Input
type="password"
value={oldPassword()}
@@ -96,9 +92,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
New Password
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
<Input
type="password"
value={newPassword()}
@@ -108,9 +102,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Confirm New Password
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm New Password</label>
<Input
type="password"
value={confirmPassword()}
@@ -120,19 +112,11 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div>
</CardContent>
<CardFooter class="flex justify-end space-x-3 pt-4">
<Button
type="button"
variant="secondary"
onClick={props.onClose}
disabled={loading()}
>
<Button type="button" variant="secondary" onClick={props.onClose} disabled={loading()}>
Cancel
</Button>
<Button
type="submit"
disabled={loading()}
>
{loading() ? 'Changing...' : 'Change Password'}
<Button type="submit" disabled={loading()}>
{loading() ? "Changing..." : "Change Password"}
</Button>
</CardFooter>
</form>

View File

@@ -1,7 +1,7 @@
import { Component, createResource, For, Show } from 'solid-js';
import { commandsApi } from '../api/client';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Component, createResource, For, Show } from "solid-js";
import { commandsApi } from "../api/client";
import { Button } from "./ui/Button";
import { Card } from "./ui/Card";
const CommandQueue: Component = () => {
const [commands, { refetch }] = createResource(commandsApi.list);
@@ -11,7 +11,7 @@ const CommandQueue: Component = () => {
await commandsApi.updateStatus(id, { status });
refetch();
} catch (err) {
console.error('Failed to update command:', err);
console.error("Failed to update command:", err);
alert(`Error: ${err}`);
}
};
@@ -26,26 +26,27 @@ const CommandQueue: Component = () => {
<Card>
<Show when={!commands.loading} fallback={<div class="p-4">Loading...</div>}>
<For each={commands()} fallback={
<div class="p-8 text-center text-gray-500">
No commands in queue
</div>
}>
<For
each={commands()}
fallback={<div class="p-8 text-center text-gray-500">No commands in queue</div>}
>
{(cmd) => (
<div class="border-b p-4 hover:bg-gray-50">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${
cmd.status === 'verified' ? 'bg-green-100 text-green-800' :
cmd.status === 'rejected' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
}`}>
<span
class={`px-2 py-1 rounded-full text-xs font-semibold ${
cmd.status === "verified"
? "bg-green-100 text-green-800"
: cmd.status === "rejected"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{cmd.status}
</span>
<span class="text-sm text-gray-500">
{formatDate(cmd.received_at)}
</span>
<span class="text-sm text-gray-500">{formatDate(cmd.received_at)}</span>
</div>
<pre class="text-sm bg-gray-100 p-3 rounded overflow-x-auto">
{JSON.stringify(cmd.command, null, 2)}
@@ -56,19 +57,19 @@ const CommandQueue: Component = () => {
</div>
</Show>
</div>
<Show when={cmd.status === 'unverified'}>
<Show when={cmd.status === "unverified"}>
<div class="ml-4 flex flex-col gap-2">
<Button
size="sm"
variant="success"
onClick={() => updateCommandStatus(cmd.id, 'verified')}
onClick={() => updateCommandStatus(cmd.id, "verified")}
>
Verify
</Button>
<Button
size="sm"
variant="danger"
onClick={() => updateCommandStatus(cmd.id, 'rejected')}
onClick={() => updateCommandStatus(cmd.id, "rejected")}
>
Reject
</Button>

View File

@@ -1,40 +1,40 @@
import { Component, createSignal } from 'solid-js';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Input } from './ui/Input';
import { Component, createSignal } from "solid-js";
import { Button } from "./ui/Button";
import { Card } from "./ui/Card";
import { Input } from "./ui/Input";
interface LoginProps {
onLoginSuccess: (token: string) => void;
}
const Login: Component<LoginProps> = (props) => {
const [password, setPassword] = createSignal('');
const [error, setError] = createSignal('');
const [password, setPassword] = createSignal("");
const [error, setError] = createSignal("");
const [isLoading, setIsLoading] = createSignal(false);
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError('');
setError("");
setIsLoading(true);
try {
const response = await fetch('/admin/api/login', {
method: 'POST',
const response = await fetch("/admin/api/login", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({ password: password() }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Login failed' }));
throw new Error(errorData.error || 'Invalid password');
const errorData = await response.json().catch(() => ({ error: "Login failed" }));
throw new Error(errorData.error || "Invalid password");
}
const data = await response.json();
props.onLoginSuccess(data.token);
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
setError(err instanceof Error ? err.message : "Login failed");
} finally {
setIsLoading(false);
}
@@ -43,9 +43,7 @@ const Login: Component<LoginProps> = (props) => {
return (
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
<Card class="p-8 w-96">
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">
My Linspirer Admin Login
</h1>
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">My Linspirer Admin Login</h1>
<form onSubmit={handleSubmit}>
<div class="mb-4">
@@ -69,12 +67,8 @@ const Login: Component<LoginProps> = (props) => {
</div>
)}
<Button
type="submit"
size="lg"
disabled={isLoading()}
>
{isLoading() ? 'Logging in...' : 'Login'}
<Button type="submit" size="lg" disabled={isLoading()}>
{isLoading() ? "Logging in..." : "Login"}
</Button>
</form>

View File

@@ -1,8 +1,8 @@
import { Component, createSignal, For, Show } from 'solid-js';
import type { RequestLog } from '../types';
import { Button } from './ui/Button';
import { CardContent, CardFooter, CardHeader } from './ui/Card';
import { Modal, ModalContent } from './ui/Modal';
import { Component, createSignal, For, Show } from "solid-js";
import type { RequestLog } from "../types";
import { Button } from "./ui/Button";
import { CardContent, CardFooter, CardHeader } from "./ui/Card";
import { Modal, ModalContent } from "./ui/Modal";
interface TreeViewProps {
data: any;
@@ -12,17 +12,17 @@ interface TreeViewProps {
const TreeView: Component<TreeViewProps> = (props) => {
const [isOpen, setIsOpen] = createSignal(props.isRoot ?? false);
const isObject = typeof props.data === 'object' && props.data !== null;
const isObject = typeof props.data === "object" && props.data !== null;
const renderValue = (value: any) => {
switch (typeof value) {
case 'string':
case "string":
return <span class="text-green-600">"{value}"</span>;
case 'number':
case "number":
return <span class="text-blue-600">{value}</span>;
case 'boolean':
case "boolean":
return <span class="text-purple-600">{String(value)}</span>;
case 'object':
case "object":
if (value === null) return <span class="text-gray-500">null</span>;
// This case is handled by recursive TreeView
default:
@@ -36,9 +36,7 @@ const TreeView: Component<TreeViewProps> = (props) => {
class="flex items-center cursor-pointer hover:bg-gray-100 rounded px-1"
onClick={() => setIsOpen(!isOpen())}
>
<span class="w-4 inline-block text-gray-500">
{isObject ? (isOpen() ? '▼' : '►') : ''}
</span>
<span class="w-4 inline-block text-gray-500">{isObject ? (isOpen() ? "▼" : "►") : ""}</span>
<span class="font-semibold text-gray-800">{props.name}:</span>
<Show when={!isObject}>
<span class="ml-2">{renderValue(props.data)}</span>
@@ -55,7 +53,6 @@ const TreeView: Component<TreeViewProps> = (props) => {
);
};
interface RequestDetailsProps {
log: RequestLog;
onClose: () => void;
@@ -67,7 +64,9 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
<ModalContent class="max-w-3xl h-auto max-h-[90vh]">
<CardHeader>
<h3 class="text-lg font-medium text-gray-900">Request Details</h3>
<p class="text-sm text-gray-500">{props.log.method} at {new Date(props.log.created_at).toLocaleString()}</p>
<p class="text-sm text-gray-500">
{props.log.method} at {new Date(props.log.created_at).toLocaleString()}
</p>
</CardHeader>
<CardContent class="max-h-[70vh] overflow-y-auto">
@@ -88,11 +87,7 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
</CardContent>
<CardFooter class="flex justify-end pt-4">
<Button
type="button"
variant="secondary"
onClick={props.onClose}
>
<Button type="button" variant="secondary" onClick={props.onClose}>
Close
</Button>
</CardFooter>

View File

@@ -1,14 +1,14 @@
import { Component, createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { logsApi } from '../api/client';
import type { RequestLog as RequestLogType } from '../types';
import { Card } from './ui/Card';
import { Input } from './ui/Input';
import { Select } from './ui/Select';
import RequestDetails from './RequestDetails';
import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js";
import { logsApi } from "../api/client";
import type { RequestLog as RequestLogType } from "../types";
import { Card } from "./ui/Card";
import { Input } from "./ui/Input";
import { Select } from "./ui/Select";
import RequestDetails from "./RequestDetails";
const RequestLog: Component = () => {
const [search, setSearch] = createSignal('');
const [method, setMethod] = createSignal('');
const [search, setSearch] = createSignal("");
const [method, setMethod] = createSignal("");
const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null);
const [logs] = createResource(
@@ -17,12 +17,12 @@ const RequestLog: Component = () => {
// Solid's createResource refetches when the source accessor changes.
// We can add a debounce here if we want to avoid too many requests.
return logsApi.list(filters);
}
},
);
const methods = createMemo(() => {
if (!logs()) return [];
const allMethods = logs()!.map(log => log.method);
const allMethods = logs()!.map((log) => log.method);
return [...new Set(allMethods)];
});
@@ -38,7 +38,6 @@ const RequestLog: Component = () => {
return new Date(dateStr).toLocaleString();
};
return (
<>
<div class="space-y-4">
@@ -61,9 +60,7 @@ const RequestLog: Component = () => {
class="min-w-[200px]"
>
<option value="">All Methods</option>
<For each={methods()}>
{(m) => <option value={m}>{m}</option>}
</For>
<For each={methods()}>{(m) => <option value={m}>{m}</option>}</For>
</Select>
</div>
</div>
@@ -74,22 +71,40 @@ const RequestLog: Component = () => {
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Request Body</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Request Body
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<Show when={!logs.loading} fallback={<tr><td colspan="3" class="text-center py-4">Loading...</td></tr>}>
<For each={logs()} fallback={
<Show
when={!logs.loading}
fallback={
<tr>
<td colspan="3" class="text-center py-8 text-gray-500">
No logs found.
<td colspan="3" class="text-center py-4">
Loading...
</td>
</tr>
}>
}
>
<For
each={logs()}
fallback={
<tr>
<td colspan="3" class="text-center py-8 text-gray-500">
No logs found.
</td>
</tr>
}
>
{(log) => (
<tr class="hover:bg-gray-50 cursor-pointer" onClick={() => handleLogClick(log)}>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{log.method}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatDate(log.created_at)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{log.method}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(log.created_at)}
</td>
<td class="px-6 py-4 text-sm text-gray-500 font-mono truncate max-w-lg">
{JSON.stringify(log.request_body)}
</td>
@@ -102,9 +117,7 @@ const RequestLog: Component = () => {
</div>
</Card>
</div>
<Show when={selectedLog()}>
{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}
</Show>
<Show when={selectedLog()}>{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}</Show>
</>
);
};

View File

@@ -1,19 +1,19 @@
import { Component, createSignal, createResource, For, Show } from 'solid-js';
import { rulesApi } from '../api/client';
import type { InterceptionRule } from '../types';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Input } from './ui/Input';
import { Select } from './ui/Select';
import { Textarea } from './ui/Textarea';
import { Component, createSignal, createResource, For, Show } from "solid-js";
import { rulesApi } from "../api/client";
import type { InterceptionRule } from "../types";
import { Button } from "./ui/Button";
import { Card } from "./ui/Card";
import { Input } from "./ui/Input";
import { Select } from "./ui/Select";
import { Textarea } from "./ui/Textarea";
const RulesList: Component = () => {
const [rules, { refetch }] = createResource(rulesApi.list);
const [showEditor, setShowEditor] = createSignal(false);
const [editingId, setEditingId] = createSignal<number | null>(null);
const [editingMethod, setEditingMethod] = createSignal('');
const [editingAction, setEditingAction] = createSignal<'passthrough' | 'modify' | 'replace'>('passthrough');
const [editingResponse, setEditingResponse] = createSignal('');
const [editingMethod, setEditingMethod] = createSignal("");
const [editingAction, setEditingAction] = createSignal<"passthrough" | "modify" | "replace">("passthrough");
const [editingResponse, setEditingResponse] = createSignal("");
const toggleRule = async (rule: InterceptionRule) => {
try {
@@ -22,13 +22,13 @@ const RulesList: Component = () => {
});
refetch();
} catch (err) {
console.error('Failed to toggle rule:', err);
console.error("Failed to toggle rule:", err);
alert(`Error: ${err}`);
}
};
const deleteRule = async (id: number) => {
if (!confirm('Are you sure you want to delete this rule?')) return;
if (!confirm("Are you sure you want to delete this rule?")) return;
try {
if (id === editingId()) {
@@ -37,14 +37,14 @@ const RulesList: Component = () => {
await rulesApi.delete(id);
refetch();
} catch (err) {
console.error('Failed to delete rule:', err);
console.error("Failed to delete rule:", err);
alert(`Error: ${err}`);
}
};
const createNewRule = async () => {
if (!editingMethod()) {
alert('Please enter a method name');
alert("Please enter a method name");
return;
}
@@ -52,14 +52,14 @@ const RulesList: Component = () => {
await rulesApi.create({
method_name: editingMethod(),
action: editingAction(),
custom_response: editingAction() === 'replace' ? editingResponse() : undefined,
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
});
setShowEditor(false);
setEditingMethod('');
setEditingResponse('');
setEditingMethod("");
setEditingResponse("");
refetch();
} catch (err) {
console.error('Failed to create rule:', err);
console.error("Failed to create rule:", err);
alert(`Error: ${err}`);
}
};
@@ -68,15 +68,15 @@ const RulesList: Component = () => {
setEditingId(rule.id);
setEditingMethod(rule.method_name);
setEditingAction(rule.action);
setEditingResponse(rule.custom_response || '');
setEditingResponse(rule.custom_response || "");
setShowEditor(true);
};
const cancelEdit = () => {
setEditingId(null);
setEditingMethod('');
setEditingAction('passthrough');
setEditingResponse('');
setEditingMethod("");
setEditingAction("passthrough");
setEditingResponse("");
setShowEditor(false);
};
@@ -88,7 +88,7 @@ const RulesList: Component = () => {
}
if (!editingMethod()) {
alert('Please enter a method name');
alert("Please enter a method name");
return;
}
@@ -96,12 +96,12 @@ const RulesList: Component = () => {
await rulesApi.update(id, {
method_name: editingMethod(),
action: editingAction(),
custom_response: editingAction() === 'replace' ? editingResponse() : undefined,
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
});
cancelEdit();
refetch();
} catch (err) {
console.error('Failed to update rule:', err);
console.error("Failed to update rule:", err);
alert(`Error: ${err}`);
}
};
@@ -119,20 +119,16 @@ const RulesList: Component = () => {
}
}}
>
{showEditor() ? 'Cancel' : '+ New Rule'}
{showEditor() ? "Cancel" : "+ New Rule"}
</Button>
</div>
<Show when={showEditor()}>
<Card class="p-6 mb-4">
<h3 class="text-lg font-semibold mb-4">
{editingId() !== null ? 'Edit Rule' : 'Create New Rule'}
</h3>
<h3 class="text-lg font-semibold mb-4">{editingId() !== null ? "Edit Rule" : "Create New Rule"}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Method Name
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">Method Name</label>
<Input
type="text"
value={editingMethod()}
@@ -142,9 +138,7 @@ const RulesList: Component = () => {
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Action
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">Action</label>
<Select
value={editingAction()}
onChange={(e) => setEditingAction(e.currentTarget.value as any)}
@@ -154,11 +148,9 @@ const RulesList: Component = () => {
</Select>
</div>
<Show when={editingAction() === 'replace'}>
<Show when={editingAction() === "replace"}>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Custom Response (JSON)
</label>
<label class="block text-sm font-medium text-gray-700 mb-1">Custom Response (JSON)</label>
<Textarea
value={editingResponse()}
onInput={(e) => setEditingResponse(e.currentTarget.value)}
@@ -168,11 +160,7 @@ const RulesList: Component = () => {
</div>
</Show>
<Button
onClick={saveEdit}
>
{editingId() !== null ? 'Update Rule' : 'Create Rule'}
</Button>
<Button onClick={saveEdit}>{editingId() !== null ? "Update Rule" : "Create Rule"}</Button>
</div>
</Card>
</Show>
@@ -182,65 +170,64 @@ const RulesList: Component = () => {
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Method Name
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Action
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Status
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Actions
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<Show when={!rules.loading} fallback={
<tr><td colspan="4" class="text-center py-4">Loading...</td></tr>
}>
<For each={rules()} fallback={
<Show
when={!rules.loading}
fallback={
<tr>
<td colspan="4" class="text-center py-8 text-gray-500">
No rules configured yet. Click "+ New Rule" to create one.
<td colspan="4" class="text-center py-4">
Loading...
</td>
</tr>
}>
}
>
<For
each={rules()}
fallback={
<tr>
<td colspan="4" class="text-center py-8 text-gray-500">
No rules configured yet. Click "+ New Rule" to create one.
</td>
</tr>
}
>
{(rule) => (
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900">
{rule.method_name}
</td>
<td class="px-6 py-4 text-sm font-medium text-gray-900">{rule.method_name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class={`px-2 py-1 rounded-full text-xs ${rule.action === 'replace' ? 'bg-purple-100 text-purple-800' :
rule.action === 'modify' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
<span
class={`px-2 py-1 rounded-full text-xs ${
rule.action === "replace"
? "bg-purple-100 text-purple-800"
: rule.action === "modify"
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
}`}
>
{rule.action}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button
onClick={() => toggleRule(rule)}
class={`px-3 py-1 rounded-md text-sm font-medium ${rule.is_enabled
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{rule.is_enabled ? '✓ Enabled' : '✗ Disabled'}
class={`px-3 py-1 rounded-md text-sm font-medium ${
rule.is_enabled ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{rule.is_enabled ? "✓ Enabled" : "✗ Disabled"}
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button
variant="ghost"
onClick={() => startEdit(rule)}
class="mr-4">
<Button variant="ghost" onClick={() => startEdit(rule)} class="mr-4">
Edit
</Button>
<Button
variant="link"
onClick={() => deleteRule(rule.id)}
>
<Button variant="link" onClick={() => deleteRule(rule.id)}>
Delete
</Button>
</td>

View File

@@ -1,35 +1,37 @@
import { Component, JSX, splitProps } from 'solid-js';
import { cva, type VariantProps } from 'class-variance-authority';
import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed',
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed",
{
variants: {
variant: {
primary: 'bg-indigo-600 text-white hover:bg-indigo-700',
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
danger: 'bg-red-600 text-white hover:bg-red-700',
success: 'bg-green-600 text-white hover:bg-green-700',
ghost: 'text-indigo-600 hover:text-indigo-900',
link: 'text-red-600 hover:text-red-900',
primary: "bg-indigo-600 text-white hover:bg-indigo-700",
secondary: "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50",
danger: "bg-red-600 text-white hover:bg-red-700",
success: "bg-green-600 text-white hover:bg-green-700",
ghost: "text-indigo-600 hover:text-indigo-900",
link: "text-red-600 hover:text-red-900",
},
size: {
sm: 'px-3 py-1',
md: 'px-4 py-2',
lg: 'w-full px-4 py-2',
sm: "px-3 py-1",
md: "px-4 py-2",
lg: "w-full px-4 py-2",
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
variant: "primary",
size: "md",
},
}
},
);
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
export interface ButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button: Component<ButtonProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'size', 'class']);
const [local, others] = splitProps(props, ["variant", "size", "class"]);
return (
<button
class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}

View File

@@ -1,59 +1,39 @@
import { Component, JSX, splitProps } from 'solid-js';
import { cva, type VariantProps } from 'class-variance-authority';
import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
const cardVariants = cva('bg-white shadow rounded-lg', {
const cardVariants = cva("bg-white shadow rounded-lg", {
variants: {
variant: {
primary: 'overflow-hidden',
withPadding: 'p-6',
withPaddingAndDivider: 'p-4 divide-y divide-gray-200',
primary: "overflow-hidden",
withPadding: "p-6",
withPaddingAndDivider: "p-4 divide-y divide-gray-200",
},
},
defaultVariants: {
variant: 'primary',
variant: "primary",
},
});
export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {}
const Card: Component<CardProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']);
return (
<div
class={cardVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
const [local, others] = splitProps(props, ["variant", "class"]);
return <div class={cardVariants({ variant: local.variant, class: local.class })} {...others} />;
};
const CardHeader: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']);
return (
<div
class={`px-6 py-4 border-b border-gray-200 ${local.class}`}
{...others}
/>
);
const [local, others] = splitProps(props, ["class"]);
return <div class={`px-6 py-4 border-b border-gray-200 ${local.class}`} {...others} />;
};
const CardContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']);
return (
<div
class={`px-6 py-4 space-y-4 ${local.class}`}
{...others}
/>
);
const [local, others] = splitProps(props, ["class"]);
return <div class={`px-6 py-4 space-y-4 ${local.class}`} {...others} />;
};
const CardFooter: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']);
return (
<div
class={`px-6 py-4 border-t border-gray-200 ${local.class}`}
{...others}
/>
);
const [local, others] = splitProps(props, ["class"]);
return <div class={`px-6 py-4 border-t border-gray-200 ${local.class}`} {...others} />;
};
export { Card, CardHeader, CardContent, CardFooter, cardVariants };

View File

@@ -1,30 +1,27 @@
import { Component, JSX, splitProps } from 'solid-js';
import { cva, type VariantProps } from 'class-variance-authority';
import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
const inputVariants = cva(
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500',
"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500",
{
variants: {
variant: {
primary: '',
primary: "",
},
},
defaultVariants: {
variant: 'primary',
variant: "primary",
},
}
},
);
export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {}
export interface InputProps
extends JSX.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {}
const Input: Component<InputProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']);
return (
<input
class={inputVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
const [local, others] = splitProps(props, ["variant", "class"]);
return <input class={inputVariants({ variant: local.variant, class: local.class })} {...others} />;
};
export { Input, inputVariants };

View File

@@ -1,7 +1,7 @@
import { Component, JSX, splitProps } from 'solid-js';
import { Component, JSX, splitProps } from "solid-js";
const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']);
const [local, others] = splitProps(props, ["class"]);
return (
<div
class={`fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 ${local.class || ""}`}
@@ -11,13 +11,8 @@ const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
};
const ModalContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']);
return (
<div
class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`}
{...others}
/>
);
const [local, others] = splitProps(props, ["class"]);
return <div class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`} {...others} />;
};
export { Modal, ModalContent };

View File

@@ -1,30 +1,24 @@
import { Component, JSX, splitProps } from 'solid-js';
import { cva, type VariantProps } from 'class-variance-authority';
import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
const selectVariants = cva(
'w-full border border-gray-300 rounded-md px-3 py-2',
{
variants: {
variant: {
primary: '',
},
const selectVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2", {
variants: {
variant: {
primary: "",
},
defaultVariants: {
variant: 'primary',
},
}
);
},
defaultVariants: {
variant: "primary",
},
});
export interface SelectProps extends JSX.SelectHTMLAttributes<HTMLSelectElement>, VariantProps<typeof selectVariants> {}
export interface SelectProps
extends JSX.SelectHTMLAttributes<HTMLSelectElement>,
VariantProps<typeof selectVariants> {}
const Select: Component<SelectProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']);
return (
<select
class={selectVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
const [local, others] = splitProps(props, ["variant", "class"]);
return <select class={selectVariants({ variant: local.variant, class: local.class })} {...others} />;
};
export { Select, selectVariants };

View File

@@ -1,30 +1,24 @@
import { Component, JSX, splitProps } from 'solid-js';
import { cva, type VariantProps } from 'class-variance-authority';
import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from "class-variance-authority";
const textareaVariants = cva(
'w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm',
{
variants: {
variant: {
primary: '',
},
const textareaVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm", {
variants: {
variant: {
primary: "",
},
defaultVariants: {
variant: 'primary',
},
}
);
},
defaultVariants: {
variant: "primary",
},
});
export interface TextareaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, VariantProps<typeof textareaVariants> {}
export interface TextareaProps
extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof textareaVariants> {}
const Textarea: Component<TextareaProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']);
return (
<textarea
class={textareaVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
const [local, others] = splitProps(props, ["variant", "class"]);
return <textarea class={textareaVariants({ variant: local.variant, class: local.class })} {...others} />;
};
export { Textarea, textareaVariants };

View File

@@ -1,10 +1,10 @@
/* @refresh reload */
import { render } from 'solid-js/web';
import App from './App';
import './styles/index.css';
import { render } from "solid-js/web";
import App from "./App";
import "./styles/index.css";
const root = document.getElementById('root');
const root = document.getElementById("root");
if (!root) throw new Error('Root element not found');
if (!root) throw new Error("Root element not found");
render(() => <App />, root);

View File

@@ -1,7 +1,7 @@
export interface InterceptionRule {
id: number;
method_name: string;
action: 'passthrough' | 'modify' | 'replace';
action: "passthrough" | "modify" | "replace";
custom_response: any | null;
is_enabled: boolean;
created_at: string;
@@ -11,7 +11,7 @@ export interface InterceptionRule {
export interface Command {
id: number;
command: any;
status: 'unverified' | 'verified' | 'rejected';
status: "unverified" | "verified" | "rejected";
received_at: string;
processed_at?: string;
notes?: string;
@@ -36,21 +36,21 @@ export interface UpdateCommandRequest {
}
export interface Request {
headers: Record<string, {value: string, modified: boolean}>;
body: {value: any, modified: boolean};
headers: Record<string, { value: string; modified: boolean }>;
body: { value: any; modified: boolean };
}
export interface Response {
headers: Record<string, {value: string, modified: boolean}>;
body: {value: any, modified: boolean};
headers: Record<string, { value: string; modified: boolean }>;
body: { value: any; modified: boolean };
}
export interface RequestLog {
id: number;
method: string;
request_body: object;
response_body: object;
request_interception_action: string,
response_interception_action: string,
created_at: string;
id: number;
method: string;
request_body: object;
response_body: object;
request_interception_action: string;
response_interception_action: string;
created_at: string;
}

View File

@@ -1,11 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
};

View File

@@ -1,21 +1,21 @@
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()],
base: '/admin/',
base: "/admin/",
build: {
outDir: 'dist',
assetsDir: 'assets',
outDir: "dist",
assetsDir: "assets",
sourcemap: false,
minify: 'terser',
minify: "terser",
},
server: {
proxy: {
'/admin/api': {
target: 'http://localhost:8080',
"/admin/api": {
target: "http://localhost:8080",
changeOrigin: true,
}
}
}
},
},
},
});