Merge branch 'dev'
This commit is contained in:
commit
53f24762b0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue