Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ docker-compose*.yml
cloudbuild.yaml

# Drizzle
drizzle/

# Scripts
install.sh
Expand Down
9 changes: 9 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE
DATABASE_URL=postgresql://postgres:[YOUR-POSTGRES-PASSWORD]@[YOUR-SUPABASE-DB-HOST]:[PORT]/postgres

# Vercel Sandbox (MicroVM Infrastructure)
# Required for OIDC-based microVM authentication
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
Comment on lines +35 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Reorder environment variables to alphabetical order.

The static analysis tool (dotenv-linter) reports that VERCEL_TEAM_ID (line 38) should appear before VERCEL_TOKEN (line 36) to maintain alphabetical/conventional ordering.

🔧 Proposed reordering
-# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
-VERCEL_TOKEN=your_vercel_token
-# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
-VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
+VERCEL_PROJECT_ID=your_vercel_project_id
+# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
+VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
+VERCEL_TOKEN=your_vercel_token
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 38-38: [UnorderedKey] The VERCEL_TEAM_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.local.example around lines 35 - 38, Reorder the environment variables
in the file to maintain alphabetical order as required by dotenv-linter. Move
the VERCEL_TEAM_ID variable block (including its comment lines) to appear before
the VERCEL_TOKEN variable block. This ensures that VERCEL_TEAM_ID comes before
VERCEL_TOKEN alphabetically, satisfying the linting rule for conventional
environment variable ordering.

Source: Linters/SAST tools

# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
Comment on lines +35 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing VERCEL_PROJECT_ID environment variable assignment.

Line 39 shows the comment for VERCEL_PROJECT_ID but the actual environment variable assignment is missing. The upstream code in lib/agents/tools/index.tsx:40 conditionally registers the sandbox tool only when all three variables are set (VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID).

✅ Proposed fix to complete the configuration
 # VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
+VERCEL_PROJECT_ID=your_vercel_project_id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
VERCEL_PROJECT_ID=your_vercel_project_id
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 38-38: [UnorderedKey] The VERCEL_TEAM_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.local.example around lines 35 - 39, The VERCEL_PROJECT_ID environment
variable assignment is missing from the configuration file. After the comment
describing VERCEL_PROJECT_ID on line 39, add the actual environment variable
assignment following the same pattern as VERCEL_TOKEN and VERCEL_TEAM_ID above
it. The assignment should include the variable name VERCEL_PROJECT_ID followed
by an equals sign and a placeholder value like your_vercel_project_id to match
the format and documentation of the other two Vercel environment variables in
the file.

VERCEL_PROJECT_ID=your_vercel_project_id
97 changes: 97 additions & 0 deletions CLOUD_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Cloud Deployment Guide for QCX

This guide provides instructions for deploying the QCX stack (Next.js, PostgreSQL with pgvector/PostGIS, and Qdrant) to various cloud platforms.

## Infrastructure Overview

The application consists of:
1. **QCX Web App**: Next.js application running on Bun.
2. **PostgreSQL**: Database with `pgvector` and `PostGIS` extensions.
3. **Qdrant**: High-performance vector database (optional, for advanced vector search).

---

## 1. Deploying with Docker Compose (VPS / EC2 / Compute Engine)

The easiest way to deploy the entire stack is using `docker-compose`.

1. **Clone the repository** on your server.
2. **Create a `.env` file** based on the environment variables in `docker-compose.yaml`.
3. **Run the stack**:
```bash
docker-compose up -d --build
```
Comment on lines +14 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add production persistence guidance for docker-compose.

The Docker Compose section omits important production considerations. Add notes on volume persistence and backup strategy:

💾 Suggested additions

Add after line 22:

