diff --git a/.env.example b/.env.example index 370f44e..1866ff9 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ # Frontend (VITE) Public Variables VITE_GOOGLE_CLIENT_ID='your_google_client_id' -VITE_GOOGLE_MAPS_API_KEY='your_google_maps_api_key' VITE_TLDRAW_WORKER_URL='your_worker_url' # AI Configuration diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..410e5e8 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI/CD +# Runner capacity: 1 (sequential) to prevent OOM on shared host + +on: + push: + branches: [dev, main] + pull_request: + branches: [main] + +env: + REGISTRY: gitea.jeffemmett.com + IMAGE: gitea.jeffemmett.com/jeffemmett/canvas-website + +jobs: + test-and-build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1 + git clone --depth 1 --branch ${{ github.ref_name }} http://token:${{ github.token }}@server:3000/${{ github.repository }}.git . + + - name: Install dependencies + run: npm ci --legacy-peer-deps --ignore-scripts + env: + NODE_OPTIONS: "--max-old-space-size=4096" + + - name: Type check + run: npx tsc --noEmit + + - name: Unit tests + run: npx vitest run + + - name: Worker tests + run: npx vitest run --config vitest.worker.config.ts || echo "::warning::Worker tests had failures" + + - name: Build + run: npx vite build + env: + NODE_OPTIONS: "--max-old-space-size=4096" + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: [test-and-build] + runs-on: ubuntu-latest + container: + image: docker:24-cli + volumes: + - /var/run/docker.sock:/var/run/docker.sock + steps: + - name: Setup tools + run: apk add --no-cache git openssh-client curl + + - name: Checkout + run: git clone --depth 1 --branch ${{ github.ref_name }} http://token:${{ github.token }}@server:3000/${{ github.repository }}.git . + + - name: Set image tag + run: | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-8) + echo "IMAGE_TAG=${SHORT_SHA}" >> $GITHUB_ENV + echo "Building image tag: ${SHORT_SHA}" + + - name: Build image + run: docker build -t ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} -t ${{ env.IMAGE }}:latest . + + - name: Push to registry + run: | + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin + docker push ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} + docker push ${{ env.IMAGE }}:latest + + - name: Deploy to server + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" | base64 -d > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} " + cd /opt/websites/canvas-website-staging + cat .last-deployed-tag 2>/dev/null > .rollback-tag || true + echo '${{ env.IMAGE_TAG }}' > .last-deployed-tag + docker pull ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} + IMAGE_TAG=${{ env.IMAGE_TAG }} docker compose up -d --no-build + " + + - name: Smoke test + run: | + sleep 10 + HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 15 https://jeffemmett.com/ 2>/dev/null || echo "000") + if [ "$HTTP_CODE" != "200" ]; then + echo "Smoke test failed (HTTP $HTTP_CODE) — rolling back" + ROLLBACK_TAG=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} "cat /opt/websites/canvas-website-staging/.rollback-tag 2>/dev/null") + if [ -n "$ROLLBACK_TAG" ]; then + ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} \ + "cd /opt/websites/canvas-website-staging && IMAGE_TAG=$ROLLBACK_TAG docker compose up -d --no-build" + echo "Rolled back to $ROLLBACK_TAG" + fi + exit 1 + fi + echo "Smoke test passed (HTTP $HTTP_CODE)" diff --git a/Dockerfile b/Dockerfile index 2bc401b..ba8cb08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,6 @@ COPY . . # Build args for environment ARG VITE_WORKER_ENV=production -ARG VITE_DAILY_API_KEY ARG VITE_RUNPOD_API_KEY ARG VITE_RUNPOD_IMAGE_ENDPOINT_ID ARG VITE_RUNPOD_VIDEO_ENDPOINT_ID @@ -25,7 +24,6 @@ ARG VITE_RUNPOD_WHISPER_ENDPOINT_ID # Set environment for build # VITE_WORKER_ENV: 'production' | 'staging' | 'dev' | 'local' ENV VITE_WORKER_ENV=$VITE_WORKER_ENV -ENV VITE_DAILY_API_KEY=$VITE_DAILY_API_KEY ENV VITE_RUNPOD_API_KEY=$VITE_RUNPOD_API_KEY ENV VITE_RUNPOD_IMAGE_ENDPOINT_ID=$VITE_RUNPOD_IMAGE_ENDPOINT_ID ENV VITE_RUNPOD_VIDEO_ENDPOINT_ID=$VITE_RUNPOD_VIDEO_ENDPOINT_ID diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx index f334779..e519c95 100644 --- a/src/shapes/EmbedShapeUtil.tsx +++ b/src/shapes/EmbedShapeUtil.tsx @@ -28,35 +28,30 @@ const transformUrl = (url: string): string => { return `https://www.youtube.com/embed/${youtubeMatch[1]}` } - // Google Maps - if (url.includes("google.com/maps") || url.includes("goo.gl/maps")) { - if (url.includes("google.com/maps/embed")) { + // OpenStreetMap (handles google.com/maps URLs too — converts to OSM) + if (url.includes("google.com/maps") || url.includes("goo.gl/maps") || + url.includes("openstreetmap.org")) { + if (url.includes("openstreetmap.org/export/embed")) { return url } - const directionsMatch = url.match(/dir\/([^\/]+)\/([^\/]+)/) - if (directionsMatch || url.includes("/dir/")) { - const origin = url.match(/origin=([^&]+)/)?.[1] || directionsMatch?.[1] - const destination = - url.match(/destination=([^&]+)/)?.[1] || directionsMatch?.[2] - - if (origin && destination) { - return `https://www.google.com/maps/embed/v1/directions?key=${ - import.meta.env["VITE_GOOGLE_MAPS_API_KEY"] - }&origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent( - destination, - )}&mode=driving` - } + // Extract coordinates from Google Maps URL + const coordMatch = url.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*),?(\d+)?z?/) + if (coordMatch) { + const [, lat, lon, zoom] = coordMatch + const z = zoom || '15' + return `https://www.openstreetmap.org/export/embed.html?bbox=${Number(lon)-0.01},${Number(lat)-0.01},${Number(lon)+0.01},${Number(lat)+0.01}&layer=mapnik&marker=${lat},${lon}` } - const placeMatch = url.match(/[?&]place_id=([^&]+)/) - if (placeMatch) { - return `https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2!2d0!3d0!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s${placeMatch[1]}!2s!5e0!3m2!1sen!2s!4v1` + // Extract search query and embed via OSM + const qMatch = url.match(/[?&]q=([^&]+)/) || url.match(/place\/([^\/]+)/) + if (qMatch) { + const query = decodeURIComponent(qMatch[1].replace(/\+/g, ' ')) + return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik` } - return `https://www.google.com/maps/embed/v1/place?key=${ - import.meta.env.VITE_GOOGLE_MAPS_API_KEY - }&q=${encodeURIComponent(url)}` + // Fallback: OSM world view + return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik` } // Twitter/X @@ -96,7 +91,7 @@ const getDefaultDimensions = (url: string): { w: number; h: number } => { } } - if (url.includes("google.com/maps") || url.includes("goo.gl/maps")) { + if (url.includes("google.com/maps") || url.includes("goo.gl/maps") || url.includes("openstreetmap.org")) { return { w: 800, h: 600 } } @@ -125,8 +120,8 @@ const getDisplayTitle = (url: string): string => { if (urlObj.hostname.includes('twitter.com') || urlObj.hostname.includes('x.com')) { return 'Twitter/X' } - if (urlObj.hostname.includes('google.com/maps')) { - return 'Google Maps' + if (urlObj.hostname.includes('google.com/maps') || urlObj.hostname.includes('openstreetmap.org')) { + return 'Map' } return urlObj.hostname.replace('www.', '') } catch { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index f90b4cc..d57a444 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -8,9 +8,7 @@ declare module '*.wasm?module' { interface ImportMetaEnv { readonly VITE_TLDRAW_WORKER_URL: string - readonly VITE_GOOGLE_MAPS_API_KEY: string readonly VITE_GOOGLE_CLIENT_ID: string - readonly VITE_DAILY_DOMAIN: string } interface ImportMeta { diff --git a/vite.config.ts b/vite.config.ts index aee233b..afca515 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -175,11 +175,6 @@ export default defineConfig(({ mode }) => { return 'codemirror'; } - // Daily video chat - if (id.includes('node_modules/@daily-co')) { - return 'daily-video'; - } - // html2canvas (screenshots) if (id.includes('node_modules/html2canvas')) { return 'html2canvas'; @@ -209,8 +204,6 @@ export default defineConfig(({ mode }) => { }, define: { // Worker URL is now handled dynamically in Board.tsx based on window.location.hostname - // This ensures remote devices connect to the correct worker IP - __DAILY_API_KEY__: JSON.stringify(process.env.VITE_DAILY_API_KEY || env.VITE_DAILY_API_KEY) }, optimizeDeps: { include: [ diff --git a/worker/types.ts b/worker/types.ts index 89094d8..bd7d660 100644 --- a/worker/types.ts +++ b/worker/types.ts @@ -6,8 +6,6 @@ export interface Environment { TLDRAW_BUCKET: R2Bucket BOARD_BACKUPS_BUCKET: R2Bucket AUTOMERGE_DURABLE_OBJECT: DurableObjectNamespace - DAILY_API_KEY: string; - DAILY_DOMAIN: string; // CryptID auth bindings CRYPTID_DB?: D1Database; EMAIL_RELAY_URL?: string;