Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
872ebc1971
|
||
|
|
3934cbf3ae
|
||
|
|
36f7ce8e5d
|
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:20-slim AS frontendbuilder
|
||||||
|
|
||||||
|
WORKDIR /tmp/frontendbuilder
|
||||||
|
COPY public/ .
|
||||||
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3.11.4-slim-buster
|
||||||
|
|
||||||
|
WORKDIR /var/www/html/
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY --from=frontendbuilder /tmp/frontendbuilder/dist/ /var/www/html/public
|
||||||
|
|
||||||
|
VOLUME /var/www/html/public
|
||||||
|
|
||||||
|
CMD uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: .build/prod/app.Dockerfile
|
||||||
|
hostname: mapmaker
|
||||||
|
image: git.shihaam.dev/sarlink/mapmaker/api
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./nginx.Dockerfile
|
||||||
|
hostname: mapmaker-nginx
|
||||||
|
image: git.shihaam.dev/sarlink/mapmaker/nginx
|
||||||
+10
-13
@@ -6,7 +6,7 @@ server {
|
|||||||
error_log /dev/stdout info;
|
error_log /dev/stdout info;
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api:8000/api/;
|
proxy_pass http://mapmaker:8000/api/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -15,7 +15,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /ws/ {
|
location /ws/ {
|
||||||
proxy_pass http://api:8000/ws/;
|
proxy_pass http://mapmaker:8000/ws/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -23,22 +23,19 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# WebSocket timeout settings
|
|
||||||
proxy_read_timeout 86400s;
|
proxy_read_timeout 86400s;
|
||||||
proxy_send_timeout 86400s;
|
proxy_send_timeout 86400s;
|
||||||
proxy_connect_timeout 60s;
|
proxy_connect_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# In development, proxy to Vite dev server
|
root /var/www/html/public;
|
||||||
proxy_pass http://node:3000;
|
try_files $uri $uri/ /index.html;
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
# Cache static assets
|
||||||
proxy_set_header Connection "upgrade";
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
proxy_set_header Host $host;
|
expires 1y;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
add_header Cache-Control "public, immutable";
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
}
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
FROM node:24-bookworm-slim
|
|
||||||
|
|
||||||
WORKDIR /var/www/html/public
|
|
||||||
|
|
||||||
CMD npm run dev -- --host 0.0.0.0
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM python:3.11.4-slim-buster
|
|
||||||
|
|
||||||
WORKDIR /var/www/html/
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Build and deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Build and Push Docker Images
|
||||||
|
runs-on: builder
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REGISTRY_URL }}
|
||||||
|
username: ${{ vars.DOCKER_REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push docker images
|
||||||
|
working-directory: .build/prod
|
||||||
|
run: docker compose --progress plain build --push
|
||||||
|
|
||||||
|
|
||||||
|
- name: Deploy production
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
ssh root@10.0.1.36 -t "cd /mnt && \
|
||||||
|
docker compose --progress plain pull && \
|
||||||
|
docker compose --progress plain down && \
|
||||||
|
docker compose --progress plain up -d"
|
||||||
|
|
||||||
|
- name: Clean up dangling images
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: ssh root@10.0.1.36 -t "docker image prune -f"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useMapEvents, Polyline, Marker } from 'react-leaflet';
|
import { useMapEvents, Polyline, Marker } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
|
||||||
import { useDrawingStore } from '../../stores/drawingStore';
|
import { useDrawingStore } from '../../stores/drawingStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { mapItemService } from '../../services/mapItemService';
|
import { mapItemService } from '../../services/mapItemService';
|
||||||
@@ -12,7 +11,7 @@ interface DrawingHandlerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
||||||
const { activeTool, isDrawing, drawingPoints, setIsDrawing, addDrawingPoint, resetDrawing, setActiveTool } =
|
const { activeTool, isDrawing, drawingPoints, setIsDrawing, addDrawingPoint, resetDrawing } =
|
||||||
useDrawingStore();
|
useDrawingStore();
|
||||||
const { showToast } = useUIStore();
|
const { showToast } = useUIStore();
|
||||||
const [cursorPosition, setCursorPosition] = useState<[number, number] | null>(null);
|
const [cursorPosition, setCursorPosition] = useState<[number, number] | null>(null);
|
||||||
@@ -307,11 +306,6 @@ export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
|
|||||||
|
|
||||||
const dashArray = isWirelessTool ? '10, 10' : undefined;
|
const dashArray = isWirelessTool ? '10, 10' : undefined;
|
||||||
|
|
||||||
// Create preview line from last point to cursor
|
|
||||||
const previewPositions = cursorPosition
|
|
||||||
? [...drawingPoints, cursorPosition]
|
|
||||||
: drawingPoints;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Main line connecting all points */}
|
{/* Main line connecting all points */}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
interface LayerInfo {
|
interface LayerInfo {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Polyline, Marker, Popup, Circle, Tooltip, useMapEvents } from 'react-leaflet';
|
import { Polyline, Marker, Popup, Circle, useMapEvents } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import { mapItemService } from '../../services/mapItemService';
|
import { mapItemService } from '../../services/mapItemService';
|
||||||
import { uploadService } from '../../services/uploadService';
|
import { uploadService } from '../../services/uploadService';
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function MapView({ mapId, activeLayer, mapLayers, showShareDialog = false
|
|||||||
};
|
};
|
||||||
|
|
||||||
// WebSocket connection for real-time updates
|
// WebSocket connection for real-time updates
|
||||||
const { isConnected, permission } = useMapWebSocket({
|
const { permission } = useMapWebSocket({
|
||||||
mapId: mapId || '',
|
mapId: mapId || '',
|
||||||
onItemCreated: (item) => {
|
onItemCreated: (item) => {
|
||||||
console.log('Real-time item created:', item);
|
console.log('Real-time item created:', item);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function ShareDialog({ mapId, onClose }: ShareDialogProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await mapShareService.shareWithUser(mapId, {
|
await mapShareService.shareWithUser(mapId, {
|
||||||
user_identifier: newUserId.trim(),
|
user_id: newUserId.trim(),
|
||||||
permission: newUserPermission,
|
permission: newUserPermission,
|
||||||
});
|
});
|
||||||
setNewUserId('');
|
setNewUserId('');
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { DrawingTool } from '../../types/mapItem';
|
|||||||
import { CABLE_COLORS, CABLE_LABELS } from '../../types/mapItem';
|
import { CABLE_COLORS, CABLE_LABELS } from '../../types/mapItem';
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
mapId: string;
|
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ const INFO_TOOL: ToolButton = {
|
|||||||
description: 'Information Marker',
|
description: 'Information Marker',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Toolbar({ mapId, readOnly = false }: ToolbarProps) {
|
export function Toolbar({ readOnly = false }: ToolbarProps) {
|
||||||
const { activeTool, setActiveTool } = useDrawingStore();
|
const { activeTool, setActiveTool } = useDrawingStore();
|
||||||
|
|
||||||
const renderIcon = (tool: ToolButton, isDisabled: boolean) => {
|
const renderIcon = (tool: ToolButton, isDisabled: boolean) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function useMapWebSocket({
|
|||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [permission, setPermission] = useState<'read' | 'edit' | null>(null);
|
const [permission, setPermission] = useState<'read' | 'edit' | null>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||||
const reconnectAttemptsRef = useRef(0);
|
const reconnectAttemptsRef = useRef(0);
|
||||||
|
|
||||||
// Use refs for callbacks to avoid reconnecting when they change
|
// Use refs for callbacks to avoid reconnecting when they change
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function Dashboard() {
|
|||||||
{selectedMapId && (
|
{selectedMapId && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700">
|
<div className="border-t border-gray-200 dark:border-gray-700">
|
||||||
<Toolbar mapId={selectedMapId} />
|
<Toolbar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map Style section */}
|
{/* Map Style section */}
|
||||||
@@ -84,7 +84,7 @@ export function Dashboard() {
|
|||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Map Style</h3>
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Map Style</h3>
|
||||||
<LayerSwitcher
|
<LayerSwitcher
|
||||||
activeLayer={activeLayer}
|
activeLayer={activeLayer}
|
||||||
onLayerChange={setActiveLayer}
|
onLayerChange={(layer) => setActiveLayer(layer as MapLayer)}
|
||||||
layers={MAP_LAYERS}
|
layers={MAP_LAYERS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { MapContainer, TileLayer, useMap } from 'react-leaflet';
|
import { MapContainer, TileLayer, useMap } from 'react-leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { LayerSwitcher } from '../components/map/LayerSwitcher';
|
import { LayerSwitcher } from '../components/map/LayerSwitcher';
|
||||||
import { DrawingHandler } from '../components/map/DrawingHandler';
|
import { DrawingHandler } from '../components/map/DrawingHandler';
|
||||||
import { MapItemsLayer } from '../components/map/MapItemsLayer';
|
import { MapItemsLayer } from '../components/map/MapItemsLayer';
|
||||||
import { Toolbar } from '../components/map/Toolbar';
|
import { Toolbar } from '../components/map/Toolbar';
|
||||||
import { MapView } from '../components/map/MapView';
|
|
||||||
import { useMapWebSocket } from '../hooks/useMapWebSocket';
|
import { useMapWebSocket } from '../hooks/useMapWebSocket';
|
||||||
import { apiClient } from '../services/api';
|
import { apiClient } from '../services/api';
|
||||||
import { useUIStore } from '../stores/uiStore';
|
import { useUIStore } from '../stores/uiStore';
|
||||||
@@ -49,7 +48,6 @@ function MapController() {
|
|||||||
|
|
||||||
export function SharedMap() {
|
export function SharedMap() {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { darkMode, toggleDarkMode } = useUIStore();
|
const { darkMode, toggleDarkMode } = useUIStore();
|
||||||
const [activeLayer, setActiveLayer] = useState<MapLayer>('osm');
|
const [activeLayer, setActiveLayer] = useState<MapLayer>('osm');
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
@@ -201,7 +199,7 @@ export function SharedMap() {
|
|||||||
style={{ zIndex: 9999 }}
|
style={{ zIndex: 9999 }}
|
||||||
>
|
>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700">
|
<div className="border-t border-gray-200 dark:border-gray-700">
|
||||||
<Toolbar mapId={mapData.id} readOnly={isReadOnly} />
|
<Toolbar readOnly={isReadOnly} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map Style section */}
|
{/* Map Style section */}
|
||||||
@@ -209,7 +207,7 @@ export function SharedMap() {
|
|||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Map Style</h3>
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 px-1 mb-2">Map Style</h3>
|
||||||
<LayerSwitcher
|
<LayerSwitcher
|
||||||
activeLayer={activeLayer}
|
activeLayer={activeLayer}
|
||||||
onLayerChange={setActiveLayer}
|
onLayerChange={(layer) => setActiveLayer(layer as MapLayer)}
|
||||||
layers={MAP_LAYERS}
|
layers={MAP_LAYERS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +227,7 @@ export function SharedMap() {
|
|||||||
url={MAP_LAYERS[activeLayer].url}
|
url={MAP_LAYERS[activeLayer].url}
|
||||||
attribution={MAP_LAYERS[activeLayer].attribution}
|
attribution={MAP_LAYERS[activeLayer].attribution}
|
||||||
maxZoom={MAP_LAYERS[activeLayer].maxZoom}
|
maxZoom={MAP_LAYERS[activeLayer].maxZoom}
|
||||||
maxNativeZoom={MAP_LAYERS[activeLayer].maxNativeZoom}
|
maxNativeZoom={(MAP_LAYERS[activeLayer] as any).maxNativeZoom}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Drawing handler for edit access */}
|
{/* Drawing handler for edit access */}
|
||||||
|
|||||||
Reference in New Issue
Block a user