4.  **Verify persistence** (important for production):
    - Ensure `postgres_data` and `qdrant_data` volumes are properly mounted on the host.
    - Implement regular backups of these volumes.
    - Consider external managed databases (RDS, Cloud SQL, Neon) for production workloads.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 21-21: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLOUD_DEPLOYMENT.md` around lines 14 - 23, The Docker Compose deployment
section in CLOUD_DEPLOYMENT.md is missing critical production considerations for
data persistence. Add a new step 4 after the "Run the stack" command that
addresses volume persistence by ensuring postgres_data and qdrant_data volumes
are properly mounted on the host, provides guidance on implementing regular
backups of these volumes, and recommends considering external managed databases
(RDS, Cloud SQL, Neon) as alternatives for production workloads to ensure data
durability and reliability.


---

## 2. Deploying to Render

Render is a great choice for managed services.

### PostgreSQL (Managed)
1. Create a **New PostgreSQL** instance on Render.
2. **Note**: Standard Render Postgres does not include `pgvector` or `PostGIS` by default on all plans. You may need to use a Docker-based Postgres on Render or ensure your plan supports these extensions.
3. If using Render's managed Postgres, run the extensions command manually via a SQL client:
```sql
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS vector;
```

### Web Service
1. Create a **New Web Service** pointing to your repository.
2. Select **Docker** as the runtime.
3. Specify the **Dockerfile path** as `Dockerfile`.
4. Add environment variables:
* `DATABASE_URL`: Your Render Postgres connection string.
* `EXECUTE_MIGRATIONS`: `true`
* `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN`: Your Mapbox token.
* (Add other necessary API keys like Google, xAI, etc.)

### Qdrant (Optional)
1. Create a **New Private Service** or **Web Service**.
2. Select **Docker** as the runtime.
3. Use the image: `qdrant/qdrant:latest`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin Qdrant image version for production.

Line 53 uses qdrant/qdrant:latest, which is a production anti-pattern. Latest tags can change unexpectedly, breaking deployments. Pin to a specific version (e.g., qdrant/qdrant:v1.13.0).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLOUD_DEPLOYMENT.md` at line 53, The Qdrant Docker image specification uses
the `latest` tag which can change unexpectedly and cause production deployments
to break. Replace the image reference `qdrant/qdrant:latest` with a specific
pinned version like `qdrant/qdrant:v1.13.0` to ensure consistent and
reproducible deployments.

4. Set `QDRANT_URL` in your Web Service to point to this service.

---

## 3. Deploying to Google Cloud (GCP)

### Cloud Run
1. **Build and Push** the image to Google Artifact Registry:
```bash
docker build -t gcr.io/YOUR_PROJECT/qcx .
docker push gcr.io/YOUR_PROJECT/qcx
```
2. **Deploy to Cloud Run**:
```bash
gcloud run deploy qcx --image gcr.io/YOUR_PROJECT/qcx --platform managed
```
Comment on lines +68 to +69

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Cloud Run deployment command is incomplete for production.

Line 68's gcloud run deploy command is missing critical flags required for a functional deployment:

  • --region: Specifies the Cloud Run region
  • --allow-unauthenticated: Required if the service should be public
  • --set-env-vars: Must pass DATABASE_URL, QDRANT_URL, NODE_ENV, and API keys

Additionally, Cloud Run is serverless and ephemeral—a direct DATABASE_URL connection without connection pooling (PgBouncer, Supavisor, or Cloud SQL Proxy) will exhaust connection limits. Add guidance to use Cloud SQL Proxy or a dedicated connection pool.

📋 Suggested improvements to Cloud Run deployment section
2.  **Deploy to Cloud Run**:
    ```bash
    gcloud run deploy qcx \
      --image gcr.io/YOUR_PROJECT/qcx \
      --platform managed \
      --region us-central1 \
      --allow-unauthenticated \
      --set-env-vars=DATABASE_URL=<YOUR_DATABASE_URL>,QDRANT_URL=<YOUR_QDRANT_URL>,NODE_ENV=production \
      --memory 1Gi \
      --cpu 1 \
      --timeout 3600
    ```

