tr]:last:border-b-0 dark:bg-zinc-800/50",
33 | className
34 | )}
35 | {...props} />
36 | ))
37 | TableFooter.displayName = "TableFooter"
38 |
39 | const TableRow = React.forwardRef(({ className, ...props }, ref) => (
40 |
47 | ))
48 | TableRow.displayName = "TableRow"
49 |
50 | const TableHead = React.forwardRef(({ className, ...props }, ref) => (
51 | [role=checkbox]]:translate-y-[2px] dark:text-zinc-400",
55 | className
56 | )}
57 | {...props} />
58 | ))
59 | TableHead.displayName = "TableHead"
60 |
61 | const TableCell = React.forwardRef(({ className, ...props }, ref) => (
62 | [role=checkbox]]:translate-y-[2px]",
66 | className
67 | )}
68 | {...props} />
69 | ))
70 | TableCell.displayName = "TableCell"
71 |
72 | const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
73 |
77 | ))
78 | TableCaption.displayName = "TableCaption"
79 |
80 | export {
81 | Table,
82 | TableHeader,
83 | TableBody,
84 | TableFooter,
85 | TableHead,
86 | TableRow,
87 | TableCell,
88 | TableCaption,
89 | }
90 |
--------------------------------------------------------------------------------
/components/ui/tabs.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef(({ className, ...props }, ref) => (
11 |
18 | ))
19 | TabsList.displayName = TabsPrimitive.List.displayName
20 |
21 | const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
22 |
29 | ))
30 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
31 |
32 | const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
33 |
40 | ))
41 | TabsContent.displayName = TabsPrimitive.Content.displayName
42 |
43 | export { Tabs, TabsList, TabsTrigger, TabsContent }
44 |
--------------------------------------------------------------------------------
/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 | ()
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/components/ui/toaster.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 | (
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 | (
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 | )
30 | );
31 | })}
32 |
33 | )
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from "react"
3 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext({
9 | size: "default",
10 | variant: "default",
11 | })
12 |
13 | const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => (
14 |
18 |
19 | {children}
20 |
21 |
22 | ))
23 |
24 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
25 |
26 | const ToggleGroupItem = React.forwardRef(({ className, children, variant, size, ...props }, ref) => {
27 | const context = React.useContext(ToggleGroupContext)
28 |
29 | return (
30 | (
37 | {children}
38 | )
39 | );
40 | })
41 |
42 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
43 |
44 | export { ToggleGroup, ToggleGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/toggle-switch.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const ToggleSwitch = ({
4 | options = [
5 | { value: 12, label: "12h" },
6 | { value: 24, label: "24h" },
7 | ],
8 | name,
9 | defaultValue,
10 | onChange,
11 | }) => {
12 | const [selected, setSelected] = useState(defaultValue || options[0].value);
13 |
14 | const handleSelect = (newValue) => {
15 | setSelected(newValue);
16 | onChange?.(newValue);
17 | };
18 |
19 | return (
20 |
21 |
22 | {/* Sliding highlight */}
23 |
28 |
29 | {options.map((option) => (
30 |
handleSelect(option.value)}
34 | className={`relative z-10 w-12 h-8 text-sm font-medium transition-colors duration-200 ${
35 | selected === option.value
36 | ? "text-white dark:text-gray-900"
37 | : "text-gray-500 hover:text-gray-900"
38 | }`}
39 | role="radio"
40 | aria-checked={selected === option.value}
41 | >
42 | {option.label}
43 |
44 | ))}
45 |
46 | );
47 | };
48 |
49 | export default ToggleSwitch;
50 |
--------------------------------------------------------------------------------
/components/ui/toggle.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-zinc-100 hover:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-zinc-100 data-[state=on]:text-zinc-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:hover:bg-zinc-800 dark:hover:text-zinc-400 dark:focus-visible:ring-zinc-300 dark:data-[state=on]:bg-zinc-800 dark:data-[state=on]:text-zinc-50",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-zinc-200 bg-transparent shadow-sm hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
17 | },
18 | size: {
19 | default: "h-9 px-2 min-w-9",
20 | sm: "h-8 px-1.5 min-w-8",
21 | lg: "h-10 px-2.5 min-w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef(({ className, variant, size, ...props }, ref) => (
32 |
36 | ))
37 |
38 | Toggle.displayName = TogglePrimitive.Root.displayName
39 |
40 | export { Toggle, toggleVariants }
41 |
--------------------------------------------------------------------------------
/components/ui/tooltip.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
15 |
16 |
24 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/docker-compose-dbonly.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | image: postgres:13
4 | restart: unless-stopped
5 | environment:
6 | - POSTGRES_DB=postgres
7 | - POSTGRES_USER=postgres
8 | - POSTGRES_PASSWORD=password # Change this to a secure password
9 | volumes:
10 | - db-data:/var/lib/postgresql/data
11 | - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
12 | - ./migrations.sql:/migrations.sql
13 |
14 | # Make sure you download the migrations.sql file if you are updating your existing database. If you changed the user or database name, you will need to plug that in in the command below.
15 | command: >
16 | bash -c "
17 | docker-entrypoint.sh postgres &
18 | until pg_isready; do sleep 1; done;
19 | psql -U postgres -d postgres -f /migrations.sql;
20 | wait
21 | "
22 | ports:
23 | - "5432:5432"
24 | healthcheck:
25 | test: ["CMD-SHELL", "pg_isready -U postgres"]
26 | interval: 10s
27 | timeout: 5s
28 | retries: 5
29 |
30 | volumes:
31 | db-data:
32 |
--------------------------------------------------------------------------------
/docker-compose.without-database.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | app:
4 | image: algertc/alpr-dashboard:latest
5 | restart: unless-stopped
6 | ports:
7 | - "3000:3000" # Change the first port to the port you want to expose
8 | environment:
9 | - NODE_ENV=production
10 | - ADMIN_PASSWORD=password # Change this to a secure password
11 | - TZ= America/Los_Angeles # Change this to match your time zone. Time zones can be found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
12 | - DB_PASSWORD=password # Change this to your postgres password
13 | - DB_HOST=db:5432 #host:port of your postgres database
14 | - DB_NAME=postgres
15 | - DB_USER=postgres
16 | depends_on:
17 | - db
18 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | image: algertc/alpr-dashboard:latest
4 | restart: unless-stopped
5 | ports:
6 | - "3000:3000" # Change the first port to the port you want to expose
7 | environment:
8 | - NODE_ENV=production
9 | - ADMIN_PASSWORD=password # Change this to a secure password
10 | - DB_PASSWORD=password # Change this to match your postgres password
11 | - TZ= America/Los_Angeles # Change this to match your time zone. Time zones can be found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
12 | depends_on:
13 | - db
14 | volumes:
15 | - app-auth:/app/auth
16 | - app-config:/app/config
17 | - app-plate_images:/app/storage
18 | logging:
19 | driver: "json-file"
20 | options:
21 | max-size: "5m"
22 | max-file: "3"
23 |
24 | db:
25 | image: postgres:13
26 | restart: unless-stopped
27 | environment:
28 | - POSTGRES_DB=postgres
29 | - POSTGRES_USER=postgres
30 | - POSTGRES_PASSWORD=password # Change this to a secure password
31 | - TZ= America/Los_Angeles # Change this to match your time zone. Time zones can be found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
32 | volumes:
33 | - db-data:/var/lib/postgresql/data
34 | - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
35 | - ./migrations.sql:/migrations.sql
36 |
37 | # Make sure you download the migrations.sql file if you are updating your existing database. If you changed the user or database name, you will need to plug that in in the command below.
38 | command: >
39 | bash -c "
40 | docker-entrypoint.sh postgres &
41 | until pg_isready; do sleep 1; done;
42 | psql -U postgres -d postgres -f /migrations.sql;
43 | wait
44 | "
45 | ports:
46 | - "5432:5432"
47 | healthcheck:
48 | test: ["CMD-SHELL", "pg_isready -U postgres"]
49 | interval: 10s
50 | timeout: 5s
51 | retries: 5
52 |
53 | volumes:
54 | db-data:
55 | app-auth:
56 | driver: local
57 | driver_opts:
58 | type: none
59 | o: bind
60 | device: ./auth
61 | app-config:
62 | driver: local
63 | driver_opts:
64 | type: none
65 | o: bind
66 | device: ./config
67 | app-plate_images:
68 | driver: local
69 | driver_opts:
70 | type: none
71 | o: bind
72 | device: ./storage
73 |
--------------------------------------------------------------------------------
/example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "api": "alpr",
4 | "found": {
5 | "success": true,
6 | "processMs": 436,
7 | "inferenceMs": 354,
8 | "predictions": [
9 | {
10 | "confidence": 0.9422435760498047,
11 | "label": "Plate: T121396C",
12 | "plate": "T121396C",
13 | "x_min": 469,
14 | "y_min": 557,
15 | "x_max": 614,
16 | "y_max": 677
17 | },
18 | {
19 | "confidence": 0.7209769487380981,
20 | "label": "Plate: KYJ5",
21 | "plate": "KYJ5",
22 | "x_min": 210,
23 | "y_min": 481,
24 | "x_max": 295,
25 | "y_max": 558
26 | }
27 | ],
28 | "message": "Found Plate: T121396C, Plate: KYJ5",
29 | "moduleId": "ALPR",
30 | "moduleName": "License Plate Reader",
31 | "code": 200,
32 | "command": "alpr",
33 | "requestId": "6a5d387c-807a-47b6-a4f7-17425b934318",
34 | "inferenceDevice": "GPU",
35 | "analysisRoundTripMs": 633,
36 | "processedBy": "localhost",
37 | "timestampUTC": "Tue, 19 Nov 2024 23:51:20 GMT"
38 | }
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/hooks/use-mobile.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange);
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/hooks/use-toast.js:
--------------------------------------------------------------------------------
1 | "use client";
2 | // Inspired by react-hot-toast library
3 | import * as React from "react"
4 |
5 | const TOAST_LIMIT = 1
6 | const TOAST_REMOVE_DELAY = 1000000
7 |
8 | const actionTypes = {
9 | ADD_TOAST: "ADD_TOAST",
10 | UPDATE_TOAST: "UPDATE_TOAST",
11 | DISMISS_TOAST: "DISMISS_TOAST",
12 | REMOVE_TOAST: "REMOVE_TOAST"
13 | }
14 |
15 | let count = 0
16 |
17 | function genId() {
18 | count = (count + 1) % Number.MAX_SAFE_INTEGER
19 | return count.toString();
20 | }
21 |
22 | const toastTimeouts = new Map()
23 |
24 | const addToRemoveQueue = (toastId) => {
25 | if (toastTimeouts.has(toastId)) {
26 | return
27 | }
28 |
29 | const timeout = setTimeout(() => {
30 | toastTimeouts.delete(toastId)
31 | dispatch({
32 | type: "REMOVE_TOAST",
33 | toastId: toastId,
34 | })
35 | }, TOAST_REMOVE_DELAY)
36 |
37 | toastTimeouts.set(toastId, timeout)
38 | }
39 |
40 | export const reducer = (state, action) => {
41 | switch (action.type) {
42 | case "ADD_TOAST":
43 | return {
44 | ...state,
45 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
46 | };
47 |
48 | case "UPDATE_TOAST":
49 | return {
50 | ...state,
51 | toasts: state.toasts.map((t) =>
52 | t.id === action.toast.id ? { ...t, ...action.toast } : t),
53 | };
54 |
55 | case "DISMISS_TOAST": {
56 | const { toastId } = action
57 |
58 | // ! Side effects ! - This could be extracted into a dismissToast() action,
59 | // but I'll keep it here for simplicity
60 | if (toastId) {
61 | addToRemoveQueue(toastId)
62 | } else {
63 | state.toasts.forEach((toast) => {
64 | addToRemoveQueue(toast.id)
65 | })
66 | }
67 |
68 | return {
69 | ...state,
70 | toasts: state.toasts.map((t) =>
71 | t.id === toastId || toastId === undefined
72 | ? {
73 | ...t,
74 | open: false,
75 | }
76 | : t),
77 | };
78 | }
79 | case "REMOVE_TOAST":
80 | if (action.toastId === undefined) {
81 | return {
82 | ...state,
83 | toasts: [],
84 | }
85 | }
86 | return {
87 | ...state,
88 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
89 | };
90 | }
91 | }
92 |
93 | const listeners = []
94 |
95 | let memoryState = { toasts: [] }
96 |
97 | function dispatch(action) {
98 | memoryState = reducer(memoryState, action)
99 | listeners.forEach((listener) => {
100 | listener(memoryState)
101 | })
102 | }
103 |
104 | function toast({
105 | ...props
106 | }) {
107 | const id = genId()
108 |
109 | const update = (props) =>
110 | dispatch({
111 | type: "UPDATE_TOAST",
112 | toast: { ...props, id },
113 | })
114 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
115 |
116 | dispatch({
117 | type: "ADD_TOAST",
118 | toast: {
119 | ...props,
120 | id,
121 | open: true,
122 | onOpenChange: (open) => {
123 | if (!open) dismiss()
124 | },
125 | },
126 | })
127 |
128 | return {
129 | id: id,
130 | dismiss,
131 | update,
132 | }
133 | }
134 |
135 | function useToast() {
136 | const [state, setState] = React.useState(memoryState)
137 |
138 | React.useEffect(() => {
139 | listeners.push(setState)
140 | return () => {
141 | const index = listeners.indexOf(setState)
142 | if (index > -1) {
143 | listeners.splice(index, 1)
144 | }
145 | };
146 | }, [state])
147 |
148 | return {
149 | ...state,
150 | toast,
151 | dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
152 | };
153 | }
154 |
155 | export { useToast, toast }
156 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./*"]
6 | },
7 | "jsx": "react-jsx",
8 | "module": "CommonJS",
9 | "target": "ES6"
10 | },
11 | "include": [
12 | "**/*.js",
13 | "**/*.jsx",
14 | "**/*.ts",
15 | "**/*.tsx",
16 | "auth.json",
17 | "next.config.js",
18 | "middleware.js"
19 | ],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/json-dump-example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "api": "alpr",
4 | "found": {
5 | "success": true,
6 | "processMs": 65,
7 | "inferenceMs": 62,
8 | "predictions": [
9 | {
10 | "confidence": 0.9400946199893951,
11 | "label": "Plate: SGX 7923G",
12 | "plate": "SGX 7923G",
13 | "x_min": 195,
14 | "y_min": 169,
15 | "x_max": 298,
16 | "y_max": 233,
17 | "plate_annotation": "0 0.504090 0.598214 0.210634 0.190476",
18 | "ocr_annotation": [
19 | [
20 | [
21 | [10.0, 0.0],
22 | [89.0, 5.0],
23 | [86.0, 37.0],
24 | [8.0, 31.0]
25 | ],
26 | ["SGX", 0.9637429118156433]
27 | ],
28 | [
29 | [
30 | [0.0, 22.0],
31 | [102.0, 25.0],
32 | [101.0, 60.0],
33 | [0.0, 57.0]
34 | ],
35 | ["7923G", 0.916446328163147]
36 | ]
37 | ],
38 | "valid_ocr_annotation": true
39 | },
40 | {
41 | "confidence": 0.9081299901008606,
42 | "label": "Plate: HF7499",
43 | "plate": "HF7499",
44 | "x_min": 1705,
45 | "y_min": 398,
46 | "x_max": 1827,
47 | "y_max": 483,
48 | "plate_annotation": "1 0.919792 0.407870 0.063542 0.078704",
49 | "ocr_annotation": [
50 | [
51 | [
52 | [10.0, 0.0],
53 | [89.0, 5.0],
54 | [86.0, 37.0],
55 | [8.0, 31.0]
56 | ],
57 | ["HF7499", 0.7667529218456391]
58 | ]
59 | ],
60 |
61 | "valid_ocr_annotation": true
62 | }
63 | ],
64 |
65 | "message": "Found Plate: SGX 7923G",
66 | "moduleId": "ALPR",
67 | "moduleName": "License Plate Reader",
68 | "code": 200,
69 | "command": "alpr",
70 | "requestId": "ad98962a-3db6-4d61-978f-e268548daf0f",
71 | "inferenceDevice": "GPU",
72 | "analysisRoundTripMs": 69,
73 | "processedBy": "localhost",
74 | "timestampUTC": "Sun 09 Feb 2025 03:46:58 GMT"
75 | }
76 | }
77 | ]
78 |
--------------------------------------------------------------------------------
/lib/cleanupService.js:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 | import { getConfig } from "@/lib/config";
3 |
4 | async function cleanupRecords() {
5 | const config = await getConfig();
6 | const maxRecords = config.maxRecords;
7 |
8 | const {
9 | rows: [{ count }],
10 | } = await db.sql`
11 | SELECT COUNT(*) as count FROM plate_reads
12 | `;
13 |
14 | // Only cleanup if we're 10% over the limit
15 | if (count > maxRecords * 1.1) {
16 | const deleteCount = count - maxRecords;
17 |
18 | await db.sql`
19 | DELETE FROM plate_reads
20 | WHERE id IN (
21 | SELECT id
22 | FROM plate_reads
23 | ORDER BY created_at ASC
24 | LIMIT ${deleteCount}
25 | )
26 | `;
27 |
28 | console.log(
29 | `Cleaned up ${deleteCount} records. Current count: ${count}, Max limit: ${maxRecords}`
30 | );
31 | }
32 | }
33 |
34 | // Check every 15 minutes
35 | setInterval(cleanupRecords, 1000 * 60 * 15);
36 |
37 | // Run once on startup
38 | cleanupRecords();
39 |
--------------------------------------------------------------------------------
/lib/clientUtils.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/lib/clientUtils.js
--------------------------------------------------------------------------------
/lib/mqtt-client.js:
--------------------------------------------------------------------------------
1 | import mqtt from "mqtt";
2 | import { getPool } from "@/lib/db";
3 |
4 | let mqttClient = null;
5 |
6 | export function initMqtt() {
7 | if (mqttClient) {
8 | console.log("MQTT client already initialized");
9 | return;
10 | }
11 |
12 | console.log("Initializing MQTT client");
13 | mqttClient = mqtt.connect("mqtt://localhost:1883");
14 |
15 | mqttClient.on("connect", () => {
16 | console.log("Connected to MQTT broker");
17 | });
18 |
19 | mqttClient.on("message", async (topic, message) => {
20 | try {
21 | const data = JSON.parse(message);
22 |
23 | if (!data?.plate_number) return;
24 |
25 | const timestamp = data.timestamp || new Date().toISOString();
26 |
27 | const pool = await getPool();
28 | const dbClient = await pool.connect();
29 | try {
30 | await dbClient.query("BEGIN");
31 |
32 | // First check if this exact plate read already exists
33 | const existingRead = await dbClient.query(
34 | `SELECT id FROM plate_reads
35 | WHERE plate_number = $1 AND timestamp = $2`,
36 | [data.plate_number, timestamp]
37 | );
38 |
39 | if (existingRead.rows.length > 0) {
40 | console.log(
41 | `Skipping duplicate read: ${data.plate_number} at ${timestamp}`
42 | );
43 | await dbClient.query("ROLLBACK");
44 | return;
45 | }
46 |
47 | // If not exists, proceed with insert
48 | await dbClient.query(
49 | `INSERT INTO plates (plate_number)
50 | VALUES ($1)
51 | ON CONFLICT (plate_number) DO NOTHING`,
52 | [data.plate_number]
53 | );
54 |
55 | await dbClient.query(
56 | `INSERT INTO plate_reads (plate_number, image_data, timestamp)
57 | VALUES ($1, $2, $3)`,
58 | [data.plate_number, data.Image || null, timestamp]
59 | );
60 |
61 | await dbClient.query("COMMIT");
62 | console.log(
63 | `Processed new plate read: ${data.plate_number} at ${timestamp}`
64 | );
65 | } catch (error) {
66 | await dbClient.query("ROLLBACK");
67 | console.error("Error processing plate read:", error);
68 | } finally {
69 | dbClient.release();
70 | }
71 | } catch (error) {
72 | console.error("Error processing message:", error);
73 | }
74 | });
75 | }
76 |
--------------------------------------------------------------------------------
/lib/notifications.js:
--------------------------------------------------------------------------------
1 | // lib/notifications.js
2 | import { getConfig } from "@/lib/settings";
3 |
4 | // Cache for config to avoid repeated disk reads
5 | let configCache = null;
6 | let configLastLoaded = 0;
7 | const CONFIG_CACHE_TTL = 60000; // 1 minute cache
8 |
9 | async function getPushoverConfig() {
10 | // Refresh cache if expired or doesn't exist
11 | if (!configCache || Date.now() - configLastLoaded > CONFIG_CACHE_TTL) {
12 | const config = await getConfig();
13 | configCache = config.notifications?.pushover;
14 | configLastLoaded = Date.now();
15 | }
16 |
17 | if (!configCache?.enabled) {
18 | throw new Error("Pushover notifications are not enabled");
19 | }
20 |
21 | if (!configCache?.app_token || !configCache?.user_key) {
22 | throw new Error("Pushover configuration is missing or incomplete");
23 | }
24 |
25 | return configCache;
26 | }
27 |
28 | async function buildNotificationPayload(
29 | plateNumber,
30 | config,
31 | customMessage = null,
32 | imageData = null
33 | ) {
34 | // Build message without requiring plate details
35 | let message = customMessage;
36 |
37 | if (!message) {
38 | message = `🚗 Plate ${plateNumber} Detected\n`;
39 | // message += `\n🕒 Time: ${new Date().toLocaleString()}`;
40 | }
41 |
42 | const basePayload = {
43 | token: config.app_token,
44 | user: config.user_key,
45 | title: customMessage ? "ALPR Test Notification" : `${plateNumber} Detected`,
46 | priority: config.priority || 1,
47 | message: message,
48 | };
49 |
50 | // Add optional configuration if present
51 | if (config.sound) basePayload.sound = config.sound;
52 | if (config.device) basePayload.device = config.device;
53 | if (config.url) basePayload.url = config.url;
54 |
55 | // Add image if available
56 | if (imageData) {
57 | const base64Data = imageData.replace(/^data:image\/[a-z]+;base64,/, "");
58 | basePayload.attachment_base64 = base64Data;
59 | basePayload.attachment_type = "image/jpeg";
60 | }
61 |
62 | return basePayload;
63 | }
64 |
65 | export async function sendPushoverNotification(
66 | plateNumber,
67 | customMessage = null,
68 | imageData = null
69 | ) {
70 | try {
71 | const config = await getPushoverConfig();
72 |
73 | if (!plateNumber) {
74 | throw new Error("Plate number is required");
75 | }
76 |
77 | const payload = await buildNotificationPayload(
78 | plateNumber,
79 | config,
80 | customMessage,
81 | imageData
82 | );
83 |
84 | const response = await fetch("https://api.pushover.net/1/messages.json", {
85 | method: "POST",
86 | headers: {
87 | "Content-Type": "application/json",
88 | },
89 | body: JSON.stringify(payload),
90 | });
91 |
92 | if (!response.ok) {
93 | const errorText = await response.text();
94 | throw new Error(`Pushover API error: ${errorText}`);
95 | }
96 |
97 | const result = await response.json();
98 | return {
99 | success: true,
100 | data: result,
101 | };
102 | } catch (error) {
103 | console.error("Notification error:", error);
104 | return {
105 | success: false,
106 | error: error.message || "Failed to send notification",
107 | };
108 | }
109 | }
110 |
111 | // Utility to validate Pushover configuration
112 | export async function validatePushoverConfig() {
113 | try {
114 | const config = await getPushoverConfig();
115 |
116 | const response = await fetch(
117 | "https://api.pushover.net/1/users/validate.json",
118 | {
119 | method: "POST",
120 | headers: {
121 | "Content-Type": "application/json",
122 | },
123 | body: JSON.stringify({
124 | token: config.app_token,
125 | user: config.user_key,
126 | }),
127 | }
128 | );
129 |
130 | const result = await response.json();
131 | return {
132 | success: response.ok,
133 | data: result,
134 | };
135 | } catch (error) {
136 | return {
137 | success: false,
138 | error: error.message,
139 | };
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/lib/sse.js:
--------------------------------------------------------------------------------
1 | // lib/sse.js
2 | // This is a simplified in-memory store for SSE connections.
3 | // NOT SUITABLE FOR PRODUCTION DEPLOYMENTS THAT SCALE (e.g., Vercel, serverless functions).
4 | // For production, consider a distributed pub/sub system like Redis, Pusher, Ably, etc.
5 |
6 | let clients = []; // Stores objects like { res: HttpResponse, heartbeat: IntervalID }
7 |
8 | export function addClient(req, res) {
9 | // Pass req to manage its 'close' event
10 | res.setHeader("Content-Type", "text/event-stream");
11 | res.setHeader("Cache-Control", "no-cache");
12 | res.setHeader("Connection", "keep-alive");
13 | res.setHeader("X-Accel-Buffering", "no"); // Disable Nginx buffering if applicable
14 |
15 | const heartbeat = setInterval(() => {
16 | try {
17 | res.write(
18 | `event: heartbeat\ndata: ${JSON.stringify({
19 | message: "keep-alive",
20 | })}\n\n`
21 | );
22 | } catch (e) {
23 | console.error(
24 | "SSE: Error sending heartbeat, client likely disconnected:",
25 | e.message
26 | );
27 | removeClient(res); // Clean up if heartbeat fails
28 | }
29 | }, 30000); // Send heartbeat every 30 seconds
30 |
31 | clients.push({ res, heartbeat });
32 | console.log(`SSE: Client connected. Total clients: ${clients.length}`);
33 |
34 | // Handle client disconnect directly here
35 | req.on("close", () => {
36 | console.log(
37 | `SSE: Client connection closed for ${
38 | req.socket?.remoteAddress || "unknown"
39 | }`
40 | );
41 | removeClient(res);
42 | });
43 | }
44 |
45 | export function removeClient(resToRemove) {
46 | clients = clients.filter((client) => {
47 | if (client.res === resToRemove) {
48 | clearInterval(client.heartbeat); // Clear the specific heartbeat interval
49 | return false; // Remove this client
50 | }
51 | return true; // Keep other clients
52 | });
53 | console.log(`SSE: Client disconnected. Total clients: ${clients.length}`);
54 | }
55 |
56 | export function sendEventToClients(event, data) {
57 | const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
58 | clients.forEach((client) => {
59 | try {
60 | client.res.write(payload);
61 | } catch (error) {
62 | console.error(
63 | "SSE: Error sending event to client, removing:",
64 | error.message
65 | );
66 | // If there's an error writing, assume client disconnected
67 | removeClient(client.res);
68 | }
69 | });
70 | }
71 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function formatTimeRange(hour, timeFormat) {
9 | if (timeFormat === 24) {
10 | return `${String(hour).padStart(2, "0")}:00`;
11 | }
12 |
13 | const period = hour >= 12 ? "PM" : "AM";
14 | const adjustedHour = hour % 12 || 12;
15 | return `${adjustedHour}:00 ${period}`;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/version.js:
--------------------------------------------------------------------------------
1 | export async function getVersionInfo() {
2 | try {
3 | const localVersion = require("../package.json").version;
4 |
5 | const [packageResponse, changelogResponse] = await Promise.all([
6 | fetch(
7 | "https://raw.githubusercontent.com/algertc/ALPR-Database/main/package.json",
8 | { next: { revalidate: 3600 } }
9 | ),
10 | fetch(
11 | "https://raw.githubusercontent.com/algertc/ALPR-Database/main/CHANGELOG.md",
12 | { next: { revalidate: 3600 } }
13 | ),
14 | ]);
15 |
16 | if (!packageResponse.ok) {
17 | throw new Error(`Version check failed: ${packageResponse.statusText}`);
18 | }
19 |
20 | const data = await packageResponse.json();
21 | let changelog = null;
22 |
23 | if (changelogResponse.ok) {
24 | const changelogText = await changelogResponse.text();
25 | changelog = parseChangelog(changelogText);
26 | }
27 |
28 | return {
29 | current: localVersion,
30 | latest: data.version,
31 | needsUpdate: localVersion !== data.version,
32 | changelog,
33 | checkError: null,
34 | };
35 | } catch (error) {
36 | console.error("Error checking version:", error);
37 | const localVersion = require("../package.json").version;
38 | return {
39 | current: localVersion,
40 | latest: "unknown",
41 | needsUpdate: false,
42 | changelog: null,
43 | checkError: error.message,
44 | };
45 | }
46 | }
47 |
48 | export async function getLocalVersionInfo() {
49 | try {
50 | const localVersion = require("../package.json").version;
51 | return localVersion;
52 | } catch (error) {
53 | console.error("Error getting local version:", error);
54 | return "unknown";
55 | }
56 | }
57 |
58 | function parseChangelog(markdown) {
59 | const versions = [];
60 | let currentVersion = null;
61 | let currentChanges = [];
62 | let inVersionSection = false;
63 |
64 | const lines = markdown.split("\n");
65 |
66 | for (let i = 0; i < lines.length; i++) {
67 | const line = lines[i].trim();
68 |
69 | // Match version headers like "## [0.1.8] - 03-19-2025"
70 | const versionMatch = line.match(
71 | /^##\s*\[([\d.]+)\]\s*-\s*(\d{2}-\d{2}-\d{4})/
72 | );
73 |
74 | if (versionMatch) {
75 | // If we were already processing a version, save it before starting a new one
76 | if (currentVersion) {
77 | versions.push({ ...currentVersion, changes: currentChanges });
78 | }
79 |
80 | currentVersion = {
81 | version: versionMatch[1],
82 | date: versionMatch[2],
83 | };
84 | currentChanges = [];
85 | inVersionSection = true;
86 |
87 | // Look ahead to see if there's a paragraph after the version
88 | let j = i + 1;
89 | let paragraphText = "";
90 |
91 | // Skip empty lines
92 | while (j < lines.length && lines[j].trim() === "") {
93 | j++;
94 | }
95 |
96 | // Collect paragraph text (text that's not a bullet point and not a header)
97 | while (
98 | j < lines.length &&
99 | !lines[j].trim().startsWith("-") &&
100 | !lines[j].trim().startsWith("##") &&
101 | lines[j].trim() !== ""
102 | ) {
103 | paragraphText += " " + lines[j].trim();
104 | j++;
105 | }
106 |
107 | // If we found paragraph text, add it as the first item
108 | if (paragraphText.trim()) {
109 | currentChanges.push(paragraphText.trim());
110 | }
111 | } else if (inVersionSection && line.startsWith("- ")) {
112 | // Add bullet points as usual
113 | currentChanges.push(line.substring(2));
114 | } else if (line.startsWith("##")) {
115 | // Another header that's not a version - end the current section
116 | inVersionSection = false;
117 | }
118 | }
119 |
120 | // Don't forget to add the last version
121 | if (currentVersion) {
122 | versions.push({ ...currentVersion, changes: currentChanges });
123 | }
124 |
125 | return versions;
126 | }
127 |
--------------------------------------------------------------------------------
/logging/logger.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import winston from "winston";
4 | import Transport from "winston-transport";
5 |
6 | class LimitedLineTransport extends Transport {
7 | constructor(opts) {
8 | super(opts);
9 | this.filename = opts.filename;
10 | this.maxLines = opts.maxLines;
11 | }
12 |
13 | log(info, callback) {
14 | try {
15 | let lines = [];
16 | if (fs.existsSync(this.filename)) {
17 | lines = fs
18 | .readFileSync(this.filename, "utf8")
19 | .split("\n")
20 | .filter(Boolean);
21 | }
22 | lines.push(JSON.stringify(info));
23 |
24 | // Keep only last maxLines
25 | if (lines.length > this.maxLines) {
26 | lines = lines.slice(-this.maxLines);
27 | }
28 |
29 | fs.writeFileSync(this.filename, lines.join("\n") + "\n");
30 | } catch (error) {
31 | console.error("Error writing to log file:", error);
32 | }
33 | callback();
34 | }
35 | }
36 |
37 | if (typeof window === "undefined" && !global.__loggerInitialized) {
38 | const LOG_DIR = path.join(process.cwd(), "logs");
39 | const LOG_FILE = path.join(LOG_DIR, "app.log");
40 | const MAX_LINES = 1000;
41 |
42 | try {
43 | if (!fs.existsSync(LOG_DIR)) {
44 | fs.mkdirSync(LOG_DIR, { recursive: true });
45 | }
46 | } catch (error) {
47 | console.error("Failed to create logs directory:", error);
48 | }
49 |
50 | // Configure winston with JSON format
51 | const logger = winston.createLogger({
52 | format: winston.format.combine(
53 | winston.format.timestamp(),
54 | winston.format.json()
55 | ),
56 | transports: [
57 | new LimitedLineTransport({
58 | filename: LOG_FILE,
59 | maxLines: MAX_LINES,
60 | }),
61 | ],
62 | });
63 |
64 | // Store original console methods
65 | const originalMethods = {
66 | log: console.log.bind(console),
67 | error: console.error.bind(console),
68 | warn: console.warn.bind(console),
69 | };
70 |
71 | // Override console methods
72 | console.log = (...args) => {
73 | logger.info(args.join(" "));
74 | originalMethods.log(...args);
75 | };
76 |
77 | console.error = (...args) => {
78 | logger.error(args.join(" "));
79 | originalMethods.error(...args);
80 | };
81 |
82 | console.warn = (...args) => {
83 | logger.warn(args.join(" "));
84 | originalMethods.warn(...args);
85 | };
86 |
87 | global.__loggerInitialized = true;
88 | }
89 |
--------------------------------------------------------------------------------
/migrations.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
2 | CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public;
3 |
4 | -- Modify plate_notifications
5 | ALTER TABLE IF EXISTS public.plate_notifications
6 | ADD COLUMN IF NOT EXISTS priority integer DEFAULT 1;
7 |
8 | -- Modify plate_reads
9 | ALTER TABLE IF EXISTS public.plate_reads
10 | ADD COLUMN IF NOT EXISTS camera_name character varying(25),
11 | ADD COLUMN IF NOT EXISTS image_path varchar(255),
12 | ADD COLUMN IF NOT EXISTS thumbnail_path varchar(255),
13 | ADD COLUMN IF NOT EXISTS bi_path varchar(100),
14 | ADD COLUMN IF NOT EXISTS plate_annotation varchar(255),
15 | ADD COLUMN IF NOT EXISTS crop_coordinates int[],
16 | ADD COLUMN IF NOT EXISTS ocr_annotation jsonb,
17 | ADD COLUMN IF NOT EXISTS confidence decimal,
18 | ADD COLUMN IF NOT EXISTS bi_zone varchar(30),
19 | ADD COLUMN IF NOT EXISTS validated boolean DEFAULT false;
20 |
21 | -- Modify known_plates
22 | ALTER TABLE IF EXISTS public.known_plates
23 | ADD COLUMN IF NOT EXISTS ignore BOOLEAN DEFAULT FALSE;
24 |
25 | -- Modify plates
26 | ALTER TABLE IF EXISTS public.plates
27 | ADD COLUMN IF NOT EXISTS occurrence_count INTEGER NOT NULL DEFAULT 0;
28 |
29 | -- Create index if not exists
30 | DO $$
31 | BEGIN
32 | IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_plates_occurrence_count') THEN
33 | CREATE INDEX idx_plates_occurrence_count ON plates(occurrence_count);
34 | END IF;
35 | END $$;
36 |
37 | -- Count incrementing function
38 | CREATE OR REPLACE FUNCTION update_plate_occurrence_count()
39 | RETURNS TRIGGER AS $$
40 | BEGIN
41 | -- Handle INSERT operation
42 | IF TG_OP = 'INSERT' THEN
43 | INSERT INTO plates (plate_number, occurrence_count)
44 | VALUES (NEW.plate_number, 1)
45 | ON CONFLICT (plate_number)
46 | DO UPDATE SET occurrence_count = plates.occurrence_count + 1;
47 |
48 | -- Handle UPDATE operation (plate number correction)
49 | ELSIF TG_OP = 'UPDATE' AND OLD.plate_number != NEW.plate_number THEN
50 | -- Increment the new plate number count (or create if not exists)
51 | INSERT INTO plates (plate_number, occurrence_count)
52 | VALUES (NEW.plate_number, 1)
53 | ON CONFLICT (plate_number)
54 | DO UPDATE SET occurrence_count = plates.occurrence_count + 1;
55 |
56 | -- Only decrement the old plate if it still exists
57 | UPDATE plates
58 | SET occurrence_count = occurrence_count - 1
59 | WHERE plate_number = OLD.plate_number;
60 |
61 | -- Clean up if occurrence count reaches zero
62 | DELETE FROM plates
63 | WHERE plate_number = OLD.plate_number
64 | AND occurrence_count <= 0;
65 |
66 | -- Handle DELETE operation
67 | ELSIF TG_OP = 'DELETE' THEN
68 | -- Only attempt to decrement if the plate still exists
69 | UPDATE plates
70 | SET occurrence_count = occurrence_count - 1
71 | WHERE plate_number = OLD.plate_number;
72 |
73 | -- Clean up if occurrence count reaches zero
74 | DELETE FROM plates
75 | WHERE plate_number = OLD.plate_number
76 | AND occurrence_count <= 0;
77 | END IF;
78 |
79 | RETURN NULL;
80 | END;
81 | $$ LANGUAGE plpgsql;
82 |
83 | -- Update trigger to also handle UPDATE operations
84 | DO $$
85 | BEGIN
86 | -- Drop the existing trigger if it exists
87 | DROP TRIGGER IF EXISTS plate_reads_count_trigger ON plate_reads;
88 |
89 | -- Create the updated trigger
90 | CREATE TRIGGER plate_reads_count_trigger
91 | AFTER INSERT OR UPDATE OR DELETE ON plate_reads
92 | FOR EACH ROW
93 | EXECUTE FUNCTION update_plate_occurrence_count();
94 | END $$;
95 |
96 | -- Clerical stuff
97 | CREATE TABLE IF NOT EXISTS devmgmt (
98 | id SERIAL PRIMARY KEY,
99 | update1 BOOLEAN DEFAULT FALSE
100 | );
101 | INSERT INTO devmgmt (id, update1)
102 | SELECT 1, false
103 | WHERE NOT EXISTS (SELECT 1 FROM devmgmt);
104 |
105 | ALTER TABLE IF EXISTS public.devmgmt
106 | ADD COLUMN IF NOT EXISTS training_last_record INTEGER DEFAULT 0;
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const nextConfig = {
4 | outputFileTracingIncludes: {
5 | "/**": [
6 | "/.next",
7 | "/public",
8 | "/app",
9 | "/lib",
10 | "/components",
11 | "/config",
12 | "/middleware.js",
13 | "/hooks",
14 | "/auth",
15 | "/package.json",
16 | ],
17 | },
18 | experimental: {
19 | serverActions: {
20 | bodySizeLimit: "8mb",
21 | },
22 | },
23 | };
24 |
25 | module.exports = nextConfig;
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blueiris-alpr-database",
3 | "version": "0.1.8",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.9.1",
13 | "@radix-ui/react-accordion": "^1.2.1",
14 | "@radix-ui/react-alert-dialog": "^1.1.2",
15 | "@radix-ui/react-aspect-ratio": "^1.1.0",
16 | "@radix-ui/react-avatar": "^1.1.1",
17 | "@radix-ui/react-checkbox": "^1.1.2",
18 | "@radix-ui/react-collapsible": "^1.1.1",
19 | "@radix-ui/react-context-menu": "^2.2.2",
20 | "@radix-ui/react-dialog": "^1.1.2",
21 | "@radix-ui/react-dropdown-menu": "^2.1.2",
22 | "@radix-ui/react-hover-card": "^1.1.4",
23 | "@radix-ui/react-label": "^2.1.0",
24 | "@radix-ui/react-menubar": "^1.1.2",
25 | "@radix-ui/react-navigation-menu": "^1.2.1",
26 | "@radix-ui/react-popover": "^1.1.2",
27 | "@radix-ui/react-progress": "^1.1.0",
28 | "@radix-ui/react-radio-group": "^1.2.1",
29 | "@radix-ui/react-scroll-area": "^1.2.0",
30 | "@radix-ui/react-select": "^2.1.2",
31 | "@radix-ui/react-separator": "^1.1.1",
32 | "@radix-ui/react-slider": "^1.2.1",
33 | "@radix-ui/react-slot": "^1.1.0",
34 | "@radix-ui/react-switch": "^1.1.1",
35 | "@radix-ui/react-tabs": "^1.1.1",
36 | "@radix-ui/react-toast": "^1.2.2",
37 | "@radix-ui/react-toggle": "^1.1.0",
38 | "@radix-ui/react-toggle-group": "^1.1.0",
39 | "@radix-ui/react-tooltip": "^1.1.3",
40 | "@techstark/opencv-js": "^4.10.0-release.1",
41 | "@vercel/postgres": "^0.10.0",
42 | "archiver": "^7.0.1",
43 | "bcrypt": "^6.0.0",
44 | "canvas": "^3.1.0",
45 | "class-variance-authority": "^0.7.0",
46 | "clsx": "^2.1.1",
47 | "cmdk": "1.0.0",
48 | "csv-writer": "^1.6.0",
49 | "date-fns": "^4.1.0",
50 | "embla-carousel-react": "^8.3.1",
51 | "fs": "^0.0.1-security",
52 | "input-otp": "^1.4.1",
53 | "iron-session": "^8.0.4",
54 | "jimp": "^1.6.0",
55 | "jose": "^5.9.6",
56 | "js-yaml": "^4.1.0",
57 | "lodash": "^4.17.21",
58 | "lottie-react": "^2.4.1",
59 | "lucide-react": "^0.456.0",
60 | "mqtt": "^5.10.1",
61 | "next": "15.0.3",
62 | "next-themes": "^0.4.3",
63 | "perspective-transform": "^1.1.3",
64 | "pg": "^8.13.1",
65 | "react": "^19.0.0-rc-66855b96-20241106",
66 | "react-day-picker": "8.10.1",
67 | "react-dom": "^19.0.0-rc-66855b96-20241106",
68 | "react-hook-form": "^7.53.2",
69 | "react-icons": "^5.4.0",
70 | "react-resizable-panels": "^2.1.6",
71 | "recharts": "^2.13.3",
72 | "sonner": "^1.7.0",
73 | "split2": "^4.2.0",
74 | "tailwind-merge": "^2.5.4",
75 | "tailwindcss": "^3.4.1",
76 | "tailwindcss-animate": "^1.0.7",
77 | "tailwindcss-displaymodes": "^1.0.8",
78 | "use-debounce": "^10.0.4",
79 | "uuid": "^11.1.0",
80 | "vaul": "^1.1.1",
81 | "winston": "^3.17.0",
82 | "zod": "^3.23.8"
83 | },
84 | "devDependencies": {
85 | "@shadcn/ui": "^0.0.4",
86 | "@types/mqtt": "^2.5.0",
87 | "@types/react": "18.3.12",
88 | "eslint": "^8",
89 | "eslint-config-next": "15.0.3",
90 | "postcss": "^8",
91 | "tailwindcss": "^3.4.1",
92 | "typescript": "5.6.3"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/1024.png
--------------------------------------------------------------------------------
/public/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/180.png
--------------------------------------------------------------------------------
/public/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/512.png
--------------------------------------------------------------------------------
/public/alpr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/alpr.jpg
--------------------------------------------------------------------------------
/public/fallback.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/fallback.jpg
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/icon.png
--------------------------------------------------------------------------------
/public/icon512_maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/icon512_maskable.png
--------------------------------------------------------------------------------
/public/icon512_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/icon512_rounded.png
--------------------------------------------------------------------------------
/public/license-plate.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/license-plate.ico
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#ffffff",
3 | "background_color": "#09090b",
4 | "icons": [
5 | {
6 | "src": "/192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/1024.png",
12 | "sizes": "1024x1024",
13 | "type": "image/png",
14 | "purpose": "any maskable"
15 | }
16 | ],
17 | "orientation": "any",
18 | "display": "standalone",
19 | "dir": "auto",
20 | "lang": "en-US",
21 | "name": "ALPR Database",
22 | "short_name": "ALPR",
23 | "start_url": "/",
24 | "scope": "/",
25 | "description": "algertc/alpr-database"
26 | }
27 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/placeholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/placeholder.jpg
--------------------------------------------------------------------------------
/public/splash_screens/10.2__iPad_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.2__iPad_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/10.2__iPad_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.2__iPad_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/10.5__iPad_Air_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.5__iPad_Air_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/10.5__iPad_Air_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.5__iPad_Air_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/10.9__iPad_Air_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.9__iPad_Air_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/10.9__iPad_Air_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/10.9__iPad_Air_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/11__iPad_Pro_M4_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/11__iPad_Pro_M4_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/11__iPad_Pro_M4_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/11__iPad_Pro_M4_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/12.9__iPad_Pro_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/12.9__iPad_Pro_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/12.9__iPad_Pro_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/12.9__iPad_Pro_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/13__iPad_Pro_M4_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/13__iPad_Pro_M4_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/13__iPad_Pro_M4_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/13__iPad_Pro_M4_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/8.3__iPad_Mini_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/8.3__iPad_Mini_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/8.3__iPad_Mini_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/8.3__iPad_Mini_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_11__iPhone_XR_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_11__iPhone_XR_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_11__iPhone_XR_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_11__iPhone_XR_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Pro_Max_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Pro_Max_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Pro_Max_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Pro_Max_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Pro_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Pro_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16_Pro_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16_Pro_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png
--------------------------------------------------------------------------------
/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png
--------------------------------------------------------------------------------
/public/test-plate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algertc/ALPR-Database/9b7422865b5c18648faba297c5901e0d9faad253/public/test-plate.jpg
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | screens: {
12 | "2xl": "2000px",
13 | },
14 | colors: {
15 | background: "hsl(var(--background))",
16 | foreground: "hsl(var(--foreground))",
17 | sidebar: {
18 | DEFAULT: "hsl(var(--sidebar-background))",
19 | foreground: "hsl(var(--sidebar-foreground))",
20 | primary: "hsl(var(--sidebar-primary))",
21 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
22 | accent: "hsl(var(--sidebar-accent))",
23 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
24 | border: "hsl(var(--sidebar-border))",
25 | ring: "hsl(var(--sidebar-ring))",
26 | },
27 | card: {
28 | DEFAULT: "hsl(var(--card))",
29 | foreground: "hsl(var(--card-foreground))",
30 | },
31 | popover: {
32 | DEFAULT: "hsl(var(--popover))",
33 | foreground: "hsl(var(--popover-foreground))",
34 | },
35 | primary: {
36 | DEFAULT: "hsl(var(--primary))",
37 | foreground: "hsl(var(--primary-foreground))",
38 | },
39 | secondary: {
40 | DEFAULT: "hsl(var(--secondary))",
41 | foreground: "hsl(var(--secondary-foreground))",
42 | },
43 | muted: {
44 | DEFAULT: "hsl(var(--muted))",
45 | foreground: "hsl(var(--muted-foreground))",
46 | },
47 | accent: {
48 | DEFAULT: "hsl(var(--accent))",
49 | foreground: "hsl(var(--accent-foreground))",
50 | },
51 | destructive: {
52 | DEFAULT: "hsl(var(--destructive))",
53 | foreground: "hsl(var(--destructive-foreground))",
54 | },
55 | border: "hsl(var(--border))",
56 | input: "hsl(var(--input))",
57 | ring: "hsl(var(--ring))",
58 | chart: {
59 | 1: "hsl(var(--chart-1))",
60 | 2: "hsl(var(--chart-2))",
61 | 3: "hsl(var(--chart-3))",
62 | 4: "hsl(var(--chart-4))",
63 | 5: "hsl(var(--chart-5))",
64 | },
65 | },
66 | borderRadius: {
67 | lg: "var(--radius)",
68 | md: "calc(var(--radius) - 2px)",
69 | sm: "calc(var(--radius) - 4px)",
70 | },
71 | keyframes: {
72 | "accordion-down": {
73 | from: {
74 | height: "0",
75 | },
76 | to: {
77 | height: "var(--radix-accordion-content-height)",
78 | },
79 | },
80 | "accordion-up": {
81 | from: {
82 | height: "var(--radix-accordion-content-height)",
83 | },
84 | to: {
85 | height: "0",
86 | },
87 | },
88 | },
89 | animation: {
90 | "accordion-down": "accordion-down 0.2s ease-out",
91 | "accordion-up": "accordion-up 0.2s ease-out",
92 | },
93 | },
94 | },
95 | plugins: [require("tailwindcss-animate")],
96 | };
97 |
--------------------------------------------------------------------------------
/test-mqtt.js:
--------------------------------------------------------------------------------
1 | // test-mqtt.js
2 | import mqtt from 'mqtt';
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | // Create a test client
7 | const client = mqtt.connect('mqtt://localhost:1883');
8 |
9 | // Read a sample image and convert to base64
10 | const imageBuffer = fs.readFileSync(path.join(process.cwd(), 'test-plate.jpg'));
11 | const base64Image = imageBuffer.toString('base64');
12 |
13 | // Create test payload
14 | const testPayload = {
15 | plate_number: "ABC123",
16 | image: base64Image,
17 | timestamp: new Date().toISOString(),
18 | vehicle_description: "Test vehicle - Blue Toyota Camry"
19 | };
20 |
21 | client.on('connect', () => {
22 | console.log('Connected to broker');
23 |
24 | // Publish the test message
25 | client.publish('alpr/plate_reads', JSON.stringify(testPayload), (err) => {
26 | if (err) {
27 | console.error('Error publishing:', err);
28 | } else {
29 | console.log('Test message published successfully');
30 | }
31 | // Close the client
32 | client.end();
33 | });
34 | });
35 |
36 | client.on('error', (err) => {
37 | console.error('MQTT error:', err);
38 | client.end();
39 | });
--------------------------------------------------------------------------------