remove old web implementations

This commit is contained in:
Nick Mowen 2023-12-08 07:16:35 -07:00
parent 5c86f6d3f6
commit 23fbbd3c12
5 changed files with 270 additions and 501 deletions

View File

@ -3,111 +3,142 @@ import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer";
import { Card } from "../ui/card";
import { FrigateConfig } from "@/types/frigateConfig";
import ActivityIndicator from "../ui/activity-indicator";
import { LuCircle, LuClock, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu";
import { IoMdExit } from "react-icons/io"
import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt } from "react-icons/md";
import {
LuCircle,
LuClock,
LuPlay,
LuPlayCircle,
LuTruck,
} from "react-icons/lu";
import { IoMdExit } from "react-icons/io";
import {
MdFaceUnlock,
MdOutlineLocationOn,
MdOutlinePictureInPictureAlt,
} from "react-icons/md";
import { HiOutlineVideoCamera } from "react-icons/hi";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
type HistoryCardProps = {
timeline: Card,
allPreviews?: Preview[],
}
timeline: Card;
allPreviews?: Preview[];
};
export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) {
const { data: config } = useSWR<FrigateConfig>("config");
export default function HistoryCard({
allPreviews,
timeline,
}: HistoryCardProps) {
const { data: config } = useSWR<FrigateConfig>("config");
if (!config) {
return <ActivityIndicator />
}
if (!config) {
return <ActivityIndicator />;
}
return (
<Card className="my-2 mr-2 bg-secondary">
<PreviewThumbnailPlayer
camera={timeline.camera}
allPreviews={allPreviews || []}
startTs={Object.values(timeline.entries)[0].timestamp}
/>
<div className="p-2">
<div className="text-sm flex">
<LuClock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(timeline.time, { strftime_fmt: config.ui.time_format == '24hour' ? '%H:%M:%S' : '%I:%M:%S' })}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" />
{timeline.camera.replaceAll('_', ' ')}
</div>
<div className="my-2 text-sm font-medium">
Activity:
</div>
{Object.entries(timeline.entries).map(([_, entry]) => {
return (
<div key={entry.timestamp} className="flex text-xs capitalize my-1 items-center">
{getTimelineIcon(entry)}
{getTimelineItemDescription(entry)}
</div>
);
})}
return (
<Card className="my-2 mr-2 bg-secondary">
<PreviewThumbnailPlayer
camera={timeline.camera}
allPreviews={allPreviews || []}
startTs={Object.values(timeline.entries)[0].timestamp}
/>
<div className="p-2">
<div className="text-sm flex">
<LuClock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(timeline.time, {
strftime_fmt:
config.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S",
})}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" />
{timeline.camera.replaceAll("_", " ")}
</div>
<div className="my-2 text-sm font-medium">Activity:</div>
{Object.entries(timeline.entries).map(([_, entry]) => {
return (
<div
key={entry.timestamp}
className="flex text-xs capitalize my-1 items-center"
>
{getTimelineIcon(entry)}
{getTimelineItemDescription(entry)}
</div>
</Card>
);
);
})}
</div>
</Card>
);
}
function getTimelineIcon(timelineItem: Timeline) {
switch (timelineItem.class_type) {
case 'visible':
return <LuPlay className="w-4 mr-1" />;
case 'gone':
return <IoMdExit className="w-4 mr-1" />;
case 'active':
return <LuPlayCircle className="w-4 mr-1" />;
case 'stationary':
return <LuCircle className="w-4 mr-1" />;
case 'entered_zone':
return <MdOutlineLocationOn className="w-4 mr-1" />;
case 'attribute':
switch (timelineItem.data.attribute) {
case 'face':
return <MdFaceUnlock className="w-4 mr-1" />;
case 'license_plate':
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
default:
return <LuTruck className="w-4 mr-1" />;
}
case 'sub_label':
switch (timelineItem.data.label) {
case 'person':
return <MdFaceUnlock className="w-4 mr-1" />;
case 'car':
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
}
}
switch (timelineItem.class_type) {
case "visible":
return <LuPlay className="w-4 mr-1" />;
case "gone":
return <IoMdExit className="w-4 mr-1" />;
case "active":
return <LuPlayCircle className="w-4 mr-1" />;
case "stationary":
return <LuCircle className="w-4 mr-1" />;
case "entered_zone":
return <MdOutlineLocationOn className="w-4 mr-1" />;
case "attribute":
switch (timelineItem.data.attribute) {
case "face":
return <MdFaceUnlock className="w-4 mr-1" />;
case "license_plate":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
default:
return <LuTruck className="w-4 mr-1" />;
}
case "sub_label":
switch (timelineItem.data.label) {
case "person":
return <MdFaceUnlock className="w-4 mr-1" />;
case "car":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
}
}
}
function getTimelineItemDescription(timelineItem: Timeline) {
const label = ((Array.isArray(timelineItem.data.sub_label) ? timelineItem.data.sub_label[0] : timelineItem.data.sub_label) || timelineItem.data.label).replaceAll('_', ' ');
const label = (
(Array.isArray(timelineItem.data.sub_label)
? timelineItem.data.sub_label[0]
: timelineItem.data.sub_label) || timelineItem.data.label
).replaceAll("_", " ");
switch (timelineItem.class_type) {
case 'visible':
return `${label} detected`;
case 'entered_zone':
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`;
case 'active':
return `${label} became active`;
case 'stationary':
return `${label} became stationary`;
case 'attribute': {
let title = '';
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`;
} else {
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`;
}
return title;
switch (timelineItem.class_type) {
case "visible":
return `${label} detected`;
case "entered_zone":
return `${label} entered ${timelineItem.data.zones
.join(" and ")
.replaceAll("_", " ")}`;
case "active":
return `${label} became active`;
case "stationary":
return `${label} became stationary`;
case "attribute": {
let title = "";
if (
timelineItem.data.attribute == "face" ||
timelineItem.data.attribute == "license_plate"
) {
title = `${timelineItem.data.attribute.replaceAll(
"_",
" "
)} detected for ${label}`;
} else {
title = `${
timelineItem.data.sub_label
} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`;
}
case 'sub_label':
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case 'gone':
return `${label} left`;
return title;
}
}
case "sub_label":
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case "gone":
return `${label} left`;
}
}

View File

@ -5,88 +5,106 @@ import Heading from "@/components/ui/heading";
import ActivityIndicator from "@/components/ui/activity-indicator";
import HistoryCard from "@/components/card/HistoryCard";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";;
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
function History() {
const { data: config } = useSWR<FrigateConfig>("config");
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]);
const { data: hourlyTimeline } = useSWR<HourlyTimeline>(['timeline/hourly', { timezone }]);
const { data: allPreviews } = useSWR<Preview[]>(`preview/all/start/${hourlyTimeline?.start || 0}/end/${hourlyTimeline?.end || 0}`, { revalidateOnFocus: false });
const timezone = useMemo(
() =>
config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
[config]
);
const { data: hourlyTimeline } = useSWR<HourlyTimeline>([
"timeline/hourly",
{ timezone },
]);
const { data: allPreviews } = useSWR<Preview[]>(
`preview/all/start/${hourlyTimeline?.start || 0}/end/${
hourlyTimeline?.end || 0
}`,
{ revalidateOnFocus: false }
);
const [detailLevel, setDetailLevel] = useState<'normal' | 'extra' | 'full'>('normal');
const [detailLevel, setDetailLevel] = useState<"normal" | "extra" | "full">(
"normal"
);
const timelineCards: CardsData | never[] = useMemo(() => {
if (!hourlyTimeline) {
return [];
}
if (!hourlyTimeline) {
return [];
}
const cards: CardsData = {};
Object.keys(hourlyTimeline["hours"])
.reverse()
.forEach((hour) => {
const day = new Date(parseInt(hour) * 1000);
day.setHours(0, 0, 0, 0);
const dayKey = (day.getTime() / 1000).toString();
const source_to_types: {[key: string]: string[]} = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.source_id}-${time.getMinutes()}`;
if (key in source_to_types) {
source_to_types[key].push(i.class_type);
} else {
source_to_types[key] = [i.class_type];
}
});
if (!Object.keys(cards).includes(dayKey)) {
cards[dayKey] = {};
const cards: CardsData = {};
Object.keys(hourlyTimeline["hours"])
.reverse()
.forEach((hour) => {
const day = new Date(parseInt(hour) * 1000);
day.setHours(0, 0, 0, 0);
const dayKey = (day.getTime() / 1000).toString();
const source_to_types: { [key: string]: string[] } = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.source_id}-${time.getMinutes()}`;
if (key in source_to_types) {
source_to_types[key].push(i.class_type);
} else {
source_to_types[key] = [i.class_type];
}
cards[dayKey][hour] = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == 'normal') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
) {
add = false;
}
} else if (detailLevel == 'extra') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
i.class_type in ['attribute', 'gone', 'visible']
) {
add = false;
}
}
if (add) {
if (key in cards[dayKey][hour]) {
cards[dayKey][hour][key].entries.push(i);
} else {
cards[dayKey][hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
});
return cards;
if (!Object.keys(cards).includes(dayKey)) {
cards[dayKey] = {};
}
cards[dayKey][hour] = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == "normal") {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
1 &&
["active", "attribute", "gone", "stationary", "visible"].includes(
i.class_type
)
) {
add = false;
}
} else if (detailLevel == "extra") {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
1 &&
i.class_type in ["attribute", "gone", "visible"]
) {
add = false;
}
}
if (add) {
if (key in cards[dayKey][hour]) {
cards[dayKey][hour][key].entries.push(i);
} else {
cards[dayKey][hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
});
return cards;
}, [detailLevel, hourlyTimeline]);
if (!config || !timelineCards) {
@ -94,43 +112,59 @@ function History() {
}
return (
<>
<Heading as="h2">Review</Heading>
<div className="text-xs mb-4">Dates and times are based on the timezone {timezone}</div>
<>
<Heading as="h2">Review</Heading>
<div className="text-xs mb-4">
Dates and times are based on the timezone {timezone}
</div>
<div>
{Object.entries(timelineCards)
.reverse()
.map(([day, timelineDay]) => {
return (
<div key={day}>
<Heading as="h3">
{formatUnixTimestampToDateTime(parseInt(day), {
strftime_fmt: "%A %b %d",
})}
</Heading>
{Object.entries(timelineDay).map(([hour, timelineHour]) => {
if (Object.values(timelineHour).length == 0) {
return <></>;
}
<div>
{Object.entries(timelineCards).reverse().map(([day, timelineDay]) => {
return (
<div key={day}>
<Heading as="h3">
{formatUnixTimestampToDateTime(parseInt(day), { strftime_fmt: '%A %b %d' })}
</Heading>
{Object.entries(timelineDay).map(([hour, timelineHour]) => {
if (Object.values(timelineHour).length == 0) {
return <></>;
}
<div key={hour}>
<Heading as="h4">
{formatUnixTimestampToDateTime(parseInt(hour), {
strftime_fmt: "%I:00",
})}
</Heading>
<ScrollArea>
<div className="flex">
{Object.entries(timelineHour).map(
([key, timeline]) => {
return (
<div key={hour}>
<Heading as="h4">
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })}
</Heading>
<ScrollArea>
<div className="flex">
{Object.entries(timelineHour).map(([key, timeline]) => {
return <HistoryCard key={key} timeline={timeline} allPreviews={allPreviews} />
})}
</div>
<ScrollBar className="m-2" orientation="horizontal" />
</ScrollArea>
</div>
<HistoryCard
key={key}
timeline={timeline}
allPreviews={allPreviews}
/>
);
})}
</div>
}
)}
</div>
<ScrollBar className="m-2" orientation="horizontal" />
</ScrollArea>
</div>
);
})}
</div>
</>
})}
</div>
);
})}
</div>
</>
);
}

View File

@ -1,83 +0,0 @@
import { h } from 'preact';
import useSWR from 'swr';
import ActivityIndicator from './ActivityIndicator';
import VideoPlayer from './VideoPlayer';
import { useCallback } from 'preact/hooks';
import { useMemo } from 'react';
import { useApiHost } from '../api';
export default function PreviewPlayer({ camera, allPreviews, startTs, mode }) {
const { data: config } = useSWR('config');
const apiHost = useApiHost();
const relevantPreview = useMemo(() => {
return Object.values(allPreviews || []).find(
(preview) => preview.camera == camera && preview.start < startTs && preview.end > startTs
);
}, [allPreviews, camera, startTs]);
const onHover = useCallback(
(isHovered) => {
if (isHovered) {
this.player.play();
} else {
this.player.pause();
this.player.currentTime(startTs - relevantPreview.start);
}
},
[relevantPreview, startTs]
);
if (!relevantPreview) {
return (
<img className={getThumbWidth(camera, config)} src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`} />
);
}
return (
<div
className={mode == 'thumbnail' ? getThumbWidth(camera, config) : ''}
onMouseEnter={() => onHover(true)}
onMouseLeave={() => onHover(false)}
>
<VideoPlayer
tag={startTs}
options={{
preload: 'auto',
autoplay: false,
controls: false,
muted: true,
loadingSpinner: false,
sources: [
{
src: `${relevantPreview.src}`,
type: 'video/mp4',
},
],
}}
seekOptions={{}}
onReady={(player) => {
this.player = player;
this.player.playbackRate(8);
this.player.currentTime(startTs - relevantPreview.start);
}}
onDispose={() => {
this.player = null;
}}
/>
</div>
);
}
function getThumbWidth(camera, config) {
const detect = config.cameras[camera].detect;
if (detect.width / detect.height > 2) {
return 'w-[320px]';
}
if (detect.width / detect.height < 1.4) {
return 'w-[200px]';
}
return 'w-[240px]';
}