3.  **Set up Cloud SQL Proxy** (critical for connection pooling):
    - Cloud Run cannot maintain long-lived connections to Cloud SQL.
    - Use Cloud SQL Proxy or Supavisor to pool connections.
    - Example: Use `cloud-sql-proxy` sidecar or configure DATABASE_URL with `?sslmode=require` and proper connection string routing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLOUD_DEPLOYMENT.md` around lines 68 - 69, The gcloud run deploy qcx command
is incomplete for production deployment. Update the command to include all
required flags: add --region to specify the Cloud Run region, add
--allow-unauthenticated if the service should be public, and add --set-env-vars
to pass critical environment variables like DATABASE_URL, QDRANT_URL, NODE_ENV,
and API keys. Additionally, add a new section after the Cloud Run deployment
command that explains the connection pooling requirement for Cloud Run's
ephemeral nature, recommending the use of Cloud SQL Proxy or Supavisor to handle
database connections, since direct DATABASE_URL connections will exhaust
connection limits in a serverless environment.


### Cloud SQL
1. Create a **Cloud SQL for PostgreSQL** instance (version 15+).
2. Cloud SQL supports both `pgvector` and `postgis`. Enable them via:
```sql
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS vector;
```

---

## 4. Vector Database Options

QCX is configured to support two vector database options:

1. **PostgreSQL (pgvector)**: Integrated into the main database. Best for smaller datasets and simplicity.
2. **Qdrant**: Dedicated vector database. Recommended for large-scale production use cases requiring high performance and advanced filtering.

To use Qdrant, ensure the `QDRANT_URL` environment variable is set in your application environment.

---

## 5. Security Checklist

* [ ] Change default passwords in `docker-compose.yaml`.
* [ ] Use SSL for database connections in production (`ssl=true` in `DATABASE_URL`).
* [ ] Set `NODE_ENV=production`.
* [ ] Ensure all sensitive API keys are stored as secrets, not committed to code.
19 changes: 15 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ FROM oven/bun:1.3.5-alpine AS runner
WORKDIR /app

# Create non-root group and user, with nextjs belonging to nodejs group
RUN addgroup -g 1001 -S nodejs \
&& adduser -S -u 1001 -G nodejs nextjs
RUN addgroup -g 1001 -S nodejs && adduser -S -u 1001 -G nodejs nextjs

# Environment variables
ENV NODE_ENV=production
Expand All @@ -52,15 +51,27 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy public folder
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

# Copy migration files and entrypoint
# Note: In standalone mode, we need to ensure all migration dependencies are available.
# We copy the entire project source temporarily to a different folder if needed,
# or just ensure drizzle and lib/db are available for the migration script.
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/lib/db/migrate.ts ./lib/db/migrate.ts
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
COPY --chown=nextjs:nodejs docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh

# Switch to non-root user
USER nextjs

# Expose port
EXPOSE 3000

# Health check (uses Bun to fetch; adjust /api/health if your endpoint differs)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD ["bun", "--eval", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD ["bun", "--eval", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]

# Use entrypoint script to handle migrations
ENTRYPOINT ["./docker-entrypoint.sh"]

# Run the standalone server with Bun (fully compatible in Bun 1.3+)
CMD ["bun", "server.js"]
21 changes: 21 additions & 0 deletions Dockerfile.db
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM postgres:16-alpine

# Install build dependencies and pgvector
RUN apk add --no-cache --virtual .build-deps \
git \
build-base \
clang15 \
llvm15-dev \
postgis-dev \
postgresql-dev \
&& git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git /tmp/pgvector \
&& cd /tmp/pgvector \
&& make \
&& make install \
&& apk add --no-cache postgis \
&& rm -rf /tmp/pgvector \
&& apk del .build-deps

# Use the default entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]
33 changes: 32 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents'
import { writer } from '@/lib/agents/writer'
import { saveChat, getSystemPrompt } from '@/lib/actions/chat'
import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat'
import { Chat, AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
import { BotMessage } from '@/components/message'
Expand All @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler'
import { SandboxSection } from '@/components/sandbox-section'

// Define the type for related queries
type RelatedQueries = {
Expand All @@ -50,6 +51,20 @@ async function submit(formData?: FormData, skip?: boolean) {
console.error('Failed to parse drawnFeatures:', e);
}

if (action === 'generate_report_context') {
const messagesString = formData?.get('messages');
if (typeof messagesString !== 'string') {
return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' };
}
try {
const messages = JSON.parse(messagesString) as AIMessage[];
return await generateReportContext(messages);
} catch (e) {
console.error('Failed to parse messages for report context:', e);
return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' };
}
}

if (action === 'resolution_search') {
const file_mapbox = formData?.get('file_mapbox') as File;
const file_google = formData?.get('file_google') as File;
Expand Down Expand Up @@ -897,6 +912,22 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
),
isCollapsed: isCollapsed.value
}
case 'sandbox': {
const logs = createStreamableValue(toolOutput.logs)
logs.done(toolOutput.logs)
return {
id,
component: (
<SandboxSection
logs={logs.value}
previewUrl={toolOutput.previewUrl}
exitCode={toolOutput.exitCode}
error={toolOutput.error}
/>
),
isCollapsed: isCollapsed.value
}
}
default:
console.warn(
`Unhandled tool result in getUIStateFromAIState: ${name}`
Expand Down
Loading