Map caching, clickhouse test service
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
REDIS_HOST=
|
||||||
|
REDIS_PORT=
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
POSTGRES_HOST=
|
||||||
|
POSTGRES_DB=
|
||||||
|
POSTGRES_USER=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_PORT=
|
||||||
|
EMS_PORT=
|
||||||
|
MONITOR_PORT=
|
||||||
|
CLICKHOUSE_DB=
|
||||||
|
CLICKHOUSE_USER=
|
||||||
|
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=
|
||||||
|
CLICKHOUSE_PASSWORD=
|
@ -15,10 +15,11 @@ import Documents from "./pages/Documents"
|
|||||||
import Reports from "./pages/Reports"
|
import Reports from "./pages/Reports"
|
||||||
import Boilers from "./pages/Boilers"
|
import Boilers from "./pages/Boilers"
|
||||||
import Servers from "./pages/Servers"
|
import Servers from "./pages/Servers"
|
||||||
import { Api, Assignment, Cloud, Factory, Home, Login, Map, Password, People, Settings as SettingsIcon, Shield, Storage } from "@mui/icons-material"
|
import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage } from "@mui/icons-material"
|
||||||
import Settings from "./pages/Settings"
|
import Settings from "./pages/Settings"
|
||||||
import PasswordReset from "./pages/auth/PasswordReset"
|
import PasswordReset from "./pages/auth/PasswordReset"
|
||||||
import MapTest from "./pages/MapTest"
|
import MapTest from "./pages/MapTest"
|
||||||
|
import MonitorPage from "./pages/MonitorPage"
|
||||||
|
|
||||||
// Определение страниц с путями и компонентом для рендера
|
// Определение страниц с путями и компонентом для рендера
|
||||||
export const pages = [
|
export const pages = [
|
||||||
@ -126,6 +127,14 @@ export const pages = [
|
|||||||
drawer: true,
|
drawer: true,
|
||||||
dashboard: true
|
dashboard: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Монитор",
|
||||||
|
path: "/monitor",
|
||||||
|
icon: <MonitorHeart />,
|
||||||
|
component: <MonitorPage />,
|
||||||
|
drawer: true,
|
||||||
|
dashboard: true
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -4,32 +4,20 @@ import 'ol/ol.css'
|
|||||||
import Map from 'ol/Map'
|
import Map from 'ol/Map'
|
||||||
import View from 'ol/View'
|
import View from 'ol/View'
|
||||||
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
|
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
|
||||||
import { OSM, Source, Vector as VectorSource, XYZ } from 'ol/source'
|
import { OSM, Vector as VectorSource, XYZ } from 'ol/source'
|
||||||
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
||||||
import { get, Projection, transform, transformExtent } from 'ol/proj'
|
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material'
|
||||||
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Container, Box } from '@mui/material'
|
import { Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Rule, Straighten, Timeline, Undo, Warning } from '@mui/icons-material'
|
||||||
import { Adjust, Api, CircleOutlined, DoubleArrow, FeaturedVideoSharp, Handyman, OpenWith, RectangleOutlined, Timeline, Undo, Warning } from '@mui/icons-material'
|
|
||||||
import { Type } from 'ol/geom/Geometry'
|
import { Type } from 'ol/geom/Geometry'
|
||||||
import { altKeyOnly, click, doubleClick, noModifierKeys, platformModifierKey, pointerMove, shiftKeyOnly, singleClick } from 'ol/events/condition'
|
import { click, noModifierKeys, shiftKeyOnly } from 'ol/events/condition'
|
||||||
import Feature, { FeatureLike } from 'ol/Feature'
|
import Feature from 'ol/Feature'
|
||||||
import Style from 'ol/style/Style'
|
|
||||||
import Fill from 'ol/style/Fill'
|
|
||||||
import Stroke from 'ol/style/Stroke'
|
|
||||||
import { FlatStyleLike } from 'ol/style/flat'
|
|
||||||
import { SatelliteMapsProvider } from '../../interfaces/map'
|
import { SatelliteMapsProvider } from '../../interfaces/map'
|
||||||
import Tile from 'ol/Tile'
|
import { containsExtent } from 'ol/extent'
|
||||||
import ImageTile from 'ol/ImageTile'
|
import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
|
||||||
import { createXYZ, TileGrid } from 'ol/tilegrid'
|
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
|
||||||
import { TileCoord } from 'ol/tilecoord'
|
import { mapCenter, mapExtent } from './MapConstants'
|
||||||
import { register } from 'ol/proj/proj4'
|
|
||||||
import proj4 from 'proj4'
|
|
||||||
|
|
||||||
const MapComponent = () => {
|
const MapComponent = () => {
|
||||||
proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs')
|
|
||||||
register(proj4);
|
|
||||||
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
|
|
||||||
|
|
||||||
|
|
||||||
const mapElement = useRef<HTMLDivElement | null>(null)
|
const mapElement = useRef<HTMLDivElement | null>(null)
|
||||||
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
const [currentTool, setCurrentTool] = useState<Type | null>(null)
|
||||||
|
|
||||||
@ -37,18 +25,9 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
|
const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')
|
||||||
|
|
||||||
const gMapsSatSource = useRef<XYZ>(new XYZ({
|
const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)
|
||||||
//url: `https://khms2.google.com/kh/v=984?x={x}&y={y}&z={z}`,
|
|
||||||
url: `http://localhost:5000/tile/google/{z}/{x}/{y}`,
|
|
||||||
attributions: 'Map data © Google'
|
|
||||||
}))
|
|
||||||
|
|
||||||
const yMapsSatSource = useRef<XYZ>(new XYZ({
|
const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)
|
||||||
//url: `https://sat0{1-4}.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}&scale=1&lang=ru_RU&client_id=yandex-web-maps`,
|
|
||||||
url: `https://core-sat.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}&scale=1&lang=ru_RU`,
|
|
||||||
attributions: 'Map data © Yandex',
|
|
||||||
projection: yandexProjection,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const satLayer = useRef<TileLayer>(new TileLayer({
|
const satLayer = useRef<TileLayer>(new TileLayer({
|
||||||
source: gMapsSatSource.current,
|
source: gMapsSatSource.current,
|
||||||
@ -56,60 +35,27 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
const draw = useRef<Draw | null>(null)
|
const draw = useRef<Draw | null>(null)
|
||||||
const snap = useRef<Snap | null>(null)
|
const snap = useRef<Snap | null>(null)
|
||||||
const selectFeature = useRef<Select | null>(null)
|
|
||||||
|
const selectFeature = useRef<Select>(new Select({
|
||||||
|
condition: function (mapBrowserEvent) {
|
||||||
|
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const drawingLayer = useRef<VectorLayer | null>(null)
|
const drawingLayer = useRef<VectorLayer | null>(null)
|
||||||
const drawingLayerSource = useRef<VectorSource>(new VectorSource())
|
const drawingLayerSource = useRef<VectorSource>(new VectorSource())
|
||||||
|
|
||||||
const regionsLayer = useRef<VectorLayer>(new VectorLayer({
|
const regionsLayer = useRef<VectorLayer>(new VectorLayer({
|
||||||
source: new VectorSource({
|
source: regionsLayerSource,
|
||||||
url: 'sakha_republic.geojson',
|
style: regionsLayerStyle
|
||||||
format: new GeoJSON(),
|
|
||||||
}),
|
|
||||||
style: new Style({
|
|
||||||
stroke: new Stroke({
|
|
||||||
color: 'blue',
|
|
||||||
width: 1,
|
|
||||||
}),
|
|
||||||
fill: new Fill({
|
|
||||||
color: 'rgba(0, 0, 255, 0.1)',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const selectStyle = new Style({
|
|
||||||
fill: new Fill({
|
|
||||||
color: 'rgba(0, 0, 255, 0.3)',
|
|
||||||
}),
|
|
||||||
stroke: new Stroke({
|
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
|
||||||
width: 2,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedRegion = useRef<Feature | null>(null)
|
const selectedRegion = useRef<Feature | null>(null)
|
||||||
|
|
||||||
const geoLayer = useRef<VectorLayer | null>(null)
|
const baseLayer = useRef<TileLayer>(new TileLayer({
|
||||||
|
source: new OSM(),
|
||||||
const geoLayerSource = useRef<VectorSource>(new VectorSource({
|
|
||||||
url: 'https://openlayers.org/data/vector/ecoregions.json',
|
|
||||||
format: new GeoJSON(),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const baseLayer = useRef<TileLayer | null>(null)
|
|
||||||
|
|
||||||
const geoLayerStyle: FlatStyleLike = {
|
|
||||||
'fill-color': ['string', ['get', 'COLOR'], '#eee'],
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawingLayerStyle: FlatStyleLike = {
|
|
||||||
'fill-color': 'rgba(255, 255, 255, 0.2)',
|
|
||||||
'stroke-color': '#ffcc33',
|
|
||||||
'stroke-width': 2,
|
|
||||||
'circle-radius': 7,
|
|
||||||
'circle-fill-color': '#ffcc33',
|
|
||||||
}
|
|
||||||
|
|
||||||
const addInteractions = () => {
|
const addInteractions = () => {
|
||||||
if (currentTool) {
|
if (currentTool) {
|
||||||
draw.current = new Draw({
|
draw.current = new Draw({
|
||||||
@ -156,60 +102,43 @@ const MapComponent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zoomToFeature = (feature: Feature) => {
|
||||||
|
const geometry = feature.getGeometry()
|
||||||
|
const extent = geometry?.getExtent()
|
||||||
|
|
||||||
|
if (map.current && extent) {
|
||||||
|
map.current.getView().fit(extent, {
|
||||||
|
duration: 300,
|
||||||
|
maxZoom: 19,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
geoLayer.current = new VectorLayer({
|
|
||||||
background: '#1a2b39',
|
|
||||||
source: geoLayerSource.current,
|
|
||||||
style: geoLayerStyle,
|
|
||||||
})
|
|
||||||
|
|
||||||
baseLayer.current = new TileLayer({
|
|
||||||
source: new OSM(),
|
|
||||||
})
|
|
||||||
|
|
||||||
drawingLayer.current = new VectorLayer({
|
drawingLayer.current = new VectorLayer({
|
||||||
source: drawingLayerSource.current,
|
source: drawingLayerSource.current,
|
||||||
style: drawingLayerStyle,
|
style: drawingLayerStyle,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Center coordinates of Yakutia in EPSG:3857
|
|
||||||
const center = transform([129.7578941, 62.030804], 'EPSG:4326', 'EPSG:3857')
|
|
||||||
|
|
||||||
// Extent for Yakutia in EPSG:4326
|
|
||||||
const extent4326 = [105.0, 55.0, 170.0, 75.0] // Approximate bounding box
|
|
||||||
// Transform extent to EPSG:3857
|
|
||||||
const extent = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857')
|
|
||||||
|
|
||||||
map.current = new Map({
|
map.current = new Map({
|
||||||
layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current],
|
layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current],
|
||||||
target: mapElement.current as HTMLDivElement,
|
target: mapElement.current as HTMLDivElement,
|
||||||
view: new View({
|
view: new View({
|
||||||
center,
|
center: mapCenter,
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
extent: [
|
maxZoom: 21,
|
||||||
11388546.533293726,
|
extent: mapExtent,
|
||||||
7061866.113051185,
|
|
||||||
18924313.434856508,
|
|
||||||
13932243.11199202
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const modify = new Modify({ source: drawingLayerSource.current })
|
const modify = new Modify({ source: drawingLayerSource.current })
|
||||||
map.current.addInteraction(modify)
|
map.current.addInteraction(modify)
|
||||||
|
|
||||||
selectFeature.current = new Select({
|
|
||||||
condition: function (mapBrowserEvent) {
|
|
||||||
return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
map.current.addInteraction(selectFeature.current)
|
map.current.addInteraction(selectFeature.current)
|
||||||
|
|
||||||
selectFeature.current.on('select', (e) => {
|
selectFeature.current.on('select', (e) => {
|
||||||
const selectedFeatures = e.selected
|
const selectedFeatures = e.selected
|
||||||
|
|
||||||
console.log(selectedFeatures)
|
|
||||||
|
|
||||||
if (selectedFeatures.length > 0) {
|
if (selectedFeatures.length > 0) {
|
||||||
selectedFeatures.forEach((feature) => {
|
selectedFeatures.forEach((feature) => {
|
||||||
drawingLayerSource.current?.removeFeature(feature)
|
drawingLayerSource.current?.removeFeature(feature)
|
||||||
@ -219,26 +148,63 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
loadFeatures()
|
loadFeatures()
|
||||||
|
|
||||||
|
// Show current selected region
|
||||||
map.current.on('pointermove', function (e) {
|
map.current.on('pointermove', function (e) {
|
||||||
if (selectedRegion.current !== null) {
|
if (selectedRegion.current !== null) {
|
||||||
selectedRegion.current.setStyle(undefined)
|
selectedRegion.current.setStyle(undefined)
|
||||||
selectedRegion.current = null
|
selectedRegion.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.current && selectStyle !== null) {
|
if (map.current) {
|
||||||
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
|
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
|
||||||
selectedRegion.current = f as Feature
|
selectedRegion.current = f as Feature
|
||||||
|
|
||||||
selectedRegion.current.setStyle(selectStyle)
|
selectedRegion.current.setStyle(selectStyle)
|
||||||
|
|
||||||
if (f.get('district')) {
|
if (f.get('district')) {
|
||||||
setStatusText(f.get('district'))
|
setStatusText(f.get('district'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
map.current.on('click', function (e) {
|
||||||
|
if (selectedRegion.current !== null) {
|
||||||
|
selectedRegion.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.current) {
|
||||||
|
map.current.forEachFeatureAtPixel(e.pixel, function (f) {
|
||||||
|
selectedRegion.current = f as Feature
|
||||||
|
// Zoom to the selected feature
|
||||||
|
zoomToFeature(selectedRegion.current)
|
||||||
|
|
||||||
|
return true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hide regions layer when fully visible
|
||||||
|
map.current.on('moveend', function () {
|
||||||
|
const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
|
||||||
|
const features = regionsLayer.current.getSource()?.getFeatures()
|
||||||
|
|
||||||
|
let isViewCovered = false
|
||||||
|
|
||||||
|
features?.forEach((feature: Feature) => {
|
||||||
|
const featureExtent = feature?.getGeometry()?.getExtent()
|
||||||
|
if (viewExtent && featureExtent) {
|
||||||
|
if (containsExtent(featureExtent, viewExtent)) {
|
||||||
|
isViewCovered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
regionsLayer.current.setVisible(!isViewCovered)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
map?.current?.setTarget(undefined)
|
map?.current?.setTarget(undefined)
|
||||||
}
|
}
|
||||||
@ -259,8 +225,6 @@ const MapComponent = () => {
|
|||||||
|
|
||||||
const [statusText, setStatusText] = useState('')
|
const [statusText, setStatusText] = useState('')
|
||||||
|
|
||||||
const selected = useRef<FeatureLike | null>(null)
|
|
||||||
|
|
||||||
// Visibility setting
|
// Visibility setting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
satLayer.current?.setOpacity(satelliteOpacity)
|
satLayer.current?.setOpacity(satelliteOpacity)
|
||||||
@ -352,10 +316,14 @@ const MapComponent = () => {
|
|||||||
>
|
>
|
||||||
<OpenWith />
|
<OpenWith />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton>
|
||||||
|
<Straighten />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '500px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
|
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '600px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
|
9
client/src/components/map/MapConstants.ts
Normal file
9
client/src/components/map/MapConstants.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { transform } from "ol/proj"
|
||||||
|
|
||||||
|
const mapExtent = [11388546.533293726, 7061866.113051185, 18924313.434856508, 13932243.11199202]
|
||||||
|
const mapCenter = transform([129.7578941, 62.030804], 'EPSG:4326', 'EPSG:3857')
|
||||||
|
|
||||||
|
export {
|
||||||
|
mapExtent,
|
||||||
|
mapCenter
|
||||||
|
}
|
32
client/src/components/map/MapSources.ts
Normal file
32
client/src/components/map/MapSources.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import GeoJSON from "ol/format/GeoJSON";
|
||||||
|
import { get } from "ol/proj";
|
||||||
|
import { register } from "ol/proj/proj4";
|
||||||
|
import { XYZ } from "ol/source";
|
||||||
|
import VectorSource from "ol/source/Vector";
|
||||||
|
import proj4 from "proj4";
|
||||||
|
proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs')
|
||||||
|
register(proj4);
|
||||||
|
|
||||||
|
const yandexProjection = get('EPSG:3395')?.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]) || 'EPSG:3395'
|
||||||
|
|
||||||
|
const googleMapsSatelliteSource = new XYZ({
|
||||||
|
url: `${import.meta.env.VITE_API_EMS_URL}/tile/google/{z}/{x}/{y}`,
|
||||||
|
attributions: 'Map data © Google'
|
||||||
|
})
|
||||||
|
|
||||||
|
const yandexMapsSatelliteSource = new XYZ({
|
||||||
|
url: `${import.meta.env.VITE_API_EMS_URL}/tile/yandex/{z}/{x}/{y}`,
|
||||||
|
attributions: 'Map data © Yandex',
|
||||||
|
projection: yandexProjection,
|
||||||
|
})
|
||||||
|
|
||||||
|
const regionsLayerSource = new VectorSource({
|
||||||
|
url: 'sakha_republic.geojson',
|
||||||
|
format: new GeoJSON(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
googleMapsSatelliteSource,
|
||||||
|
yandexMapsSatelliteSource,
|
||||||
|
regionsLayerSource
|
||||||
|
}
|
38
client/src/components/map/MapStyles.ts
Normal file
38
client/src/components/map/MapStyles.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Fill from "ol/style/Fill";
|
||||||
|
import { FlatStyleLike } from "ol/style/flat";
|
||||||
|
import Stroke from "ol/style/Stroke";
|
||||||
|
import Style from "ol/style/Style";
|
||||||
|
|
||||||
|
const drawingLayerStyle: FlatStyleLike = {
|
||||||
|
'fill-color': 'rgba(255, 255, 255, 0.2)',
|
||||||
|
'stroke-color': '#ffcc33',
|
||||||
|
'stroke-width': 2,
|
||||||
|
'circle-radius': 7,
|
||||||
|
'circle-fill-color': '#ffcc33',
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectStyle = new Style({
|
||||||
|
fill: new Fill({
|
||||||
|
color: 'rgba(0, 0, 255, 0.3)',
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
width: 2,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const regionsLayerStyle = new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'blue',
|
||||||
|
width: 1,
|
||||||
|
}),
|
||||||
|
fill: new Fill({
|
||||||
|
color: 'rgba(0, 0, 255, 0.1)',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
drawingLayerStyle,
|
||||||
|
selectStyle,
|
||||||
|
regionsLayerStyle
|
||||||
|
}
|
9
client/src/pages/MonitorPage.tsx
Normal file
9
client/src/pages/MonitorPage.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
function MonitorPage() {
|
||||||
|
return (
|
||||||
|
<div>Monitor</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonitorPage
|
@ -0,0 +1,14 @@
|
|||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js";
|
||||||
|
import react from "file:///app/node_modules/@vitejs/plugin-react-swc/index.mjs";
|
||||||
|
import { nodePolyfills } from "file:///app/node_modules/vite-plugin-node-polyfills/dist/index.js";
|
||||||
|
var vite_config_default = defineConfig({
|
||||||
|
plugins: [
|
||||||
|
nodePolyfills(),
|
||||||
|
react()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
vite_config_default as default
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvYXBwL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xyXG5pbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djJ1xyXG5pbXBvcnQgeyBub2RlUG9seWZpbGxzIH0gZnJvbSAndml0ZS1wbHVnaW4tbm9kZS1wb2x5ZmlsbHMnXHJcblxyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHBsdWdpbnM6IFtcclxuICAgIG5vZGVQb2x5ZmlsbHMoKSxcclxuICAgIHJlYWN0KCksXHJcbiAgXSxcclxufSlcclxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4TCxTQUFTLG9CQUFvQjtBQUMzTixPQUFPLFdBQVc7QUFDbEIsU0FBUyxxQkFBcUI7QUFHOUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUztBQUFBLElBQ1AsY0FBYztBQUFBLElBQ2QsTUFBTTtBQUFBLEVBQ1I7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
@ -27,6 +27,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./ems
|
context: ./ems
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./ems/tile_data:/app/tiles
|
||||||
links:
|
links:
|
||||||
- redis_db:redis_db
|
- redis_db:redis_db
|
||||||
- psql_db:psql_db
|
- psql_db:psql_db
|
||||||
@ -43,6 +45,19 @@ services:
|
|||||||
- ${EMS_PORT}:${EMS_PORT}
|
- ${EMS_PORT}:${EMS_PORT}
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
monitor:
|
||||||
|
container_name: monitor
|
||||||
|
build:
|
||||||
|
context: ./monitor
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
environment:
|
||||||
|
- MONITOR_PORT=${MONITOR_PORT}
|
||||||
|
ports:
|
||||||
|
- ${MONITOR_PORT}:${MONITOR_PORT}
|
||||||
|
volumes:
|
||||||
|
- ./monitor/data:/app/data
|
||||||
|
restart: always
|
||||||
|
|
||||||
psql_db:
|
psql_db:
|
||||||
container_name: psql_db
|
container_name: psql_db
|
||||||
image: postgres:16.4-alpine
|
image: postgres:16.4-alpine
|
||||||
@ -65,6 +80,17 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
clickhouse_test:
|
||||||
redis_data:
|
container_name: clickhouse_test
|
||||||
psql_data:
|
image: clickhouse/clickhouse-server
|
||||||
|
environment:
|
||||||
|
- CLICKHOUSE_DB=${CLICKHOUSE_DB}
|
||||||
|
- CLICKHOUSE_USER=${CLICKHOUSE_USER}
|
||||||
|
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=${CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT}
|
||||||
|
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- 8123:8123
|
||||||
|
- 9000:9000
|
||||||
|
expose:
|
||||||
|
- 8123
|
||||||
|
- 9000
|
||||||
|
3
ems/.gitignore
vendored
3
ems/.gitignore
vendored
@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
tile_data
|
||||||
|
1
ems/README.md
Normal file
1
ems/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# EMS (ИКС)
|
119
ems/src/index-redis.ts
Normal file
119
ems/src/index-redis.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import express, { Request, Response } from 'express'
|
||||||
|
import { Redis } from 'ioredis'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import bodyParser from 'body-parser'
|
||||||
|
import { SatelliteMapsProvider } from './interfaces/map'
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const cors = require('cors')
|
||||||
|
|
||||||
|
const redis = new Redis({
|
||||||
|
port: Number(process.env.REDIS_PORT) || 6379,
|
||||||
|
host: process.env.REDIS_HOST,
|
||||||
|
password: process.env.REDIS_PASSWORD,
|
||||||
|
})
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const port = process.env.EMS_PORT
|
||||||
|
|
||||||
|
// Middleware to parse JSON requests
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
|
const getTileUrl = (provider: string, x: string, y: string, z: string) => {
|
||||||
|
if (provider === 'google') {
|
||||||
|
return `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`;
|
||||||
|
} else if (provider === 'yandex') {
|
||||||
|
return `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/tile/:provider/:z/:x/:y', async (req, res) => {
|
||||||
|
const { provider, x, y, z } = req.params;
|
||||||
|
const cacheKey = `${provider}:${z}:${x}:${y}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if tile is in cache
|
||||||
|
redis.get(cacheKey, async (err, cachedTile) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Redis GET error:', err);
|
||||||
|
return res.status(500).send('Server error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedTile) {
|
||||||
|
// If cached, return tile
|
||||||
|
console.log('Tile served from cache');
|
||||||
|
const imgBuffer = Buffer.from(cachedTile, 'base64');
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Content-Length': imgBuffer.length,
|
||||||
|
});
|
||||||
|
return res.end(imgBuffer);
|
||||||
|
} else {
|
||||||
|
// Fetch tile from provider
|
||||||
|
const tileUrl = getTileUrl(provider, x, y, z);
|
||||||
|
const response = await axios.get(tileUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache the tile in Redis
|
||||||
|
const base64Tile = Buffer.from(response.data).toString('base64');
|
||||||
|
redis.setex(cacheKey, 3600 * 24 * 30, base64Tile); // Cache for 1 hour
|
||||||
|
|
||||||
|
// Return the tile to the client
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Content-Length': response.data.length,
|
||||||
|
});
|
||||||
|
return res.end(response.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tile:', error);
|
||||||
|
res.status(500).send('Error fetching tile');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/hello', cors(), (req: Request, res: Response) => {
|
||||||
|
res.send('Hello, World!')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Route to store GeoJSON data
|
||||||
|
app.post('/geojson', cors(), async (req: Request, res: Response) => {
|
||||||
|
const geoJSON = req.body
|
||||||
|
|
||||||
|
if (!geoJSON || !geoJSON.features) {
|
||||||
|
return res.status(400).send('Invalid GeoJSON')
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = `geojson:${Date.now()}`;
|
||||||
|
redis.set(id, JSON.stringify(geoJSON), (err, reply) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send('Error saving GeoJSON to Redis');
|
||||||
|
}
|
||||||
|
res.send({ status: 'success', id });
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Route to fetch GeoJSON data
|
||||||
|
app.get('/geojson/:id', cors(), async (req: Request, res: Response) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
redis.get(id, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send('Error fetching GeoJSON from Redis');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
res.send(JSON.parse(data));
|
||||||
|
} else {
|
||||||
|
res.status(404).send('GeoJSON not found');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running on http://localhost:${port}`);
|
||||||
|
})
|
135
ems/src/index.ts
135
ems/src/index.ts
@ -1,119 +1,48 @@
|
|||||||
import express, { Request, Response } from 'express'
|
import express, { Request, Response } from 'express';
|
||||||
import { Redis } from 'ioredis'
|
import fs from 'fs';
|
||||||
import dotenv from 'dotenv'
|
import path from 'path';
|
||||||
import bodyParser from 'body-parser'
|
import axios from 'axios';
|
||||||
import { SatelliteMapsProvider } from './interfaces/map'
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const cors = require('cors')
|
const app = express();
|
||||||
|
const PORT = process.env.EMS_PORT;
|
||||||
|
|
||||||
const redis = new Redis({
|
const tileFolder = path.join(__dirname, '..', 'tiles');
|
||||||
port: Number(process.env.REDIS_PORT) || 6379,
|
|
||||||
host: process.env.REDIS_HOST,
|
|
||||||
password: process.env.REDIS_PASSWORD,
|
|
||||||
})
|
|
||||||
|
|
||||||
dotenv.config()
|
const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
|
||||||
|
const url = provider === 'google'
|
||||||
|
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
|
||||||
|
: `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
|
||||||
|
|
||||||
const app = express()
|
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||||
const port = process.env.EMS_PORT
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
// Middleware to parse JSON requests
|
app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
|
||||||
app.use(bodyParser.json())
|
const { provider, z, x, y } = req.params;
|
||||||
|
|
||||||
const getTileUrl = (provider: string, x: string, y: string, z: string) => {
|
if (!['google', 'yandex'].includes(provider)) {
|
||||||
if (provider === 'google') {
|
return res.status(400).send('Invalid provider');
|
||||||
return `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`;
|
|
||||||
} else if (provider === 'yandex') {
|
|
||||||
return `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`;
|
|
||||||
}
|
|
||||||
throw new Error('Invalid provider');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/tile/:provider/:z/:x/:y', async (req, res) => {
|
const tilePath = path.join(tileFolder, provider, z, x, `${y}.jpg`);
|
||||||
const { provider, x, y, z } = req.params;
|
|
||||||
const cacheKey = `${provider}:${z}:${x}:${y}`;
|
if (fs.existsSync(tilePath)) {
|
||||||
|
return res.sendFile(tilePath);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if tile is in cache
|
const tileData = await fetchTileFromAPI(provider, z, x, y);
|
||||||
redis.get(cacheKey, async (err, cachedTile) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Redis GET error:', err);
|
|
||||||
return res.status(500).send('Server error');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedTile) {
|
fs.mkdirSync(path.dirname(tilePath), { recursive: true });
|
||||||
// If cached, return tile
|
|
||||||
console.log('Tile served from cache');
|
|
||||||
const imgBuffer = Buffer.from(cachedTile, 'base64');
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'image/png',
|
|
||||||
'Content-Length': imgBuffer.length,
|
|
||||||
});
|
|
||||||
return res.end(imgBuffer);
|
|
||||||
} else {
|
|
||||||
// Fetch tile from provider
|
|
||||||
const tileUrl = getTileUrl(provider, x, y, z);
|
|
||||||
const response = await axios.get(tileUrl, {
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache the tile in Redis
|
fs.writeFileSync(tilePath, tileData);
|
||||||
const base64Tile = Buffer.from(response.data).toString('base64');
|
|
||||||
redis.setex(cacheKey, 3600 * 24 * 30, base64Tile); // Cache for 1 hour
|
|
||||||
|
|
||||||
// Return the tile to the client
|
res.contentType('image/jpeg');
|
||||||
res.writeHead(200, {
|
res.send(tileData);
|
||||||
'Content-Type': 'image/png',
|
|
||||||
'Content-Length': response.data.length,
|
|
||||||
});
|
|
||||||
return res.end(response.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching tile:', error);
|
console.error('Error fetching tile from API:', error);
|
||||||
res.status(500).send('Error fetching tile');
|
res.status(500).send('Error fetching tile from API');
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
app.get('/hello', cors(), (req: Request, res: Response) => {
|
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
||||||
res.send('Hello, World!')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Route to store GeoJSON data
|
|
||||||
app.post('/geojson', cors(), async (req: Request, res: Response) => {
|
|
||||||
const geoJSON = req.body
|
|
||||||
|
|
||||||
if (!geoJSON || !geoJSON.features) {
|
|
||||||
return res.status(400).send('Invalid GeoJSON')
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = `geojson:${Date.now()}`;
|
|
||||||
redis.set(id, JSON.stringify(geoJSON), (err, reply) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).send('Error saving GeoJSON to Redis');
|
|
||||||
}
|
|
||||||
res.send({ status: 'success', id });
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Route to fetch GeoJSON data
|
|
||||||
app.get('/geojson/:id', cors(), async (req: Request, res: Response) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
|
|
||||||
redis.get(id, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).send('Error fetching GeoJSON from Redis');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
res.send(JSON.parse(data));
|
|
||||||
} else {
|
|
||||||
res.status(404).send('GeoJSON not found');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Server running on http://localhost:${port}`);
|
|
||||||
})
|
|
||||||
|
177
monitor/.gitignore
vendored
Normal file
177
monitor/.gitignore
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
data
|
19
monitor/Dockerfile
Normal file
19
monitor/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Start with an official lightweight image
|
||||||
|
FROM oven/bun:slim
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the current directory contents into the container at /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install project dependencies
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
ENV MONITOR_PORT=$MONITOR_PORT
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE $MONITOR_PORT
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
CMD ["bun", "index.ts"]
|
15
monitor/README.md
Normal file
15
monitor/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# monitor
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.1.21. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
BIN
monitor/bun.lockb
Normal file
BIN
monitor/bun.lockb
Normal file
Binary file not shown.
97
monitor/index.ts
Normal file
97
monitor/index.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { serve } from 'bun';
|
||||||
|
import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
|
const db = new Database('./data/servers.db');
|
||||||
|
|
||||||
|
// Initialize the database table if it doesn't exist
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS servers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
ping_rate INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Function to check server availability
|
||||||
|
async function checkServer(server: { name: string; ip: string; ping_rate: number; }) {
|
||||||
|
try {
|
||||||
|
const response = await Bun.spawn(['ping', '-c', '1', server.ip]);
|
||||||
|
if (response.exitCode === 0) {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${server.name} (${server.ip}) is up.`);
|
||||||
|
} else {
|
||||||
|
console.error(`[${new Date().toISOString()}] ${server.name} (${server.ip}) is down!`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${new Date().toISOString()}] Error pinging ${server.name} (${server.ip}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to monitor servers based on their individual ping rates
|
||||||
|
async function monitorServer(server: any) {
|
||||||
|
while (true) {
|
||||||
|
await checkServer(server);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, server.ping_rate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring all servers
|
||||||
|
async function startMonitoring() {
|
||||||
|
const servers = db.query(`SELECT * FROM servers`).all();
|
||||||
|
servers.forEach(server => monitorServer(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring in the background
|
||||||
|
startMonitoring();
|
||||||
|
|
||||||
|
// API Server to manage servers
|
||||||
|
const server = serve({
|
||||||
|
port: process.env.MONITOR_PORT || 1234,
|
||||||
|
fetch: async (req) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
const method = req.method;
|
||||||
|
|
||||||
|
if (pathname === '/servers' && method === 'GET') {
|
||||||
|
const servers = db.query(`SELECT * FROM servers`).all();
|
||||||
|
return new Response(JSON.stringify(servers), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === '/server' && method === 'POST') {
|
||||||
|
const data = await req.json();
|
||||||
|
const { name, ip, ping_rate } = data;
|
||||||
|
|
||||||
|
if (!name || !ip || !ping_rate) {
|
||||||
|
return new Response('Missing fields', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO servers (name, ip, ping_rate) VALUES (?, ?, ?)`,
|
||||||
|
name, ip, ping_rate
|
||||||
|
);
|
||||||
|
return new Response('Server added', { status: 201 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith('/server/') && method === 'PUT') {
|
||||||
|
const id = pathname.split('/').pop();
|
||||||
|
const data = await req.json();
|
||||||
|
const { name, ip, ping_rate } = data;
|
||||||
|
|
||||||
|
if (!id || !name || !ip || !ping_rate) {
|
||||||
|
return new Response('Missing fields', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`UPDATE servers SET name = ?, ip = ?, ping_rate = ? WHERE id = ?`,
|
||||||
|
name, ip, ping_rate, id
|
||||||
|
);
|
||||||
|
return new Response('Server updated', { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`API server and monitoring running on http://localhost:${server.port}`);
|
11
monitor/package.json
Normal file
11
monitor/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "monitor",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
27
monitor/tsconfig.json
Normal file
27
monitor/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user