View File

@ -1,222 +0,0 @@
import Heading from '../components/Heading';
import { useMemo, useState } from 'preact/hooks';
import useSWR from 'swr';
import ActivityIndicator from '../components/ActivityIndicator';
import PreviewPlayer from '../components/PreviewPlayer';
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
import { Clock } from '../icons/Clock';
import { Camera } from '../icons/Camera';
import ActiveObjectIcon from '../icons/ActiveObject';
import PlayIcon from '../icons/Play';
import ExitIcon from '../icons/Exit';
import StationaryObjectIcon from '../icons/StationaryObject';
import FaceIcon from '../icons/Face';
import LicensePlateIcon from '../icons/LicensePlate';
import DeliveryTruckIcon from '../icons/DeliveryTruck';
import ZoneIcon from '../icons/Zone';
export default function Export() {
const { data: config } = useSWR('config');
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]);
const { data: hourlyTimeline } = useSWR(['timeline/hourly', { timezone }]);
const { data: allPreviews } = useSWR([
`preview/all/start/${Object.keys(hourlyTimeline || [0])[0]}/end/${Object.keys(hourlyTimeline || [0]).slice(-1)[0]}`,
]);
// detail levels can be normal, extra, full
const [detailLevel, setDetailLevel] = useState('normal');
const timelineCards = useMemo(() => {
if (!hourlyTimeline) {
return [];
}
const cards = {};
Object.keys(hourlyTimeline)
.reverse()
.forEach((hour) => {
const source_to_types = {};
Object.values(hourlyTimeline[hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.source_id}-${time.getMinutes()}`;
if (key in source_to_types) {
source_to_types[key].push(i.class_type);
} else {
source_to_types[key] = [i.class_type];
}
});
cards[hour] = {};
Object.values(hourlyTimeline[hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == 'normal') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
) {
add = false;
}
} else if (detailLevel == 'extra') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
i.class_type in ['attribute', 'gone', 'visible']
) {
add = false;
}
}
if (add) {
if (key in cards[hour]) {
cards[hour][key].entries.push(i);
} else {
cards[hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
});
return cards;
}, [detailLevel, hourlyTimeline]);
if (!timelineCards) {
return <ActivityIndicator />;
}
return (
<div className="space-y-4 p-2 px-4">
<Heading>Review</Heading>
<div className="text-xs">Dates and times are based on the timezone {timezone}</div>
<div>
{Object.entries(timelineCards).map(([hour, timelineHour]) => {
return (
<div key={hour}>
<Heading size="md">
{formatUnixTimestampToDateTime(hour, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}
</Heading>
<div className="flex overflow-auto">
{Object.entries(timelineHour).map(([key, timeline]) => {
return (
<div
key={key}
className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow my-2 mr-2"
>
<PreviewPlayer
camera={timeline.camera}
allPreviews={allPreviews}
startTs={Object.values(timeline.entries)[0].timestamp}
mode="thumbnail"
/>
<div className="p-2">
<div className="text-sm flex">
<Clock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(timeline.time, { ...config.ui })}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<Camera className="h-5 w-5 mr-2 inline" />
{timeline.camera.replaceAll('_', ' ')}
</div>
<Heading size="xs" className="my-2">
Activity:
</Heading>
{Object.entries(timeline.entries).map(([_, entry]) => {
return (
<div key={entry.timestamp} className="flex text-xs my-1">
{getTimelineIcon(entry)}
{getTimelineItemDescription(config, entry)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
}
function getTimelineIcon(timelineItem) {
switch (timelineItem.class_type) {
case 'visible':
return <PlayIcon className="w-4 mr-1" />;
case 'gone':
return <ExitIcon className="w-4 mr-1" />;
case 'active':
return <ActiveObjectIcon className="w-4 mr-1" />;
case 'stationary':
return <StationaryObjectIcon className="w-4 mr-1" />;
case 'entered_zone':
return <ZoneIcon className="w-4 mr-1" />;
case 'attribute':
switch (timelineItem.data.attribute) {
case 'face':
return <FaceIcon className="w-4 mr-1" />;
case 'license_plate':
return <LicensePlateIcon className="w-4 mr-1" />;
default:
return <DeliveryTruckIcon className="w-4 mr-1" />;
}
case 'sub_label':
switch (timelineItem.data.label) {
case 'person':
return <FaceIcon className="w-4 mr-1" />;
case 'car':
return <LicensePlateIcon className="w-4 mr-1" />;
}
}
}
function getTimelineItemDescription(config, timelineItem) {
const label = (timelineItem.data.sub_label || timelineItem.data.label).replaceAll('_', ' ');
switch (timelineItem.class_type) {
case 'visible':
return `${label} detected`;
case 'entered_zone':
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`;
case 'active':
return `${label} became active`;
case 'stationary':
return `${label} became stationary`;
case 'attribute': {
let title = '';
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`;
} else {
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`;
}
return title;
}
case 'sub_label':
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case 'gone':
return `${label} left`;
}
}

View File

@ -19,7 +19,16 @@ export default defineConfig({
},
'/exports': {
target: 'http://localhost:5000'
}
},
'/ws': {
target: 'ws://localhost:5000',
ws: true,
},
'/live': {
target: 'ws://localhost:5000',
changeOrigin: true,
ws: true,
},
}
},
plugins: [