diff --git a/flake.nix b/flake.nix index 9f01d57..0072096 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,7 @@ pkg-config sqlite gemini-cli + biome ]; buildInputs = [ ]; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs); diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..fa60a6c --- /dev/null +++ b/frontend/biome.json @@ -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 + } + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7445af5..e941b74 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 = () => {
-

- My Linspirer Control Panel -

+

My Linspirer Control Panel

diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index c72be76..806a1b9 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,4 +1,4 @@ -const TOKEN_KEY = 'admin_token'; +const TOKEN_KEY = "admin_token"; export const authStore = { getToken(): string | null { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3369b36..534f9d7 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -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( - endpoint: string, - options?: RequestInit -): Promise { +async function request(endpoint: string, options?: RequestInit): Promise { 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( // 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( 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( // Rules API export const rulesApi = { - list: () => - request('/rules'), + list: () => request("/rules"), create: (rule: CreateRuleRequest) => - request('/rules', { - method: 'POST', + request("/rules", { + method: "POST", body: JSON.stringify(rule), }), update: (id: number, rule: UpdateRuleRequest) => request(`/rules/${id}`, { - method: 'PUT', + method: "PUT", body: JSON.stringify(rule), }), - delete: (id: number) => - request(`/rules/${id}`, { method: 'DELETE' }), + delete: (id: number) => request(`/rules/${id}`, { method: "DELETE" }), }; // Commands API export const commandsApi = { - list: (status?: string) => - request(`/commands${status ? `?status=${status}` : ''}`), + list: (status?: string) => request(`/commands${status ? `?status=${status}` : ""}`), updateStatus: (id: number, req: UpdateCommandRequest) => request(`/commands/${id}`, { - method: 'POST', + method: "POST", body: JSON.stringify(req), }), }; // Config API export const configApi = { - get: () => request>('/config'), + get: () => request>("/config"), update: (config: Record) => - request('/config', { - method: 'PUT', + request("/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('/password', { - method: 'PUT', + request("/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(`/logs${queryString ? `?${queryString}` : ''}`); + return request(`/logs${queryString ? `?${queryString}` : ""}`); }, }; diff --git a/frontend/src/components/ChangePassword.tsx b/frontend/src/components/ChangePassword.tsx index cb8ed7e..e8673f6 100644 --- a/frontend/src/components/ChangePassword.tsx +++ b/frontend/src/components/ChangePassword.tsx @@ -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 = (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(null); const [success, setSuccess] = createSignal(false); const [loading, setLoading] = createSignal(false); @@ -26,17 +26,17 @@ const ChangePassword: Component = (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 = (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 = (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 = (props) => {
{error() && ( -
- {error()} -
+
{error()}
)} {success() && ( @@ -84,9 +82,7 @@ const ChangePassword: Component = (props) => { )}
- + = (props) => {
- + = (props) => {
- + = (props) => {
- -
diff --git a/frontend/src/components/CommandQueue.tsx b/frontend/src/components/CommandQueue.tsx index f21b73b..2a10992 100644 --- a/frontend/src/components/CommandQueue.tsx +++ b/frontend/src/components/CommandQueue.tsx @@ -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 = () => { Loading...
}> - - No commands in queue -
- }> + No commands in queue} + > {(cmd) => (
- + {cmd.status} - - {formatDate(cmd.received_at)} - + {formatDate(cmd.received_at)}
                       {JSON.stringify(cmd.command, null, 2)}
@@ -56,19 +57,19 @@ const CommandQueue: Component = () => {
                       
- +
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index 2fb8654..23b946b 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -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 = (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 = (props) => { return (
-

- My Linspirer Admin Login -

+

My Linspirer Admin Login

@@ -69,12 +67,8 @@ const Login: Component = (props) => {
)} -
diff --git a/frontend/src/components/RequestDetails.tsx b/frontend/src/components/RequestDetails.tsx index b09e505..3618fdb 100644 --- a/frontend/src/components/RequestDetails.tsx +++ b/frontend/src/components/RequestDetails.tsx @@ -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 = (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 "{value}"; - case 'number': + case "number": return {value}; - case 'boolean': + case "boolean": return {String(value)}; - case 'object': + case "object": if (value === null) return null; // This case is handled by recursive TreeView default: @@ -36,9 +36,7 @@ const TreeView: Component = (props) => { class="flex items-center cursor-pointer hover:bg-gray-100 rounded px-1" onClick={() => setIsOpen(!isOpen())} > - - {isObject ? (isOpen() ? '▼' : '►') : ''} - + {isObject ? (isOpen() ? "▼" : "►") : ""} {props.name}: {renderValue(props.data)} @@ -55,7 +53,6 @@ const TreeView: Component = (props) => { ); }; - interface RequestDetailsProps { log: RequestLog; onClose: () => void; @@ -67,7 +64,9 @@ const RequestDetails: Component = (props) => {

Request Details

-

{props.log.method} at {new Date(props.log.created_at).toLocaleString()}

+

+ {props.log.method} at {new Date(props.log.created_at).toLocaleString()} +

@@ -88,11 +87,7 @@ const RequestDetails: Component = (props) => { - diff --git a/frontend/src/components/RequestLog.tsx b/frontend/src/components/RequestLog.tsx index 8b368a1..06c55ba 100644 --- a/frontend/src/components/RequestLog.tsx +++ b/frontend/src/components/RequestLog.tsx @@ -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(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 ( <>
@@ -61,9 +60,7 @@ const RequestLog: Component = () => { class="min-w-[200px]" > - - {(m) => } - + {(m) => }
@@ -74,22 +71,40 @@ const RequestLog: Component = () => { Method Time - Request Body + + Request Body + - Loading...}> - - - No logs found. + + Loading... - }> + } + > + + + No logs found. + + + } + > {(log) => ( handleLogClick(log)}> - {log.method} - {formatDate(log.created_at)} + + {log.method} + + + {formatDate(log.created_at)} + {JSON.stringify(log.request_body)} @@ -102,9 +117,7 @@ const RequestLog: Component = () => {
- - {(log) => } - + {(log) => } ); }; diff --git a/frontend/src/components/RulesList.tsx b/frontend/src/components/RulesList.tsx index fa675fb..8ef9b9e 100644 --- a/frontend/src/components/RulesList.tsx +++ b/frontend/src/components/RulesList.tsx @@ -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(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"} -

- {editingId() !== null ? 'Edit Rule' : 'Create New Rule'} -

+

{editingId() !== null ? "Edit Rule" : "Create New Rule"}

- + {
- +
- +
- +