Swadhin Biswas

[email protected]
2026-07-14 23:30:58 +0600 committed NaN years ago
Co-auth
commit Co-auth
176 files changed
+5839 -6983
Showing 96 changed files+3347-3951
This diff is too large and has been truncated. Displaying first 96 files.
.astro/settings.json.github/workflows/docker-publish.yml.mimocode/.cron-lockDockerfile.workerREADME.mdastro.config.mjscli/src/commands/stack/index.tsdocker-compose.nas.ymldocker-compose.ymldocs-site/astro.config.mjsdocs-site/src/content/docs/FREE-DEPLOYMENT.mddocs-site/src/content/docs/RENDER-DEPLOYMENT.mddocs-site/src/content/docs/administration/configuration.mddocs-site/src/content/docs/development/architecture.mddocs-site/src/content/docs/development/contributing.mddocs-site/src/content/docs/development/database-schema.mddocs-site/src/content/docs/development/local-dev-setup.mddocs-site/src/content/docs/features/ai-review.mddocs-site/src/content/docs/getting-started/first-repository.mddocs-site/src/content/docs/getting-started/installation.mddocs-site/src/content/docs/getting-started/quick-start.mddocs-site/src/content/docs/guides/example.mddocs-site/src/content/docs/index.mdxdocs-site/src/content/docs/reference/example.mddocs/FREE-DEPLOYMENT.mddocs/RENDER-DEPLOYMENT.mddocs/administration/deploy-nas.mddocs/administration/incident-runbook.mddrizzle/0000_abandoned_king_bedlam.sqldrizzle/0000_black_monster_badoon.sqldrizzle/0001_faithful_randall.sqldrizzle/0002_third_cable.sqldrizzle/0003_married_cyclops.sqldrizzle/0004_add_performance_indexes.sqlmake-admin.tsout.jsonpackage.jsonprotection.jsonrender.yamlscripts/ssh-server.tsscripts/worker.tssrc/components/ThemeToggle.tsxsrc/components/actions/PipelineVisualizer.tsxsrc/components/admin/AdminDashboard.tsxsrc/components/admin/AlertBanner.tsxsrc/components/admin/BottomPanel.tsxsrc/components/admin/GlobeViz.tsxsrc/components/admin/LiveLogStream.tsxsrc/components/admin/QuickStatsRow.tsxsrc/components/admin/RecentActivity.tsxsrc/components/admin/SystemStatus.tsxsrc/components/admin/TrendingDevelopers.tsxsrc/components/dashboard/ActivityStream.tsxsrc/components/dashboard/StackCard.astrosrc/components/dashboard/UserAvatar.tsxsrc/components/diff/DiffView.tsxsrc/components/home/FeatureDemosSection.tsxsrc/components/home/HeroSection.tsxsrc/components/ide/WebIDE.tsxsrc/components/inbox/KeyboardReviewInbox.tsxsrc/components/issues/IssueDetail.tsxsrc/components/issues/IssuesList.tsxsrc/components/kanban/Card.tsxsrc/components/kanban/Column.tsxsrc/components/layout/Header.astrosrc/components/milestones/MilestonesList.tsxsrc/components/milestones/NewMilestoneForm.tsxsrc/components/pr/AIReviewPanel.tsxsrc/components/pr/StackVisualization.tsxsrc/components/pulls/CodeOwnersEnforcement.tsxsrc/components/pulls/CustomPRState.tsxsrc/components/pulls/PullRequestsList.tsxsrc/components/repo/FileExplorer.astrosrc/components/repo/RepoActions.tsxsrc/components/repo/RepoControls.tsxsrc/components/repo/RepoHeader.astrosrc/components/settings/MFASetup.tsxsrc/components/settings/TokensManager.tsxsrc/components/setup/SetupWizard.tsxsrc/components/ui/animated-tooltip.tsxsrc/components/ui/button.tsxsrc/components/ui/typewriter-effect.tsxsrc/db/index.tssrc/layouts/BaseLayout.astrosrc/layouts/SettingsLayout.astrosrc/lib/auth.tssrc/lib/ci-gates.tssrc/lib/git-server.tssrc/lib/git-storage.tssrc/lib/markdown.tssrc/lib/pipeline.tssrc/lib/realtime.tssrc/lib/security-advanced.tssrc/lib/ssh.tssrc/lib/storage.tssrc/lib/webhooks.ts
▼.astro/settings.json
+1-1
1
1
{
2
2
"_variables": {
3
- "lastUpdateCheck": 1783539359887
3
+ "lastUpdateCheck": 1783961044818
4
4
}
5
5
}
6
6
▼.github/workflows/docker-publish.yml
+1-1
8
8
9
9
env:
10
10
REGISTRY: docker.io
11
- IMAGE_NAME: swadhinbiswas/opencodehub
11
+ IMAGE_NAME: opencodehub/opencodehub
12
12
13
13
jobs:
14
14
build-and-push:
15
15
▼.mimocode/.cron-lock
+1-0
1
+{"pid":359576,"startedAt":1784030995286}
0
2
▼Dockerfile.worker
+44-0
1
+FROM oven/bun:1 AS base
2
+WORKDIR /app
3
+
4
+# Install dependencies
5
+FROM base AS deps
6
+RUN apt-get update && apt-get install -y python3 make g++ gcc libc6-dev && rm -rf /var/lib/apt/lists/*
7
+COPY package.json bun.lock ./
8
+COPY cli/package.json ./cli/package.json
9
+RUN for i in 1 2 3; do \
10
+ bun install --frozen-lockfile && break || \
11
+ (echo "bun install attempt $i failed, retrying in 10s..." && sleep 10); \
12
+ done
13
+
14
+# Production image
15
+FROM oven/bun:1 AS runner
16
+WORKDIR /app
17
+
18
+# Install git (needed for git operations)
19
+RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
20
+
21
+# Create data directories
22
+RUN mkdir -p /data/repositories /data/storage /data/cache && \
23
+ chown -R bun:bun /data
24
+
25
+# Copy node_modules and source
26
+COPY --from=deps --chown=bun:bun /app/node_modules ./node_modules
27
+COPY --from=deps --chown=bun:bun /app/package.json ./
28
+COPY --from=deps --chown=bun:bun /app/cli ./cli
29
+COPY --from=deps --chown=bun:bun /app/tsconfig.json ./
30
+
31
+# Copy source files needed for worker
32
+COPY --from=deps --chown=bun:bun /app/src/lib ./src/lib
33
+COPY --from=deps --chown=bun:bun /app/src/db ./src/db
34
+COPY --from=deps --chown=bun:bun /app/scripts/worker.ts ./scripts/worker.ts
35
+
36
+# Set environment variables
37
+ENV NODE_ENV=production
38
+ENV DATA_DIR=/data
39
+
40
+# Switch to non-root user
41
+USER bun
42
+
43
+# Start the worker
44
+CMD ["bun", "scripts/worker.ts"]
0
45
▼README.md
+62-422
4
4
<picture>
5
5
<source media="(prefers-color-scheme: dark)" srcset="public/logo-dark.png">
6
6
<source media="(prefers-color-scheme: light)" srcset="public/logo-light.png">
7
- <img src="public/logo-light.png" alt="OpenCodeHub Logo" width="420" />
7
+ <img src="public/logo-light.png" alt="OpenCodeHub" width="380" />
8
8
</picture>
9
9
</p>
10
10
11
-<p align="center">
12
- <strong>A modern, self-hosted Git platform for teams that want speed, control, and clean workflows.</strong>
13
-</p>
14
-
15
11
<p align="center">
16
12
<a href="https://www.npmjs.com/package/opencodehub-cli"><img src="https://img.shields.io/npm/v/opencodehub-cli?style=flat-square&label=CLI" alt="CLI version"></a>
17
13
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License"></a>
18
- <a href="docs/index.md"><img src="https://img.shields.io/badge/docs-available-success?style=flat-square" alt="Docs"></a>
19
14
<a href="https://github.com/swadhinbiswas/OpencodeHub/actions"><img src="https://img.shields.io/badge/CI-passing-success?style=flat-square" alt="CI"></a>
20
15
</p>
21
16
22
-
23
-## Table of Contents
24
-
25
-- [Why OpenCodeHub](#why-opencodehub)
26
-- [Features](#features)
27
-- [Quick Start](#quick-start)
28
-- [Architecture](#architecture)
29
-- [Documentation](#documentation)
30
-- [CLI](#cli)
31
-- [API](#api)
32
-- [Security](#security)
33
-- [Deployment](#deployment)
34
-- [Contributing](#contributing)
35
-- [License](#license)
36
-
37
-
38
-## Why OpenCodeHub
39
-
40
-Most teams eventually hit the same pain points:
41
-
42
-- **PRs are too large to review quickly** — Stacked PRs let you break changes into reviewable pieces
43
-- **Tooling is split across many services** — One platform for Git, PRs, CI/CD, issues, and wiki
44
-- **Data residency and security requirements** — Self-hosted by default, you control everything
45
-- **Merge queue complexity** — Stack-aware merge queue with speculative builds
46
-- **Slow review cycles** — AI-powered code review catches issues before human review
47
-
48
-OpenCodeHub is built for that reality.
49
-
50
-
51
-## Features
52
-
53
-### Core Platform
54
-
55
-| Feature | Status | Description |
56
-|---------|--------|-------------|
57
-| **Git Hosting** | Ready | HTTP (Smart Protocol) + SSH git push/pull |
58
-| **Pull Requests** | Ready | Comments, reviews, drafts, approvals, suggested changes |
59
-| **Issues** | Ready | Labels, milestones, project boards |
60
-| **Wiki** | Ready | Repository wiki with revision history |
61
-| **Organizations** | Ready | Teams, collaborators, repository settings |
62
-| **Branch Protection** | Ready | Required reviews, status checks, push restrictions |
63
-
64
-### Delivery Workflows
65
-
66
-| Feature | Status | Description |
67
-|---------|--------|-------------|
68
-| **Stacked PRs** | Ready | Graphite-style stack workflows in web + CLI |
69
-| **Merge Queue** | Ready | Stack-aware queue with speculative builds and priority lanes |
70
-| **CI/CD Pipelines** | Ready | GitHub Actions-compatible engine + Docker-based runner |
71
-| **Webhooks** | Ready | Outbound webhooks with event filtering |
72
-| **Automations** | Ready | Workflow automation rules |
73
-
74
-### Security & Governance
75
-
76
-| Feature | Status | Description |
77
-|---------|--------|-------------|
78
-| **Authentication** | Ready | JWT sessions, OAuth (GitHub, Google, GitLab), 2FA/TOTP |
79
-| **Authorization** | Ready | RBAC with roles, team permissions, collaborator levels |
80
-| **Rate Limiting** | Ready | Redis-backed per-endpoint rate limiting |
81
-| **CSRF Protection** | Ready | Double-submit cookie pattern |
82
-| **Secret Scanning** | Ready | Detect secrets in commits |
83
-| **Audit Logging** | Ready | Track all administrative actions |
84
-
85
-### Extensibility
86
-
87
-| Feature | Status | Description |
88
-|---------|--------|-------------|
89
-| **REST API** | Ready | 140+ API routes |
90
-| **GraphQL** | Ready | Full GraphQL endpoint |
91
-| **OpenAPI** | Ready | Auto-generated OpenAPI spec |
92
-| **CLI** | Ready | 20+ command groups (`opencodehub-cli`) |
93
-| **Storage Adapters** | Ready | 8+ backends (S3, GCS, Azure, Google Drive, etc.) |
17
+Self-hosted Git platform with stacked PRs, merge queue, CI/CD, and AI code review. Built for teams that want speed and control.
94
18
95
19
---
96
20
97
21
## Quick Start
98
22
99
-### Prerequisites
100
-
101
-- Node.js >= 20
102
-- npm or bun
103
-- git
104
-- PostgreSQL 14+ (recommended) or SQLite (dev)
105
-
106
-### Docker (Fastest)
107
-
108
23
```bash
109
-# 1. Clone
110
24
git clone https://github.com/swadhinbiswas/OpencodeHub.git
111
25
cd OpenCodeHub
112
-
113
-# 2. Configure
114
26
cp .env.example .env
115
-# Edit .env with your settings
116
-
117
-# 3. Start
118
-docker-compose up -d
119
-
120
-# 4. Create admin user
121
-docker-compose exec app bun run scripts/seed-admin.ts
122
-
123
-# 5. Open
124
-open http://localhost:4321
27
+docker compose up -d
28
+docker compose exec app bun run scripts/seed-admin.ts
125
29
```
126
30
127
-### NAS & Edge Deployment (Cloudflare / Tailscale)
128
-
129
-Deploying on a home server or NAS? Use the provided `docker-compose.nas.yml` which includes `cloudflared` and `tailscale` configurations for seamless zero-trust exposure without port forwarding.
130
-
131
-```bash
132
-# Expose via Cloudflare Tunnel
133
-TUNNEL_TOKEN=your_token docker-compose -f docker-compose.nas.yml --profile cloudflare up -d
134
-
135
-# OR Expose via Tailscale
136
-TS_AUTHKEY=your_key docker-compose -f docker-compose.nas.yml --profile tailscale up -d
137
-```
138
-See the [NAS Deployment Guide](docs/administration/deploy-nas.md) and [Exposing NAS Guide](docs/administration/expose-nas.md) for full details.
139
-
140
-
141
-### Manual Setup
142
-
143
-```bash
144
-# 1. Install dependencies
145
-npm install
146
-
147
-# 2. Configure environment
148
-cp .env.example .env
149
-# Edit .env (SQLite defaults work for dev)
150
-
151
-# 3. Initialize database
152
-npm run db:push
153
-
154
-# 4. Create admin user
155
-bun run scripts/seed-admin.ts
156
-
157
-# 5. Start development server
158
-npm run dev
159
-```
160
-
161
-App URL: `http://localhost:3000` (dev) or `http://localhost:4321` (Docker)
162
-
163
-
164
-## Architecture
165
-
166
-```
167
-Clients (Browser / Git CLI / och CLI)
168
- |
169
- v
170
-OpenCodeHub Platform (Astro SSR + React)
171
- |-- Web UI
172
- |-- REST API (140+ routes)
173
- |-- GraphQL Endpoint
174
- |-- Git HTTP Server
175
- |-- SSH Git Server (port 2222)
176
- |
177
- v
178
-Persistence Layer
179
- |-- PostgreSQL / SQLite / Turso (Drizzle ORM)
180
- |-- Redis (sessions, queues, caching)
181
- |-- Pluggable Storage (S3, GCS, Azure, local, etc.)
182
-```
183
-
184
-### Runtime Processes
185
-
186
-| Process | Command | Purpose |
187
-|---------|---------|---------|
188
-| Main App | `npm run dev` | Web UI + API + Git HTTP |
189
-| SSH Git | `npm run git:start` | SSH git push/pull |
190
-| Worker | `npm run worker:start` | Background jobs |
191
-| Runner | `npm run runner:start` | CI/CD pipeline execution |
192
-
193
-### Tech Stack
194
-
195
-| Layer | Technology |
196
-|-------|-----------|
197
-| Framework | Astro 4.x (SSR, Node standalone) |
198
-| UI | React 18 + Tailwind CSS + Radix UI |
199
-| Database | Drizzle ORM (PostgreSQL, SQLite, Turso) |
200
-| Auth | JWT (jose) + bcryptjs + TOTP (otplib) + OAuth (arctic) |
201
-| Git | Native git CLI + simple-git + isomorphic-git |
202
-| SSH | ssh2 library |
203
-| CI/CD | Dockerode (Docker API) |
204
-| Storage | Abstract adapter pattern (8+ backends) |
205
-| Queue | BullMQ + Redis |
206
-| CLI | Commander.js + Inquirer + simple-git |
207
-
208
-
209
-## Documentation
210
-
211
-Comprehensive documentation is available in the `docs/` directory:
212
-
213
-- **[Getting Started](docs/getting-started/installation.md)** — Installation and setup
214
-- **[Features](docs/features/)** — Stacked PRs, AI review, merge queue, CI/CD
215
-- **[Guides](docs/guides/)** — Team workflows, branch protection, webhooks
216
-- **[API Reference](docs/api/rest-api.md)** — REST API documentation
217
-- **[Administration](docs/administration/)** — Deployment, security, monitoring
218
-- **[Development](docs/development/)** — Architecture, contributing, testing
219
-- **[Tutorials](docs/tutorials/)** — Hands-on guides
220
-
221
-
222
-## CLI
223
-
224
-The companion CLI is published as `opencodehub-cli`:
225
-
226
-```bash
227
-# Install
228
-npm install -g opencodehub-cli
229
-
230
-# Authenticate
231
-och auth login --url http://localhost:4321
232
-
233
-# Stack workflow
234
-och stack create feature/auth-step-1
235
-och stack submit
236
-och stack sync
237
-
238
-# Merge queue
239
-och queue list
240
-och queue add <pr-number>
241
-
242
-# Interactive cockpit
243
-och focus
244
-```
31
+Open `http://localhost:4321` and create your admin account.
245
32
246
-[Full CLI Reference](docs/reference/cli-commands.md)
33
+**Requirements**: Docker, or Node.js 20+ with PostgreSQL.
247
34
248
35
---
249
36
250
-## API
37
+## What It Does
251
38
252
-OpenCodeHub provides multiple API interfaces:
39
+**Git Hosting** — HTTP smart protocol + SSH push/pull. Forks, mirroring, LFS, wiki.
253
40
254
-### REST API
41
+**Stacked PRs** — Break large changes into dependent branches. Each PR builds on the previous. Review in order, merge in order.
255
42
256
-140+ REST endpoints covering all platform features:
43
+**Merge Queue** — Stack-aware merge ordering with speculative CI builds. `main` never breaks.
257
44
258
-```bash
259
-# Authentication
260
-curl -X POST http://localhost:4321/api/auth/login \
261
- -H "Content-Type: application/json" \
262
- -d '{"login":"alice","password":"secret"}'
263
-
264
-# Create repository
265
-curl -X POST http://localhost:4321/api/repos \
266
- -H "Authorization: Bearer <token>" \
267
- -H "Content-Type: application/json" \
268
- -d '{"name":"my-project","visibility":"public"}'
269
-
270
-# List pull requests
271
-curl http://localhost:4321/api/repos/alice/my-project/pulls \
272
- -H "Authorization: Bearer <token>"
273
-```
45
+**CI/CD** — GitHub Actions-compatible workflows. Docker-based runners. Artifacts, secrets, matrix builds.
274
46
275
-### GraphQL
47
+**AI Code Review** — GPT-4, Claude, Gemini, Groq, Ollama, and 5+ more providers. Catches bugs, security issues, and style problems automatically.
276
48
277
-```bash
278
-curl -X POST http://localhost:4321/api/graphql \
279
- -H "Authorization: Bearer <token>" \
280
- -H "Content-Type: application/json" \
281
- -d '{"query": "{ repositories { name owner { username } } }"}'
282
-```
49
+**Issues & Projects** — Labels, milestones, custom fields, kanban boards.
283
50
284
-### OpenAPI Spec
51
+**175+ API Endpoints** — REST + GraphQL. Full programmatic access to everything.
285
52
286
-The OpenAPI spec is available at:
287
-```
288
-GET /api/openapi.json
289
-```
53
+**CLI** — `och` command line tool for stack workflows, reviews, and repository management.
290
54
291
55
---
292
56
293
-## Security
57
+## Deploy
294
58
295
-OpenCodeHub implements comprehensive security controls:
296
-
297
-- **Authentication**: JWT sessions with secure cookies, OAuth 2.0, 2FA/TOTP
298
-- **Authorization**: Role-based access control (RBAC), repository permissions, team permissions
299
-- **Data Protection**: AES-256-GCM encryption for workflow secrets and AI config
300
-- **Transport**: HTTPS enforcement in production, TLS 1.2+
301
-- **CSRF**: Double-submit cookie pattern for state-changing requests
302
-- **Rate Limiting**: Redis-backed per-endpoint rate limiting
303
-- **Input Validation**: Zod schema validation on all API inputs
304
-- **Secret Scanning**: Detect and block secrets in commits
305
-- **SSRF Protection**: Validate webhook URLs against private IP ranges
306
-
307
-See [Security Best Practices](docs/administration/security.md) for detailed configuration.
308
-
309
-
310
-## Deployment
311
-
312
-### Docker Compose (Recommended)
313
-
314
-The included `docker-compose.yml` spins up:
315
-- OpenCodeHub app (port 4321)
316
-- PostgreSQL database
317
-- Redis cache
318
-- CI runner (Docker-in-Docker)
319
-- Optional: MinIO for S3-compatible storage
59
+### Docker (Recommended)
320
60
321
61
```bash
322
-docker-compose up -d
62
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
63
+cd OpenCodeHub
64
+cp .env.example .env
65
+docker compose up -d
66
+docker compose exec app bun run scripts/seed-admin.ts
323
67
```
324
68
325
-### Production Checklist
69
+### Render (Free)
326
70
327
-Before deploying to production:
71
+See [Render Deployment Guide](docs/RENDER-DEPLOYMENT.md). Uses free PostgreSQL + Upstash Redis.
328
72
329
-1. **Change ALL secrets**:
330
- ```bash
331
- JWT_SECRET=$(openssl rand -hex 32)
332
- SESSION_SECRET=$(openssl rand -hex 32)
333
- INTERNAL_HOOK_SECRET=$(openssl rand -hex 32)
334
- CRON_SECRET=$(openssl rand -hex 32)
335
- RUNNER_SECRET=$(openssl rand -hex 32)
336
- WORKFLOW_SECRET_ENCRYPTION_KEY=$(openssl rand -hex 32)
337
- ```
73
+### Other Options
338
74
339
-2. **Use PostgreSQL** (not SQLite)
340
-3. **Configure HTTPS** with reverse proxy (Nginx/Caddy)
341
-4. **Set up Redis** for distributed sessions and rate limiting
342
-5. **Configure external storage** (S3/GCS/Azure instead of local)
343
-6. **Enable rate limiting**: `RATE_LIMIT_ENABLED=true`
344
-7. **Set up SMTP** for email notifications
345
-8. **Configure backups** for PostgreSQL data
346
-
347
-See [Deployment Guide](docs/administration/deployment.md) for detailed instructions.
75
+| Platform | Guide |
76
+|----------|-------|
77
+| Docker Compose | [Deployment Guide](docs/administration/deployment.md) |
78
+| NAS (Synology/TrueNAS) | [NAS Guide](docs/administration/deploy-nas.md) |
79
+| Kubernetes | [K8s Guide](docs/administration/kubernetes.md) |
80
+| Cloudflare Tunnel | [Cloudflare Guide](docs/administration/deploy-cloudflare.md) |
81
+| Free Tier Options | [Free Deployment](docs/FREE-DEPLOYMENT.md) |
348
82
349
83
---
350
84
351
-## Database
352
-
353
-OpenCodeHub uses Drizzle ORM with support for multiple database engines:
85
+## Tech Stack
354
86
355
-| Engine | Driver | Use Case |
356
-|--------|--------|----------|
357
-| **PostgreSQL** | `pg` | Production (recommended) |
358
-| **SQLite** | `better-sqlite3` | Development, small deployments |
359
-| **Turso/LibSQL** | `@libsql/client` | Edge deployments, Vercel |
360
-
361
-```bash
362
-# Generate migrations
363
-npm run db:generate
364
-
365
-# Apply migrations
366
-npm run db:migrate
367
-
368
-# Push schema (dev)
369
-npm run db:push
370
-
371
-# Open Drizzle Studio
372
-npm run db:studio
373
-```
87
+| Layer | Tech |
88
+|-------|------|
89
+| Framework | Astro 4.x SSR + React 18 |
90
+| Database | PostgreSQL, SQLite, Turso (Drizzle ORM) |
91
+| Auth | JWT, OAuth, 2FA/TOTP, SSO/SAML |
92
+| CI/CD | Docker-based runners, GitHub Actions syntax |
93
+| Storage | Local, S3, MinIO, R2, or any S3-compatible |
94
+| CLI | Commander.js (`npm i -g opencodehub-cli`) |
374
95
375
96
---
376
97
377
-## Storage
378
-
379
-All blob storage goes through an abstract `StorageAdapter`:
98
+## Documentation
380
99
381
-| Backend | Adapter | Use Case |
382
-|---------|---------|----------|
383
-| **Local** | `LocalStorageAdapter` | Development, single-node |
384
-| **S3** | `S3StorageAdapter` | Production (AWS, MinIO, R2) |
385
-| **Google Cloud** | `GCSStorageAdapter` | GCP deployments |
386
-| **Azure Blob** | `AzureStorageAdapter` | Azure deployments |
387
-| **Google Drive** | `GoogleDriveStorageAdapter` | Personal/small team |
388
-| **OneDrive** | `OneDriveStorageAdapter` | Microsoft ecosystem |
389
-| **Dropbox** | `DropboxStorageAdapter` | Simple cloud storage |
390
-| **Rclone** | `RcloneStorageAdapter` | Any rclone-compatible remote |
100
+Full docs at [docs.opencodehub.space](https://docs.opencodehub.space/):
391
101
392
-Configure via `STORAGE_DRIVER` environment variable.
102
+- [Installation](https://docs.opencodehub.space/getting-started/installation/)
103
+- [Configuration](https://docs.opencodehub.space/administration/configuration/)
104
+- [Stacked PRs](https://docs.opencodehub.space/features/stacked-prs/)
105
+- [AI Code Review](https://docs.opencodehub.space/features/ai-review/)
106
+- [CLI Reference](https://docs.opencodehub.space/reference/cli-commands/)
107
+- [API Reference](https://docs.opencodehub.space/api/rest-api/)
393
108
394
109
---
395
110
396
-## Development
111
+## CLI
397
112
398
113
```bash
399
-# Setup
400
-cp .env.example .env
401
-npm install
402
-npm run db:push
403
-bun run scripts/seed-admin.ts
404
-
405
-# Dev server
406
-npm run dev # Web UI + API (port 3000)
407
-npm run git:start # SSH git server (port 2222)
408
-npm run worker:start # Background worker
409
-npm run runner:start # CI runner
410
-
411
-# Quality
412
-npm run lint
413
-npm run typecheck
414
-npm run test
415
-npm run test:coverage
416
-
417
-# Database
418
-npm run db:generate
419
-npm run db:migrate
420
-npm run db:push
421
-npm run db:studio
422
-```
423
-
424
-See [Contributing Guide](CONTRIBUTING.md) and [Development Docs](docs/development/).
425
-
426
-
427
-## Project Structure
428
-
429
-```
430
-OpenCodeHub/
431
-├── src/ # Main application
432
-│ ├── pages/ # Astro routes (UI + API)
433
-│ ├── lib/ # Core business logic (120+ modules)
434
-│ ├── db/ # Database schema and connection
435
-│ ├── components/ # React components
436
-│ ├── middleware.ts # Global middleware
437
-│ └── runner/ # CI runner
438
-├── cli/ # CLI package (opencodehub-cli)
439
-│ └── src/commands/ # CLI command groups
440
-├── packages/ # Additional packages
441
-│ ├── git-rpc-daemon/
442
-│ ├── merge-queue-daemon/
443
-│ ├── ci-runner/
444
-│ └── sdk/
445
-├── docs/ # Documentation
446
-├── docs-site/ # Documentation website
447
-├── scripts/ # Utility scripts
448
-├── docker-compose.yml # Docker Compose setup
449
-├── Dockerfile # Main app container
450
-└── package.json
451
-```
452
-
453
-
454
-## Testing
455
-
456
-- **Unit Tests**: Vitest (`*.test.ts`)
457
-- **E2E Tests**: Playwright
458
-- **Load Tests**: Custom load tests in `tests/load/`
459
-- **Accessibility**: `@axe-core/playwright`
114
+npm install -g opencodehub-cli
460
115
461
-```bash
462
-npm run test # Run all tests
463
-npm run test:coverage # Run with coverage
116
+och auth login --url http://localhost:4321
117
+och stack create feature/auth
118
+och stack submit
119
+och focus
464
120
```
465
121
466
122
---
467
123
468
124
## Contributing
469
125
470
-Contributions are welcome!
471
-
472
-1. Fork the repository
473
-2. Create a feature branch
474
-3. Run lint and tests
475
-4. Open a pull request
126
+1. Fork the repo
127
+2. Create a branch
128
+3. `npm install && npm run db:push && npm run dev`
129
+4. Run tests: `npm run test`
130
+5. Open a PR
476
131
477
-See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and standards.
478
-
479
-
480
-## Community
481
-
482
-- **Documentation**: [docs/index.md](docs/index.md)
483
-- **Issues**: [GitHub Issues](https://github.com/swadhinbiswas/OpencodeHub/issues)
484
-- **Discussions**: [GitHub Discussions](https://github.com/swadhinbiswas/OpencodeHub/discussions)
132
+See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
485
133
486
134
---
487
135
488
136
## License
489
137
490
-MIT. See [LICENSE](LICENSE).
491
-
492
-
493
-<p align="center">
494
- <strong>Ready to get started?</strong><br>
495
- <a href="docs/getting-started/installation.md">Install OpenCodeHub</a> |
496
- <a href="docs/getting-started/quick-start.md">Quick Start</a> |
497
- <a href="docs/features/stacked-prs.md">Learn Stacked PRs</a>
498
-</p>
138
+MIT
499
139
▼astro.config.mjs
+7-0
2
2
import react from "@astrojs/react";
3
3
import tailwind from "@astrojs/tailwind";
4
4
import { defineConfig } from "astro/config";
5
+import icon from "astro-icon";
5
6
6
7
export default defineConfig({
7
8
integrations: [
8
9
react(),
10
+ icon(),
9
11
tailwind({
10
12
applyBaseStyles: false,
11
13
}),
19
21
host: true,
20
22
},
21
23
vite: {
24
+ server: {
25
+ watch: {
26
+ ignored: ['**/data/**', '**/repos/**', '**/storage/**', '**/.tmp/**', '**/postgres/**', '**/redis/**'],
27
+ },
28
+ },
22
29
optimizeDeps: {
23
30
exclude: ["nodegit", "better-sqlite3"],
24
31
},
25
32
▼cli/src/commands/stack/index.ts
+0-4
270
270
.action(async () => {
271
271
try {
272
272
const roots = await getStackTopology(git);
273
- console.log(chalk.hex("#6272a4")("No stack found."));
274
- return;
275
- }
276
-
277
273
const { printSectionHeader, formatBadge } = await import("../../lib/formatter.js");
278
274
279
275
// Use Dracula colors directly or via chalk
280
276
▼docker-compose.nas.yml
+1-1
1
1
services:
2
2
# Main OpenCodeHub application
3
3
app:
4
- image: swadhinbiswas/opencodehub:latest
4
+ image: opencodehub/opencodehub:latest
5
5
container_name: opencodehub
6
6
restart: unless-stopped
7
7
ports:
8
8
▼docker-compose.yml
+40-1
8
8
restart: unless-stopped
9
9
ports:
10
10
- "4321:4321" # Web interface
11
- - "2222:2222" # SSH Git
12
11
environment:
13
12
- NODE_ENV=production
14
13
- DATABASE_URL=postgresql://opencodehub:opencodehub@postgres:5432/opencodehub
16
15
- REDIS_URL=redis://redis:6379
17
16
- JWT_SECRET=${JWT_SECRET:-supersecretjwtkey}
18
17
- WORKFLOW_SECRET_ENCRYPTION_KEY=${WORKFLOW_SECRET_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
18
+ - INTERNAL_HOOK_SECRET=${INTERNAL_HOOK_SECRET:-supersecrethookkey}
19
+ - CRON_SECRET=${CRON_SECRET:-supersecretcronkey}
20
+ - RUNNER_SECRET=${RUNNER_SECRET:-supersecretrunnerkey}
21
+ - AI_CONFIG_ENCRYPTION_KEY=${AI_CONFIG_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
19
22
- SERVER_URL=http://localhost:4321
20
23
- DATA_DIR=/data
21
24
- STORAGE_TYPE=local
25
+ - GIT_REPOS_PATH=/data/repositories
26
+ - GIT_SSH_PORT=2222
22
27
volumes:
23
28
- ./data/opencodehub:/data
24
29
depends_on:
43
48
retries: 3
44
49
start_period: 10s
45
50
51
+ # SSH Git server
52
+ ssh-git:
53
+ build:
54
+ context: .
55
+ dockerfile: Dockerfile
56
+ container_name: opencodehub-ssh
57
+ restart: unless-stopped
58
+ command: ["bun", "scripts/ssh-server.ts"]
59
+ ports:
60
+ - "2222:2222" # SSH Git
61
+ environment:
62
+ - NODE_ENV=production
63
+ - DATABASE_URL=postgresql://opencodehub:opencodehub@postgres:5432/opencodehub
64
+ - DATABASE_TYPE=postgres
65
+ - REDIS_URL=redis://redis:6379
66
+ - JWT_SECRET=${JWT_SECRET:-supersecretjwtkey}
67
+ - WORKFLOW_SECRET_ENCRYPTION_KEY=${WORKFLOW_SECRET_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
68
+ - AI_CONFIG_ENCRYPTION_KEY=${AI_CONFIG_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
69
+ - SERVER_URL=http://app:4321
70
+ - DATA_DIR=/data
71
+ - STORAGE_TYPE=local
72
+ - GIT_REPOS_PATH=/data/repositories
73
+ - GIT_SSH_PORT=2222
74
+ - GIT_SSH_HOST_KEY=/data/ssh/host_key
75
+ volumes:
76
+ - ./data/opencodehub:/data
77
+ depends_on:
78
+ postgres:
79
+ condition: service_healthy
80
+ redis:
81
+ condition: service_healthy
82
+ networks:
83
+ - opencodehub-network
84
+
46
85
# PostgreSQL database
47
86
postgres:
48
87
image: docker.io/library/postgres:16-alpine
49
88
▼docs-site/astro.config.mjs
+36-3
4
4
5
5
// https://astro.build/config
6
6
export default defineConfig({
7
+ site: "https://docs.opencodehub.space",
7
8
integrations: [
8
9
starlight({
9
10
title: "OpenCodeHub Docs",
42
43
{ label: "Branch Protection", slug: "guides/branch-protection" },
43
44
{ label: "Webhooks", slug: "guides/webhooks" },
44
45
{ label: "Storage Adapters", slug: "guides/storage-adapters" },
46
+ { label: "CircleCI Integration", slug: "guides/circleci-integration" },
45
47
],
46
48
},
47
49
{
65
67
{ label: "Stacked PRs", slug: "features/stacked-prs" },
66
68
{ label: "AI Review", slug: "features/ai-review" },
67
69
{ label: "Merge Queue", slug: "features/merge-queue" },
70
+ { label: "CI/CD Actions", slug: "features/ci-actions" },
71
+ { label: "CI/CD Pipelines", slug: "features/ci-cd" },
68
72
{ label: "Automation Rules", slug: "features/automations" },
69
73
{ label: "PR Inbox", slug: "features/inbox" },
70
74
{ label: "Developer Metrics", slug: "features/developer-metrics" },
71
75
{ label: "Notifications", slug: "features/notifications" },
72
- { label: "CI/CD Actions", slug: "features/ci-actions" },
76
+ { label: "Slack Integration", slug: "features/slack-integration" },
73
77
{ label: "CLI Workflows", slug: "features/cli" },
74
78
],
75
79
},
86
90
{ label: "cPanel", slug: "administration/deploy-cpanel" },
87
91
{ label: "CyberPanel", slug: "administration/deploy-cyberpanel" },
88
92
{ label: "Cloudflare", slug: "administration/deploy-cloudflare" },
93
+ { label: "Coolify", slug: "administration/deploy-coolify" },
94
+ { label: "NAS (Synology/TrueNAS)", slug: "administration/deploy-nas" },
95
+ { label: "Kubernetes (Helm)", slug: "administration/kubernetes" },
89
96
],
90
97
},
91
98
{
95
102
{ label: "Configuration", slug: "administration/configuration" },
96
103
{ label: "Monitoring", slug: "administration/monitoring" },
97
104
{ label: "Security", slug: "administration/security" },
105
+ { label: "Deployment Matrix", slug: "administration/deployment-matrix" },
106
+ { label: "Secret Rotation", slug: "administration/secret-rotation" },
107
+ { label: "Expose NAS / Home Server", slug: "administration/expose-nas" },
108
+ { label: "CircleCI Cloud Workers", slug: "administration/circleci-cloud" },
109
+ { label: "CircleCI Self-Hosted", slug: "administration/circleci-worker" },
110
+ ],
111
+ },
112
+ {
113
+ label: "Operations",
114
+ items: [
115
+ { label: "Incident Runbook", slug: "administration/incident-runbook" },
116
+ { label: "Postmortem Template", slug: "administration/postmortem-template" },
117
+ { label: "Operations Drills", slug: "administration/operations-drills" },
98
118
],
99
119
},
100
120
{
103
123
},
104
124
{
105
125
label: "Development",
106
- autogenerate: { directory: "development" },
126
+ items: [
127
+ { label: "Architecture", slug: "development/architecture" },
128
+ { label: "Local Dev Setup", slug: "development/local-dev-setup" },
129
+ { label: "Contributing", slug: "development/contributing" },
130
+ { label: "Database Schema", slug: "development/database-schema" },
131
+ { label: "Database Migrations", slug: "development/database-migrations" },
132
+ { label: "Testing", slug: "development/testing" },
133
+ { label: "Testing & CI", slug: "development/testing-pipelines" },
134
+ { label: "Security Hardening", slug: "development/security-hardening" },
135
+ { label: "Observability", slug: "development/observability" },
136
+ { label: "Publishing CLI", slug: "development/publishing-cli" },
137
+ ],
107
138
},
108
139
{
109
140
label: "Reference",
110
- autogenerate: { directory: "reference" },
141
+ items: [
142
+ { label: "Glossary", slug: "reference/glossary" },
143
+ ],
111
144
},
112
145
],
113
146
}),
114
147
▼docs-site/src/content/docs/FREE-DEPLOYMENT.md
+395-0
1
+# OpenCodeHub — Free Deployment Options
2
+
3
+> Deploy OpenCodeHub for free with persistent file storage for git repositories.
4
+
5
+---
6
+
7
+## Quick Comparison
8
+
9
+| Platform | Cost | Storage | RAM | CPU | Best For |
10
+|----------|------|---------|-----|-----|----------|
11
+| **Oracle Cloud Free** | $0 forever | 200GB | 24GB | 4 ARM cores | Best overall |
12
+| **Home Server/NAS** | $0 | Unlimited | Your hardware | Your hardware | Privacy-focused |
13
+| **Fly.io** | $0 (limited) | 3GB free | 256MB | Shared | Quick testing |
14
+| **Google Cloud Free** | $0 forever | 30GB | 1GB | 0.25 vCPU | Light usage |
15
+| **Railway** | $5 trial | 1GB | 512MB | Shared | Quick demo |
16
+| **Hetzner** | ~€4/mo | 40GB | 4GB | 2 vCPU | Best value paid |
17
+
18
+---
19
+
20
+## Option 1: Oracle Cloud Free Tier (Recommended)
21
+
22
+**Always Free** — No credit card charge, no time limit.
23
+
24
+### What You Get
25
+- 4 ARM cores (Ampere A1)
26
+- 24 GB RAM
27
+- 200 GB boot volume
28
+- 2 x 1TB block volumes (total 2TB)
29
+- Free PostgreSQL database
30
+- Free Redis
31
+
32
+### Steps
33
+
34
+```bash
35
+# 1. Sign up at cloud.oracle.com
36
+# 2. Create a VM instance:
37
+# - Name: opencodehub
38
+# - Image: Ubuntu 22.04
39
+# - Shape: VM.Standard.A1.Flex (4 OCPU, 24 GB RAM)
40
+# - Upload SSH key
41
+# - Create boot volume: 100 GB
42
+
43
+# 3. SSH into your instance
44
+ssh -i ~/.ssh/key ubuntu@YOUR_PUBLIC_IP
45
+
46
+# 4. Install Docker
47
+curl -fsSL https://get.docker.com -o get-docker.sh
48
+sudo sh get-docker.sh
49
+sudo usermod -aG docker $USER
50
+
51
+# 5. Install Docker Compose
52
+sudo apt install docker-compose-plugin -y
53
+
54
+# 6. Clone OpenCodeHub
55
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
56
+cd OpencodeHub
57
+
58
+# 7. Configure
59
+cp .env.example .env
60
+nano .env # Edit secrets
61
+
62
+# 8. Start
63
+sudo docker compose up -d
64
+
65
+# 9. Create admin
66
+sudo docker compose exec app bun run scripts/seed-admin.ts
67
+```
68
+
69
+### Access Your Instance
70
+```
71
+Web: http://YOUR_PUBLIC_IP:4321
72
+SSH: ssh://git@YOUR_PUBLIC_IP:2222/owner/repo.git
73
+HTTPS: Set up Nginx + Let's Encrypt (free)
74
+```
75
+
76
+---
77
+
78
+## Option 2: Home Server / NAS
79
+
80
+**$0 forever** — Use hardware you already own.
81
+
82
+### What You Need
83
+- Any computer running Linux/Mac/Windows
84
+- 2GB+ RAM recommended
85
+- 20GB+ free disk space
86
+
87
+### Quick Start (Linux/Mac)
88
+
89
+```bash
90
+# Install Docker
91
+curl -fsSL https://get.docker.com | sh
92
+
93
+# Clone and start
94
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
95
+cd OpencodeHub
96
+cp .env.example .env
97
+nano .env # Set secrets
98
+docker compose up -d
99
+
100
+# Create admin
101
+docker compose exec app bun run scripts/seed-admin.ts
102
+```
103
+
104
+### NAS Deployment
105
+
106
+**Synology / TrueNAS / QNAP:**
107
+1. Install Docker from Package Center
108
+2. Use Docker Compose or Portainer
109
+3. Mount a shared folder for `/data`
110
+
111
+### Access from Anywhere
112
+- **Tailscale** (free): `tailscale up` → access via Tailscale IP
113
+- **Cloudflare Tunnel** (free): Expose without opening ports
114
+- **Port forwarding**: Open port 4321 on your router
115
+
116
+---
117
+
118
+## Option 3: Fly.io Free Tier
119
+
120
+**Free** — 3GB persistent volume included.
121
+
122
+### Steps
123
+
124
+```bash
125
+# 1. Install flyctl
126
+curl -L https://fly.io/install.sh | sh
127
+
128
+# 2. Login
129
+fly auth login
130
+
131
+# 3. Launch
132
+fly launch --name opencodehub
133
+
134
+# 4. Add persistent volume
135
+fly volume create opencodehub_data --region sjc --size 3
136
+
137
+# 5. Set secrets
138
+fly secrets set JWT_SECRET=$(openssl rand -hex 32)
139
+fly secrets set SESSION_SECRET=$(openssl rand -hex 32)
140
+fly secrets set DATABASE_URL="postgresql://..."
141
+fly secrets set REDIS_URL="redis://..."
142
+
143
+# 6. Deploy
144
+fly deploy
145
+```
146
+
147
+### Limitations
148
+- 3GB storage (enough for small repos)
149
+- 256MB RAM (may be slow for large repos)
150
+- Apps休眠 after inactivity (cold starts take ~30s)
151
+
152
+---
153
+
154
+## Option 4: Google Cloud Free Tier
155
+
156
+**Always Free** — e2-micro instance.
157
+
158
+### Steps
159
+
160
+```bash
161
+# 1. Sign up at cloud.google.com
162
+# 2. Create e2-micro VM:
163
+# - Machine type: e2-micro (0.25 vCPU, 1GB RAM)
164
+# - Boot disk: 30GB SSD (free)
165
+# - Firewall: Allow HTTP (80), SSH (22)
166
+
167
+# 3. SSH in and install Docker
168
+ssh -i ~/.ssh/key USER@EXTERNAL_IP
169
+sudo apt update && sudo apt install docker.io docker-compose-plugin -y
170
+
171
+# 4. Deploy OpenCodeHub
172
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
173
+cd OpencodeHub
174
+cp .env.example .env
175
+nano .env
176
+docker compose up -d
177
+```
178
+
179
+### Limitations
180
+- Only 1GB RAM (tight for production)
181
+- 0.25 vCPU (slow builds)
182
+- Good for personal/light use
183
+
184
+---
185
+
186
+## Option 5: Self-Hosted on Existing VPS
187
+
188
+If you already have a VPS (Hetzner, DigitalOcean, Linode, etc.):
189
+
190
+```bash
191
+# SSH into your server
192
+ssh root@YOUR_SERVER_IP
193
+
194
+# One-line install
195
+curl -fsSL https://raw.githubusercontent.com/swadhinbiswas/OpencodeHub/main/install.sh | bash
196
+```
197
+
198
+---
199
+
200
+## Option 6: Docker on Windows/Mac
201
+
202
+For local development or personal use:
203
+
204
+```bash
205
+# Install Docker Desktop for Windows/Mac
206
+# https://docs.docker.com/get-docker/
207
+
208
+# Clone and start
209
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
210
+cd OpencodeHub
211
+cp .env.example .env
212
+nano .env
213
+docker compose up -d
214
+
215
+# Create admin
216
+docker compose exec app bun run scripts/seed-admin.ts
217
+```
218
+
219
+---
220
+
221
+## Storage Configuration
222
+
223
+### Local Storage (Default)
224
+```bash
225
+STORAGE_TYPE=local
226
+STORAGE_PATH=/data/storage
227
+GIT_REPOS_PATH=/data/repositories
228
+```
229
+
230
+### S3-Compatible (MinIO on same server)
231
+```bash
232
+# Add to docker-compose.yml:
233
+minio:
234
+ image: minio/minio
235
+ command: server /data --console-address ":9001"
236
+ volumes:
237
+ - ./data/minio:/data
238
+ ports:
239
+ - "9001:9001"
240
+
241
+STORAGE_TYPE=s3
242
+STORAGE_ENDPOINT=http://minio:9000
243
+STORAGE_BUCKET=opencodehub
244
+STORAGE_ACCESS_KEY_ID=minioadmin
245
+STORAGE_SECRET_ACCESS_KEY=minioadmin
246
+```
247
+
248
+---
249
+
250
+## HTTPS Setup (Free)
251
+
252
+### With Let's Encrypt + Nginx
253
+
254
+```bash
255
+# Install Nginx and Certbot
256
+sudo apt install nginx certbot python3-certbot-nginx -y
257
+
258
+# Create Nginx config
259
+sudo nano /etc/nginx/sites-available/opencodehub
260
+```
261
+
262
+```nginx
263
+server {
264
+ listen 80;
265
+ server_name git.yourdomain.com;
266
+ return 301 https://$host$request_uri;
267
+}
268
+
269
+server {
270
+ listen 443 ssl http2;
271
+ server_name git.yourdomain.com;
272
+
273
+ ssl_certificate /etc/letsencrypt/live/git.yourdomain.com/fullchain.pem;
274
+ ssl_certificate_key /etc/letsencrypt/live/git.yourdomain.com/privkey.pem;
275
+
276
+ client_max_body_size 500M;
277
+
278
+ location / {
279
+ proxy_pass http://localhost:4321;
280
+ proxy_set_header Host $host;
281
+ proxy_set_header X-Real-IP $remote_addr;
282
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
283
+ proxy_set_header X-Forwarded-Proto $scheme;
284
+ }
285
+
286
+ # WebSocket support
287
+ location /api/realtime {
288
+ proxy_pass http://localhost:4321;
289
+ proxy_http_version 1.1;
290
+ proxy_set_header Upgrade $http_upgrade;
291
+ proxy_set_header Connection "upgrade";
292
+ }
293
+}
294
+```
295
+
296
+```bash
297
+# Enable and get SSL
298
+sudo ln -s /etc/nginx/sites-available/opencodehub /etc/nginx/sites-enabled/
299
+sudo certbot --nginx -d git.yourdomain.com
300
+```
301
+
302
+---
303
+
304
+## Environment Variables (Production)
305
+
306
+```bash
307
+# Generate all secrets
308
+JWT_SECRET=$(openssl rand -hex 32)
309
+SESSION_SECRET=$(openssl rand -hex 32)
310
+INTERNAL_HOOK_SECRET=$(openssl rand -hex 32)
311
+CRON_SECRET=$(openssl rand -hex 32)
312
+RUNNER_SECRET=$(openssl rand -hex 32)
313
+AI_CONFIG_ENCRYPTION_KEY=$(openssl rand -hex 32)
314
+WORKFLOW_SECRET_ENCRYPTION_KEY=$(openssl rand -hex 32)
315
+
316
+# Your .env file
317
+DATABASE_DRIVER=postgres
318
+DATABASE_URL=postgresql://opencodehub:PASSWORD@localhost:5432/opencodehub
319
+DATABASE_SSL=false
320
+REDIS_URL=redis://localhost:6379
321
+SITE_URL=https://git.yourdomain.com
322
+STORAGE_TYPE=local
323
+GIT_REPOS_PATH=/data/repositories
324
+NODE_ENV=production
325
+```
326
+
327
+---
328
+
329
+## Backup Strategy
330
+
331
+### Automated Backup Script
332
+```bash
333
+#!/bin/bash
334
+# backup.sh — Run daily via cron
335
+BACKUP_DIR="/backups/$(date +%Y-%m-%d)"
336
+mkdir -p "$BACKUP_DIR"
337
+
338
+# Database dump
339
+docker compose exec -T postgres pg_dump -U opencodehub opencodehub > "$BACKUP_DIR/db.sql"
340
+
341
+# Git repos
342
+tar czf "$BACKUP_DIR/repos.tar.gz" ./data/repositories/
343
+
344
+# Upload to S3 (optional)
345
+# aws s3 sync "$BACKUP_DIR" s3://your-backup-bucket/opencodehub/
346
+```
347
+
348
+### Cron Job
349
+```bash
350
+# Add to crontab
351
+crontab -e
352
+0 2 * * * /path/to/backup.sh
353
+```
354
+
355
+---
356
+
357
+## Monitoring
358
+
359
+### Health Check
360
+```bash
361
+# Check if app is running
362
+curl -f http://localhost:4321/api/health || echo "DOWN"
363
+
364
+# Check Docker status
365
+docker compose ps
366
+docker compose logs --tail=50 app
367
+```
368
+
369
+### Uptime Robot (Free)
370
+1. Sign up at uptimerobot.com
371
+2. Add HTTP monitor: `http://YOUR_IP:4321/api/health`
372
+3. Get notified if your instance goes down
373
+
374
+---
375
+
376
+## Recommended Free Setup
377
+
378
+For a **personal Git server** that stores all your code:
379
+
380
+| Component | Choice |
381
+|-----------|--------|
382
+| **Cloud** | Oracle Cloud Free Tier |
383
+| **Server** | ARM A1 (4 cores, 24GB RAM) |
384
+| **Storage** | 100GB boot + 1TB block volume |
385
+| **Database** | PostgreSQL (Docker) |
386
+| **Cache** | Redis (Docker) |
387
+| **HTTPS** | Nginx + Let's Encrypt |
388
+| **Access** | Tailscale (private) or port forwarding |
389
+| **Backups** | Cron script + local S3 |
390
+
391
+**Total cost: $0/month forever.**
392
+
393
+---
394
+
395
+*Generated for OpenCodeHub free deployment.*
0
396
▼docs-site/src/content/docs/RENDER-DEPLOYMENT.md
+163-0
1
+# OpenCodeHub — Render Deployment Guide
2
+
3
+> Deploy to Render (Singapore) with free PostgreSQL + Upstash Redis. $0/month.
4
+
5
+---
6
+
7
+## What You Get
8
+
9
+- OpenCodeHub running in Asia (Singapore)
10
+- Free PostgreSQL database
11
+- Free Redis via Upstash
12
+- 1GB persistent storage for git repos
13
+- HTTPS included
14
+- Admin account
15
+
16
+---
17
+
18
+## Step 1: Create Upstash Redis (2 min)
19
+
20
+1. Go to **https://upstash.com** → Sign up (free)
21
+2. Click **Create Database**
22
+3. Settings:
23
+ - **Name**: `opencodehub`
24
+ - **Region**: **Asia Pacific (Singapore)**
25
+ - **Plan**: Pay as you go (free tier: 10K commands/day)
26
+4. Click **Create**
27
+5. Copy the **Redis URL** (looks like `rediss://...@...`)
28
+6. Keep this tab open — you'll need it in Step 3
29
+
30
+---
31
+
32
+## Step 2: Deploy to Render (5 min)
33
+
34
+1. Go to **https://render.com** → Sign up (free)
35
+2. Click **New** → **Blueprint**
36
+3. Connect your GitHub account
37
+4. Select your OpenCodeHub repository
38
+5. Render detects `render.yaml` and shows:
39
+ - `opencodehub-db` (PostgreSQL — Singapore)
40
+ - `opencodehub` (Web Service — Singapore)
41
+6. Click **Apply**
42
+7. Wait for deployment (~3-5 min first time)
43
+
44
+---
45
+
46
+## Step 3: Set Redis URL (1 min)
47
+
48
+1. Go to your Web Service → **Environment** tab
49
+2. Find `REDIS_URL` (shows "Not set")
50
+3. Click the pencil icon → Paste your **Upstash Redis URL**
51
+4. Click **Save**
52
+
53
+---
54
+
55
+## Step 4: Set Site URL (1 min)
56
+
57
+1. In the same **Environment** tab
58
+2. Find `SITE_URL` (shows "Not set")
59
+3. Click the pencil icon → Enter your Render URL:
60
+ ```
61
+ https://opencodehub.onrender.com
62
+ ```
63
+ (Replace with your actual URL from the service overview)
64
+4. Click **Save**
65
+
66
+---
67
+
68
+## Step 5: Manual Deploy
69
+
70
+1. Go to your Web Service → **Manual Deploy**
71
+2. Click **Deploy latest commit**
72
+3. Wait for build to complete (~2-3 min)
73
+4. Click the **URL** at the top to open your instance
74
+
75
+---
76
+
77
+## Step 6: Create Admin Account
78
+
79
+You'll see the **Setup Wizard**. Fill in:
80
+
81
+| Field | Value |
82
+|-------|-------|
83
+| Username | `admin` (or your choice) |
84
+| Email | Your email |
85
+| Password | Strong password |
86
+
87
+Click **Create Account** — you're now logged in as admin!
88
+
89
+---
90
+
91
+## Step 7: Test It
92
+
93
+```bash
94
+# Create a repo via web UI, then clone:
95
+git clone https://opencodehub.onrender.com/your-username/my-repo.git
96
+
97
+# Push code:
98
+cd my-repo
99
+echo "# Hello" > README.md
100
+git add . && git commit -m "Initial"
101
+git push origin main
102
+```
103
+
104
+---
105
+
106
+## Done! Summary
107
+
108
+| Component | Service | Region | Cost |
109
+|-----------|---------|--------|------|
110
+| App | Render Free | Singapore | $0 |
111
+| PostgreSQL | Render Free | Singapore | $0 |
112
+| Redis | Upstash Free | Singapore | $0 |
113
+| Storage | Render Disk 1GB | Singapore | $0 |
114
+| **Total** | | | **$0/month** |
115
+
116
+---
117
+
118
+## Keep Alive (Optional)
119
+
120
+Render free tier spins down after 15min inactivity. To prevent:
121
+
122
+1. Go to **https://uptimerobot.com** → Sign up (free)
123
+2. Add Monitor → Type: HTTP(s)
124
+3. URL: `https://opencodehub.onrender.com/api/health`
125
+4. Monitoring interval: 5 minutes
126
+5. Save — your app stays awake
127
+
128
+---
129
+
130
+## Upgrade Path (When You Grow)
131
+
132
+| Need | Upgrade | Cost |
133
+|------|---------|------|
134
+| Always-on (no spin-down) | Render Starter | $7/mo |
135
+| More storage (10GB+) | Render Disk upgrade | $1-7/mo |
136
+| More Redis commands | Upstash Pay-as-you-go | ~$0.20/10K |
137
+| PostgreSQL expiry | Render Starter DB | $7/mo |
138
+
139
+---
140
+
141
+## Troubleshooting
142
+
143
+**App won't start?**
144
+- Check **Logs** tab in Render
145
+- Verify `REDIS_URL` is set correctly
146
+- Ensure `SITE_URL` matches your Render URL
147
+
148
+**Redis connection failed?**
149
+- Verify Redis URL from Upstash (starts with `rediss://`)
150
+- Check Upstash dashboard shows "Active"
151
+- Ensure region is Singapore
152
+
153
+**Git clone fails?**
154
+- Use HTTPS: `git clone https://your-url.onrender.com/user/repo.git`
155
+- SSH not available on Render free tier (use HTTPS)
156
+
157
+**Slow first request?**
158
+- Normal for free tier (cold start ~30s)
159
+- Add UptimeRobot ping to keep it warm
160
+
161
+---
162
+
163
+*OpenCodeHub — Deployed on Render, Asia Region, $0/month.*
0
164
▼docs-site/src/content/docs/administration/configuration.md
+186-36
2
2
title: "Configuration Reference"
3
3
---
4
4
5
+OpenCodeHub is configured via environment variables. Copy `.env.example` to `.env` and edit.
5
6
6
-OpenCodeHub is configured exclusively via environment variables.
7
+## Server
7
8
8
-## Core
9
+| Variable | Description | Default |
10
+|----------|-------------|---------|
11
+| `HOST` | Bind address | `0.0.0.0` |
12
+| `PORT` | HTTP port | `4321` |
13
+| `SITE_URL` | Public URL of the instance | `http://localhost:4321` |
14
+| `APP_NAME` | Instance name | `OpenCodeHub` |
15
+| `NODE_ENV` | `development` or `production` | `development` |
9
16
10
-| Variable | Description | Required | Default |
11
-|----------|-------------|----------|---------|
12
-| `SITE_URL` | Public URL of the instance | Yes | `http://localhost:3000` |
13
-| `PORT` | Port to listen on | No | `3000` |
14
-| `NODE_ENV` | `development` or `production` | No | `development` |
17
+## Database
15
18
16
-## Security (Critical)
19
+| Variable | Description | Default |
20
+|----------|-------------|---------|
21
+| `DATABASE_DRIVER` | `postgres`, `sqlite`, or `turso` (LibSQL) | `postgres` |
22
+| `DATABASE_URL` | Connection string | — |
23
+| `DATABASE_POOL_SIZE` | Connection pool size | `10` |
24
+| `DATABASE_SSL` | Enable PostgreSQL SSL | `false` |
25
+| `DATABASE_AUTH_TOKEN` | Auth token (Turso/LibSQL only) | — |
26
+
27
+:::caution[Production]
28
+PostgreSQL is **required** for production. All schemas use `pgTable`. SQLite is for development only.
29
+:::
30
+
31
+## Redis
32
+
33
+| Variable | Description | Default |
34
+|----------|-------------|---------|
35
+| `REDIS_URL` | Redis connection string | `redis://localhost:6379` |
36
+| `REDIS_PASSWORD` | Redis password | — |
37
+
38
+Redis is required in production for distributed locking, rate limiting, and sessions. For edge deployments (Vercel), use Upstash Redis with `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`.
39
+
40
+## Authentication
17
41
18
42
| Variable | Description | Generate With |
19
43
|----------|-------------|---------------|
20
-| `JWT_SECRET` | Signs auth tokens | `openssl rand -hex 32` |
21
-| `SESSION_SECRET` | Signs session cookies | `openssl rand -hex 32` |
22
-| `INTERNAL_HOOK_SECRET` | Secures git hooks | `openssl rand -hex 32` |
44
+| `JWT_SECRET` | Signs auth tokens | `openssl rand -base64 32` |
45
+| `JWT_EXPIRES_IN` | Token expiry | `7d` |
46
+| `SESSION_SECRET` | Signs session cookies | `openssl rand -base64 32` |
23
47
24
-## Database
48
+## Security Secrets
25
49
26
-| Variable | Description | Example |
50
+All secrets below must be unique. Generate each with `openssl rand -hex 32`.
51
+
52
+| Variable | Purpose |
53
+|----------|---------|
54
+| `INTERNAL_HOOK_SECRET` | Secures git hook communication between Git server and app |
55
+| `CRON_SECRET` | Protects cron endpoints (GC, mirror sync, notification digests) |
56
+| `AI_CONFIG_ENCRYPTION_KEY` | Encrypts AI provider API keys at rest |
57
+| `WORKFLOW_SECRET_ENCRYPTION_KEY` | Encrypts CI/CD workflow secrets at rest |
58
+| `RUNNER_SECRET` | Shared secret between server and CI runners |
59
+
60
+## OAuth Providers
61
+
62
+| Variable | Description |
63
+|----------|-------------|
64
+| `OAUTH_GOOGLE_CLIENT_ID` | Google OAuth client ID |
65
+| `OAUTH_GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
66
+| `OAUTH_GITHUB_CLIENT_ID` | GitHub OAuth client ID |
67
+| `OAUTH_GITHUB_CLIENT_SECRET` | GitHub OAuth client secret |
68
+| `OAUTH_GITLAB_CLIENT_ID` | GitLab OAuth client ID |
69
+| `OAUTH_GITLAB_CLIENT_SECRET` | GitLab OAuth client secret |
70
+
71
+OpenCodeHub also supports SSO via SAML and OIDC. Configure these through the admin dashboard.
72
+
73
+## Git Configuration
74
+
75
+| Variable | Description | Default |
27
76
|----------|-------------|---------|
28
-| `DATABASE_DRIVER` | `postgres`, `mysql`, `sqlite`, `libsql` | `postgres` |
29
-| `DATABASE_URL` | Connection string | `postgresql://user:pass@host:5432/db` |
30
-| `DATABASE_AUTH_TOKEN`| Auth token (Turso/LibSQL only) | |
77
+| `GIT_REPOS_PATH` | Directory for git repositories | `./data/repositories` |
78
+| `GIT_SSH_PORT` | SSH git server port | `2222` |
79
+| `GIT_SSH_HOST_KEY` | Path to SSH host key | `./data/ssh/host_key` |
31
80
32
81
## Storage
33
82
34
83
| Variable | Description | Default |
35
84
|----------|-------------|---------|
36
-| `STORAGE_TYPE` | `local` (filesystem) or `s3` (any S3-compatible object store) | `local` |
37
-| `STORAGE_PATH` | Path for local storage | `./data/storage` |
38
-| `STORAGE_BUCKET` | Bucket name (required for `s3`) | |
39
-| `STORAGE_REGION` | S3 region | `us-east-1` |
40
-| `STORAGE_ENDPOINT` | Custom S3 endpoint (MinIO, R2, Garage, SeaweedFS, Ceph RGW, etc.) | |
41
-| `STORAGE_ACCESS_KEY_ID` | S3 access key | |
42
-| `STORAGE_SECRET_ACCESS_KEY` | S3 secret key | |
85
+| `STORAGE_TYPE` | `local` or `s3` | `local` |
86
+| `STORAGE_PATH` | Local storage path | `./data/storage` |
43
87
44
-> Only `local` and `s3` storage backends are supported. Previous releases
45
-> also supported `gdrive`, `azure`, `gcs`, `dropbox`, `onedrive`, `ftp`, and
46
-> an rclone-as-adapter option; these have been removed in favour of
47
-> S3-compatible object storage. See `docs/guides/storage-adapters.md`.
88
+### S3-Compatible Storage
48
89
49
-## AI Review
90
+Works with AWS S3, MinIO, Cloudflare R2, Garage, SeaweedFS, Ceph RGW, Wasabi, Backblaze B2, and any S3-compatible service.
50
91
51
92
| Variable | Description |
52
93
|----------|-------------|
53
-| `AI_PROVIDER` | `openai` or `anthropic` |
54
-| `OPENAI_API_KEY` | OpenAI API Key |
55
-| `ANTHROPIC_API_KEY` | Anthropic API Key |
94
+| `STORAGE_BUCKET` | Bucket name (required for S3) |
95
+| `STORAGE_REGION` | S3 region (default: `us-east-1`) |
96
+| `STORAGE_ENDPOINT` | Custom S3 endpoint (for MinIO, R2, etc.) |
97
+| `STORAGE_ACCESS_KEY_ID` | S3 access key |
98
+| `STORAGE_SECRET_ACCESS_KEY` | S3 secret key |
99
+
100
+:::tip[Performance]
101
+For best performance, use **local SSD storage**. Object storage (S3/R2) introduces latency. Use S3 for multi-node deployments or backup.
102
+:::
103
+
104
+## CI/CD Runner
105
+
106
+| Variable | Description | Default |
107
+|----------|-------------|---------|
108
+| `RUNNER_ENABLED` | Enable the CI runner | `true` |
109
+| `RUNNER_DOCKER_SOCKET` | Docker socket path | `/var/run/docker.sock` |
110
+| `RUNNER_WORKSPACE_PATH` | Workspace directory | `./data/runner/workspaces` |
111
+| `RUNNER_ARTIFACTS_PATH` | Artifacts directory | `./data/runner/artifacts` |
112
+| `RUNNER_CONCURRENT_JOBS` | Max concurrent jobs | `2` |
113
+| `RUNNER_TIMEOUT` | Job timeout in seconds | `3600` |
114
+| `RUNNER_REGISTRATION_TOKEN_TTL_DAYS` | Token TTL | `90` |
56
115
57
116
## Email (SMTP)
58
117
118
+| Variable | Description | Default |
119
+|----------|-------------|---------|
120
+| `SMTP_HOST` | SMTP hostname | — |
121
+| `SMTP_PORT` | SMTP port | `587` |
122
+| `SMTP_USER` | SMTP username | — |
123
+| `SMTP_PASSWORD` | SMTP password | — |
124
+| `SMTP_FROM` | From address | `[email protected]` |
125
+
126
+## Logging
127
+
128
+| Variable | Description | Default |
129
+|----------|-------------|---------|
130
+| `LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` |
131
+| `LOG_FILE` | Log file path | `./data/logs/opencodehub.log` |
132
+
133
+## Rate Limiting
134
+
135
+| Variable | Description | Default |
136
+|----------|-------------|---------|
137
+| `RATE_LIMIT_ENABLED` | Enable rate limiting | `true` |
138
+| `RATE_LIMIT_AUTH` | Auth endpoints (req/15min) | `5` |
139
+| `RATE_LIMIT_API` | API endpoints (req/min) | `100` |
140
+| `RATE_LIMIT_GIT` | Git operations (req/min) | `200` |
141
+| `RATE_LIMIT_GENERAL` | General endpoints (req/min) | `60` |
142
+| `RATE_LIMIT_SKIP_DEV` | Skip in development | `false` |
143
+| `CSRF_SKIP_DEV` | Skip CSRF in development | `false` |
144
+
145
+## CORS & Request Limits
146
+
147
+| Variable | Description | Default |
148
+|----------|-------------|---------|
149
+| `ALLOWED_ORIGINS` | Comma-separated allowed origins | `http://localhost:4321` |
150
+| `MAX_REQUEST_BODY_MB` | Max request body size (MB) | `10` |
151
+| `MAX_PACK_SIZE_MB` | Max git pack size (MB) | `500` |
152
+| `GIT_PROCESS_TIMEOUT_SECS` | Git process timeout (sec) | `300` |
153
+
154
+## Feature Flags
155
+
156
+| Variable | Description | Default |
157
+|----------|-------------|---------|
158
+| `ENABLE_REGISTRATION` | Allow new user signups | `true` |
159
+| `ENABLE_WIKI` | Enable repository wikis | `true` |
160
+| `ENABLE_ISSUES` | Enable issue tracking | `true` |
161
+| `ENABLE_ACTIONS` | Enable CI/CD pipelines | `true` |
162
+| `ENABLE_PACKAGES` | Enable package registry | `false` |
163
+| `ENABLE_AI_ASSISTANT` | Enable AI code review | `true` |
164
+
165
+## AI Providers
166
+
167
+Configure one or more AI providers for code review. Keys are encrypted at rest using `AI_CONFIG_ENCRYPTION_KEY`.
168
+
169
+| Variable | Provider |
170
+|----------|----------|
171
+| `OPENAI_API_KEY` | OpenAI (GPT-4, GPT-4o) |
172
+| `ANTHROPIC_API_KEY` | Anthropic (Claude) |
173
+| `GROQ_API_KEY` | Groq (fast inference) |
174
+| `BYTEZ_API_KEY` | Bytez |
175
+| `OPENROUTER_API_KEY` | OpenRouter (multi-model) |
176
+| `TOGETHER_API_KEY` | Together AI |
177
+| `GOOGLE_AI_API_KEY` | Google AI (Gemini) |
178
+
179
+Ollama is supported for local/self-hosted AI. No API key required — just set the Ollama endpoint in the admin dashboard.
180
+
181
+## External Review Agent
182
+
183
+| Variable | Description |
184
+|----------|-------------|
185
+| `EXTERNAL_AGENT_WEBHOOK_URL` | Webhook URL for external AI agent |
186
+| `EXTERNAL_AGENT_API_KEY` | API key for the external agent |
187
+| `EXTERNAL_AGENT_CALLBACK_SECRET` | Callback verification secret |
188
+
189
+## Backup
190
+
191
+| Variable | Description | Default |
192
+|----------|-------------|---------|
193
+| `BACKUP_ENCRYPTION_KEY` | Encryption key for backups | — |
194
+| `BACKUP_S3_BUCKET` | S3 bucket for backups | — |
195
+| `BACKUP_S3_PREFIX` | S3 prefix | `opencodehub-backups` |
196
+| `BACKUP_RETENTION_DAYS` | Retention period | `30` |
197
+| `BACKUP_DIR` | Local backup directory | `./backups` |
198
+
199
+## Air-gapped Mode
200
+
201
+| Variable | Description | Default |
202
+|----------|-------------|---------|
203
+| `AIR_GAPPED_MODE` | Disable all external API calls | `false` |
204
+
205
+When enabled, OpenCodeHub operates fully offline. AI features are disabled unless you run a local model (e.g., Ollama).
206
+
207
+## Observability
208
+
59
209
| Variable | Description |
60
210
|----------|-------------|
61
-| `SMTP_HOST` | Hostname (e.g. `smtp.gmail.com`) |
62
-| `SMTP_PORT` | Port (e.g. `587`) |
63
-| `SMTP_USER` | Username |
64
-| `SMTP_PASSWORD`| Password |
65
-| `SMTP_FROM` | From address |
211
+| `GRAFANA_OTLP_ENDPOINT` | Grafana Cloud OTLP endpoint |
212
+| `GRAFANA_INSTANCE_ID` | Grafana Cloud instance ID |
213
+| `GRAFANA_API_KEY` | Grafana Cloud API key |
214
+
215
+OpenCodeHub exports OpenTelemetry traces and Prometheus metrics. See [Monitoring](/administration/monitoring/) for setup details.
66
216
▼docs-site/src/content/docs/development/architecture.md
+105-54
3
3
description: "High-level overview of OpenCodeHub's components and data flow."
4
4
---
5
5
6
-import { Tabs, TabItem } from '@astrojs/starlight/components';
7
-
8
-OpenCodeHub is built for performance and scalability, using a modern stack optimized for self-hosting.
9
-
10
-## C4 Container Diagram
11
-
12
-This diagram visualizes the high-level interaction between the user, the CLI, and the server infrastructure.
13
-
14
-C4Container
15
- title Container diagram for OpenCodeHub System
16
-
17
- Person(developer, "Developer", "Uses CLI and Web UI to manage code")
18
-
19
- System_Boundary(c1, "OpenCodeHub Platform") {
20
- Container(cli, "CLI Tool", "TypeScript, Node.js", "Handles stacking, pushing, and rapid stack manipulation")
21
- Container(web_app, "Web Application", "Astro, SSR", "Serves UI and handles API requests")
22
- Container(git_server, "Git Server", "Node.js Stream", "Smart HTTP Protocol handler for cloning/pushing")
23
-
24
- ContainerDb(database, "PostgreSQL", "SQL Database", "Stores user data, PR metadata, and Merge Queue state")
25
- ContainerDb(redis, "Redis", "In-Memory", "Cache, Session Storage & Realtime Pub/Sub")
26
- ContainerDb(s3, "Object Storage", "S3 / MinIO", "Stores raw Git packfiles and LFS objects")
27
- }
28
-
29
- Rel(developer, cli, "Uses", "Terminal")
30
- Rel(developer, web_app, "Visits", "HTTPS")
31
-
32
- Rel(cli, web_app, "API Calls", "JSON/REST")
33
- Rel(cli, git_server, "Git Push/Pull", "HTTPS/Smart Protocol")
34
-
35
- Rel(web_app, database, "Reads/Writes", "SQL/Drizzle")
36
- Rel(web_app, redis, "Uses", "Cache & Sessions")
37
-
38
- Rel(git_server, s3, "Streams Objects", "S3 API")
39
- Rel(web_app, s3, "Reads Objects", "S3 API")
40
-
41
-## Component Breakdown
42
-
43
-<Tabs>
44
- <TabItem label="Web App (Astro)">
45
- The core of the system. Built with **Astro** in SSR mode.
46
- - **Frontend**: Lightweight, HTML-first pages with islands architecture.
47
- - **API**: Functional endpoints for the CLI and third-party tools.
48
- - **Auth**: Lucia Auth for secure session management.
49
- </TabItem>
50
- <TabItem label="Git Server">
51
- A custom implementation of the Git Smart HTTP protocol.
52
- - **Performance**: Streams packfiles directly to/from S3 without buffering to disk.
53
- - **Stateless**: Can be scaled horizontally across multiple nodes.
54
- </TabItem>
55
- <TabItem label="Storage Layer">
56
- - **Metadata**: PostgreSQL handles the complex relationships between PRs, comments, and repos.
57
- - **Git Data**: We treat Git objects as immutable blobs, perfect for S3-compatible storage (AWS, Cloudflare R2, MinIO).
58
- </TabItem>
59
-</Tabs>
6
+OpenCodeHub is a modular monolith built for performance and self-hosting simplicity.
7
+
8
+## System Overview
9
+
10
+```
11
+┌─────────────────────────────────────────────────────────────┐
12
+│ CLIENTS │
13
+│ Browser │ Git CLI (HTTP/SSH) │ OpenCodeHub CLI (och) │
14
+└─────────────────────────────────────────────────────────────┘
15
+ │
16
+┌─────────────────────────────────────────────────────────────┐
17
+│ OPENCODEHUB PLATFORM │
18
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
19
+│ │ Web UI │ │ REST API │ │ GraphQL Endpoint │ │
20
+│ │ (Astro+React)│ │ (175+ routes)│ │ (src/pages/api/) │ │
21
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
22
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
23
+│ │ Git Server │ │ SSH Server │ │ Pipeline Runner │ │
24
+│ │ (HTTP RPC) │ │ (ssh2) │ │ (Docker executor) │ │
25
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
26
+└─────────────────────────────────────────────────────────────┘
27
+ │
28
+┌─────────────────────────────────────────────────────────────┐
29
+│ PERSISTENCE & INFRASTRUCTURE │
30
+│ PostgreSQL/SQLite/Turso │ Redis │ Pluggable Storage │
31
+└─────────────────────────────────────────────────────────────┘
32
+```
33
+
34
+## Runtime Processes
35
+
36
+| Process | Command | Purpose |
37
+|---------|---------|---------|
38
+| Main App | `npm run dev` | Web UI + API + Git HTTP |
39
+| SSH Git | `npm run git:start` | SSH git push/pull server |
40
+| Worker | `npm run worker:start` | Background jobs (BullMQ queues) |
41
+| Runner | `npm run runner:start` | CI/CD pipeline execution (Docker) |
42
+
43
+## Tech Stack
44
+
45
+| Layer | Technology |
46
+|-------|-----------|
47
+| Framework | Astro 4.x (SSR mode, `@astrojs/node` standalone adapter) |
48
+| UI | React 18 + Tailwind CSS + Radix UI primitives |
49
+| Database ORM | Drizzle ORM |
50
+| DB Drivers | PostgreSQL (pg), SQLite (better-sqlite3), LibSQL/Turso |
51
+| Auth | JWT (jose) + bcryptjs + TOTP (otplib) + OAuth (arctic) + SSO/SAML/OIDC |
52
+| Git | Native `git` CLI + `simple-git` + `isomorphic-git` + `nodegit` |
53
+| SSH | `ssh2` library |
54
+| CI/CD | Dockerode (Docker API) |
55
+| Storage | Abstract adapter: `local` filesystem or `s3`-compatible |
56
+| AI | OpenAI, Anthropic, Google, Groq, Ollama, OpenRouter, Together, Bytez |
57
+| Queue | BullMQ + Redis |
58
+| Observability | OpenTelemetry + Prometheus + Pino |
59
+
60
+## Request Flow
61
+
62
+```
63
+Browser/Git CLI → Astro middleware (auth, rate limit, CSRF)
64
+ ↓
65
+ Route handler (src/pages/api/*.ts)
66
+ ↓
67
+ Business logic (src/lib/*.ts)
68
+ ↓
69
+ Database (src/db/index.ts → Drizzle)
70
+```
71
+
72
+## Authentication
73
+
74
+- **Web**: Cookie-based JWT (`och_session`)
75
+- **API**: Bearer token (JWT or Personal Access Token `och_...`)
76
+- **Git HTTP**: Basic auth with PAT as password
77
+- **Git SSH**: Public key auth against `deployKeys` table
78
+- **SSO**: SAML and OIDC via admin-configured providers
79
+
80
+## Git Protocol Handling
81
+
82
+- **HTTP**: `src/pages/git/` routes → `src/lib/git-server.ts` → spawns `git upload-pack/receive-pack`
83
+- **SSH**: `src/lib/ssh.ts` using `ssh2` library → spawns git processes
84
+- **Smart HTTP**: Full pkt-line protocol implementation with sideband
85
+
86
+## Storage Abstraction
87
+
88
+All blob storage goes through `StorageAdapter`:
89
+- `LocalStorageAdapter` — filesystem at `STORAGE_PATH`
90
+- `S3StorageAdapter` — any S3-compatible store (AWS, MinIO, R2, Garage, Wasabi, etc.)
91
+
92
+Path-style addressing is enabled automatically when `STORAGE_ENDPOINT` is set.
93
+
94
+## Database
95
+
96
+`src/db/index.ts` provides a factory pattern:
97
+- Reads `DATABASE_DRIVER` and `DATABASE_URL`
98
+- Returns appropriate Drizzle instance
99
+- **Production**: PostgreSQL required (schemas use `pgTable`)
100
+- **Dev**: SQLite or Turso for simplicity
101
+
102
+38 schema tables cover users, repos, PRs, issues, projects, workflows, merge queue, stacked PRs, AI reviews, security policies, and more.
103
+
104
+## Background Workers
105
+
106
+The worker process (`npm run worker:start`) handles:
107
+- BullMQ job queues for webhooks, notifications, CI triggers
108
+- Cron jobs (garbage collection, mirror sync, notification digests)
109
+- Email delivery via SMTP
110
+- Real-time SSE event broadcasting
60
111
▼docs-site/src/content/docs/development/contributing.md
+78-41
2
2
title: "Contributing Guide"
3
3
---
4
4
5
-
6
5
Thank you for your interest in contributing to OpenCodeHub! We welcome contributions from everyone.
7
6
8
-## 🛠 Local Development Setup
9
-
10
-To contribute code, you need to set up the project locally.
7
+## Local Development Setup
11
8
12
9
### Prerequisites
13
10
14
-- **Node.js**: v18 or v20 (LTS).
15
-- **Bun**: v1.0+ (Our package manager/runner).
16
-- **Docker**: For running the database locally.
11
+- **Node.js** 20+ (or Bun 1.0+)
12
+- **Docker** (for PostgreSQL and Redis)
13
+- **Git** 2.30+
17
14
18
15
### Step 1: Clone & Install
19
16
20
17
```bash
21
18
git clone https://github.com/swadhinbiswas/OpencodeHub.git
22
19
cd OpencodeHub
23
-
24
-# Install dependencies (fast!)
25
-bun install
20
+npm install
26
21
```
27
22
28
23
### Step 2: Database Setup
29
24
30
-Start a local Postgres and Redis instance using Docker:
25
+Start PostgreSQL and Redis:
31
26
32
27
```bash
33
28
docker-compose up -d postgres redis
34
29
```
35
30
36
-Copy the environment config:
31
+Copy and configure environment:
32
+
37
33
```bash
38
34
cp .env.example .env
35
+# The default .env.example works with docker-compose services
39
36
```
40
-*Note: The default `.env.example` is pre-configured to work with the docker-compose services.*
41
37
42
-Push the schema to the database:
38
+Push schema to database:
39
+
43
40
```bash
44
-bun db:push
41
+npm run db:push
45
42
```
46
43
47
-### Step 3: Run the App
48
-
49
-Start the development server:
44
+### Step 3: Seed & Run
50
45
51
46
```bash
52
-bun dev
47
+# Create admin user
48
+bun run scripts/seed-admin.ts
49
+
50
+# Start dev server
51
+npm run dev
53
52
```
54
53
55
-Visit `http://localhost:3000`.
54
+Visit `http://localhost:4321`.
56
55
56
+## Running Tests
57
57
58
-## 🧪 Running Tests
59
-
60
-We use **Vitest** for unit tests. Use TDD!
58
+We use **Vitest** for unit/integration tests and **Playwright** for E2E tests.
61
59
62
60
```bash
63
61
# Run all tests
64
-bun test
62
+npm run test
63
+
64
+# Run with coverage
65
+npm run test:coverage
65
66
66
67
# Run specific test file
67
-bun test user.test.ts
68
+npx vitest run tests/unit/auth.test.ts
68
69
69
70
# Watch mode
70
-bun test --watch
71
+npx vitest --watch
71
72
```
72
73
73
-
74
-## 🎨 Code Style
74
+**Current status: 546 tests passing across 114 test files (100% pass rate)**
75
75
76
-We use **ESLint** and **Prettier** to enforce code style.
76
+## Code Quality
77
77
78
-- **Lint**: `bun lint`
79
-- **Format**: `bun format`
78
+```bash
79
+npm run lint # Astro check
80
+npm run typecheck # TypeScript check
81
+npm run test # Test suite
82
+```
80
83
81
84
### Commit Messages
82
85
83
-We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification.
86
+We follow [Conventional Commits](https://www.conventionalcommits.org/):
84
87
85
88
- `feat: add new login page`
86
89
- `fix: resolve crash on startup`
87
90
- `docs: update troubleshooting guide`
91
+- `refactor: simplify permission check`
92
+- `test: add coverage for merge queue`
88
93
94
+## Submitting a Pull Request
95
+
96
+1. **Fork** the repository
97
+2. Create a **feature branch**: `git checkout -b feat/my-feature`
98
+3. Make your changes with tests
99
+4. Run `npm run test` and `npm run typecheck` to verify
100
+5. Commit with a conventional commit message
101
+6. Push to your fork
102
+7. Open a **Pull Request** against `main`
103
+8. Wait for CI checks to pass
104
+
105
+## Project Structure
106
+
107
+```
108
+src/
109
+├── pages/ # Astro file-based routing
110
+│ ├── api/ # REST API routes (175+ endpoints)
111
+│ └── [owner]/[repo]/ # Repository pages
112
+├── lib/ # Core business logic (130+ modules)
113
+├── db/ # Database layer (Drizzle ORM)
114
+│ ├── schema/ # 38 schema table definitions
115
+│ └── index.ts # DB connection factory
116
+├── components/ # React components
117
+├── runner/ # CI runner (Docker executor)
118
+└── middleware.ts # Auth, rate limit, CSRF
119
+
120
+cli/
121
+├── src/commands/ # CLI command groups (20+)
122
+└── src/lib/ # CLI utilities
123
+
124
+docs-site/ # This documentation site (Starlight)
125
+```
89
126
90
-## 🚀 Submitting a Pull Request
127
+## Areas for Contribution
91
128
92
-1. **Fork** the repository.
93
-2. Create a **feature branch**: `git checkout -b feat/my-feature`.
94
-3. Commit your changes.
95
-4. Push to your fork.
96
-5. Open a **Pull Request**.
97
-6. Wait for CI checks to pass.
98
-7. A team member will review your code!
129
+- **CI/CD Features**: Matrix builds, caching, workflow templates
130
+- **Package Registry**: npm and Docker registry enhancements
131
+- **AI Review**: New provider integrations, review quality improvements
132
+- **CLI**: New commands, better error messages
133
+- **Tests**: Increase coverage, especially for edge cases
134
+- **Documentation**: Tutorials, guides, API examples
135
+- **Accessibility**: WCAG compliance, keyboard navigation
99
136
100
-Thank you for helping build OpenCodeHub! ❤️
137
+Thank you for helping build OpenCodeHub!
101
138
▼docs-site/src/content/docs/development/database-schema.md
+65-7
2
2
title: "Database Schema"
3
3
---
4
4
5
+OpenCodeHub uses Drizzle ORM with 38 schema tables defined in `src/db/schema/`.
5
6
6
-The database schema is defined using Drizzle ORM in `drizzle/schema.ts`.
7
+## Schema Files
7
8
8
-## Core Tables
9
+| File | Tables | Purpose |
10
+|------|--------|---------|
11
+| `users.ts` | `users` | User accounts, passwords, OAuth, 2FA, admin flags |
12
+| `repositories.ts` | `repositories` | Git repos, visibility, disk paths, settings |
13
+| `pull-requests.ts` | `pullRequests` | PRs, states, branches, merge status |
14
+| `issues.ts` | `issues` | Issue tracking |
15
+| `projects.ts` | `projects` | Project boards, columns, cards |
16
+| `stacked-prs.ts` | `prStacks`, `prStackEntries` | Stacked PR relationships |
17
+| `merge-queue.ts` | `mergeQueue` | Queue entries with priority, CI status, attempts |
18
+| `workflows.ts` | `workflows`, `workflowRuns`, `workflowJobs`, `workflowSteps` | CI/CD pipeline execution |
19
+| `pipeline-runners.ts` | `pipelineRunners` | Registered CI runners |
20
+| `ai-reviews.ts` | `aiReviews` | AI code review results |
21
+| `ai-review-rules.ts` | `aiReviewRules` | AI review rule configurations |
22
+| `developer-metrics.ts` | `developerMetrics` | PR metrics, cycle time |
23
+| `branch-protection.ts` | `branchProtection`, `reviewRequirements`, `requiredStatusChecks` | Branch rules |
24
+| `webhooks.ts` | `webhooks` | Outbound webhook configs |
25
+| `automations.ts` | `automations` | Workflow automation rules |
26
+| `slack-integration.ts` | `slackIntegration` | Slack notification configs |
27
+| `organizations.ts` | `organizations` | Organizations |
28
+| `teams.ts` | `teams` | Team membership |
29
+| `roles.ts` | `roles` | Role-based access control |
30
+| `activity.ts` | `activity` | Activity/audit log |
31
+| `security.ts` | `security` | Security scan results |
32
+| `security-policies.ts` | `securityPolicies` | Security policy definitions |
33
+| `sso.ts` | `sso` | SSO/OIDC configuration |
34
+| `wiki.ts` | `wiki` | Repository wiki pages |
35
+| `deploy-keys.ts` | `deployKeys` | SSH deploy keys |
36
+| `path-permissions.ts` | `pathPermissions` | File-level access control |
37
+| `pr-states.ts` | `prStates` | Custom PR states |
38
+| `issue-statuses.ts` | `issueStatuses` | Custom issue statuses |
39
+| `custom-fields.ts` | `customFields` | Custom issue/project fields |
40
+| `auto-merge-rules.ts` | `autoMergeRules` | Auto-merge configuration |
41
+| `external-ci.ts` | `externalCIConfigs`, `externalBuilds` | External CI integration |
42
+| `review-rules.ts` | `reviewRules` | Review requirements and reviewer routing |
43
+| `integrations.ts` | `integrations` | Code quality, dashboard, deployment integrations |
44
+| `packages.ts` | `packages` | Package registry metadata |
45
+| `inbox-sections.ts` | `inboxSections` | Custom inbox sections |
46
+| `notification-preferences.ts` | `notificationPreferences` | Per-user notification settings |
47
+| `system-config.ts` | `systemConfig` | System-wide configuration |
9
48
10
-- **users**: Stores user accounts, hashed passwords, and profile info.
11
-- **repositories**: Stores repository metadata (owner, name, visibility).
12
-- **pull_requests**: Stores PR data, status, and description.
13
-- **sessions**: Stores active login sessions (if using database sessions).
49
+## Relationships
50
+
51
+Key relationships between tables:
52
+- `repositories` → `users` (owner)
53
+- `pullRequests` → `repositories` (repo) + `users` (author)
54
+- `prStacks` → `repositories` + `prStackEntries` → `pullRequests`
55
+- `mergeQueue` → `pullRequests` + `repositories`
56
+- `workflowRuns` → `workflows` + `repositories`
57
+- `issues` → `repositories` + `users`
58
+- `projects` → `repositories`
14
59
15
60
## Migrations
16
61
26
71
npm run db:migrate
27
72
```
28
73
29
-**Push schema (Prototyping):**
74
+**Push schema (prototyping — no migration files):**
30
75
```bash
31
76
npm run db:push
32
77
```
78
+
79
+**Open Drizzle Studio GUI:**
80
+```bash
81
+npm run db:studio
82
+```
83
+
84
+## Database Support
85
+
86
+| Driver | Use Case | Notes |
87
+|--------|----------|-------|
88
+| `postgres` | Production | Required. All schemas use `pgTable` |
89
+| `sqlite` | Development | Simple local setup |
90
+| `turso` | Edge (Vercel) | LibSQL for serverless deployments |
33
91
▼docs-site/src/content/docs/development/local-dev-setup.md
+73-19
5
5
6
6
## Prerequisites
7
7
8
-- Node.js 18+ (project `engines` specify Node 20+ for runtime)
9
-- Git 2.30+
10
-- PostgreSQL/MySQL/SQLite (PostgreSQL recommended)
8
+- **Node.js** 20+ (or Bun 1.0+)
9
+- **Git** 2.30+
10
+- **PostgreSQL** 14+ (recommended) or SQLite (for quick setup)
11
+- **Redis** 7+ (optional for local dev, required for background workers)
11
12
12
-## Install dependencies
13
+## Quick Setup
13
14
14
15
```bash
15
-npm install
16
-```
16
+# Clone the repository
17
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
18
+cd OpencodeHub
17
19
18
-## Environment
20
+# Install dependencies
21
+npm install
19
22
20
-```bash
23
+# Copy environment config
21
24
cp .env.example .env
22
-```
23
25
24
-Set required values like `JWT_SECRET`, `SESSION_SECRET`, `INTERNAL_HOOK_SECRET`, and `SITE_URL`.
26
+# Edit .env with your settings (at minimum, set the secrets)
27
+# Generate secrets with: openssl rand -base64 32
28
+```
25
29
26
30
## Database
27
31
28
32
```bash
33
+# Push schema to database (creates tables)
29
34
npm run db:push
35
+
36
+# Or generate and run migrations
37
+npm run db:generate
38
+npm run db:migrate
39
+
40
+# Open Drizzle Studio GUI
41
+npm run db:studio
42
+```
43
+
44
+For SQLite (simplest setup), set in `.env`:
45
+```
46
+DATABASE_DRIVER=sqlite
47
+DATABASE_URL=file:./data/opencodehub.db
30
48
```
31
49
32
-## Seed admin user
50
+## Seed Admin User
33
51
34
52
```bash
35
53
bun run scripts/seed-admin.ts
36
54
```
37
55
38
-## Run dev server
56
+## Start Development
39
57
40
58
```bash
59
+# Main app (Web UI + API + Git HTTP)
41
60
npm run dev
61
+
62
+# SSH Git server (optional, for ssh:// clone/push)
63
+npm run git:start
64
+
65
+# Background worker (optional, for webhooks/notifications)
66
+npm run worker:start
67
+
68
+# CI runner (optional, for pipeline execution)
69
+npm run runner:start
70
+```
71
+
72
+The dev server runs at `http://localhost:4321`.
73
+
74
+## Useful Scripts
75
+
76
+| Command | Purpose |
77
+|---------|---------|
78
+| `npm run dev` | Start Astro dev server |
79
+| `npm run build` | Production build |
80
+| `npm run test` | Run Vitest test suite |
81
+| `npm run test:coverage` | Run tests with coverage |
82
+| `npm run lint` | Run Astro check |
83
+| `npm run typecheck` | TypeScript type checking |
84
+| `npm run db:push` | Push schema to database |
85
+| `npm run db:generate` | Generate Drizzle migrations |
86
+| `npm run db:migrate` | Run migrations |
87
+| `npm run db:studio` | Open Drizzle Studio |
88
+| `npm run git:start` | Start SSH git server |
89
+| `npm run worker:start` | Start background worker |
90
+| `npm run runner:start` | Start CI runner |
91
+
92
+## Docker Compose (Alternative)
93
+
94
+For a full stack with PostgreSQL and Redis:
95
+
96
+```bash
97
+docker-compose up -d
98
+docker-compose exec app bun run scripts/seed-admin.ts
42
99
```
43
100
44
-Open the app at `http://localhost:3000`.
101
+This starts the app on port 4321 and SSH on port 2222.
45
102
46
-## Useful scripts
103
+## IDE Setup
47
104
48
-- `npm run db:generate`
49
-- `npm run db:migrate`
50
-- `npm run db:studio`
51
-- `npm run lint`
52
-- `npm run test`
105
+- **VS Code**: Install the Astro extension for syntax highlighting
106
+- **Recommended extensions**: Tailwind CSS IntelliSense, ESLint
53
107
▼docs-site/src/content/docs/features/ai-review.md
+88-459
2
2
title: "AI Code Review"
3
3
---
4
4
5
-> Get instant, intelligent code reviews powered by GPT-4 or Claude
6
-
7
5
AI Code Review uses advanced language models to automatically review your pull requests, catching bugs, security issues, and code quality problems before they reach production.
8
6
9
-## 📖 Table of Contents
10
-
11
-- [Overview](#overview)
12
-- [Setup](#setup)
13
- - [Configure AI Provider](#configure-ai-provider)
14
- - [Enable for Repository](#enable-for-repository)
15
-- [Using AI Review](#using-ai-review)
16
-- [Understanding Review Output](#understanding-review-output)
17
-- [Customizing Reviews](#customizing-reviews)
18
-- [Cost Considerations](#cost-considerations)
19
-- [Best Practices](#best-practices)
20
-- [Troubleshooting](#troubleshooting)
7
+## Supported Providers
21
8
9
+OpenCodeHub supports 10+ AI providers. Configure one or more in your `.env` file:
22
10
23
-## Overview
11
+| Provider | Models | API Key Env Var |
12
+|----------|--------|-----------------|
13
+| **OpenAI** | GPT-4, GPT-4o, GPT-4 Turbo | `OPENAI_API_KEY` |
14
+| **Anthropic** | Claude 3.5 Sonnet, Claude 3 Opus | `ANTHROPIC_API_KEY` |
15
+| **Google** | Gemini 1.5 Pro, Gemini Flash | `GOOGLE_AI_API_KEY` |
16
+| **Groq** | Llama 3, Mixtral (fast inference) | `GROQ_API_KEY` |
17
+| **OpenRouter** | Multi-model gateway | `OPENROUTER_API_KEY` |
18
+| **Together** | Llama, Mixtral, CodeLlama | `TOGETHER_API_KEY` |
19
+| **Bytez** | Various models | `BYTEZ_API_KEY` |
20
+| **Ollama** | Any local model (Llama, CodeLlama, etc.) | No key needed |
21
+| **External Agent** | Custom webhook | `EXTERNAL_AGENT_WEBHOOK_URL` |
24
22
25
-### What AI Review Catches
23
+:::tip[Recommended]
24
+For best results, use **GPT-4o** or **Claude 3.5 Sonnet**. For fast/cheap reviews, use **Groq** or **Ollama** with a local model.
25
+:::
26
26
27
-🔒 **Security Vulnerabilities**
27
+## What AI Review Catches
28
28
29
-- SQL injection risks
30
-- XSS (Cross-Site Scripting) vectors
31
-- Authentication bypasses
32
-- Secrets/credentials in code
29
+**Security Vulnerabilities**
30
+- SQL injection, XSS, CSRF
31
+- Authentication/authorization bypasses
32
+- Exposed secrets or credentials
33
33
- Insecure dependencies
34
34
35
-⚡ **Performance Issues**
36
-
35
+**Performance Issues**
37
36
- N+1 database queries
38
-- Inefficient algorithms (O(n²) → O(n log n))
37
+- Inefficient algorithms
39
38
- Memory leaks
40
-- Blocking operations in async code
41
39
- Missing indexes
42
40
43
-📝 **Code Quality**
44
-
41
+**Code Quality**
45
42
- Best practice violations
46
43
- Code smells (long functions, deep nesting)
47
44
- Incomplete error handling
48
45
- Missing input validation
49
-- Poor naming conventions
50
-
51
-🐛 **Potential Bugs**
52
46
47
+**Potential Bugs**
53
48
- Null/undefined pointer exceptions
54
49
- Race conditions
55
50
- Off-by-one errors
56
-- Logic flaws
57
51
- Type mismatches
58
52
59
-### How It Works
53
+## How It Works
60
54
61
55
```
62
56
1. Developer opens/updates PR
63
57
↓
64
-2. OpenCodeHub sends diff to AI
58
+2. OpenCodeHub sends diff to AI provider
65
59
↓
66
60
3. AI analyzes code changes
67
61
↓
71
64
5. Developer addresses feedback
72
65
```
73
66
74
-**Privacy note:** Code is sent to AI provider (OpenAI/Anthropic). Use on-premise models for sensitive code.
75
-
67
+**Privacy note:** Code is sent to the configured AI provider. Use Ollama or the external agent for sensitive codebases.
76
68
77
69
## Setup
78
70
79
-### Configure AI Provider
80
-
81
-**Option 1: OpenAI (GPT-4)**
82
-
83
-1. Get API key from [platform.openai.com](https://platform.openai.com/api-keys)
84
-
85
-2. Add to `.env`:
71
+### Step 1: Configure AI Provider
86
72
73
+**OpenAI:**
87
74
```bash
88
- AI_PROVIDER=openai
89
- OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxx
90
- OPENAI_MODEL=gpt-4-turbo-preview # or gpt-4, gpt-3.5-turbo
75
+OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxx
91
76
```
92
77
93
-3. Restart OpenCodeHub
94
-
95
-**Option 2: Anthropic (Claude)**
96
-
97
-1. Get API key from [console.anthropic.com](https://console.anthropic.com/)
98
-
99
-2. Add to `.env`:
100
-
78
+**Anthropic:**
101
79
```bash
102
- AI_PROVIDER=anthropic
103
- ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxx
104
- ANTHROPIC_MODEL=claude-3-opus-20240229 # or claude-3-sonnet
80
+ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxx
105
81
```
106
82
107
-3. Restart OpenCodeHub
108
-
109
-**Option 3: Azure OpenAI**
110
-
83
+**Google Gemini:**
111
84
```bash
112
-AI_PROVIDER=azure-openai
113
-AZURE_OPENAI_API_KEY=xxxxx
114
-AZURE_OPENAI_ENDPOINT=https://yourinstance.openai.azure.com/
115
-AZURE_OPENAI_DEPLOYMENT=your-deployment-name
85
+GOOGLE_AI_API_KEY=xxxxxxxxxxxxx
116
86
```
117
87
118
-**Option 4: Self-Hosted (Coming Soon)**
119
-
120
-OpenCodeHub will support local models via Ollama:
88
+**Groq (fast, cheap):**
89
+```bash
90
+GROQ_API_KEY=gsk_xxxxxxxxxxxxx
91
+```
121
92
93
+**Ollama (local, private):**
122
94
```bash
123
-AI_PROVIDER=ollama
124
-OLLAMA_ENDPOINT=http://localhost:11434
125
-OLLAMA_MODEL=codellama:13b
95
+# No env var needed — configure in Settings → AI
96
+# Just install Ollama: https://ollama.com
97
+# Pull a model: ollama pull codellama:13b
126
98
```
127
99
128
-### Enable for Repository
100
+**Multiple providers:** Configure all keys. The system picks the best available provider.
129
101
130
-**Via Web UI:**
102
+### Step 2: Enable for Repository
131
103
132
-1. Navigate to **Repository → Settings → AI Review**
133
-2. Toggle **"Enable AI Code Review"**
134
-3. Configure settings:
135
- - **Trigger**: `On PR open`, `On push`, or `Manual only`
136
- - **Auto-comment**: Post comments automatically
137
- - **Severity threshold**: `All`, `Warning+`, or `Critical only`
138
- - **Max files**: Limit review to N files (cost control)
104
+**Via Web UI:**
105
+1. Go to **Repository → Settings → AI Review**
106
+2. Toggle **Enable AI Code Review**
107
+3. Configure trigger, severity threshold, and max files
139
108
4. Click **Save**
140
109
141
110
**Via API:**
142
-
143
111
```bash
144
-curl -X PATCH https://git.yourcompany.com/api/repos/123/ai-review \
112
+curl -X PATCH https://git.yourcompany.com/api/repos/OWNER/REPO/settings \
145
113
-H "Authorization: Bearer $TOKEN" \
146
- -d '{
147
- "enabled": true,
148
- "trigger": "on_pr_open",
149
- "auto_comment": true,
150
- "severity_threshold": "warning"
151
- }'
114
+ -d '{ "aiReview": { "enabled": true } }'
152
115
```
153
116
154
117
**Via CLI:**
155
-
156
118
```bash
157
-och repo config ai-review --enable \
158
- --trigger on_pr_open \
159
- --auto-comment
119
+och repo config ai-review --enable
160
120
```
161
121
162
-
163
122
## Using AI Review
164
123
165
124
### Automatic Reviews
166
125
167
-If enabled, AI reviews trigger automatically:
168
-
169
-```bash
170
-# 1. Create PR
171
-git push origin feature-branch
172
-# → AI review starts automatically
173
-
174
-# 2. Update PR
175
-git push origin feature-branch
176
-# → AI re-reviews changed files only
177
-```
178
-
179
-You'll see:
180
-
181
-- ✅ **Status check**: "AI Review: In Progress" → "AI Review: Complete"
182
-- 💬 **Inline comments**: On specific lines with issues
183
-- 📊 **Summary comment**: Overall assessment
126
+When enabled, AI reviews trigger automatically when PRs are opened or updated.
184
127
185
128
### Manual Reviews
186
129
187
-Request AI review on-demand:
188
-
189
-**Via Web UI:**
190
-
191
-1. Open PR page
192
-2. Click **"Request AI Review"** button
193
-3. Wait ~30-60 seconds
194
-4. Review appears as comments
195
-
196
-**Via CLI:**
130
+**Web UI:** Open PR page → Click "Request AI Review"
197
131
132
+**CLI:**
198
133
```bash
199
-# Review specific PR
200
-och review ai 125
201
-
202
-# Wait for the review to complete
203
-och review ai 125 --wait
134
+och review ai <pr-number>
204
135
```
205
136
206
-**Via API:**
207
-
137
+**API:**
208
138
```bash
209
139
curl -X POST https://git.yourcompany.com/api/repos/OWNER/REPO/pulls/125/ai-review \
210
140
-H "Authorization: Bearer $TOKEN"
211
141
```
212
142
213
-
214
143
## Understanding Review Output
215
144
216
145
### Severity Levels
217
146
218
-AI assigns severity to each finding:
219
-
220
-| Severity | Icon | Meaning | Example |
221
-| --------------- | ---------- | -------------------------------------- | ----------------------------------------- |
222
-| 🔴 **CRITICAL** | Must fix | Security vulnerability, data loss risk | SQL injection, exposed API key |
223
-| 🟡 **WARNING** | Should fix | Performance issue, bad practice | N+1 query, missing error handling |
224
-| 🔵 **INFO** | Consider | Suggestion, refactoring opportunity | Better variable name, code simplification |
225
-
226
-### Example Review Comment
227
-
228
-````markdown
229
-🔴 CRITICAL: SQL Injection Vulnerability
230
-
231
-**File:** `src/api/users.ts`
232
-**Line:** 45
233
-
234
-**Issue:**
235
-User input is directly concatenated into SQL query without sanitization.
236
-
237
-**Current Code:**
238
-
239
-```typescript
240
-const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
241
-```
242
-````
243
-
244
-**Risk:**
245
-An attacker could inject malicious SQL:
246
-
247
-```
248
-userEmail = "' OR '1'='1"
249
-→ Query becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
250
-→ Returns ALL users (authentication bypass)
251
-```
252
-
253
-**Fix:**
254
-Use parameterized queries or ORM:
255
-
256
-```typescript
257
-const query = `SELECT * FROM users WHERE email = ?`;
258
-const results = await db.query(query, [userEmail]);
259
-
260
-// Or with ORM:
261
-const user = await db.users.findOne({ email: userEmail });
262
-```
263
-
264
-**References:**
265
-
266
-- [OWASP SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection)
267
-- [Node.js SQL Injection Prevention](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
268
-
269
-````
147
+| Severity | Meaning | Example |
148
+|----------|---------|---------|
149
+| **CRITICAL** | Must fix — security/data loss | SQL injection, exposed key |
150
+| **WARNING** | Should fix — perf/best practice | N+1 query, missing error handling |
151
+| **INFO** | Consider — suggestion | Better variable name |
270
152
271
153
### Summary Comment
272
154
273
-AI posts an overall summary:
274
-
275
-```markdown
276
-## 🤖 AI Code Review Summary
155
+AI posts an overall summary with statistics, critical issues, and recommendations.
277
156
278
-**Overall Assessment:** ⚠️ Needs Attention
157
+### Custom Rules
279
158
280
-### Statistics
281
-- Files reviewed: 8
282
-- Critical issues: 2
283
-- Warnings: 5
284
-- Info: 3
285
-
286
-### Critical Issues
287
-1. 🔴 SQL Injection in `src/api/users.ts:45`
288
-2. 🔴 Hardcoded API key in `src/config.ts:12`
289
-
290
-### Recommendations
291
-1. Add input validation to all API endpoints
292
-2. Move secrets to environment variables
293
-3. Consider adding integration tests for auth flows
294
-
295
-### Positive Notes
296
-- ✅ Good error handling in payment service
297
-- ✅ Well-structured database migrations
298
-- ✅ Comprehensive TypeScript types
299
-
300
-**Estimated fix time:** 2-3 hours
301
-````
302
-
303
-
304
-## Customizing Reviews
305
-
306
-### Review Rules
307
-
308
-Create `.opencodehub/ai-review.yml` in your repository:
159
+Create `.opencodehub/ai-review.yml` in your repo:
309
160
310
161
```yaml
311
-# AI Review Configuration
312
-
313
-# Which files to review
314
162
include:
315
163
- "src/**/*.ts"
316
- - "src/**/*.tsx"
317
- - "lib/**/*.js"
318
-
319
-# Which files to skip
320
164
exclude:
321
165
- "**/*.test.ts"
322
- - "**/*.spec.ts"
323
- - "**/generated/**"
324
166
- "dist/**"
325
167
326
-# Focus areas (weighted 1-10)
327
168
focus:
328
- security: 10 # Highest priority
169
+ security: 10
329
170
performance: 8
330
171
bugs: 8
331
172
code_quality: 5
332
- documentation: 3
333
-
334
-# Severity thresholds
335
-report:
336
- critical: always
337
- warning: always
338
- info: on_request # Only if --verbose flag used
339
173
340
-# Custom rules
341
174
custom_rules:
342
175
- name: "No console.log in production"
343
- pattern: 'console\\.log\\('
176
+ pattern: 'console\.log\('
344
177
severity: warning
345
178
message: "Remove console.log before merging"
346
-
347
- - name: "Require error handling"
348
- pattern: 'await.*\\(.*\\)'
349
- check: must_have_try_catch
350
- severity: warning
351
-
352
-# AI instructions
353
-instructions: >
354
- Pay special attention to:
355
- 1. Authentication and authorization logic
356
- 2. Database query efficiency
357
- 3. Input validation
358
- 4. Proper error handling
359
-
360
- Our coding standards:
361
- - TypeScript strict mode
362
- - Functional programming preferred
363
- - Maximum function length: 50 lines
364
- - Maximum cyclomatic complexity: 10
365
179
```
366
180
367
-### Custom Prompts
368
-
369
-Use **Settings → AI Review Rules** to add repository‑specific prompts and checks. Rules are applied automatically when AI reviews run.
370
-
371
-### Language-Specific Reviews
372
-
373
-AI automatically adapts to programming language:
374
-
375
-- **TypeScript/JavaScript**: Type safety, async/await, React best practices
376
-- **Python**: PEP 8, type hints, Django/Flask patterns
377
-- **Go**: Go idioms, error handling, concurrency
378
-- **Rust**: Borrow checker, unsafe code, idiomatic Rust
379
-- **Java**: Design patterns, Spring best practices, thread safety
380
-
181
+Or use **Settings → AI Review Rules** in the web UI.
381
182
382
183
## Cost Considerations
383
184
384
-### Pricing
385
-
386
-AI review costs depend on provider and model:
387
-
388
-**OpenAI (GPT-4 Turbo):**
389
-
390
-- ~$0.01 per 1K tokens (input)
391
-- ~$0.03 per 1K tokens (output)
392
-- **Typical PR**: 500-2000 tokens = **$0.02-0.10 per review**
393
-
394
-**Anthropic (Claude 3 Opus):**
395
-
396
-- ~$0.015 per 1K tokens (input)
397
-- ~$0.075 per 1K tokens (output)
398
-- **Typical PR**: **$0.03-0.15 per review**
399
-
400
-**Example monthly cost:**
401
-
402
-- 50 PRs/month × $0.05/review = **$2.50/month**
403
-- 500 PRs/month × $0.05/review = **$25/month**
404
-
405
-### Cost Optimization
406
-
407
-To reduce cost:
408
-
409
-- Keep PRs small and focused.
410
-- Avoid very large diffs in a single review.
185
+| Provider | Approx Cost per Review |
186
+|----------|----------------------|
187
+| Groq | Free (rate limited) |
188
+| Ollama | Free (local) |
189
+| OpenAI GPT-4o | ~$0.02-0.10 |
190
+| Anthropic Claude | ~$0.03-0.15 |
191
+| Google Gemini | ~$0.01-0.05 |
411
192
412
-**3. Skip test files:**
413
-
414
-```yaml
415
-exclude:
416
- - "**/*.test.*"
417
- - "**/*.spec.*"
418
-```
419
-
420
-**4. Review only on-request:**
421
-
422
-```yaml
423
-trigger: manual # Don't auto-review every push
424
-```
425
-
426
-**5. Use severity threshold:**
427
-
428
-```yaml
429
-severity_threshold: warning # Skip "info" level comments
430
-```
431
-
432
-
433
-## Best Practices
434
-
435
-### 💡 When to Use AI Review
436
-
437
-**✅ Good Use Cases:**
438
-
439
-- Security-sensitive code (auth, payments)
440
-- Complex algorithms
441
-- Database queries
442
-- API endpoints
443
-- Third-party integrations
444
-
445
-**❌ Less Useful:**
446
-
447
-- Generated code
448
-- Configuration files
449
-- Simple refactoring
450
-- Documentation-only changes
451
-
452
-### 💡 Responding to AI Feedback
453
-
454
-**1. Don't auto-fix everything**
455
-
456
-- AI can be wrong
457
-- Context matters
458
-- Use judgment
459
-
460
-**2. Engage in discussion**
461
-
462
-```markdown
463
-@ai-review That's a good catch, but in this case we're using a
464
-trusted source and the data is already sanitized earlier in the pipeline.
465
-
466
-See line 23 where we validate input.
467
-```
468
-
469
-**3. Track false positives**
470
-
471
-```yaml
472
-# .opencodehub/ai-review.yml
473
-ignore:
474
- - file: src/utils/legacy.ts
475
- reason: "Legacy code, requires major refactor"
476
- until: 2024-12-31
477
-```
478
-
479
-### 💡 Combining with Human Review
480
-
481
-AI complements, doesn't replace humans:
482
-
483
-```
484
-1. AI reviews first (catches obvious issues)
485
-2. Developer fixes critical/warnings
486
-3. Human reviewer approves
487
-```
488
-
489
-This workflow:
490
-
491
-- Reduces human reviewer burden
492
-- Catches more bugs
493
-- Faster feedback cycles
494
-
193
+**Cost optimization:**
194
+- Keep PRs small and focused
195
+- Skip test files in review config
196
+- Use severity threshold to filter info-level comments
197
+- Use Groq or Ollama for routine reviews, GPT-4/Claude for critical code
495
198
496
199
## Troubleshooting
497
200
498
-### "AI review taking too long"
499
-
500
-**Cause:** Large PR (many files/lines).
501
-
502
-**Solution:**
503
-
504
-```bash
505
-# Check review status
506
-och review status 125
507
-
508
-# Retry the review
509
-och review ai 125
510
-```
511
-
512
-### "AI review failed"
513
-
514
-**Causes:**
515
-
516
-1. API key invalid/expired
517
-2. Rate limit exceeded
518
-3. Timeout (very large PR)
519
-
520
-**Solutions:**
201
+**AI review taking too long:** Large PR (many files). Split into smaller PRs.
521
202
522
-```bash
523
-# 1. Verify API key
524
-echo $OPENAI_API_KEY
525
-
526
-# 2. Check rate limits
527
-curl https://api.openai.com/v1/usage \
528
- -H "Authorization: Bearer $OPENAI_API_KEY"
529
-
530
-# 3. Split large PR or review manually.
531
-```
532
-
533
-### "Too many false positives"
534
-
535
-**Solution:** Use **Settings → AI Review Rules** to refine prompts and reduce false positives.
536
-
537
-### "Missing obvious bugs"
538
-
539
-**Cause:** AI missed context or edge case.
540
-
541
-**Solution:** Add custom instructions in **Settings → AI Review Rules** so reviews incorporate your conventions.
542
-
543
-
544
-## Advanced: CI/CD Integration
545
-
546
-### Block merge on critical findings
203
+**AI review failed:** Check API key is valid. Verify provider is configured in Settings → AI.
547
204
548
-```yaml
549
-# .github/workflows/ai-review.yml
550
-name: AI Code Review
551
-on: [pull_request]
552
-
553
-jobs:
554
- ai-review:
555
- runs-on: ubuntu-latest
556
- steps:
557
- - name: Run AI Review
558
- run: och review ai ${{ github.event.pull_request.number }}
559
- env:
560
- OCH_TOKEN: ${{ secrets.OCH_TOKEN }}
561
-
562
- - name: Check for critical issues
563
- run: |
564
- REVIEW=$(curl -s \
565
- -H "Authorization: Bearer $OCH_TOKEN" \
566
- https://git.yourcompany.com/api/repos/OWNER/REPO/pulls/${{ github.event.pull_request.number }}/ai-review/latest)
567
- SEVERITY=$(echo "$REVIEW" | jq -r '.data.overallSeverity')
568
- if [ "$SEVERITY" = "critical" ]; then
569
- echo "❌ Critical issues found"
570
- exit 1
571
- fi
572
-```
205
+**Too many false positives:** Refine rules in Settings → AI Review Rules. Add custom instructions.
573
206
207
+**Missing obvious bugs:** Add custom instructions describing your conventions and patterns.
574
208
575
209
## See Also
576
210
577
-- [Stacked Pull Requests](stacked-prs.md)
578
-- [Smart Merge Queue](merge-queue.md)
579
-- [CI/CD Actions](ci-actions.md)
580
-
581
-
582
-## Need Help?
583
-
584
-- 💬 [Discord Community](https://discord.gg/opencodehub)
585
-- 🐛 [Report Issues](https://github.com/swadhinbiswas/OpencodeHub/issues)
586
-- 📖 [Full Documentation](/)
211
+- [Stacked PRs](/features/stacked-prs/)
212
+- [Merge Queue](/features/merge-queue/)
213
+- [CI/CD Actions](/features/ci-actions/)
214
+- [Configuration Reference](/administration/configuration/#ai-providers)
587
215
▼docs-site/src/content/docs/getting-started/first-repository.md
+73-32
2
2
title: "Create Your First Repository"
3
3
---
4
4
5
-
6
5
This guide walks you through the complete lifecycle of a repository in OpenCodeHub.
7
6
8
-## Step 1: Creation
9
-
10
-1. Click the **+** button in the top navbar and select **New Repository**.
11
-2. **Owner**: Select yourself or an organization.
12
-3. **Repository Name**: Enter `hello-world`.
13
-4. **Description**: "My first project on OpenCodeHub".
14
-5. **Visibility**: Select **Public**.
15
-6. **Initialize**: Check **Initialize with README**.
7
+## Step 1: Create the Repository
16
8
17
-Click **Create Repository**. You'll be taken to your new repository's home page.
9
+1. Click the **+** button in the top navbar → **New Repository**
10
+2. **Owner**: Select yourself or an organization
11
+3. **Repository Name**: Enter `hello-world`
12
+4. **Description**: "My first project on OpenCodeHub"
13
+5. **Visibility**: Select **Public**
14
+6. **Initialize**: Check **Initialize with README**
18
15
19
-## Step 2: Cloning
16
+Click **Create Repository**.
20
17
21
-To work on your computer, you need to clone the repository.
18
+## Step 2: Clone
22
19
23
20
### SSH (Recommended)
24
-Ensure you have added your SSH key in **Settings > SSH Keys**.
21
+
22
+Add your SSH key in **Settings → SSH Keys** first.
25
23
26
24
```bash
27
-git clone [email protected]:username/hello-world.git
25
+git clone ssh://git@your-server:2222/username/hello-world.git
28
26
```
29
27
30
28
### HTTPS
29
+
31
30
```bash
32
-git clone https://your-instance.com/username/hello-world.git
31
+git clone https://git.yourcompany.com/username/hello-world.git
33
32
```
34
33
35
-## Step 3: Making Changes
34
+For HTTPS, you'll need a Personal Access Token. Generate one at **Settings → Tokens**.
36
35
37
-Open the folder in your favorite editor (VS Code, etc.).
36
+## Step 3: Make Changes
38
37
39
38
```bash
40
39
cd hello-world
41
-```
42
40
43
-Modify `README.md`:
44
-```markdown
45
-# Hello World
46
-This is my first change!
47
-```
41
+# Edit files
42
+echo "# Hello World\n\nThis is my first change!" > README.md
48
43
49
-Commit the change:
50
-```bash
44
+# Commit
51
45
git add README.md
52
46
git commit -m "Update README"
53
47
```
54
48
55
-## Step 4: Pushing
56
-
57
-Push your changes back to OpenCodeHub:
49
+## Step 4: Push
58
50
59
51
```bash
60
52
git push origin main
61
53
```
62
-(Or use `och push` if you installed the CLI).
63
54
64
-## Step 5: Viewing Changes
55
+Refresh the repository page to see your changes.
56
+
57
+## Step 5: Create a Pull Request
58
+
59
+1. Create a feature branch:
60
+```bash
61
+git checkout -b feat/add-feature
62
+```
63
+
64
+2. Make changes and push:
65
+```bash
66
+echo "New feature" > feature.txt
67
+git add feature.txt
68
+git commit -m "Add new feature"
69
+git push -u origin feat/add-feature
70
+```
71
+
72
+3. In the web UI, go to **Pull Requests** → **New Pull Request**
73
+4. Select `feat/add-feature` as source, `main` as target
74
+5. Add a title and description
75
+6. Click **Create Pull Request**
76
+
77
+## Step 6: Review and Merge
78
+
79
+1. Add reviewers on the PR page
80
+2. Reviewers can approve or request changes
81
+3. Once approved, click **Merge** or add to the **Merge Queue**
82
+
83
+## Step 7: Explore Features
84
+
85
+### Wiki
86
+Go to the **Wiki** tab to create documentation for your project.
87
+
88
+### Issues
89
+Go to **Issues** → **New Issue** to track bugs and feature requests.
90
+
91
+### Projects
92
+Go to **Projects** to create a kanban board for task management.
93
+
94
+### Actions
95
+Go to **Actions** to view CI/CD pipeline runs. Create `.github/workflows/*.yml` files to set up automated builds.
96
+
97
+### Settings
98
+Go to **Settings** to configure:
99
+- **Branch Protection** — Require reviews, status checks
100
+- **Collaborators** — Add team members
101
+- **Webhooks** — Integrations with external services
102
+- **Deploy Keys** — SSH keys for automated deployments
65
103
66
-Refresh your repository page in the browser. You should see your updated `README.md` displayed and the latest commit listed.
104
+## Next Steps
67
105
68
-Congratulations! You've successfully hosted code on OpenCodeHub.
106
+- **[Stacked PRs](/features/stacked-prs/)** — Break large changes into reviewable stacks
107
+- **[AI Code Review](/features/ai-review/)** — Get automated code feedback
108
+- **[CI/CD](/features/ci-actions/)** — Set up automated pipelines
109
+- **[CLI](/reference/cli-overview/)** — Manage everything from the terminal
69
110
▼docs-site/src/content/docs/getting-started/installation.md
+78-37
2
2
title: "Installation Guide"
3
3
---
4
4
5
+OpenCodeHub is a self-hosted Git platform. You can run it via **Docker** (recommended) or **Node.js** (for development).
5
6
6
-OpenCodeHub allows you to host your own GitHub-like platform. You can run it via **Docker** (recommended for production) or **Node.js** (for development/custom setups).
7
-
8
-## 📋 System Requirements
7
+## System Requirements
9
8
10
9
| Component | Minimum | Recommended |
11
10
|-----------|---------|-------------|
14
13
| **Disk** | 10GB | 50GB SSD |
15
14
| **OS** | Linux (Ubuntu/Debian) | Linux |
16
15
17
-
18
-## ⚡ 1-Click Installation (Recommended)
19
-
20
-The easiest way to install OpenCodeHub on a VPS, NAS, or edge device is using the automated installation script. This script automatically handles Docker setup, environment variables, database generation, and configuring the unified `DATA_DIR`.
16
+## 1-Click Installation (Recommended)
21
17
22
18
```bash
23
19
curl -sSL https://raw.githubusercontent.com/swadhinbiswas/OpencodeHub/main/install.sh | bash
24
20
```
25
21
26
22
The script will:
27
-1. Ensure Docker and Docker Compose are installed.
28
-2. Clone the latest stable release.
29
-3. Automatically configure a unified `/data` mount point (`DATA_DIR`) for all your databases, git repos, and storage to easily enable painless backups.
30
-4. Prompt you to create an initial Admin user account.
31
-
32
-### Manual Setup (Advanced)
23
+1. Ensure Docker and Docker Compose are installed
24
+2. Clone the latest stable release
25
+3. Configure a unified `DATA_DIR` for databases, git repos, and storage
26
+4. Prompt you to create an initial admin user
33
27
34
-If you prefer to configure Docker Compose manually:
28
+## Manual Docker Setup
35
29
36
-1. Clone the repository:
37
30
```bash
31
+# Clone
38
32
git clone https://github.com/swadhinbiswas/OpencodeHub.git
39
-cd OpenCodeHub
40
-```
33
+cd OpencodeHub
41
34
42
-2. Configure environment:
43
-```bash
35
+# Configure
44
36
cp .env.example .env
45
-```
46
-Open `.env` and set your `DATA_DIR` (e.g., `DATA_DIR=/home/user/opencodehub-data`). The system will automatically map the database, SSH keys, git repositories, and object storage paths to this unified folder.
37
+# Edit .env — at minimum, change all secrets!
47
38
48
-3. Start services:
49
-```bash
39
+# Start
50
40
docker-compose up -d
41
+
42
+# Create admin user
43
+docker-compose exec app bun run scripts/seed-admin.ts
51
44
```
52
45
53
-4. Initialize the database and admin user:
46
+The app runs on **port 4321** (HTTP) and **port 2222** (SSH git).
47
+
48
+:::caution[Change Secrets]
49
+Before going live, generate new secrets for all `*_SECRET` and `*_KEY` variables:
54
50
```bash
55
-bun run scripts/seed-admin.ts
51
+openssl rand -base64 32
56
52
```
53
+:::
57
54
55
+## Docker Compose Services
58
56
59
-## 🛠 Nginx Reverse Proxy (SSL)
57
+| Service | Port | Purpose |
58
+|---------|------|---------|
59
+| `app` | 4321, 2222 | Main application + SSH git |
60
+| `postgres` | 5432 | PostgreSQL database |
61
+| `redis` | 6379 | Redis cache/queues |
62
+| `runner` | — | CI/CD Docker runner |
60
63
61
-In production, you **must** use HTTPS. Here is a standard Nginx configuration.
64
+## Nginx Reverse Proxy (SSL)
65
+
66
+In production, use HTTPS. Standard Nginx config:
62
67
63
68
```nginx
64
69
server {
80
83
client_max_body_size 500M;
81
84
82
85
location / {
83
- proxy_pass http://localhost:3000;
86
+ proxy_pass http://localhost:4321;
84
87
proxy_set_header Host $host;
85
88
proxy_set_header X-Real-IP $remote_addr;
86
89
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
89
92
}
90
93
```
91
94
95
+See [Nginx Deployment](/administration/deploy-nginx/) for the full config with WebSocket support and Git optimizations.
92
96
93
-## ✅ Production Checklist
97
+## SSH Git Access
94
98
95
-Before going live to users:
99
+The SSH git server starts automatically on port 2222. Clone with:
96
100
97
-- [ ] **Secrets Rotated**: Default secrets replaced with strong random strings.
98
-- [ ] **HTTPS Enabled**: SSL certificate configured via Nginx/Caddy.
99
-- [ ] **Rate Limiting**: `RATE_LIMIT_*` env vars adjusted for expected load.
100
-- [ ] **Monitoring**: Grafana/Sentry configured for error tracking ([Guide](../administration/monitoring.md)).
101
+```bash
102
+git clone ssh://git@your-server:2222/owner/repo.git
103
+```
101
104
105
+The host key is auto-generated at `GIT_SSH_HOST_KEY` on first start.
102
106
103
-## Next Steps
107
+## Node.js Setup (Development)
108
+
109
+```bash
110
+# Install dependencies
111
+npm install
112
+
113
+# Configure
114
+cp .env.example .env
115
+
116
+# Push schema to database
117
+npm run db:push
118
+
119
+# Create admin user
120
+bun run scripts/seed-admin.ts
121
+
122
+# Start dev server
123
+npm run dev
124
+```
125
+
126
+See [Local Dev Setup](/development/local-dev-setup/) for details.
104
127
105
-Now that you have OpenCodeHub up and running, let's look around:
128
+## Production Checklist
129
+
130
+Before going live:
131
+
132
+- [ ] All secrets rotated (JWT_SECRET, SESSION_SECRET, INTERNAL_HOOK_SECRET, etc.)
133
+- [ ] HTTPS enabled via Nginx/Caddy
134
+- [ ] PostgreSQL used (not SQLite)
135
+- [ ] Redis configured for distributed locking
136
+- [ ] Rate limiting enabled (`RATE_LIMIT_ENABLED=true`)
137
+- [ ] Email configured (SMTP) for notifications
138
+- [ ] Storage backend configured (S3 recommended for multi-node)
139
+- [ ] Backup strategy configured
140
+- [ ] Monitoring set up (see [Monitoring](/administration/monitoring/))
141
+
142
+## Next Steps
106
143
107
-👉 **[Quick Start Guide](quick-start.md)**
144
+- **[Quick Start Guide](/getting-started/quick-start/)** — Navigate the dashboard and create your first repo
145
+- **[Configuration Reference](/administration/configuration/)** — All environment variables
146
+- **[Docker Deployment](/administration/deploy-docker/)** — Production Docker setup
108
147
▼docs-site/src/content/docs/getting-started/quick-start.md
+94-26
2
2
title: "Quick Start Guide"
3
3
---
4
4
5
-
6
-Welcome to OpenCodeHub! This guide will help you navigate the platform and start collaborating in minutes.
5
+Welcome to OpenCodeHub! This guide will help you get started in minutes.
7
6
8
7
## 1. Login
9
-Navigate to your instance URL (e.g., `http://localhost:3000`) and log in with the admin credentials you created during installation.
10
-
11
-## 2. Navigating the Dashboard
12
-Your dashboard gives you a quick overview of:
13
-- **Recent Repositories**: Projects you've worked on recently.
14
-- **Your Pull Requests**: PRs requiring your attention.
15
-- **Activity Feed**: What your team is doing.
16
-
17
-## 3. Create a Repository
18
-1. Click the **+** (Plus) icon in the top right navigation bar.
19
-2. Select **New Repository**.
20
-3. Enter a **Repository Name** (e.g., `my-awesome-project`).
21
-4. Choose Visibility:
22
- - **Public**: Visible to everyone (if your instance allows it).
23
- - **Private**: Visible only to you and collaborators.
24
-5. Click **Create Repository**.
25
-
26
-## 4. Collaborate
27
-1. **Invite Members**: Go to **Settings** > **Collaborators** to add team members.
28
-2. **Create Issues**: Use the **Issues** tab to track tasks and bugs.
29
-3. **Open Pull Requests**: Push a branch and open a PR to review code.
30
8
9
+Navigate to your instance URL (e.g., `http://localhost:4321`) and log in with the admin credentials you created during installation.
31
10
32
-## Next Steps
11
+## 2. Create a Repository
12
+
13
+1. Click the **+** icon in the top navigation bar
14
+2. Select **New Repository**
15
+3. Enter a name (e.g., `my-project`)
16
+4. Choose visibility: **Public** or **Private**
17
+5. Optionally initialize with a README
18
+6. Click **Create Repository**
19
+
20
+## 3. Push Code
21
+
22
+```bash
23
+# Clone your new repository
24
+git clone https://git.yourcompany.com/your-username/my-project.git
25
+cd my-project
26
+
27
+# Add your code
28
+echo "# My Project" > README.md
29
+git add .
30
+git commit -m "Initial commit"
31
+git push -u origin main
32
+```
33
+
34
+Or use SSH:
35
+
36
+```bash
37
+git clone ssh://git@your-server:2222/your-username/my-project.git
38
+```
39
+
40
+## 4. Explore the Dashboard
41
+
42
+Your dashboard shows:
43
+- **Recent Repositories** you've worked on
44
+- **Pull Requests** requiring your attention
45
+- **Stacked PRs** if you're using the stack workflow
46
+- **Activity Feed** from your team
47
+
48
+## 5. Create a Pull Request
49
+
50
+1. Navigate to your repository
51
+2. Click **Pull Requests** → **New Pull Request**
52
+3. Select source and target branches
53
+4. Add a title and description
54
+5. Click **Create Pull Request**
55
+
56
+See [First Repository](/getting-started/first-repository/) for a complete walkthrough.
33
57
34
-Now that you're exploring the UI, try pushing your first code:
58
+## 6. Try Key Features
59
+
60
+### Stacked PRs
61
+Break large changes into reviewable stacks:
62
+```bash
63
+# Install the CLI
64
+npm install -g opencodehub-cli
65
+
66
+# Login
67
+och auth login
68
+
69
+# Create a stacked branch
70
+och stack create feature/part-1
71
+
72
+# Make changes, commit, push
73
+och stack submit
74
+```
75
+
76
+### AI Code Review
77
+Enable in **Settings** → **AI Review**. Configure your preferred provider (OpenAI, Anthropic, etc.) and AI will automatically review PRs for bugs and security issues.
78
+
79
+### Merge Queue
80
+Add PRs to the merge queue from the PR page. The queue ensures `main` never breaks by running CI on the merged result before pushing.
81
+
82
+### CI/CD Pipelines
83
+Create `.github/workflows/*.yml` files in your repository. OpenCodeHub uses GitHub Actions-compatible syntax.
84
+
85
+## 7. Install the CLI
86
+
87
+```bash
88
+npm install -g opencodehub-cli
89
+
90
+# Authenticate
91
+och auth login --url https://git.yourcompany.com
92
+
93
+# Check status
94
+och status
95
+```
96
+
97
+See [CLI Overview](/reference/cli-overview/) for all commands.
98
+
99
+## Next Steps
35
100
36
-👉 **[Create Your First Repository](first-repository.md)**
101
+- **[First Repository](/getting-started/first-repository/)** — Complete repository lifecycle
102
+- **[Stacked PRs](/features/stacked-prs/)** — Learn the stack workflow
103
+- **[CLI Reference](/reference/cli-commands/)** — All CLI commands
104
+- **[Configuration](/administration/configuration/)** — Customize your instance
37
105
▼docs-site/src/content/docs/guides/example.md
+0-9
1
-title: Example Guide
2
-description: A guide in my new Starlight docs site.
3
-
4
-Guides lead a user through a specific task they want to accomplish, often with a sequence of steps.
5
-Writing a good guide requires thinking about what your users are trying to do.
6
-
7
-## Further reading
8
-
9
-- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diátaxis framework
10
0
▼docs-site/src/content/docs/index.mdx
+103-70
1
1
---
2
2
title: OpenCodeHub Documentation
3
-description: The open-source, AI-native Git server with Stacked PRs and Smart Merge Queues.
3
+description: The open-source, self-hosted Git platform with Stacked PRs, AI Code Review, and Smart Merge Queues.
4
4
hero:
5
5
title: OpenCodeHub
6
- tagline: The fully self-hosted GitHub alternative with SSH hosting, CI/CD pipelines, and a modern developer experience.
6
+ tagline: The fully self-hosted GitHub alternative with SSH hosting, CI/CD pipelines, AI code review, and a modern developer experience.
7
7
---
8
8
9
9
import { Card, CardGrid } from "@astrojs/starlight/components";
11
11
## Why OpenCodeHub?
12
12
13
13
<CardGrid stagger>
14
- <Card title="🚀 Stacked PRs" icon="rocket">
15
- Break large features into small, dependent steps. Ship faster without the
16
- review bottleneck.
14
+ <Card title="Stacked PRs" icon="rocket">
15
+ Break large features into small, dependent steps. Graphite-style stacked branches with automatic rebase and merge readiness checks.
17
16
</Card>
18
- <Card title="🤖 AI Code Review" icon="star">
19
- Built-in LLM agents (GPT-4/Claude) review your code for bugs and security
20
- issues automatically.
17
+ <Card title="AI Code Review" icon="star">
18
+ Built-in LLM agents (GPT-4, Claude, Gemini, Groq, Ollama, and 10+ providers) review your code for bugs and security issues automatically.
21
19
</Card>
22
- <Card title="🔀 Smart Merge Queue" icon="random">
23
- Automate your merging strategy. Ensure `main` never breaks with speculative
24
- CI runs.
20
+ <Card title="Smart Merge Queue" icon="random">
21
+ Stack-aware merge queue with speculative CI builds, priority lanes, and conflict detection. Ensure `main` never breaks.
25
22
</Card>
26
- <Card title="🏠 Fully Self-Hosted" icon="laptop">
27
- Keep your code on your servers. No "per-seat" pricing. No vendor lock-in.
23
+ <Card title="CI/CD Pipelines" icon="add-document">
24
+ GitHub Actions-compatible workflow engine with Docker-based runners, artifact storage, and secrets management.
25
+ </Card>
26
+ <Card title="Fully Self-Hosted" icon="laptop">
27
+ Keep your code on your servers. No per-seat pricing. No vendor lock-in. Deploy with Docker, Podman, or bare metal.
28
+ </Card>
29
+ <Card title="Package Registry" icon="seti:db">
30
+ Built-in npm and Docker container registry. Host private packages alongside your source code.
28
31
</Card>
29
32
</CardGrid>
30
33
31
-## Architecture Overview
32
-
33
-![OpenCodeHub Architecture](/architecture.svg)
34
+## Core Capabilities
34
35
35
-OpenCodeHub is built on a modern, scalable architecture designed for performance and flexibility.
36
+<CardGrid>
37
+ <Card title="Git Hosting" icon="seti:git">
38
+ Full Git support with HTTP smart protocol and SSH. Repository forking, mirroring, LFS, and CODEOWNERS enforcement.
39
+ </Card>
40
+ <Card title="Pull Requests" icon="seti:code">
41
+ Full PR lifecycle with inline comments, file-level approvals, review templates, reviewer routing rules, and auto-merge.
42
+ </Card>
43
+ <Card title="Issues & Projects" icon="seti:folder">
44
+ Issue tracking with custom fields and statuses. Drag-and-drop project boards with columns and cards.
45
+ </Card>
46
+ <Card title="Security" icon="seti:lock">
47
+ Secret scanning, vulnerability scanning, security policies, path-level permissions, and audit logging.
48
+ </Card>
49
+ <Card title="Webhooks & Automations" icon="seti:webhook">
50
+ Outbound webhooks with HMAC signing. Rule-based automations for PR workflows, reviews, and deployments.
51
+ </Card>
52
+ <Card title="Organizations & Teams" icon="seti:user">
53
+ Multi-tenant organizations with role-based access control, team management, and fine-grained permissions.
54
+ </Card>
55
+</CardGrid>
36
56
37
57
## Deploy Anywhere
38
58
39
59
<CardGrid>
40
- <Card title="🐳 Docker" icon="add-document">
41
- The recommended way to deploy. Full Docker Compose setup with PostgreSQL and
42
- Redis. [Deploy with Docker →](/administration/deploy-docker/)
60
+ <Card title="Docker" icon="add-document">
61
+ The recommended way to deploy. Full Docker Compose setup with PostgreSQL and Redis.
62
+ [Deploy with Docker →](/administration/deploy-docker/)
43
63
</Card>
44
- <Card title="🚀 Coolify" icon="rocket">
45
- Self-hosted PaaS alternative to Heroku/Render. Auto-deploy with built-in SSL
46
- and managed databases. [Deploy on Coolify →](/administration/deploy-coolify/)
64
+ <Card title="Podman" icon="add-document">
65
+ Rootless containers with native systemd integration. Ideal for security-focused deployments.
66
+ [Deploy with Podman →](/administration/deploy-podman/)
47
67
</Card>
48
- <Card title="🖥️ CyberPanel" icon="laptop">
49
- Modern hosting panel with OpenLiteSpeed. Docker or native Node.js deployment.
50
- [Deploy on CyberPanel →](/administration/deploy-cyberpanel/)
68
+ <Card title="Kubernetes" icon="add-document">
69
+ Deploy to Kubernetes using the bundled Helm chart with horizontal scaling.
70
+ [Deploy on Kubernetes →](/administration/kubernetes/)
51
71
</Card>
52
- <Card title="📁 cPanel" icon="seti:folder">
53
- Deploy on shared hosting using Node.js or PM2. Best for budget hosting.
54
- [Deploy on cPanel →](/administration/deploy-cpanel/)
72
+ <Card title="Coolify" icon="rocket">
73
+ Self-hosted PaaS alternative to Heroku/Render. Auto-deploy with built-in SSL.
74
+ [Deploy on Coolify →](/administration/deploy-coolify/)
55
75
</Card>
56
- <Card title="💾 NAS" icon="seti:db">
76
+ <Card title="NAS" icon="seti:db">
57
77
Run on Synology, TrueNAS, or QNAP. Perfect for private home/team Git servers.
58
78
[Deploy on NAS →](/administration/deploy-nas/)
59
79
</Card>
60
- <Card title="🦭 Podman" icon="add-document">
61
- Rootless containers with native systemd integration. Ideal for
62
- security-focused deployments. [Deploy with Podman
63
- →](/administration/deploy-podman/)
64
- </Card>
65
- <Card title="☁️ Cloudflare" icon="add-document">
66
- Use Cloudflare Tunnel + R2 for zero-port exposure and S3-compatible storage.
80
+ <Card title="Cloudflare" icon="add-document">
81
+ Cloudflare Tunnel + R2 for zero-port exposure and S3-compatible storage.
67
82
[Deploy with Cloudflare →](/administration/deploy-cloudflare/)
68
83
</Card>
69
- <Card title="🌐 Nginx" icon="add-document">
70
- Production-ready reverse proxy config with SSL and Git-specific
71
- optimizations. [Configure Nginx →](/administration/deploy-nginx/)
84
+ <Card title="Nginx" icon="add-document">
85
+ Production-ready reverse proxy config with SSL and Git-specific optimizations.
86
+ [Configure Nginx →](/administration/deploy-nginx/)
87
+ </Card>
88
+ <Card title="cPanel / CyberPanel" icon="seti:folder">
89
+ Deploy on shared hosting or VPS panels using Node.js + PM2.
90
+ [cPanel →](/administration/deploy-cpanel/) | [CyberPanel →](/administration/deploy-cyberpanel/)
72
91
</Card>
73
92
</CardGrid>
74
93
75
94
## Tech Stack
76
95
77
-| Layer | Technology |
78
-| ------------ | ------------------------------------- |
79
-| **Frontend** | Astro, React, Tailwind CSS, shadcn/ui |
80
-| **Backend** | Node.js, Astro API Routes, simple-git |
81
-| **Database** | PostgreSQL, Turso (LibSQL), SQLite |
82
-| **Storage** | Local disk, S3, R2 |
83
-| **Auth** | JWT, 2FA, OAuth (GitHub, Google) |
84
-| **CI/CD** | GitHub Actions compatible runners |
96
+| Layer | Technology |
97
+| --- | --- |
98
+| **Framework** | Astro 4.x (SSR, Node standalone adapter) |
99
+| **Frontend** | React 18, Tailwind CSS, Radix UI, Framer Motion |
100
+| **Backend** | Astro API Routes (175+ endpoints), GraphQL |
101
+| **Database** | PostgreSQL (prod), SQLite (dev), Turso/LibSQL (edge) |
102
+| **ORM** | Drizzle ORM (38 schema tables) |
103
+| **Auth** | JWT, bcryptjs, TOTP 2FA, OAuth (Google, GitHub), SSO/SAML, OIDC |
104
+| **Git** | simple-git, isomorphic-git, nodegit, ssh2 |
105
+| **Storage** | Local filesystem or S3-compatible (AWS, MinIO, R2, Garage, Wasabi) |
106
+| **CI/CD** | GitHub Actions-compatible engine, Docker-based runners |
107
+| **AI** | OpenAI, Anthropic, Google, Groq, Ollama, OpenRouter, Together, Bytez |
108
+| **Queue** | BullMQ + Redis |
109
+| **Observability** | OpenTelemetry, Prometheus metrics, Pino logging |
110
+
111
+## Quick Start
112
+
113
+```bash
114
+# Clone and start with Docker Compose
115
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
116
+cd OpencodeHub
117
+cp .env.example .env # Edit secrets!
118
+docker-compose up -d
119
+
120
+# Create admin user
121
+docker-compose exec app bun run scripts/seed-admin.ts
122
+
123
+# Open in browser
124
+open http://localhost:4321
125
+```
126
+
127
+:::tip[SSH Git Access]
128
+The SSH git server starts automatically on port 2222. Clone with:
129
+`git clone ssh://git@localhost:2222/owner/repo.git`
130
+:::
85
131
86
132
## Quick Links
87
133
88
134
<CardGrid>
89
135
<Card title="Installation" icon="add-document">
90
- Get OpenCodeHub running in minutes using our 1-click install script. [Read the guide
91
- →](/getting-started/installation/)
136
+ Get OpenCodeHub running in minutes. [Read the guide →](/getting-started/installation/)
92
137
</Card>
93
- <Card title="CircleCI Integration" icon="seti:check">
94
- Connect CircleCI as your external CI provider. Trigger builds and report
95
- PR checks automatically. [Set up CircleCI →](/guides/circleci-integration/)
138
+ <Card title="Configuration" icon="seti:config">
139
+ All environment variables and settings. [View config →](/administration/configuration/)
96
140
</Card>
97
141
<Card title="CLI Reference" icon="seti:shell">
98
- Learn the `och` CLI commands for managing stacks and repositories. [View
99
- commands →](/reference/cli-commands/)
142
+ The `och` CLI with 20+ command groups. [View commands →](/reference/cli-commands/)
100
143
</Card>
101
144
<Card title="API Reference" icon="open-book">
102
- Integrate with OpenCodeHub using the REST API. [Explore the API
103
- →](/api/rest-api/)
145
+ REST API with 175+ endpoints. [Explore the API →](/api/rest-api/)
146
+ </Card>
147
+ <Card title="Architecture" icon="seti:code">
148
+ System design, data flow, and component overview. [View architecture →](/development/architecture/)
104
149
</Card>
105
150
<Card title="Contributing" icon="github">
106
- Help make OpenCodeHub better. Set up your local dev environment. [Start
107
- contributing →](/development/contributing/)
151
+ Help make OpenCodeHub better. [Start contributing →](/development/contributing/)
108
152
</Card>
109
153
</CardGrid>
110
-
111
-## Storage Options
112
-
113
-:::tip[Performance Recommendation]
114
-For best performance, use **local SSD storage** via `DATA_DIR` in production. Object storage (S3/R2) introduces latency. See [Storage Adapters](/guides/storage-adapters/#️-performance-considerations) for details.
115
-:::
116
-
117
-| Storage Type | Performance | Cost | Best For |
118
-| ---------------- | ----------- | ---- | ----------------------- |
119
-| **Local SSD** | ⚡ Fastest | $$ | Production, self-hosted |
120
-| **S3/R2** | 🐢 Slow | $ | Backup, multi-node |
121
154
▼docs-site/src/content/docs/reference/example.md
+0-9
1
-title: Example Reference
2
-description: A reference page in my new Starlight docs site.
3
-
4
-Reference pages are ideal for outlining how things work in terse and clear terms.
5
-Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you're documenting.
6
-
7
-## Further reading
8
-
9
-- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework
10
0
▼docs/FREE-DEPLOYMENT.md
+395-0
1
+# OpenCodeHub — Free Deployment Options
2
+
3
+> Deploy OpenCodeHub for free with persistent file storage for git repositories.
4
+
5
+---
6
+
7
+## Quick Comparison
8
+
9
+| Platform | Cost | Storage | RAM | CPU | Best For |
10
+|----------|------|---------|-----|-----|----------|
11
+| **Oracle Cloud Free** | $0 forever | 200GB | 24GB | 4 ARM cores | Best overall |
12
+| **Home Server/NAS** | $0 | Unlimited | Your hardware | Your hardware | Privacy-focused |
13
+| **Fly.io** | $0 (limited) | 3GB free | 256MB | Shared | Quick testing |
14
+| **Google Cloud Free** | $0 forever | 30GB | 1GB | 0.25 vCPU | Light usage |
15
+| **Railway** | $5 trial | 1GB | 512MB | Shared | Quick demo |
16
+| **Hetzner** | ~€4/mo | 40GB | 4GB | 2 vCPU | Best value paid |
17
+
18
+---
19
+
20
+## Option 1: Oracle Cloud Free Tier (Recommended)
21
+
22
+**Always Free** — No credit card charge, no time limit.
23
+
24
+### What You Get
25
+- 4 ARM cores (Ampere A1)
26
+- 24 GB RAM
27
+- 200 GB boot volume
28
+- 2 x 1TB block volumes (total 2TB)
29
+- Free PostgreSQL database
30
+- Free Redis
31
+
32
+### Steps
33
+
34
+```bash
35
+# 1. Sign up at cloud.oracle.com
36
+# 2. Create a VM instance:
37
+# - Name: opencodehub
38
+# - Image: Ubuntu 22.04
39
+# - Shape: VM.Standard.A1.Flex (4 OCPU, 24 GB RAM)
40
+# - Upload SSH key
41
+# - Create boot volume: 100 GB
42
+
43
+# 3. SSH into your instance
44
+ssh -i ~/.ssh/key ubuntu@YOUR_PUBLIC_IP
45
+
46
+# 4. Install Docker
47
+curl -fsSL https://get.docker.com -o get-docker.sh
48
+sudo sh get-docker.sh
49
+sudo usermod -aG docker $USER
50
+
51
+# 5. Install Docker Compose
52
+sudo apt install docker-compose-plugin -y
53
+
54
+# 6. Clone OpenCodeHub
55
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
56
+cd OpencodeHub
57
+
58
+# 7. Configure
59
+cp .env.example .env
60
+nano .env # Edit secrets
61
+
62
+# 8. Start
63
+sudo docker compose up -d
64
+
65
+# 9. Create admin
66
+sudo docker compose exec app bun run scripts/seed-admin.ts
67
+```
68
+
69
+### Access Your Instance
70
+```
71
+Web: http://YOUR_PUBLIC_IP:4321
72
+SSH: ssh://git@YOUR_PUBLIC_IP:2222/owner/repo.git
73
+HTTPS: Set up Nginx + Let's Encrypt (free)
74
+```
75
+
76
+---
77
+
78
+## Option 2: Home Server / NAS
79
+
80
+**$0 forever** — Use hardware you already own.
81
+
82
+### What You Need
83
+- Any computer running Linux/Mac/Windows
84
+- 2GB+ RAM recommended
85
+- 20GB+ free disk space
86
+
87
+### Quick Start (Linux/Mac)
88
+
89
+```bash
90
+# Install Docker
91
+curl -fsSL https://get.docker.com | sh
92
+
93
+# Clone and start
94
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
95
+cd OpencodeHub
96
+cp .env.example .env
97
+nano .env # Set secrets
98
+docker compose up -d
99
+
100
+# Create admin
101
+docker compose exec app bun run scripts/seed-admin.ts
102
+```
103
+
104
+### NAS Deployment
105
+
106
+**Synology / TrueNAS / QNAP:**
107
+1. Install Docker from Package Center
108
+2. Use Docker Compose or Portainer
109
+3. Mount a shared folder for `/data`
110
+
111
+### Access from Anywhere
112
+- **Tailscale** (free): `tailscale up` → access via Tailscale IP
113
+- **Cloudflare Tunnel** (free): Expose without opening ports
114
+- **Port forwarding**: Open port 4321 on your router
115
+
116
+---
117
+
118
+## Option 3: Fly.io Free Tier
119
+
120
+**Free** — 3GB persistent volume included.
121
+
122
+### Steps
123
+
124
+```bash
125
+# 1. Install flyctl
126
+curl -L https://fly.io/install.sh | sh
127
+
128
+# 2. Login
129
+fly auth login
130
+
131
+# 3. Launch
132
+fly launch --name opencodehub
133
+
134
+# 4. Add persistent volume
135
+fly volume create opencodehub_data --region sjc --size 3
136
+
137
+# 5. Set secrets
138
+fly secrets set JWT_SECRET=$(openssl rand -hex 32)
139
+fly secrets set SESSION_SECRET=$(openssl rand -hex 32)
140
+fly secrets set DATABASE_URL="postgresql://..."
141
+fly secrets set REDIS_URL="redis://..."
142
+
143
+# 6. Deploy
144
+fly deploy
145
+```
146
+
147
+### Limitations
148
+- 3GB storage (enough for small repos)
149
+- 256MB RAM (may be slow for large repos)
150
+- Apps休眠 after inactivity (cold starts take ~30s)
151
+
152
+---
153
+
154
+## Option 4: Google Cloud Free Tier
155
+
156
+**Always Free** — e2-micro instance.
157
+
158
+### Steps
159
+
160
+```bash
161
+# 1. Sign up at cloud.google.com
162
+# 2. Create e2-micro VM:
163
+# - Machine type: e2-micro (0.25 vCPU, 1GB RAM)
164
+# - Boot disk: 30GB SSD (free)
165
+# - Firewall: Allow HTTP (80), SSH (22)
166
+
167
+# 3. SSH in and install Docker
168
+ssh -i ~/.ssh/key USER@EXTERNAL_IP
169
+sudo apt update && sudo apt install docker.io docker-compose-plugin -y
170
+
171
+# 4. Deploy OpenCodeHub
172
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
173
+cd OpencodeHub
174
+cp .env.example .env
175
+nano .env
176
+docker compose up -d
177
+```
178
+
179
+### Limitations
180
+- Only 1GB RAM (tight for production)
181
+- 0.25 vCPU (slow builds)
182
+- Good for personal/light use
183
+
184
+---
185
+
186
+## Option 5: Self-Hosted on Existing VPS
187
+
188
+If you already have a VPS (Hetzner, DigitalOcean, Linode, etc.):
189
+
190
+```bash
191
+# SSH into your server
192
+ssh root@YOUR_SERVER_IP
193
+
194
+# One-line install
195
+curl -fsSL https://raw.githubusercontent.com/swadhinbiswas/OpencodeHub/main/install.sh | bash
196
+```
197
+
198
+---
199
+
200
+## Option 6: Docker on Windows/Mac
201
+
202
+For local development or personal use:
203
+
204
+```bash
205
+# Install Docker Desktop for Windows/Mac
206
+# https://docs.docker.com/get-docker/
207
+
208
+# Clone and start
209
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
210
+cd OpencodeHub
211
+cp .env.example .env
212
+nano .env
213
+docker compose up -d
214
+
215
+# Create admin
216
+docker compose exec app bun run scripts/seed-admin.ts
217
+```
218
+
219
+---
220
+
221
+## Storage Configuration
222
+
223
+### Local Storage (Default)
224
+```bash
225
+STORAGE_TYPE=local
226
+STORAGE_PATH=/data/storage
227
+GIT_REPOS_PATH=/data/repositories
228
+```
229
+
230
+### S3-Compatible (MinIO on same server)
231
+```bash
232
+# Add to docker-compose.yml:
233
+minio:
234
+ image: minio/minio
235
+ command: server /data --console-address ":9001"
236
+ volumes:
237
+ - ./data/minio:/data
238
+ ports:
239
+ - "9001:9001"
240
+
241
+STORAGE_TYPE=s3
242
+STORAGE_ENDPOINT=http://minio:9000
243
+STORAGE_BUCKET=opencodehub
244
+STORAGE_ACCESS_KEY_ID=minioadmin
245
+STORAGE_SECRET_ACCESS_KEY=minioadmin
246
+```
247
+
248
+---
249
+
250
+## HTTPS Setup (Free)
251
+
252
+### With Let's Encrypt + Nginx
253
+
254
+```bash
255
+# Install Nginx and Certbot
256
+sudo apt install nginx certbot python3-certbot-nginx -y
257
+
258
+# Create Nginx config
259
+sudo nano /etc/nginx/sites-available/opencodehub
260
+```
261
+
262
+```nginx
263
+server {
264
+ listen 80;
265
+ server_name git.yourdomain.com;
266
+ return 301 https://$host$request_uri;
267
+}
268
+
269
+server {
270
+ listen 443 ssl http2;
271
+ server_name git.yourdomain.com;
272
+
273
+ ssl_certificate /etc/letsencrypt/live/git.yourdomain.com/fullchain.pem;
274
+ ssl_certificate_key /etc/letsencrypt/live/git.yourdomain.com/privkey.pem;
275
+
276
+ client_max_body_size 500M;
277
+
278
+ location / {
279
+ proxy_pass http://localhost:4321;
280
+ proxy_set_header Host $host;
281
+ proxy_set_header X-Real-IP $remote_addr;
282
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
283
+ proxy_set_header X-Forwarded-Proto $scheme;
284
+ }
285
+
286
+ # WebSocket support
287
+ location /api/realtime {
288
+ proxy_pass http://localhost:4321;
289
+ proxy_http_version 1.1;
290
+ proxy_set_header Upgrade $http_upgrade;
291
+ proxy_set_header Connection "upgrade";
292
+ }
293
+}
294
+```
295
+
296
+```bash
297
+# Enable and get SSL
298
+sudo ln -s /etc/nginx/sites-available/opencodehub /etc/nginx/sites-enabled/
299
+sudo certbot --nginx -d git.yourdomain.com
300
+```
301
+
302
+---
303
+
304
+## Environment Variables (Production)
305
+
306
+```bash
307
+# Generate all secrets
308
+JWT_SECRET=$(openssl rand -hex 32)
309
+SESSION_SECRET=$(openssl rand -hex 32)
310
+INTERNAL_HOOK_SECRET=$(openssl rand -hex 32)
311
+CRON_SECRET=$(openssl rand -hex 32)
312
+RUNNER_SECRET=$(openssl rand -hex 32)
313
+AI_CONFIG_ENCRYPTION_KEY=$(openssl rand -hex 32)
314
+WORKFLOW_SECRET_ENCRYPTION_KEY=$(openssl rand -hex 32)
315
+
316
+# Your .env file
317
+DATABASE_DRIVER=postgres
318
+DATABASE_URL=postgresql://opencodehub:PASSWORD@localhost:5432/opencodehub
319
+DATABASE_SSL=false
320
+REDIS_URL=redis://localhost:6379
321
+SITE_URL=https://git.yourdomain.com
322
+STORAGE_TYPE=local
323
+GIT_REPOS_PATH=/data/repositories
324
+NODE_ENV=production
325
+```
326
+
327
+---
328
+
329
+## Backup Strategy
330
+
331
+### Automated Backup Script
332
+```bash
333
+#!/bin/bash
334
+# backup.sh — Run daily via cron
335
+BACKUP_DIR="/backups/$(date +%Y-%m-%d)"
336
+mkdir -p "$BACKUP_DIR"
337
+
338
+# Database dump
339
+docker compose exec -T postgres pg_dump -U opencodehub opencodehub > "$BACKUP_DIR/db.sql"
340
+
341
+# Git repos
342
+tar czf "$BACKUP_DIR/repos.tar.gz" ./data/repositories/
343
+
344
+# Upload to S3 (optional)
345
+# aws s3 sync "$BACKUP_DIR" s3://your-backup-bucket/opencodehub/
346
+```
347
+
348
+### Cron Job
349
+```bash
350
+# Add to crontab
351
+crontab -e
352
+0 2 * * * /path/to/backup.sh
353
+```
354
+
355
+---
356
+
357
+## Monitoring
358
+
359
+### Health Check
360
+```bash
361
+# Check if app is running
362
+curl -f http://localhost:4321/api/health || echo "DOWN"
363
+
364
+# Check Docker status
365
+docker compose ps
366
+docker compose logs --tail=50 app
367
+```
368
+
369
+### Uptime Robot (Free)
370
+1. Sign up at uptimerobot.com
371
+2. Add HTTP monitor: `http://YOUR_IP:4321/api/health`
372
+3. Get notified if your instance goes down
373
+
374
+---
375
+
376
+## Recommended Free Setup
377
+
378
+For a **personal Git server** that stores all your code:
379
+
380
+| Component | Choice |
381
+|-----------|--------|
382
+| **Cloud** | Oracle Cloud Free Tier |
383
+| **Server** | ARM A1 (4 cores, 24GB RAM) |
384
+| **Storage** | 100GB boot + 1TB block volume |
385
+| **Database** | PostgreSQL (Docker) |
386
+| **Cache** | Redis (Docker) |
387
+| **HTTPS** | Nginx + Let's Encrypt |
388
+| **Access** | Tailscale (private) or port forwarding |
389
+| **Backups** | Cron script + local S3 |
390
+
391
+**Total cost: $0/month forever.**
392
+
393
+---
394
+
395
+*Generated for OpenCodeHub free deployment.*
0
396
▼docs/RENDER-DEPLOYMENT.md
+163-0
1
+# OpenCodeHub — Render Deployment Guide
2
+
3
+> Deploy to Render (Singapore) with free PostgreSQL + Upstash Redis. $0/month.
4
+
5
+---
6
+
7
+## What You Get
8
+
9
+- OpenCodeHub running in Asia (Singapore)
10
+- Free PostgreSQL database
11
+- Free Redis via Upstash
12
+- 1GB persistent storage for git repos
13
+- HTTPS included
14
+- Admin account
15
+
16
+---
17
+
18
+## Step 1: Create Upstash Redis (2 min)
19
+
20
+1. Go to **https://upstash.com** → Sign up (free)
21
+2. Click **Create Database**
22
+3. Settings:
23
+ - **Name**: `opencodehub`
24
+ - **Region**: **Asia Pacific (Singapore)**
25
+ - **Plan**: Pay as you go (free tier: 10K commands/day)
26
+4. Click **Create**
27
+5. Copy the **Redis URL** (looks like `rediss://...@...`)
28
+6. Keep this tab open — you'll need it in Step 3
29
+
30
+---
31
+
32
+## Step 2: Deploy to Render (5 min)
33
+
34
+1. Go to **https://render.com** → Sign up (free)
35
+2. Click **New** → **Blueprint**
36
+3. Connect your GitHub account
37
+4. Select your OpenCodeHub repository
38
+5. Render detects `render.yaml` and shows:
39
+ - `opencodehub-db` (PostgreSQL — Singapore)
40
+ - `opencodehub` (Web Service — Singapore)
41
+6. Click **Apply**
42
+7. Wait for deployment (~3-5 min first time)
43
+
44
+---
45
+
46
+## Step 3: Set Redis URL (1 min)
47
+
48
+1. Go to your Web Service → **Environment** tab
49
+2. Find `REDIS_URL` (shows "Not set")
50
+3. Click the pencil icon → Paste your **Upstash Redis URL**
51
+4. Click **Save**
52
+
53
+---
54
+
55
+## Step 4: Set Site URL (1 min)
56
+
57
+1. In the same **Environment** tab
58
+2. Find `SITE_URL` (shows "Not set")
59
+3. Click the pencil icon → Enter your Render URL:
60
+ ```
61
+ https://opencodehub.onrender.com
62
+ ```
63
+ (Replace with your actual URL from the service overview)
64
+4. Click **Save**
65
+
66
+---
67
+
68
+## Step 5: Manual Deploy
69
+
70
+1. Go to your Web Service → **Manual Deploy**
71
+2. Click **Deploy latest commit**
72
+3. Wait for build to complete (~2-3 min)
73
+4. Click the **URL** at the top to open your instance
74
+
75
+---
76
+
77
+## Step 6: Create Admin Account
78
+
79
+You'll see the **Setup Wizard**. Fill in:
80
+
81
+| Field | Value |
82
+|-------|-------|
83
+| Username | `admin` (or your choice) |
84
+| Email | Your email |
85
+| Password | Strong password |
86
+
87
+Click **Create Account** — you're now logged in as admin!
88
+
89
+---
90
+
91
+## Step 7: Test It
92
+
93
+```bash
94
+# Create a repo via web UI, then clone:
95
+git clone https://opencodehub.onrender.com/your-username/my-repo.git
96
+
97
+# Push code:
98
+cd my-repo
99
+echo "# Hello" > README.md
100
+git add . && git commit -m "Initial"
101
+git push origin main
102
+```
103
+
104
+---
105
+
106
+## Done! Summary
107
+
108
+| Component | Service | Region | Cost |
109
+|-----------|---------|--------|------|
110
+| App | Render Free | Singapore | $0 |
111
+| PostgreSQL | Render Free | Singapore | $0 |
112
+| Redis | Upstash Free | Singapore | $0 |
113
+| Storage | Render Disk 1GB | Singapore | $0 |
114
+| **Total** | | | **$0/month** |
115
+
116
+---
117
+
118
+## Keep Alive (Optional)
119
+
120
+Render free tier spins down after 15min inactivity. To prevent:
121
+
122
+1. Go to **https://uptimerobot.com** → Sign up (free)
123
+2. Add Monitor → Type: HTTP(s)
124
+3. URL: `https://opencodehub.onrender.com/api/health`
125
+4. Monitoring interval: 5 minutes
126
+5. Save — your app stays awake
127
+
128
+---
129
+
130
+## Upgrade Path (When You Grow)
131
+
132
+| Need | Upgrade | Cost |
133
+|------|---------|------|
134
+| Always-on (no spin-down) | Render Starter | $7/mo |
135
+| More storage (10GB+) | Render Disk upgrade | $1-7/mo |
136
+| More Redis commands | Upstash Pay-as-you-go | ~$0.20/10K |
137
+| PostgreSQL expiry | Render Starter DB | $7/mo |
138
+
139
+---
140
+
141
+## Troubleshooting
142
+
143
+**App won't start?**
144
+- Check **Logs** tab in Render
145
+- Verify `REDIS_URL` is set correctly
146
+- Ensure `SITE_URL` matches your Render URL
147
+
148
+**Redis connection failed?**
149
+- Verify Redis URL from Upstash (starts with `rediss://`)
150
+- Check Upstash dashboard shows "Active"
151
+- Ensure region is Singapore
152
+
153
+**Git clone fails?**
154
+- Use HTTPS: `git clone https://your-url.onrender.com/user/repo.git`
155
+- SSH not available on Render free tier (use HTTPS)
156
+
157
+**Slow first request?**
158
+- Normal for free tier (cold start ~30s)
159
+- Add UptimeRobot ping to keep it warm
160
+
161
+---
162
+
163
+*OpenCodeHub — Deployed on Render, Asia Region, $0/month.*
0
164
▼docs/administration/deploy-nas.md
+4-4
154
154
-v /volume1/opencodehub/storage:/data/storage \
155
155
-v /volume1/opencodehub/ssh:/data/ssh \
156
156
-v /volume1/opencodehub/cache:/data/cache \
157
- swadhinbiswas/opencodehub:latest
157
+ opencodehub/opencodehub:latest
158
158
```
159
159
160
160
> **Note:** On Synology, `host.docker.internal` may not work. Use your NAS IP address instead (e.g., `192.168.1.100`).
301
301
302
302
services:
303
303
app:
304
- image: swadhinbiswas/opencodehub:latest
304
+ image: opencodehub/opencodehub:latest
305
305
container_name: opencodehub
306
306
restart: unless-stopped
307
307
ports:
443
443
444
444
#### Create OpenCodeHub:
445
445
446
-1. Search and install `swadhinbiswas/opencodehub:latest`
446
+1. Search and install `opencodehub/opencodehub:latest`
447
447
2. Name: `opencodehub`
448
448
3. Network: `opencodehub-network`
449
449
4. Environment variables (same as Synology example)
496
496
497
497
services:
498
498
app:
499
- image: swadhinbiswas/opencodehub:latest
499
+ image: opencodehub/opencodehub:latest
500
500
container_name: opencodehub
501
501
restart: unless-stopped
502
502
ports:
503
503
▼docs/administration/incident-runbook.md
+1-1
48
48
kubectl set env deployment/opencodehub FEATURE_X_DISABLED=true
49
49
50
50
# Option B: Rollback (Docker)
51
-docker run --rm swadhinbiswas/opencodehub:previous-tag
51
+docker run --rm opencodehub/opencodehub:previous-tag
52
52
# Update docker-compose.yml with previous tag
53
53
docker compose -f docker-compose.production.yml up -d
54
54
55
55
▼drizzle/0000_abandoned_king_bedlam.sql
+0-36
1
-ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "ai_config" text;
2
-
3
-CREATE TABLE IF NOT EXISTS "organizations" (
4
- "id" text PRIMARY KEY NOT NULL,
5
- "name" text NOT NULL,
6
- "display_name" text,
7
- "description" text,
8
- "email" text,
9
- "location" text,
10
- "website" text,
11
- "avatar_url" text,
12
- "is_verified" boolean DEFAULT false,
13
- "created_at" timestamp DEFAULT now() NOT NULL,
14
- "updated_at" timestamp DEFAULT now() NOT NULL,
15
- CONSTRAINT "organizations_name_unique" UNIQUE("name")
16
-);
17
-
18
-CREATE TABLE IF NOT EXISTS "organization_members" (
19
- "organization_id" text NOT NULL,
20
- "user_id" text NOT NULL,
21
- "role" text DEFAULT 'member' NOT NULL,
22
- "created_at" timestamp DEFAULT now() NOT NULL,
23
- CONSTRAINT "organization_members_organization_id_user_id_pk" PRIMARY KEY("organization_id","user_id")
24
-);
25
-
26
-DO $$ BEGIN
27
- ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;
28
-EXCEPTION
29
- WHEN duplicate_object THEN null;
30
-END $$;
31
-
32
-DO $$ BEGIN
33
- ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
34
-EXCEPTION
35
- WHEN duplicate_object THEN null;
36
-END $$;
37
0
▼drizzle/0000_black_monster_badoon.sql
+0-992
1
-CREATE TABLE `email_verification_tokens` (
2
- `id` text PRIMARY KEY NOT NULL,
3
- `user_id` text NOT NULL,
4
- `token` text NOT NULL,
5
- `expires_at` text NOT NULL,
6
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
7
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
8
-);
9
-CREATE UNIQUE INDEX `email_verification_tokens_token_unique` ON `email_verification_tokens` (`token`);--> statement-breakpoint
10
-CREATE TABLE `gpg_keys` (
11
- `id` text PRIMARY KEY NOT NULL,
12
- `user_id` text NOT NULL,
13
- `name` text NOT NULL,
14
- `key_id` text NOT NULL,
15
- `public_key` text NOT NULL,
16
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
17
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
18
-);
19
-CREATE TABLE `oauth_accounts` (
20
- `id` text PRIMARY KEY NOT NULL,
21
- `user_id` text NOT NULL,
22
- `provider` text NOT NULL,
23
- `provider_account_id` text NOT NULL,
24
- `access_token` text,
25
- `refresh_token` text,
26
- `expires_at` text,
27
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
28
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
29
-);
30
-CREATE TABLE `password_reset_tokens` (
31
- `id` text PRIMARY KEY NOT NULL,
32
- `user_id` text NOT NULL,
33
- `token` text NOT NULL,
34
- `expires_at` text NOT NULL,
35
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
36
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
37
-);
38
-CREATE UNIQUE INDEX `password_reset_tokens_token_unique` ON `password_reset_tokens` (`token`);--> statement-breakpoint
39
-CREATE TABLE `personal_access_tokens` (
40
- `id` text PRIMARY KEY NOT NULL,
41
- `user_id` text NOT NULL,
42
- `name` text NOT NULL,
43
- `token` text NOT NULL,
44
- `expires_at` text,
45
- `last_used_at` text,
46
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
47
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
48
-);
49
-CREATE UNIQUE INDEX `personal_access_tokens_token_unique` ON `personal_access_tokens` (`token`);--> statement-breakpoint
50
-CREATE TABLE `sessions` (
51
- `id` text PRIMARY KEY NOT NULL,
52
- `user_id` text NOT NULL,
53
- `token` text NOT NULL,
54
- `user_agent` text,
55
- `ip_address` text,
56
- `expires_at` text NOT NULL,
57
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
58
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
59
-);
60
-CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
61
-CREATE TABLE `ssh_keys` (
62
- `id` text PRIMARY KEY NOT NULL,
63
- `user_id` text NOT NULL,
64
- `title` text NOT NULL,
65
- `fingerprint` text NOT NULL,
66
- `public_key` text NOT NULL,
67
- `key_type` text NOT NULL,
68
- `last_used_at` text,
69
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
70
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
71
-);
72
-CREATE UNIQUE INDEX `ssh_keys_fingerprint_unique` ON `ssh_keys` (`fingerprint`);--> statement-breakpoint
73
-CREATE TABLE `users` (
74
- `id` text PRIMARY KEY NOT NULL,
75
- `username` text NOT NULL,
76
- `email` text NOT NULL,
77
- `password_hash` text,
78
- `display_name` text,
79
- `bio` text,
80
- `avatar_url` text,
81
- `location` text,
82
- `website` text,
83
- `company` text,
84
- `is_admin` integer DEFAULT false,
85
- `is_active` integer DEFAULT true,
86
- `email_verified` integer DEFAULT false,
87
- `two_factor_enabled` integer DEFAULT false,
88
- `two_factor_secret` text,
89
- `last_login_at` text,
90
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
91
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL
92
-);
93
-CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
94
-CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
95
-CREATE TABLE `branches` (
96
- `id` text PRIMARY KEY NOT NULL,
97
- `repository_id` text NOT NULL,
98
- `name` text NOT NULL,
99
- `commit_sha` text NOT NULL,
100
- `is_protected` integer DEFAULT false,
101
- `is_default` integer DEFAULT false,
102
- `protection_rules` text,
103
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
104
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
105
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
106
-);
107
-CREATE TABLE `commits` (
108
- `id` text PRIMARY KEY NOT NULL,
109
- `repository_id` text NOT NULL,
110
- `sha` text NOT NULL,
111
- `message` text NOT NULL,
112
- `author_name` text NOT NULL,
113
- `author_email` text NOT NULL,
114
- `author_date` text NOT NULL,
115
- `committer_name` text NOT NULL,
116
- `committer_email` text NOT NULL,
117
- `committer_date` text NOT NULL,
118
- `parent_shas` text,
119
- `tree_sha` text,
120
- `user_id` text,
121
- `stats` text,
122
- `signature` text,
123
- `is_verified` integer DEFAULT false,
124
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
125
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
126
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
127
-);
128
-CREATE TABLE `releases` (
129
- `id` text PRIMARY KEY NOT NULL,
130
- `repository_id` text NOT NULL,
131
- `tag_id` text,
132
- `name` text NOT NULL,
133
- `body` text,
134
- `is_draft` integer DEFAULT false,
135
- `is_prerelease` integer DEFAULT false,
136
- `author_id` text,
137
- `assets` text,
138
- `download_count` integer DEFAULT 0,
139
- `published_at` text,
140
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
141
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
142
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
143
- FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE no action ON DELETE no action,
144
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
145
-);
146
-CREATE TABLE `repositories` (
147
- `id` text PRIMARY KEY NOT NULL,
148
- `name` text NOT NULL,
149
- `slug` text NOT NULL,
150
- `description` text,
151
- `owner_id` text NOT NULL,
152
- `owner_type` text DEFAULT 'user' NOT NULL,
153
- `visibility` text DEFAULT 'public' NOT NULL,
154
- `default_branch` text DEFAULT 'main' NOT NULL,
155
- `disk_path` text NOT NULL,
156
- `ssh_clone_url` text,
157
- `http_clone_url` text,
158
- `star_count` integer DEFAULT 0,
159
- `fork_count` integer DEFAULT 0,
160
- `watch_count` integer DEFAULT 0,
161
- `open_issue_count` integer DEFAULT 0,
162
- `open_pr_count` integer DEFAULT 0,
163
- `size` integer DEFAULT 0,
164
- `is_fork` integer DEFAULT false,
165
- `forked_from_id` text,
166
- `is_archived` integer DEFAULT false,
167
- `is_mirror` integer DEFAULT false,
168
- `mirror_url` text,
169
- `has_issues` integer DEFAULT true,
170
- `has_wiki` integer DEFAULT true,
171
- `has_actions` integer DEFAULT true,
172
- `allow_forking` integer DEFAULT true,
173
- `license_type` text,
174
- `topics` text,
175
- `language` text,
176
- `languages` text,
177
- `last_activity_at` text,
178
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
179
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
180
- FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
181
- FOREIGN KEY (`forked_from_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action
182
-);
183
-CREATE TABLE `repository_collaborators` (
184
- `id` text PRIMARY KEY NOT NULL,
185
- `repository_id` text NOT NULL,
186
- `user_id` text NOT NULL,
187
- `role` text DEFAULT 'developer' NOT NULL,
188
- `added_by_id` text,
189
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
190
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
191
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
192
- FOREIGN KEY (`added_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
193
-);
194
-CREATE TABLE `repository_stars` (
195
- `id` text PRIMARY KEY NOT NULL,
196
- `repository_id` text NOT NULL,
197
- `user_id` text NOT NULL,
198
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
199
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
200
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
201
-);
202
-CREATE TABLE `repository_watchers` (
203
- `id` text PRIMARY KEY NOT NULL,
204
- `repository_id` text NOT NULL,
205
- `user_id` text NOT NULL,
206
- `watch_level` text DEFAULT 'watching' NOT NULL,
207
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
208
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
209
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
210
-);
211
-CREATE TABLE `tags` (
212
- `id` text PRIMARY KEY NOT NULL,
213
- `repository_id` text NOT NULL,
214
- `name` text NOT NULL,
215
- `commit_sha` text NOT NULL,
216
- `message` text,
217
- `tagger_name` text,
218
- `tagger_email` text,
219
- `tagged_at` text,
220
- `is_release` integer DEFAULT false,
221
- `release_id` text,
222
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
223
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
224
-);
225
-CREATE TABLE `webhooks` (
226
- `id` text PRIMARY KEY NOT NULL,
227
- `repository_id` text NOT NULL,
228
- `url` text NOT NULL,
229
- `secret` text,
230
- `events` text NOT NULL,
231
- `is_active` integer DEFAULT true,
232
- `last_delivery_at` text,
233
- `last_delivery_status` integer,
234
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
235
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
236
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
237
-);
238
-CREATE TABLE `issue_assignees` (
239
- `id` text PRIMARY KEY NOT NULL,
240
- `issue_id` text NOT NULL,
241
- `user_id` text NOT NULL,
242
- `assigned_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
243
- FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
244
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
245
-);
246
-CREATE TABLE `issue_comments` (
247
- `id` text PRIMARY KEY NOT NULL,
248
- `issue_id` text NOT NULL,
249
- `author_id` text NOT NULL,
250
- `body` text NOT NULL,
251
- `reactions` text,
252
- `is_edited` integer DEFAULT false,
253
- `edited_at` text,
254
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
255
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
256
- FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
257
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
258
-);
259
-CREATE TABLE `issue_labels` (
260
- `id` text PRIMARY KEY NOT NULL,
261
- `issue_id` text NOT NULL,
262
- `label_id` text NOT NULL,
263
- FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
264
- FOREIGN KEY (`label_id`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE cascade
265
-);
266
-CREATE TABLE `issue_subscribers` (
267
- `id` text PRIMARY KEY NOT NULL,
268
- `issue_id` text NOT NULL,
269
- `user_id` text NOT NULL,
270
- `subscribed_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
271
- FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
272
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
273
-);
274
-CREATE TABLE `issues` (
275
- `id` text PRIMARY KEY NOT NULL,
276
- `repository_id` text NOT NULL,
277
- `number` integer NOT NULL,
278
- `title` text NOT NULL,
279
- `body` text,
280
- `state` text DEFAULT 'open' NOT NULL,
281
- `author_id` text NOT NULL,
282
- `assignee_id` text,
283
- `milestone_id` text,
284
- `is_pinned` integer DEFAULT false,
285
- `is_locked` integer DEFAULT false,
286
- `lock_reason` text,
287
- `comment_count` integer DEFAULT 0,
288
- `reactions` text,
289
- `closed_at` text,
290
- `closed_by_id` text,
291
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
292
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
293
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
294
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
295
- FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
296
- FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON UPDATE no action ON DELETE no action,
297
- FOREIGN KEY (`closed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
298
-);
299
-CREATE TABLE `labels` (
300
- `id` text PRIMARY KEY NOT NULL,
301
- `repository_id` text NOT NULL,
302
- `name` text NOT NULL,
303
- `color` text DEFAULT '#6b7280' NOT NULL,
304
- `description` text,
305
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
306
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
307
-);
308
-CREATE TABLE `milestones` (
309
- `id` text PRIMARY KEY NOT NULL,
310
- `repository_id` text NOT NULL,
311
- `title` text NOT NULL,
312
- `description` text,
313
- `state` text DEFAULT 'open' NOT NULL,
314
- `due_date` text,
315
- `open_issue_count` integer DEFAULT 0,
316
- `closed_issue_count` integer DEFAULT 0,
317
- `closed_at` text,
318
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
319
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
320
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
321
-);
322
-CREATE TABLE `pull_request_assignees` (
323
- `id` text PRIMARY KEY NOT NULL,
324
- `pull_request_id` text NOT NULL,
325
- `user_id` text NOT NULL,
326
- `assigned_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
327
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
328
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
329
-);
330
-CREATE TABLE `pull_request_checks` (
331
- `id` text PRIMARY KEY NOT NULL,
332
- `pull_request_id` text NOT NULL,
333
- `name` text NOT NULL,
334
- `status` text NOT NULL,
335
- `conclusion` text,
336
- `head_sha` text NOT NULL,
337
- `external_id` text,
338
- `details_url` text,
339
- `output` text,
340
- `started_at` text,
341
- `completed_at` text,
342
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
343
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
344
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade
345
-);
346
-CREATE TABLE `pull_request_comments` (
347
- `id` text PRIMARY KEY NOT NULL,
348
- `pull_request_id` text NOT NULL,
349
- `review_id` text,
350
- `author_id` text NOT NULL,
351
- `body` text NOT NULL,
352
- `path` text,
353
- `line` integer,
354
- `side` text,
355
- `start_line` integer,
356
- `start_side` text,
357
- `commit_sha` text,
358
- `original_commit_sha` text,
359
- `original_line` integer,
360
- `in_reply_to_id` text,
361
- `reactions` text,
362
- `is_resolved` integer DEFAULT false,
363
- `resolved_by_id` text,
364
- `resolved_at` text,
365
- `is_edited` integer DEFAULT false,
366
- `edited_at` text,
367
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
368
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
369
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
370
- FOREIGN KEY (`review_id`) REFERENCES `pull_request_reviews`(`id`) ON UPDATE no action ON DELETE no action,
371
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
372
- FOREIGN KEY (`in_reply_to_id`) REFERENCES `pull_request_comments`(`id`) ON UPDATE no action ON DELETE no action,
373
- FOREIGN KEY (`resolved_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
374
-);
375
-CREATE TABLE `pull_request_labels` (
376
- `id` text PRIMARY KEY NOT NULL,
377
- `pull_request_id` text NOT NULL,
378
- `label_id` text NOT NULL,
379
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
380
- FOREIGN KEY (`label_id`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE cascade
381
-);
382
-CREATE TABLE `pull_request_reviewers` (
383
- `id` text PRIMARY KEY NOT NULL,
384
- `pull_request_id` text NOT NULL,
385
- `user_id` text NOT NULL,
386
- `is_required` integer DEFAULT false,
387
- `requested_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
388
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
389
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
390
-);
391
-CREATE TABLE `pull_request_reviews` (
392
- `id` text PRIMARY KEY NOT NULL,
393
- `pull_request_id` text NOT NULL,
394
- `reviewer_id` text NOT NULL,
395
- `state` text NOT NULL,
396
- `body` text,
397
- `commit_sha` text,
398
- `submitted_at` text,
399
- `dismissed_at` text,
400
- `dismissed_by_id` text,
401
- `dismissal_reason` text,
402
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
403
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
404
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
405
- FOREIGN KEY (`reviewer_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
406
- FOREIGN KEY (`dismissed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
407
-);
408
-CREATE TABLE `pull_requests` (
409
- `id` text PRIMARY KEY NOT NULL,
410
- `repository_id` text NOT NULL,
411
- `number` integer NOT NULL,
412
- `title` text NOT NULL,
413
- `body` text,
414
- `state` text DEFAULT 'open' NOT NULL,
415
- `author_id` text NOT NULL,
416
- `assignee_id` text,
417
- `milestone_id` text,
418
- `head_branch` text NOT NULL,
419
- `head_sha` text NOT NULL,
420
- `head_repository_id` text,
421
- `base_branch` text NOT NULL,
422
- `base_sha` text NOT NULL,
423
- `is_draft` integer DEFAULT false,
424
- `is_merged` integer DEFAULT false,
425
- `merged_at` text,
426
- `merged_by_id` text,
427
- `merge_sha` text,
428
- `merge_method` text,
429
- `additions` integer DEFAULT 0,
430
- `deletions` integer DEFAULT 0,
431
- `changed_files` integer DEFAULT 0,
432
- `comment_count` integer DEFAULT 0,
433
- `review_count` integer DEFAULT 0,
434
- `mergeable` integer,
435
- `mergeable_state` text,
436
- `rebaseable` integer,
437
- `maintainer_can_modify` integer DEFAULT true,
438
- `allow_auto_merge` integer DEFAULT false,
439
- `auto_merge_method` text,
440
- `closed_at` text,
441
- `closed_by_id` text,
442
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
443
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
444
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
445
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
446
- FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
447
- FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON UPDATE no action ON DELETE no action,
448
- FOREIGN KEY (`head_repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action,
449
- FOREIGN KEY (`merged_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
450
- FOREIGN KEY (`closed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
451
-);
452
-CREATE TABLE `scheduled_workflows` (
453
- `id` text PRIMARY KEY NOT NULL,
454
- `workflow_id` text NOT NULL,
455
- `cron_expression` text NOT NULL,
456
- `timezone` text DEFAULT 'UTC',
457
- `is_enabled` integer DEFAULT true,
458
- `last_run_at` text,
459
- `next_run_at` text,
460
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
461
- FOREIGN KEY (`workflow_id`) REFERENCES `workflows`(`id`) ON UPDATE no action ON DELETE cascade
462
-);
463
-CREATE TABLE `workflow_artifacts` (
464
- `id` text PRIMARY KEY NOT NULL,
465
- `run_id` text NOT NULL,
466
- `job_id` text,
467
- `name` text NOT NULL,
468
- `size_bytes` integer NOT NULL,
469
- `storage_path` text NOT NULL,
470
- `mime_type` text,
471
- `download_count` integer DEFAULT 0,
472
- `expires_at` text,
473
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
474
- FOREIGN KEY (`run_id`) REFERENCES `workflow_runs`(`id`) ON UPDATE no action ON DELETE cascade,
475
- FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE no action
476
-);
477
-CREATE TABLE `workflow_jobs` (
478
- `id` text PRIMARY KEY NOT NULL,
479
- `run_id` text NOT NULL,
480
- `name` text NOT NULL,
481
- `status` text DEFAULT 'queued' NOT NULL,
482
- `conclusion` text,
483
- `runner_id` text,
484
- `runner_name` text,
485
- `runner_group_id` text,
486
- `runner_group_name` text,
487
- `container_id` text,
488
- `container_image` text,
489
- `needs` text,
490
- `environment` text,
491
- `environment_url` text,
492
- `matrix` text,
493
- `started_at` text,
494
- `completed_at` text,
495
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
496
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
497
- FOREIGN KEY (`run_id`) REFERENCES `workflow_runs`(`id`) ON UPDATE no action ON DELETE cascade
498
-);
499
-CREATE TABLE `workflow_logs` (
500
- `id` text PRIMARY KEY NOT NULL,
501
- `job_id` text NOT NULL,
502
- `step_id` text,
503
- `log_level` text DEFAULT 'info' NOT NULL,
504
- `message` text NOT NULL,
505
- `timestamp` text NOT NULL,
506
- `line_number` integer,
507
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
508
- FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE cascade,
509
- FOREIGN KEY (`step_id`) REFERENCES `workflow_steps`(`id`) ON UPDATE no action ON DELETE no action
510
-);
511
-CREATE TABLE `workflow_runs` (
512
- `id` text PRIMARY KEY NOT NULL,
513
- `workflow_id` text NOT NULL,
514
- `repository_id` text NOT NULL,
515
- `run_number` integer NOT NULL,
516
- `run_attempt` integer DEFAULT 1 NOT NULL,
517
- `name` text NOT NULL,
518
- `display_title` text,
519
- `status` text DEFAULT 'queued' NOT NULL,
520
- `conclusion` text,
521
- `event` text NOT NULL,
522
- `head_branch` text,
523
- `head_sha` text NOT NULL,
524
- `base_branch` text,
525
- `base_sha` text,
526
- `pull_request_id` text,
527
- `triggered_by_id` text,
528
- `started_at` text,
529
- `completed_at` text,
530
- `workflow_config` text,
531
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
532
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
533
- FOREIGN KEY (`workflow_id`) REFERENCES `workflows`(`id`) ON UPDATE no action ON DELETE cascade,
534
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
535
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE no action,
536
- FOREIGN KEY (`triggered_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
537
-);
538
-CREATE TABLE `workflow_secrets` (
539
- `id` text PRIMARY KEY NOT NULL,
540
- `repository_id` text NOT NULL,
541
- `name` text NOT NULL,
542
- `encrypted_value` text NOT NULL,
543
- `environment` text,
544
- `created_by_id` text,
545
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
546
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
547
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
548
- FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
549
-);
550
-CREATE TABLE `workflow_steps` (
551
- `id` text PRIMARY KEY NOT NULL,
552
- `job_id` text NOT NULL,
553
- `number` integer NOT NULL,
554
- `name` text NOT NULL,
555
- `status` text DEFAULT 'queued' NOT NULL,
556
- `conclusion` text,
557
- `uses` text,
558
- `run` text,
559
- `shell` text,
560
- `working_directory` text,
561
- `env` text,
562
- `with` text,
563
- `if` text,
564
- `continue_on_error` integer DEFAULT false,
565
- `timeout_minutes` integer,
566
- `started_at` text,
567
- `completed_at` text,
568
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
569
- FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE cascade
570
-);
571
-CREATE TABLE `workflow_variables` (
572
- `id` text PRIMARY KEY NOT NULL,
573
- `repository_id` text NOT NULL,
574
- `name` text NOT NULL,
575
- `value` text NOT NULL,
576
- `environment` text,
577
- `created_by_id` text,
578
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
579
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
580
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
581
- FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
582
-);
583
-CREATE TABLE `workflows` (
584
- `id` text PRIMARY KEY NOT NULL,
585
- `repository_id` text NOT NULL,
586
- `name` text NOT NULL,
587
- `path` text NOT NULL,
588
- `state` text DEFAULT 'active' NOT NULL,
589
- `badge_url` text,
590
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
591
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
592
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
593
-);
594
-CREATE TABLE `wiki_pages` (
595
- `id` text PRIMARY KEY NOT NULL,
596
- `repository_id` text NOT NULL,
597
- `slug` text NOT NULL,
598
- `title` text NOT NULL,
599
- `content` text NOT NULL,
600
- `format` text DEFAULT 'markdown' NOT NULL,
601
- `parent_id` text,
602
- `order` integer DEFAULT 0,
603
- `last_editor_id` text,
604
- `view_count` integer DEFAULT 0,
605
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
606
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
607
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
608
- FOREIGN KEY (`parent_id`) REFERENCES `wiki_pages`(`id`) ON UPDATE no action ON DELETE no action,
609
- FOREIGN KEY (`last_editor_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
610
-);
611
-CREATE TABLE `wiki_revisions` (
612
- `id` text PRIMARY KEY NOT NULL,
613
- `page_id` text NOT NULL,
614
- `content` text NOT NULL,
615
- `message` text,
616
- `author_id` text NOT NULL,
617
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
618
- FOREIGN KEY (`page_id`) REFERENCES `wiki_pages`(`id`) ON UPDATE no action ON DELETE cascade,
619
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
620
-);
621
-CREATE TABLE `organization_members` (
622
- `id` text PRIMARY KEY NOT NULL,
623
- `organization_id` text NOT NULL,
624
- `user_id` text NOT NULL,
625
- `role` text DEFAULT 'member' NOT NULL,
626
- `joined_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
627
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
628
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
629
-);
630
-CREATE TABLE `organizations` (
631
- `id` text PRIMARY KEY NOT NULL,
632
- `name` text NOT NULL,
633
- `display_name` text,
634
- `description` text,
635
- `avatar_url` text,
636
- `website` text,
637
- `location` text,
638
- `email` text,
639
- `is_verified` integer DEFAULT false,
640
- `visibility` text DEFAULT 'public' NOT NULL,
641
- `default_repo_permission` text DEFAULT 'read',
642
- `members_can_create_repos` integer DEFAULT true,
643
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
644
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL
645
-);
646
-CREATE UNIQUE INDEX `organizations_name_unique` ON `organizations` (`name`);--> statement-breakpoint
647
-CREATE TABLE `team_members` (
648
- `id` text PRIMARY KEY NOT NULL,
649
- `team_id` text NOT NULL,
650
- `user_id` text NOT NULL,
651
- `role` text DEFAULT 'member' NOT NULL,
652
- `joined_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
653
- FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE cascade,
654
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
655
-);
656
-CREATE TABLE `team_repositories` (
657
- `id` text PRIMARY KEY NOT NULL,
658
- `team_id` text NOT NULL,
659
- `repository_id` text NOT NULL,
660
- `permission` text DEFAULT 'read' NOT NULL,
661
- `added_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
662
- FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE cascade,
663
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
664
-);
665
-CREATE TABLE `teams` (
666
- `id` text PRIMARY KEY NOT NULL,
667
- `organization_id` text NOT NULL,
668
- `name` text NOT NULL,
669
- `slug` text NOT NULL,
670
- `description` text,
671
- `privacy` text DEFAULT 'visible' NOT NULL,
672
- `parent_id` text,
673
- `permission` text DEFAULT 'read' NOT NULL,
674
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
675
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
676
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
677
- FOREIGN KEY (`parent_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE no action
678
-);
679
-CREATE TABLE `activities` (
680
- `id` text PRIMARY KEY NOT NULL,
681
- `user_id` text NOT NULL,
682
- `repository_id` text,
683
- `organization_id` text,
684
- `type` text NOT NULL,
685
- `action` text NOT NULL,
686
- `ref_type` text,
687
- `ref_name` text,
688
- `target_type` text,
689
- `target_id` text,
690
- `payload` text,
691
- `is_public` integer DEFAULT true,
692
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
693
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
694
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
695
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action
696
-);
697
-CREATE TABLE `audit_logs` (
698
- `id` text PRIMARY KEY NOT NULL,
699
- `user_id` text,
700
- `organization_id` text,
701
- `repository_id` text,
702
- `action` text NOT NULL,
703
- `actor_type` text DEFAULT 'user' NOT NULL,
704
- `actor_id` text,
705
- `actor_ip` text,
706
- `actor_user_agent` text,
707
- `target_type` text,
708
- `target_id` text,
709
- `target_name` text,
710
- `data` text,
711
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
712
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
713
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action,
714
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action
715
-);
716
-CREATE TABLE `notifications` (
717
- `id` text PRIMARY KEY NOT NULL,
718
- `user_id` text NOT NULL,
719
- `repository_id` text,
720
- `type` text NOT NULL,
721
- `title` text NOT NULL,
722
- `body` text,
723
- `url` text,
724
- `actor_id` text,
725
- `subject_type` text,
726
- `subject_id` text,
727
- `reason` text NOT NULL,
728
- `is_read` integer DEFAULT false,
729
- `read_at` text,
730
- `is_archived` integer DEFAULT false,
731
- `archived_at` text,
732
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
733
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
734
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
735
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action,
736
- FOREIGN KEY (`actor_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
737
-);
738
-CREATE TABLE `security_scans` (
739
- `id` text PRIMARY KEY NOT NULL,
740
- `repository_id` text NOT NULL,
741
- `status` text DEFAULT 'queued' NOT NULL,
742
- `started_at` integer,
743
- `completed_at` integer,
744
- `critical_count` integer DEFAULT 0,
745
- `high_count` integer DEFAULT 0,
746
- `medium_count` integer DEFAULT 0,
747
- `low_count` integer DEFAULT 0,
748
- `unknown_count` integer DEFAULT 0,
749
- `logs` text,
750
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
751
-);
752
-CREATE TABLE `security_vulnerabilities` (
753
- `id` text PRIMARY KEY NOT NULL,
754
- `scan_id` text NOT NULL,
755
- `vulnerability_id` text NOT NULL,
756
- `pkg_name` text NOT NULL,
757
- `installed_version` text,
758
- `fixed_version` text,
759
- `severity` text NOT NULL,
760
- `title` text,
761
- `description` text,
762
- `target` text,
763
- `class` text,
764
- FOREIGN KEY (`scan_id`) REFERENCES `security_scans`(`id`) ON UPDATE no action ON DELETE cascade
765
-);
766
-CREATE TABLE `pipeline_runners` (
767
- `id` text PRIMARY KEY NOT NULL,
768
- `repository_id` text,
769
- `token` text NOT NULL,
770
- `name` text NOT NULL,
771
- `os` text,
772
- `arch` text,
773
- `version` text,
774
- `status` text DEFAULT 'offline' NOT NULL,
775
- `last_seen_at` text,
776
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
777
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
778
-);
779
-CREATE TABLE `pr_stack_entries` (
780
- `id` text PRIMARY KEY NOT NULL,
781
- `stack_id` text NOT NULL,
782
- `pull_request_id` text NOT NULL,
783
- `stack_order` integer NOT NULL,
784
- `parent_pr_id` text,
785
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
786
- FOREIGN KEY (`stack_id`) REFERENCES `pr_stacks`(`id`) ON UPDATE no action ON DELETE cascade,
787
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
788
- FOREIGN KEY (`parent_pr_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE no action
789
-);
790
-CREATE TABLE `pr_stacks` (
791
- `id` text PRIMARY KEY NOT NULL,
792
- `repository_id` text NOT NULL,
793
- `name` text,
794
- `base_branch` text NOT NULL,
795
- `status` text DEFAULT 'active' NOT NULL,
796
- `created_by_id` text NOT NULL,
797
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
798
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
799
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
800
- FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
801
-);
802
-CREATE TABLE `merge_queue` (
803
- `id` text PRIMARY KEY NOT NULL,
804
- `repository_id` text NOT NULL,
805
- `pull_request_id` text NOT NULL,
806
- `stack_id` text,
807
- `status` text DEFAULT 'pending' NOT NULL,
808
- `priority` integer DEFAULT 0 NOT NULL,
809
- `position` integer,
810
- `ci_status` text DEFAULT 'pending',
811
- `ci_run_id` text,
812
- `added_by_id` text NOT NULL,
813
- `added_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
814
- `estimated_merge_at` text,
815
- `started_at` text,
816
- `completed_at` text,
817
- `failure_reason` text,
818
- `retry_count` integer DEFAULT 0,
819
- `merge_method` text DEFAULT 'merge',
820
- `delete_on_merge` integer DEFAULT true,
821
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
822
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
823
- FOREIGN KEY (`stack_id`) REFERENCES `pr_stacks`(`id`) ON UPDATE no action ON DELETE no action,
824
- FOREIGN KEY (`added_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
825
-);
826
-CREATE TABLE `ai_review_suggestions` (
827
- `id` text PRIMARY KEY NOT NULL,
828
- `ai_review_id` text NOT NULL,
829
- `path` text NOT NULL,
830
- `line` integer,
831
- `end_line` integer,
832
- `severity` text NOT NULL,
833
- `type` text NOT NULL,
834
- `category` text,
835
- `title` text NOT NULL,
836
- `message` text NOT NULL,
837
- `suggested_fix` text,
838
- `explanation` text,
839
- `is_applied` integer DEFAULT false,
840
- `is_dismissed` integer DEFAULT false,
841
- `applied_at` text,
842
- `applied_by_id` text,
843
- `dismissed_at` text,
844
- `dismissed_by_id` text,
845
- `dismiss_reason` text,
846
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
847
- FOREIGN KEY (`ai_review_id`) REFERENCES `ai_reviews`(`id`) ON UPDATE no action ON DELETE cascade,
848
- FOREIGN KEY (`applied_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
849
- FOREIGN KEY (`dismissed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
850
-);
851
-CREATE TABLE `ai_reviews` (
852
- `id` text PRIMARY KEY NOT NULL,
853
- `pull_request_id` text NOT NULL,
854
- `status` text DEFAULT 'pending' NOT NULL,
855
- `model` text NOT NULL,
856
- `provider` text NOT NULL,
857
- `stack_context` text,
858
- `includes_stack_context` integer DEFAULT false,
859
- `summary` text,
860
- `overall_severity` text,
861
- `suggestions_count` integer DEFAULT 0,
862
- `tokens_used` integer,
863
- `prompt_tokens` integer,
864
- `completion_tokens` integer,
865
- `cost_cents` integer,
866
- `raw_response` text,
867
- `triggered_by_id` text,
868
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
869
- `started_at` text,
870
- `completed_at` text,
871
- `error_message` text,
872
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
873
- FOREIGN KEY (`triggered_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
874
-);
875
-CREATE TABLE `pr_metrics` (
876
- `id` text PRIMARY KEY NOT NULL,
877
- `pull_request_id` text NOT NULL,
878
- `repository_id` text NOT NULL,
879
- `author_id` text NOT NULL,
880
- `time_to_first_review` integer,
881
- `time_to_approval` integer,
882
- `time_to_merge` integer,
883
- `total_cycle_time` integer,
884
- `review_rounds` integer DEFAULT 1,
885
- `reviewers_count` integer DEFAULT 0,
886
- `comments_count` integer DEFAULT 0,
887
- `changes_requested_count` integer DEFAULT 0,
888
- `lines_added` integer DEFAULT 0,
889
- `lines_removed` integer DEFAULT 0,
890
- `files_changed` integer DEFAULT 0,
891
- `commits` integer DEFAULT 1,
892
- `is_stacked` integer DEFAULT false,
893
- `stack_position` integer,
894
- `pr_created_at` text,
895
- `first_review_at` text,
896
- `approved_at` text,
897
- `merged_at` text,
898
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
899
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
900
- FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
901
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
902
- FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
903
-);
904
-CREATE TABLE `repo_metrics` (
905
- `id` text PRIMARY KEY NOT NULL,
906
- `repository_id` text NOT NULL,
907
- `week_of` text NOT NULL,
908
- `prs_opened` integer DEFAULT 0,
909
- `prs_merged` integer DEFAULT 0,
910
- `prs_closed` integer DEFAULT 0,
911
- `avg_time_to_first_review` integer,
912
- `avg_time_to_merge` integer,
913
- `avg_review_rounds` integer,
914
- `avg_lines_changed` integer,
915
- `avg_files_changed` integer,
916
- `stacked_prs` integer DEFAULT 0,
917
- `stacked_prs_percentage` integer DEFAULT 0,
918
- `active_authors` integer DEFAULT 0,
919
- `active_reviewers` integer DEFAULT 0,
920
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
921
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
922
-);
923
-CREATE TABLE `review_metrics` (
924
- `id` text PRIMARY KEY NOT NULL,
925
- `user_id` text NOT NULL,
926
- `repository_id` text,
927
- `organization_id` text,
928
- `week_of` text NOT NULL,
929
- `prs_authored` integer DEFAULT 0,
930
- `prs_authored_merged` integer DEFAULT 0,
931
- `avg_time_to_merge_authored` integer,
932
- `lines_authored_added` integer DEFAULT 0,
933
- `lines_authored_removed` integer DEFAULT 0,
934
- `prs_reviewed` integer DEFAULT 0,
935
- `avg_time_to_review` integer,
936
- `comments_given` integer DEFAULT 0,
937
- `approvals_given` integer DEFAULT 0,
938
- `changes_requested_given` integer DEFAULT 0,
939
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
940
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
941
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
942
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
943
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action
944
-);
945
-CREATE TABLE `slack_channel_mappings` (
946
- `id` text PRIMARY KEY NOT NULL,
947
- `workspace_id` text NOT NULL,
948
- `repository_id` text NOT NULL,
949
- `channel_id` text NOT NULL,
950
- `channel_name` text,
951
- `notify_on` text DEFAULT '["pr_created","pr_merged","review_requested","ci_failed"]',
952
- `notify_branches` text,
953
- `notify_authors` text,
954
- `is_active` integer DEFAULT true,
955
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
956
- `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
957
- FOREIGN KEY (`workspace_id`) REFERENCES `slack_workspaces`(`id`) ON UPDATE no action ON DELETE cascade,
958
- FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
959
-);
960
-CREATE TABLE `slack_user_mappings` (
961
- `id` text PRIMARY KEY NOT NULL,
962
- `user_id` text NOT NULL,
963
- `workspace_id` text NOT NULL,
964
- `slack_user_id` text NOT NULL,
965
- `slack_username` text,
966
- `dm_preferences` text DEFAULT '{"review_requested":true,"pr_approved":true,"ci_failed":true}',
967
- `dnd_enabled` integer DEFAULT false,
968
- `dnd_start` text,
969
- `dnd_end` text,
970
- `dnd_timezone` text,
971
- `is_active` integer DEFAULT true,
972
- `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
973
- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
974
- FOREIGN KEY (`workspace_id`) REFERENCES `slack_workspaces`(`id`) ON UPDATE no action ON DELETE cascade
975
-);
976
-CREATE TABLE `slack_workspaces` (
977
- `id` text PRIMARY KEY NOT NULL,
978
- `organization_id` text,
979
- `team_id` text NOT NULL,
980
- `team_name` text,
981
- `team_domain` text,
982
- `access_token` text NOT NULL,
983
- `bot_user_id` text,
984
- `bot_access_token` text,
985
- `scopes` text,
986
- `installed_by_id` text NOT NULL,
987
- `installed_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
988
- `is_active` integer DEFAULT true,
989
- `last_used_at` text,
990
- FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
991
- FOREIGN KEY (`installed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
992
-);
993
0
▼drizzle/0001_faithful_randall.sql
+0-124
1
-CREATE TABLE "project_cards" (
2
- "id" text PRIMARY KEY NOT NULL,
3
- "column_id" text NOT NULL,
4
- "content_id" text,
5
- "content_type" text,
6
- "note" text,
7
- "order" integer NOT NULL,
8
- "created_at" timestamp DEFAULT now() NOT NULL,
9
- "updated_at" timestamp DEFAULT now() NOT NULL,
10
- "creator_id" text NOT NULL
11
-);
12
-CREATE TABLE "project_columns" (
13
- "id" text PRIMARY KEY NOT NULL,
14
- "project_id" text NOT NULL,
15
- "name" text NOT NULL,
16
- "order" integer NOT NULL,
17
- "created_at" timestamp DEFAULT now() NOT NULL,
18
- "updated_at" timestamp DEFAULT now() NOT NULL
19
-);
20
-CREATE TABLE "projects" (
21
- "id" text PRIMARY KEY NOT NULL,
22
- "repository_id" text NOT NULL,
23
- "name" text NOT NULL,
24
- "description" text,
25
- "number" integer NOT NULL,
26
- "created_at" timestamp DEFAULT now() NOT NULL,
27
- "updated_at" timestamp DEFAULT now() NOT NULL,
28
- "creator_id" text NOT NULL
29
-);
30
-CREATE TABLE "merge_queue" (
31
- "id" text PRIMARY KEY NOT NULL,
32
- "repository_id" text NOT NULL,
33
- "pull_request_id" text NOT NULL,
34
- "stack_id" text,
35
- "status" text DEFAULT 'pending' NOT NULL,
36
- "priority" integer DEFAULT 0,
37
- "position" integer DEFAULT 0,
38
- "ci_status" text DEFAULT 'pending',
39
- "merge_method" text DEFAULT 'merge',
40
- "delete_on_merge" boolean DEFAULT true,
41
- "execution_branch" text,
42
- "attempt_count" integer DEFAULT 0,
43
- "last_attempt_at" timestamp,
44
- "added_by_id" text,
45
- "added_at" timestamp DEFAULT now() NOT NULL,
46
- "queued_at" timestamp DEFAULT now() NOT NULL,
47
- "started_at" timestamp,
48
- "completed_at" timestamp,
49
- "failure_reason" text
50
-);
51
-CREATE TABLE "merge_queue_speculative_runs" (
52
- "id" text PRIMARY KEY NOT NULL,
53
- "repository_id" text NOT NULL,
54
- "pull_request_ids" text NOT NULL,
55
- "branch_name" text NOT NULL,
56
- "base_branch" text NOT NULL,
57
- "status" text DEFAULT 'pending' NOT NULL,
58
- "ci_url" text,
59
- "created_at" timestamp DEFAULT now() NOT NULL,
60
- "started_at" timestamp,
61
- "completed_at" timestamp,
62
- "failure_reason" text,
63
- "commit_sha" text
64
-);
65
-CREATE TABLE "sso_configs" (
66
- "id" text PRIMARY KEY NOT NULL,
67
- "name" text NOT NULL,
68
- "type" text DEFAULT 'oidc' NOT NULL,
69
- "issuer" text NOT NULL,
70
- "client_id" text NOT NULL,
71
- "client_secret" text NOT NULL,
72
- "scopes" text DEFAULT 'openid,profile,email',
73
- "authorization_url" text,
74
- "token_url" text,
75
- "userinfo_url" text,
76
- "jwks_uri" text,
77
- "enabled" boolean DEFAULT false,
78
- "auto_create_users" boolean DEFAULT true,
79
- "allowed_domains" text,
80
- "default_role" text DEFAULT 'member',
81
- "organization_id" text,
82
- "created_at" timestamp DEFAULT now() NOT NULL,
83
- "updated_at" timestamp DEFAULT now() NOT NULL,
84
- "created_by_id" text
85
-);
86
-ALTER TABLE "scheduled_workflows" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
87
-ALTER TABLE "workflow_artifacts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
88
-ALTER TABLE "workflow_jobs" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
89
-ALTER TABLE "workflow_logs" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
90
-ALTER TABLE "workflow_runs" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
91
-ALTER TABLE "workflow_secrets" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
92
-ALTER TABLE "workflow_steps" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
93
-ALTER TABLE "workflow_variables" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
94
-ALTER TABLE "workflows" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
95
-ALTER TABLE "merge_queue_items" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
96
-DROP TABLE "scheduled_workflows" CASCADE;--> statement-breakpoint
97
-DROP TABLE "workflow_artifacts" CASCADE;--> statement-breakpoint
98
-DROP TABLE "workflow_jobs" CASCADE;--> statement-breakpoint
99
-DROP TABLE "workflow_logs" CASCADE;--> statement-breakpoint
100
-DROP TABLE "workflow_runs" CASCADE;--> statement-breakpoint
101
-DROP TABLE "workflow_secrets" CASCADE;--> statement-breakpoint
102
-DROP TABLE "workflow_steps" CASCADE;--> statement-breakpoint
103
-DROP TABLE "workflow_variables" CASCADE;--> statement-breakpoint
104
-DROP TABLE "workflows" CASCADE;--> statement-breakpoint
105
-DROP TABLE "merge_queue_items" CASCADE;--> statement-breakpoint
106
-ALTER TABLE "pull_request_comments" ADD COLUMN "suggestion_content" text;--> statement-breakpoint
107
-ALTER TABLE "pull_request_comments" ADD COLUMN "suggestion_applied" boolean DEFAULT false;--> statement-breakpoint
108
-ALTER TABLE "pull_request_comments" ADD COLUMN "suggestion_applied_by_id" text;--> statement-breakpoint
109
-ALTER TABLE "pull_request_comments" ADD COLUMN "suggestion_applied_at" timestamp;--> statement-breakpoint
110
-ALTER TABLE "pull_request_comments" ADD COLUMN "suggestion_commit_sha" text;--> statement-breakpoint
111
-ALTER TABLE "webhooks" ADD COLUMN "provider" text DEFAULT 'generic';--> statement-breakpoint
112
-ALTER TABLE "webhooks" ADD COLUMN "name" text;--> statement-breakpoint
113
-ALTER TABLE "webhooks" ADD COLUMN "enabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint
114
-ALTER TABLE "project_cards" ADD CONSTRAINT "project_cards_column_id_project_columns_id_fk" FOREIGN KEY ("column_id") REFERENCES "public"."project_columns"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
115
-ALTER TABLE "project_cards" ADD CONSTRAINT "project_cards_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
116
-ALTER TABLE "project_columns" ADD CONSTRAINT "project_columns_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
117
-ALTER TABLE "projects" ADD CONSTRAINT "projects_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
118
-ALTER TABLE "projects" ADD CONSTRAINT "projects_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
119
-ALTER TABLE "merge_queue" ADD CONSTRAINT "merge_queue_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
120
-ALTER TABLE "merge_queue" ADD CONSTRAINT "merge_queue_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
121
-ALTER TABLE "merge_queue" ADD CONSTRAINT "merge_queue_added_by_id_users_id_fk" FOREIGN KEY ("added_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
122
-ALTER TABLE "merge_queue_speculative_runs" ADD CONSTRAINT "merge_queue_speculative_runs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
123
-ALTER TABLE "sso_configs" ADD CONSTRAINT "sso_configs_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
124
-ALTER TABLE "pull_request_comments" ADD CONSTRAINT "pull_request_comments_suggestion_applied_by_id_users_id_fk" FOREIGN KEY ("suggestion_applied_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
125
0
▼drizzle/0002_third_cable.sql
+0-144
1
-CREATE TABLE "scheduled_workflows" (
2
- "id" text PRIMARY KEY NOT NULL,
3
- "workflow_id" text NOT NULL,
4
- "cron_expression" text NOT NULL,
5
- "timezone" text DEFAULT 'UTC',
6
- "is_enabled" boolean DEFAULT true,
7
- "last_run_at" timestamp,
8
- "next_run_at" timestamp,
9
- "created_at" timestamp DEFAULT now() NOT NULL
10
-);
11
-CREATE TABLE "workflow_artifacts" (
12
- "id" text PRIMARY KEY NOT NULL,
13
- "run_id" text NOT NULL,
14
- "job_id" text,
15
- "name" text NOT NULL,
16
- "size_bytes" integer NOT NULL,
17
- "storage_path" text NOT NULL,
18
- "mime_type" text,
19
- "download_count" integer DEFAULT 0,
20
- "expires_at" timestamp,
21
- "created_at" timestamp DEFAULT now() NOT NULL
22
-);
23
-CREATE TABLE "workflow_jobs" (
24
- "id" text PRIMARY KEY NOT NULL,
25
- "run_id" text NOT NULL,
26
- "name" text NOT NULL,
27
- "status" text DEFAULT 'queued' NOT NULL,
28
- "conclusion" text,
29
- "runner_id" text,
30
- "runner_name" text,
31
- "runner_group_id" text,
32
- "runner_group_name" text,
33
- "container_id" text,
34
- "container_image" text,
35
- "needs" text,
36
- "environment" text,
37
- "environment_url" text,
38
- "matrix" text,
39
- "started_at" timestamp,
40
- "completed_at" timestamp,
41
- "created_at" timestamp DEFAULT now() NOT NULL,
42
- "updated_at" timestamp DEFAULT now() NOT NULL
43
-);
44
-CREATE TABLE "workflow_logs" (
45
- "id" text PRIMARY KEY NOT NULL,
46
- "job_id" text NOT NULL,
47
- "step_id" text,
48
- "log_level" text DEFAULT 'info' NOT NULL,
49
- "message" text NOT NULL,
50
- "timestamp" text NOT NULL,
51
- "line_number" integer,
52
- "created_at" timestamp DEFAULT now() NOT NULL
53
-);
54
-CREATE TABLE "workflow_runs" (
55
- "id" text PRIMARY KEY NOT NULL,
56
- "workflow_id" text NOT NULL,
57
- "repository_id" text NOT NULL,
58
- "run_number" integer NOT NULL,
59
- "run_attempt" integer DEFAULT 1 NOT NULL,
60
- "name" text NOT NULL,
61
- "display_title" text,
62
- "status" text DEFAULT 'queued' NOT NULL,
63
- "conclusion" text,
64
- "event" text NOT NULL,
65
- "head_branch" text,
66
- "head_sha" text NOT NULL,
67
- "base_branch" text,
68
- "base_sha" text,
69
- "pull_request_id" text,
70
- "triggered_by_id" text,
71
- "started_at" timestamp,
72
- "completed_at" timestamp,
73
- "workflow_config" text,
74
- "created_at" timestamp DEFAULT now() NOT NULL,
75
- "updated_at" timestamp DEFAULT now() NOT NULL
76
-);
77
-CREATE TABLE "workflow_secrets" (
78
- "id" text PRIMARY KEY NOT NULL,
79
- "repository_id" text NOT NULL,
80
- "name" text NOT NULL,
81
- "encrypted_value" text NOT NULL,
82
- "environment" text,
83
- "created_by_id" text,
84
- "created_at" timestamp DEFAULT now() NOT NULL,
85
- "updated_at" timestamp DEFAULT now() NOT NULL
86
-);
87
-CREATE TABLE "workflow_steps" (
88
- "id" text PRIMARY KEY NOT NULL,
89
- "job_id" text NOT NULL,
90
- "number" integer NOT NULL,
91
- "name" text NOT NULL,
92
- "status" text DEFAULT 'queued' NOT NULL,
93
- "conclusion" text,
94
- "uses" text,
95
- "run" text,
96
- "shell" text,
97
- "working_directory" text,
98
- "env" text,
99
- "with" text,
100
- "if" text,
101
- "continue_on_error" boolean DEFAULT false,
102
- "timeout_minutes" integer,
103
- "started_at" timestamp,
104
- "completed_at" timestamp,
105
- "created_at" timestamp DEFAULT now() NOT NULL
106
-);
107
-CREATE TABLE "workflow_variables" (
108
- "id" text PRIMARY KEY NOT NULL,
109
- "repository_id" text NOT NULL,
110
- "name" text NOT NULL,
111
- "value" text NOT NULL,
112
- "environment" text,
113
- "created_by_id" text,
114
- "created_at" timestamp DEFAULT now() NOT NULL,
115
- "updated_at" timestamp DEFAULT now() NOT NULL
116
-);
117
-CREATE TABLE "workflows" (
118
- "id" text PRIMARY KEY NOT NULL,
119
- "repository_id" text NOT NULL,
120
- "name" text NOT NULL,
121
- "path" text NOT NULL,
122
- "state" text DEFAULT 'active' NOT NULL,
123
- "badge_url" text,
124
- "created_at" timestamp DEFAULT now() NOT NULL,
125
- "updated_at" timestamp DEFAULT now() NOT NULL
126
-);
127
-ALTER TABLE "issues" ADD COLUMN "type" text DEFAULT 'issue' NOT NULL;--> statement-breakpoint
128
-ALTER TABLE "issues" ADD COLUMN "parent_id" text;--> statement-breakpoint
129
-ALTER TABLE "scheduled_workflows" ADD CONSTRAINT "scheduled_workflows_workflow_id_workflows_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflows"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
130
-ALTER TABLE "workflow_artifacts" ADD CONSTRAINT "workflow_artifacts_run_id_workflow_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
131
-ALTER TABLE "workflow_artifacts" ADD CONSTRAINT "workflow_artifacts_job_id_workflow_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."workflow_jobs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
132
-ALTER TABLE "workflow_jobs" ADD CONSTRAINT "workflow_jobs_run_id_workflow_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
133
-ALTER TABLE "workflow_logs" ADD CONSTRAINT "workflow_logs_job_id_workflow_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."workflow_jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
134
-ALTER TABLE "workflow_logs" ADD CONSTRAINT "workflow_logs_step_id_workflow_steps_id_fk" FOREIGN KEY ("step_id") REFERENCES "public"."workflow_steps"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
135
-ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_workflow_id_workflows_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflows"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
136
-ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
137
-ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
138
-ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_triggered_by_id_users_id_fk" FOREIGN KEY ("triggered_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
139
-ALTER TABLE "workflow_secrets" ADD CONSTRAINT "workflow_secrets_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
140
-ALTER TABLE "workflow_secrets" ADD CONSTRAINT "workflow_secrets_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
141
-ALTER TABLE "workflow_steps" ADD CONSTRAINT "workflow_steps_job_id_workflow_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."workflow_jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
142
-ALTER TABLE "workflow_variables" ADD CONSTRAINT "workflow_variables_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
143
-ALTER TABLE "workflow_variables" ADD CONSTRAINT "workflow_variables_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
144
-ALTER TABLE "workflows" ADD CONSTRAINT "workflows_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;
145
0
▼drizzle/0003_married_cyclops.sql
+0-547
1
-CREATE TABLE "team_members" (
2
- "team_id" text NOT NULL,
3
- "user_id" text NOT NULL,
4
- "role" text DEFAULT 'member' NOT NULL,
5
- "created_at" timestamp DEFAULT now() NOT NULL,
6
- CONSTRAINT "team_members_team_id_user_id_pk" PRIMARY KEY("team_id","user_id")
7
-);
8
-CREATE TABLE "teams" (
9
- "id" text PRIMARY KEY NOT NULL,
10
- "organization_id" text NOT NULL,
11
- "name" text NOT NULL,
12
- "slug" text NOT NULL,
13
- "description" text,
14
- "privacy" text DEFAULT 'visible',
15
- "created_at" timestamp DEFAULT now() NOT NULL,
16
- "updated_at" timestamp DEFAULT now() NOT NULL
17
-);
18
-CREATE TABLE "repository_path_permissions" (
19
- "id" text PRIMARY KEY NOT NULL,
20
- "repository_id" text NOT NULL,
21
- "path_pattern" text NOT NULL,
22
- "user_id" text,
23
- "team_id" text,
24
- "permission" text DEFAULT 'write' NOT NULL,
25
- "require_approval" text DEFAULT 'false',
26
- "created_at" timestamp DEFAULT now() NOT NULL,
27
- "updated_at" timestamp DEFAULT now() NOT NULL
28
-);
29
-CREATE TABLE "pr_state_definitions" (
30
- "id" text PRIMARY KEY NOT NULL,
31
- "repository_id" text NOT NULL,
32
- "name" text NOT NULL,
33
- "display_name" text NOT NULL,
34
- "description" text,
35
- "color" text DEFAULT '#6B7280' NOT NULL,
36
- "icon" text,
37
- "order" integer DEFAULT 0 NOT NULL,
38
- "is_default" boolean DEFAULT false,
39
- "is_final" boolean DEFAULT false,
40
- "allow_merge" boolean DEFAULT false,
41
- "created_at" timestamp DEFAULT now() NOT NULL,
42
- "updated_at" timestamp DEFAULT now() NOT NULL
43
-);
44
-CREATE TABLE "pr_state_reviewers" (
45
- "id" text PRIMARY KEY NOT NULL,
46
- "state_definition_id" text NOT NULL,
47
- "user_id" text,
48
- "team_id" text,
49
- "required_count" integer DEFAULT 1,
50
- "created_at" timestamp DEFAULT now() NOT NULL
51
-);
52
-CREATE TABLE "pr_state_transitions" (
53
- "id" text PRIMARY KEY NOT NULL,
54
- "pull_request_id" text NOT NULL,
55
- "from_state" text,
56
- "to_state" text NOT NULL,
57
- "changed_by_id" text NOT NULL,
58
- "reason" text,
59
- "created_at" timestamp DEFAULT now() NOT NULL
60
-);
61
-CREATE TABLE "breaking_changes" (
62
- "id" text PRIMARY KEY NOT NULL,
63
- "pull_request_id" text NOT NULL,
64
- "change_type" text NOT NULL,
65
- "severity" text NOT NULL,
66
- "description" text NOT NULL,
67
- "affected_files" jsonb,
68
- "suggested_action" text,
69
- "acknowledged" boolean DEFAULT false,
70
- "created_at" timestamp DEFAULT now() NOT NULL
71
-);
72
-CREATE TABLE "change_set_items" (
73
- "id" text PRIMARY KEY NOT NULL,
74
- "change_set_id" text NOT NULL,
75
- "pull_request_id" text,
76
- "repository_id" text NOT NULL,
77
- "order" integer DEFAULT 0,
78
- "created_at" timestamp DEFAULT now() NOT NULL
79
-);
80
-CREATE TABLE "change_sets" (
81
- "id" text PRIMARY KEY NOT NULL,
82
- "name" text NOT NULL,
83
- "description" text,
84
- "created_by_id" text NOT NULL,
85
- "status" text DEFAULT 'draft' NOT NULL,
86
- "created_at" timestamp DEFAULT now() NOT NULL,
87
- "updated_at" timestamp DEFAULT now() NOT NULL
88
-);
89
-CREATE TABLE "chat_integrations" (
90
- "id" text PRIMARY KEY NOT NULL,
91
- "organization_id" text,
92
- "repository_id" text,
93
- "provider" text NOT NULL,
94
- "name" text NOT NULL,
95
- "webhook_url" text,
96
- "api_token" text,
97
- "channel_id" text,
98
- "is_enabled" boolean DEFAULT true,
99
- "events" jsonb DEFAULT '[]'::jsonb,
100
- "created_at" timestamp DEFAULT now() NOT NULL,
101
- "updated_at" timestamp DEFAULT now() NOT NULL
102
-);
103
-CREATE TABLE "cloud_configs" (
104
- "id" text PRIMARY KEY NOT NULL,
105
- "repository_id" text,
106
- "organization_id" text,
107
- "provider" text NOT NULL,
108
- "name" text NOT NULL,
109
- "region" text,
110
- "credentials" jsonb,
111
- "settings" jsonb,
112
- "is_enabled" boolean DEFAULT true,
113
- "created_at" timestamp DEFAULT now() NOT NULL,
114
- "updated_at" timestamp DEFAULT now() NOT NULL
115
-);
116
-CREATE TABLE "code_quality_configs" (
117
- "id" text PRIMARY KEY NOT NULL,
118
- "repository_id" text NOT NULL,
119
- "provider" text NOT NULL,
120
- "project_key" text,
121
- "api_token" text,
122
- "server_url" text,
123
- "webhook_secret" text,
124
- "is_enabled" boolean DEFAULT true,
125
- "report_on_pr" boolean DEFAULT true,
126
- "block_on_fail" boolean DEFAULT false,
127
- "min_coverage" real,
128
- "created_at" timestamp DEFAULT now() NOT NULL,
129
- "updated_at" timestamp DEFAULT now() NOT NULL
130
-);
131
-CREATE TABLE "code_quality_issues" (
132
- "id" text PRIMARY KEY NOT NULL,
133
- "repository_id" text NOT NULL,
134
- "pull_request_id" text,
135
- "commit_sha" text NOT NULL,
136
- "provider" text NOT NULL,
137
- "issue_type" text NOT NULL,
138
- "severity" text NOT NULL,
139
- "message" text NOT NULL,
140
- "file" text,
141
- "line" integer,
142
- "rule" text,
143
- "effort" text,
144
- "status" text DEFAULT 'open',
145
- "created_at" timestamp DEFAULT now() NOT NULL
146
-);
147
-CREATE TABLE "coverage_reports" (
148
- "id" text PRIMARY KEY NOT NULL,
149
- "repository_id" text NOT NULL,
150
- "pull_request_id" text,
151
- "commit_sha" text NOT NULL,
152
- "provider" text NOT NULL,
153
- "coverage" real NOT NULL,
154
- "lines_covered" integer,
155
- "lines_total" integer,
156
- "branch_coverage" real,
157
- "delta" real,
158
- "status" text NOT NULL,
159
- "report_url" text,
160
- "created_at" timestamp DEFAULT now() NOT NULL
161
-);
162
-CREATE TABLE "cross_repo_issue_links" (
163
- "id" text PRIMARY KEY NOT NULL,
164
- "source_issue_id" text NOT NULL,
165
- "target_issue_id" text NOT NULL,
166
- "link_type" text NOT NULL,
167
- "created_by_id" text NOT NULL,
168
- "created_at" timestamp DEFAULT now() NOT NULL
169
-);
170
-CREATE TABLE "custom_dashboards" (
171
- "id" text PRIMARY KEY NOT NULL,
172
- "user_id" text NOT NULL,
173
- "name" text NOT NULL,
174
- "description" text,
175
- "is_public" boolean DEFAULT false,
176
- "layout" jsonb,
177
- "created_at" timestamp DEFAULT now() NOT NULL,
178
- "updated_at" timestamp DEFAULT now() NOT NULL
179
-);
180
-CREATE TABLE "custom_field_definitions" (
181
- "id" text PRIMARY KEY NOT NULL,
182
- "repository_id" text NOT NULL,
183
- "name" text NOT NULL,
184
- "field_type" text NOT NULL,
185
- "description" text,
186
- "is_required" boolean DEFAULT false,
187
- "options" text,
188
- "default_value" text,
189
- "display_order" integer DEFAULT 0,
190
- "created_at" timestamp DEFAULT now() NOT NULL,
191
- "updated_at" timestamp DEFAULT now() NOT NULL
192
-);
193
-CREATE TABLE "custom_field_values" (
194
- "id" text PRIMARY KEY NOT NULL,
195
- "issue_id" text NOT NULL,
196
- "field_id" text NOT NULL,
197
- "value" text,
198
- "created_at" timestamp DEFAULT now() NOT NULL,
199
- "updated_at" timestamp DEFAULT now() NOT NULL
200
-);
201
-CREATE TABLE "dashboard_widgets" (
202
- "id" text PRIMARY KEY NOT NULL,
203
- "dashboard_id" text NOT NULL,
204
- "widget_type" text NOT NULL,
205
- "title" text NOT NULL,
206
- "config" jsonb,
207
- "position" jsonb,
208
- "created_at" timestamp DEFAULT now() NOT NULL
209
-);
210
-CREATE TABLE "cloud_deployments" (
211
- "id" text PRIMARY KEY NOT NULL,
212
- "config_id" text NOT NULL,
213
- "pull_request_id" text,
214
- "commit_sha" text NOT NULL,
215
- "environment" text NOT NULL,
216
- "status" text NOT NULL,
217
- "url" text,
218
- "logs" text,
219
- "started_at" timestamp,
220
- "completed_at" timestamp,
221
- "created_at" timestamp DEFAULT now() NOT NULL
222
-);
223
-CREATE TABLE "email_settings" (
224
- "id" text PRIMARY KEY NOT NULL,
225
- "organization_id" text,
226
- "smtp_host" text,
227
- "smtp_port" text,
228
- "smtp_user" text,
229
- "smtp_pass" text,
230
- "from_address" text,
231
- "from_name" text,
232
- "is_enabled" boolean DEFAULT true,
233
- "created_at" timestamp DEFAULT now() NOT NULL
234
-);
235
-CREATE TABLE "external_builds" (
236
- "id" text PRIMARY KEY NOT NULL,
237
- "config_id" text NOT NULL,
238
- "pull_request_id" text,
239
- "external_build_id" text NOT NULL,
240
- "build_number" text,
241
- "status" text NOT NULL,
242
- "url" text,
243
- "started_at" timestamp,
244
- "completed_at" timestamp,
245
- "created_at" timestamp DEFAULT now() NOT NULL
246
-);
247
-CREATE TABLE "external_ci_configs" (
248
- "id" text PRIMARY KEY NOT NULL,
249
- "repository_id" text NOT NULL,
250
- "provider" text NOT NULL,
251
- "name" text NOT NULL,
252
- "base_url" text NOT NULL,
253
- "api_token" text,
254
- "project_id" text,
255
- "webhook_secret" text,
256
- "is_enabled" boolean DEFAULT true,
257
- "sync_status" boolean DEFAULT true,
258
- "created_at" timestamp DEFAULT now() NOT NULL,
259
- "updated_at" timestamp DEFAULT now() NOT NULL
260
-);
261
-CREATE TABLE "file_approvals" (
262
- "id" text PRIMARY KEY NOT NULL,
263
- "pull_request_id" text NOT NULL,
264
- "path" text NOT NULL,
265
- "approved_by_id" text NOT NULL,
266
- "approved_at" timestamp DEFAULT now() NOT NULL,
267
- "commit_sha" text NOT NULL,
268
- "comment" text,
269
- "created_at" timestamp DEFAULT now() NOT NULL,
270
- "updated_at" timestamp DEFAULT now() NOT NULL
271
-);
272
-CREATE TABLE "file_hotspots" (
273
- "id" text PRIMARY KEY NOT NULL,
274
- "repository_id" text NOT NULL,
275
- "file_path" text NOT NULL,
276
- "change_count" integer DEFAULT 0,
277
- "bug_count" integer DEFAULT 0,
278
- "review_count" integer DEFAULT 0,
279
- "complexity_score" real,
280
- "last_modified" timestamp,
281
- "calculated_at" timestamp DEFAULT now() NOT NULL
282
-);
283
-CREATE TABLE "ip_allow_lists" (
284
- "id" text PRIMARY KEY NOT NULL,
285
- "organization_id" text NOT NULL,
286
- "name" text NOT NULL,
287
- "cidr_block" text NOT NULL,
288
- "description" text,
289
- "is_enabled" boolean DEFAULT true,
290
- "created_at" timestamp DEFAULT now() NOT NULL,
291
- "created_by_id" text NOT NULL
292
-);
293
-CREATE TABLE "issue_tracker_configs" (
294
- "id" text PRIMARY KEY NOT NULL,
295
- "repository_id" text NOT NULL,
296
- "provider" text NOT NULL,
297
- "name" text NOT NULL,
298
- "api_url" text,
299
- "api_token" text,
300
- "project_key" text,
301
- "webhook_secret" text,
302
- "is_enabled" boolean DEFAULT true,
303
- "sync_to_external" boolean DEFAULT true,
304
- "sync_from_external" boolean DEFAULT true,
305
- "created_at" timestamp DEFAULT now() NOT NULL,
306
- "updated_at" timestamp DEFAULT now() NOT NULL
307
-);
308
-CREATE TABLE "issue_tracker_links" (
309
- "id" text PRIMARY KEY NOT NULL,
310
- "config_id" text NOT NULL,
311
- "local_issue_id" text,
312
- "external_id" text NOT NULL,
313
- "external_key" text,
314
- "external_url" text,
315
- "last_sync_at" timestamp,
316
- "created_at" timestamp DEFAULT now() NOT NULL
317
-);
318
-CREATE TABLE "license_scans" (
319
- "id" text PRIMARY KEY NOT NULL,
320
- "repository_id" text NOT NULL,
321
- "commit_sha" text NOT NULL,
322
- "package_name" text NOT NULL,
323
- "package_version" text,
324
- "license" text NOT NULL,
325
- "license_type" text NOT NULL,
326
- "is_compliant" boolean NOT NULL,
327
- "policy_violation" text,
328
- "created_at" timestamp DEFAULT now() NOT NULL
329
-);
330
-CREATE TABLE "merge_gates" (
331
- "id" text PRIMARY KEY NOT NULL,
332
- "repository_id" text NOT NULL,
333
- "name" text NOT NULL,
334
- "description" text,
335
- "gate_type" text NOT NULL,
336
- "config" text,
337
- "condition_script" text,
338
- "is_enabled" boolean DEFAULT true,
339
- "order" integer DEFAULT 0,
340
- "created_at" timestamp DEFAULT now() NOT NULL
341
-);
342
-CREATE TABLE "metric_snapshots" (
343
- "id" text PRIMARY KEY NOT NULL,
344
- "repository_id" text,
345
- "organization_id" text,
346
- "metric_type" text NOT NULL,
347
- "value" real NOT NULL,
348
- "dimensions" jsonb,
349
- "timestamp" timestamp DEFAULT now() NOT NULL
350
-);
351
-CREATE TABLE "migration_detections" (
352
- "id" text PRIMARY KEY NOT NULL,
353
- "pull_request_id" text NOT NULL,
354
- "migration_type" text NOT NULL,
355
- "tool" text,
356
- "files" jsonb,
357
- "is_reversible" boolean,
358
- "requires_downtime" boolean,
359
- "notes" text,
360
- "created_at" timestamp DEFAULT now() NOT NULL
361
-);
362
-CREATE TABLE "pr_issue_links" (
363
- "id" text PRIMARY KEY NOT NULL,
364
- "pull_request_id" text NOT NULL,
365
- "issue_id" text NOT NULL,
366
- "link_type" text NOT NULL,
367
- "created_by_id" text NOT NULL,
368
- "created_at" timestamp DEFAULT now() NOT NULL
369
-);
370
-CREATE TABLE "rate_limit_logs" (
371
- "id" text PRIMARY KEY NOT NULL,
372
- "rule_id" text NOT NULL,
373
- "key" text NOT NULL,
374
- "request_count" integer NOT NULL,
375
- "window_start" timestamp NOT NULL,
376
- "blocked" boolean DEFAULT false
377
-);
378
-CREATE TABLE "rate_limit_rules" (
379
- "id" text PRIMARY KEY NOT NULL,
380
- "name" text NOT NULL,
381
- "path" text NOT NULL,
382
- "method" text,
383
- "window_ms" integer NOT NULL,
384
- "max_requests" integer NOT NULL,
385
- "key_type" text DEFAULT 'ip',
386
- "is_enabled" boolean DEFAULT true,
387
- "created_at" timestamp DEFAULT now() NOT NULL
388
-);
389
-CREATE TABLE "repository_workflows" (
390
- "id" text PRIMARY KEY NOT NULL,
391
- "repository_id" text NOT NULL,
392
- "name" text NOT NULL,
393
- "path" text NOT NULL,
394
- "content" text NOT NULL,
395
- "template_id" text,
396
- "is_enabled" boolean DEFAULT true,
397
- "created_at" timestamp DEFAULT now() NOT NULL,
398
- "updated_at" timestamp DEFAULT now() NOT NULL
399
-);
400
-CREATE TABLE "required_status_checks" (
401
- "id" text PRIMARY KEY NOT NULL,
402
- "repository_id" text NOT NULL,
403
- "branch" text NOT NULL,
404
- "check_name" text NOT NULL,
405
- "is_required" boolean DEFAULT true,
406
- "strict_mode" boolean DEFAULT true,
407
- "created_at" timestamp DEFAULT now() NOT NULL
408
-);
409
-CREATE TABLE "review_templates" (
410
- "id" text PRIMARY KEY NOT NULL,
411
- "repository_id" text NOT NULL,
412
- "organization_id" text,
413
- "name" text NOT NULL,
414
- "description" text,
415
- "category" text,
416
- "content" text NOT NULL,
417
- "is_default" boolean DEFAULT false NOT NULL,
418
- "created_by_id" text NOT NULL,
419
- "created_at" timestamp DEFAULT now() NOT NULL,
420
- "updated_at" timestamp DEFAULT now() NOT NULL
421
-);
422
-CREATE TABLE "saml_configs" (
423
- "id" text PRIMARY KEY NOT NULL,
424
- "organization_id" text NOT NULL,
425
- "entity_id" text NOT NULL,
426
- "sso_url" text NOT NULL,
427
- "certificate" text NOT NULL,
428
- "signature_algorithm" text DEFAULT 'RSA-SHA256',
429
- "digest_algorithm" text DEFAULT 'SHA256',
430
- "is_enabled" boolean DEFAULT true,
431
- "created_at" timestamp DEFAULT now() NOT NULL,
432
- "updated_at" timestamp DEFAULT now() NOT NULL
433
-);
434
-CREATE TABLE "secret_scan_results" (
435
- "id" text PRIMARY KEY NOT NULL,
436
- "repository_id" text NOT NULL,
437
- "commit_sha" text NOT NULL,
438
- "secret_type" text NOT NULL,
439
- "file" text NOT NULL,
440
- "line" integer,
441
- "snippet" text,
442
- "severity" text NOT NULL,
443
- "status" text DEFAULT 'open',
444
- "resolved_at" timestamp,
445
- "resolved_by_id" text,
446
- "created_at" timestamp DEFAULT now() NOT NULL
447
-);
448
-CREATE TABLE "workflow_states" (
449
- "id" text PRIMARY KEY NOT NULL,
450
- "repository_id" text NOT NULL,
451
- "name" text NOT NULL,
452
- "category" text NOT NULL,
453
- "color" text DEFAULT '#6b7280',
454
- "icon" text,
455
- "display_order" integer DEFAULT 0,
456
- "is_default" boolean DEFAULT false,
457
- "is_closed_state" boolean DEFAULT false,
458
- "created_at" timestamp DEFAULT now() NOT NULL
459
-);
460
-CREATE TABLE "workflow_templates" (
461
- "id" text PRIMARY KEY NOT NULL,
462
- "name" text NOT NULL,
463
- "description" text,
464
- "category" text NOT NULL,
465
- "language" text,
466
- "content" text NOT NULL,
467
- "is_official" boolean DEFAULT false,
468
- "is_public" boolean DEFAULT true,
469
- "created_by_id" text,
470
- "downloads" text DEFAULT '0',
471
- "created_at" timestamp DEFAULT now() NOT NULL,
472
- "updated_at" timestamp DEFAULT now() NOT NULL
473
-);
474
-CREATE TABLE "workflow_transitions" (
475
- "id" text PRIMARY KEY NOT NULL,
476
- "repository_id" text NOT NULL,
477
- "from_state_id" text,
478
- "to_state_id" text NOT NULL,
479
- "requires_comment" boolean DEFAULT false,
480
- "created_at" timestamp DEFAULT now() NOT NULL
481
-);
482
-ALTER TABLE "deploy_keys" ALTER COLUMN "read_only" SET NOT NULL;--> statement-breakpoint
483
-ALTER TABLE "deploy_keys" ADD COLUMN "fingerprint" text NOT NULL;--> statement-breakpoint
484
-ALTER TABLE "deploy_keys" ADD COLUMN "last_used_at" timestamp;--> statement-breakpoint
485
-ALTER TABLE "repositories" ADD COLUMN "is_template" boolean DEFAULT false;--> statement-breakpoint
486
-ALTER TABLE "repositories" ADD COLUMN "last_mirror_sync_at" timestamp;--> statement-breakpoint
487
-ALTER TABLE "repositories" ADD COLUMN "mirror_sync_status" text;--> statement-breakpoint
488
-ALTER TABLE "pull_request_checks" ADD COLUMN "completed_at" timestamp;--> statement-breakpoint
489
-ALTER TABLE "pull_request_checks" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
490
-ALTER TABLE "pull_requests" ADD COLUMN "auto_merge_enabled_by_id" text;--> statement-breakpoint
491
-ALTER TABLE "pull_requests" ADD COLUMN "auto_merge_enabled_at" timestamp;--> statement-breakpoint
492
-ALTER TABLE "pull_requests" ADD COLUMN "custom_state" text;--> statement-breakpoint
493
-ALTER TABLE "pull_requests" ADD COLUMN "custom_state_changed_at" timestamp;--> statement-breakpoint
494
-ALTER TABLE "audit_logs" ADD COLUMN "organization_id" text;--> statement-breakpoint
495
-ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
496
-ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
497
-ALTER TABLE "teams" ADD CONSTRAINT "teams_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
498
-ALTER TABLE "repository_path_permissions" ADD CONSTRAINT "repository_path_permissions_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
499
-ALTER TABLE "repository_path_permissions" ADD CONSTRAINT "repository_path_permissions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
500
-ALTER TABLE "repository_path_permissions" ADD CONSTRAINT "repository_path_permissions_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
501
-ALTER TABLE "pr_state_definitions" ADD CONSTRAINT "pr_state_definitions_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
502
-ALTER TABLE "pr_state_reviewers" ADD CONSTRAINT "pr_state_reviewers_state_definition_id_pr_state_definitions_id_fk" FOREIGN KEY ("state_definition_id") REFERENCES "public"."pr_state_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
503
-ALTER TABLE "pr_state_reviewers" ADD CONSTRAINT "pr_state_reviewers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
504
-ALTER TABLE "pr_state_reviewers" ADD CONSTRAINT "pr_state_reviewers_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
505
-ALTER TABLE "pr_state_transitions" ADD CONSTRAINT "pr_state_transitions_changed_by_id_users_id_fk" FOREIGN KEY ("changed_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
506
-ALTER TABLE "breaking_changes" ADD CONSTRAINT "breaking_changes_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
507
-ALTER TABLE "change_set_items" ADD CONSTRAINT "change_set_items_change_set_id_change_sets_id_fk" FOREIGN KEY ("change_set_id") REFERENCES "public"."change_sets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
508
-ALTER TABLE "change_set_items" ADD CONSTRAINT "change_set_items_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
509
-ALTER TABLE "change_set_items" ADD CONSTRAINT "change_set_items_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
510
-ALTER TABLE "change_sets" ADD CONSTRAINT "change_sets_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
511
-ALTER TABLE "cloud_configs" ADD CONSTRAINT "cloud_configs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
512
-ALTER TABLE "code_quality_configs" ADD CONSTRAINT "code_quality_configs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
513
-ALTER TABLE "code_quality_issues" ADD CONSTRAINT "code_quality_issues_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
514
-ALTER TABLE "coverage_reports" ADD CONSTRAINT "coverage_reports_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
515
-ALTER TABLE "cross_repo_issue_links" ADD CONSTRAINT "cross_repo_issue_links_source_issue_id_issues_id_fk" FOREIGN KEY ("source_issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
516
-ALTER TABLE "cross_repo_issue_links" ADD CONSTRAINT "cross_repo_issue_links_target_issue_id_issues_id_fk" FOREIGN KEY ("target_issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
517
-ALTER TABLE "custom_dashboards" ADD CONSTRAINT "custom_dashboards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
518
-ALTER TABLE "custom_field_definitions" ADD CONSTRAINT "custom_field_definitions_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
519
-ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
520
-ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_field_id_custom_field_definitions_id_fk" FOREIGN KEY ("field_id") REFERENCES "public"."custom_field_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
521
-ALTER TABLE "dashboard_widgets" ADD CONSTRAINT "dashboard_widgets_dashboard_id_custom_dashboards_id_fk" FOREIGN KEY ("dashboard_id") REFERENCES "public"."custom_dashboards"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
522
-ALTER TABLE "cloud_deployments" ADD CONSTRAINT "cloud_deployments_config_id_cloud_configs_id_fk" FOREIGN KEY ("config_id") REFERENCES "public"."cloud_configs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
523
-ALTER TABLE "external_builds" ADD CONSTRAINT "external_builds_config_id_external_ci_configs_id_fk" FOREIGN KEY ("config_id") REFERENCES "public"."external_ci_configs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
524
-ALTER TABLE "external_ci_configs" ADD CONSTRAINT "external_ci_configs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
525
-ALTER TABLE "file_approvals" ADD CONSTRAINT "file_approvals_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
526
-ALTER TABLE "file_approvals" ADD CONSTRAINT "file_approvals_approved_by_id_users_id_fk" FOREIGN KEY ("approved_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
527
-ALTER TABLE "file_hotspots" ADD CONSTRAINT "file_hotspots_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
528
-ALTER TABLE "issue_tracker_configs" ADD CONSTRAINT "issue_tracker_configs_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
529
-ALTER TABLE "issue_tracker_links" ADD CONSTRAINT "issue_tracker_links_config_id_issue_tracker_configs_id_fk" FOREIGN KEY ("config_id") REFERENCES "public"."issue_tracker_configs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
530
-ALTER TABLE "issue_tracker_links" ADD CONSTRAINT "issue_tracker_links_local_issue_id_issues_id_fk" FOREIGN KEY ("local_issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
531
-ALTER TABLE "license_scans" ADD CONSTRAINT "license_scans_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
532
-ALTER TABLE "merge_gates" ADD CONSTRAINT "merge_gates_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
533
-ALTER TABLE "metric_snapshots" ADD CONSTRAINT "metric_snapshots_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
534
-ALTER TABLE "migration_detections" ADD CONSTRAINT "migration_detections_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
535
-ALTER TABLE "pr_issue_links" ADD CONSTRAINT "pr_issue_links_pull_request_id_pull_requests_id_fk" FOREIGN KEY ("pull_request_id") REFERENCES "public"."pull_requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
536
-ALTER TABLE "pr_issue_links" ADD CONSTRAINT "pr_issue_links_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
537
-ALTER TABLE "rate_limit_logs" ADD CONSTRAINT "rate_limit_logs_rule_id_rate_limit_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."rate_limit_rules"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
538
-ALTER TABLE "repository_workflows" ADD CONSTRAINT "repository_workflows_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
539
-ALTER TABLE "repository_workflows" ADD CONSTRAINT "repository_workflows_template_id_workflow_templates_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."workflow_templates"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
540
-ALTER TABLE "required_status_checks" ADD CONSTRAINT "required_status_checks_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
541
-ALTER TABLE "review_templates" ADD CONSTRAINT "review_templates_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
542
-ALTER TABLE "secret_scan_results" ADD CONSTRAINT "secret_scan_results_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
543
-ALTER TABLE "workflow_states" ADD CONSTRAINT "workflow_states_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
544
-ALTER TABLE "workflow_transitions" ADD CONSTRAINT "workflow_transitions_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
545
-ALTER TABLE "workflow_transitions" ADD CONSTRAINT "workflow_transitions_from_state_id_workflow_states_id_fk" FOREIGN KEY ("from_state_id") REFERENCES "public"."workflow_states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
546
-ALTER TABLE "workflow_transitions" ADD CONSTRAINT "workflow_transitions_to_state_id_workflow_states_id_fk" FOREIGN KEY ("to_state_id") REFERENCES "public"."workflow_states"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
547
-ALTER TABLE "pull_requests" ADD CONSTRAINT "pull_requests_auto_merge_enabled_by_id_users_id_fk" FOREIGN KEY ("auto_merge_enabled_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
548
0
▼drizzle/0004_add_performance_indexes.sql
+0-157
1
-
2
-CREATE INDEX IF NOT EXISTS "merge_queue_repo_status_idx" ON "merge_queue" ("repository_id", "status");--> statement-breakpoint
3
-CREATE INDEX IF NOT EXISTS "merge_queue_repo_position_idx" ON "merge_queue" ("repository_id", "position");--> statement-breakpoint
4
-CREATE INDEX IF NOT EXISTS "merge_queue_pr_status_idx" ON "merge_queue" ("pull_request_id", "status");--> statement-breakpoint
5
-CREATE INDEX IF NOT EXISTS "merge_queue_status_idx" ON "merge_queue" ("status");--> statement-breakpoint
6
-CREATE INDEX IF NOT EXISTS "merge_queue_stack_idx" ON "merge_queue" ("stack_id");--> statement-breakpoint
7
-CREATE INDEX IF NOT EXISTS "merge_queue_speculative_runs_repo_idx" ON "merge_queue_speculative_runs" ("repository_id");--> statement-breakpoint
8
-CREATE INDEX IF NOT EXISTS "merge_queue_speculative_runs_repo_status_idx" ON "merge_queue_speculative_runs" ("repository_id", "status");--> statement-breakpoint
9
-
10
-CREATE INDEX IF NOT EXISTS "pr_stacks_repo_idx" ON "pr_stacks" ("repository_id");--> statement-breakpoint
11
-CREATE INDEX IF NOT EXISTS "pr_stacks_repo_status_idx" ON "pr_stacks" ("repository_id", "status");--> statement-breakpoint
12
-CREATE INDEX IF NOT EXISTS "pr_stack_entries_stack_order_idx" ON "pr_stack_entries" ("stack_id", "stack_order");--> statement-breakpoint
13
-CREATE INDEX IF NOT EXISTS "pr_stack_entries_pr_idx" ON "pr_stack_entries" ("pull_request_id");--> statement-breakpoint
14
-CREATE UNIQUE INDEX IF NOT EXISTS "pr_stack_entries_stack_order_uniq" ON "pr_stack_entries" ("stack_id", "stack_order");--> statement-breakpoint
15
-
16
-CREATE INDEX IF NOT EXISTS "workflow_jobs_status_idx" ON "workflow_jobs" ("status");--> statement-breakpoint
17
-CREATE INDEX IF NOT EXISTS "workflow_jobs_runner_idx" ON "workflow_jobs" ("runner_id");--> statement-breakpoint
18
-CREATE INDEX IF NOT EXISTS "workflow_steps_job_idx" ON "workflow_steps" ("job_id");--> statement-breakpoint
19
-CREATE INDEX IF NOT EXISTS "workflow_steps_job_number_idx" ON "workflow_steps" ("job_id", "number");--> statement-breakpoint
20
-CREATE INDEX IF NOT EXISTS "workflow_logs_job_idx" ON "workflow_logs" ("job_id");--> statement-breakpoint
21
-CREATE INDEX IF NOT EXISTS "workflow_logs_step_idx" ON "workflow_logs" ("step_id");--> statement-breakpoint
22
-CREATE INDEX IF NOT EXISTS "workflow_logs_job_created_idx" ON "workflow_logs" ("job_id", "created_at");--> statement-breakpoint
23
-CREATE INDEX IF NOT EXISTS "workflow_artifacts_run_idx" ON "workflow_artifacts" ("run_id");--> statement-breakpoint
24
-CREATE INDEX IF NOT EXISTS "workflow_artifacts_job_idx" ON "workflow_artifacts" ("job_id");--> statement-breakpoint
25
-CREATE INDEX IF NOT EXISTS "workflow_artifacts_expires_idx" ON "workflow_artifacts" ("expires_at");--> statement-breakpoint
26
-CREATE INDEX IF NOT EXISTS "workflow_secrets_repo_idx" ON "workflow_secrets" ("repository_id");--> statement-breakpoint
27
-CREATE UNIQUE INDEX IF NOT EXISTS "workflow_secrets_repo_name_env_uniq" ON "workflow_secrets" ("repository_id", "name", "environment");--> statement-breakpoint
28
-CREATE INDEX IF NOT EXISTS "workflow_variables_repo_idx" ON "workflow_variables" ("repository_id");--> statement-breakpoint
29
-CREATE INDEX IF NOT EXISTS "scheduled_workflows_workflow_idx" ON "scheduled_workflows" ("workflow_id");--> statement-breakpoint
30
-CREATE INDEX IF NOT EXISTS "scheduled_workflows_enabled_next_idx" ON "scheduled_workflows" ("is_enabled", "next_run_at");--> statement-breakpoint
31
-
32
-CREATE INDEX IF NOT EXISTS "ai_reviews_pr_created_idx" ON "ai_reviews" ("pull_request_id", "created_at");--> statement-breakpoint
33
-CREATE INDEX IF NOT EXISTS "ai_reviews_status_idx" ON "ai_reviews" ("status");--> statement-breakpoint
34
-CREATE INDEX IF NOT EXISTS "ai_review_suggestions_review_idx" ON "ai_review_suggestions" ("ai_review_id");--> statement-breakpoint
35
-CREATE INDEX IF NOT EXISTS "ai_review_suggestions_review_severity_idx" ON "ai_review_suggestions" ("ai_review_id", "severity");--> statement-breakpoint
36
-
37
-CREATE INDEX IF NOT EXISTS "issue_labels_issue_idx" ON "issue_labels" ("issue_id");--> statement-breakpoint
38
-CREATE INDEX IF NOT EXISTS "issue_labels_label_idx" ON "issue_labels" ("label_id");--> statement-breakpoint
39
-CREATE INDEX IF NOT EXISTS "milestones_repo_idx" ON "milestones" ("repository_id");--> statement-breakpoint
40
-CREATE INDEX IF NOT EXISTS "milestones_repo_state_idx" ON "milestones" ("repository_id", "state");--> statement-breakpoint
41
-CREATE INDEX IF NOT EXISTS "issue_assignees_issue_idx" ON "issue_assignees" ("issue_id");--> statement-breakpoint
42
-CREATE INDEX IF NOT EXISTS "issue_assignees_user_idx" ON "issue_assignees" ("user_id");--> statement-breakpoint
43
-CREATE INDEX IF NOT EXISTS "issue_subscribers_issue_idx" ON "issue_subscribers" ("issue_id");--> statement-breakpoint
44
-CREATE INDEX IF NOT EXISTS "issue_subscribers_user_idx" ON "issue_subscribers" ("user_id");--> statement-breakpoint
45
-
46
-CREATE INDEX IF NOT EXISTS "pr_labels_pr_idx" ON "pull_request_labels" ("pull_request_id");--> statement-breakpoint
47
-CREATE INDEX IF NOT EXISTS "pr_labels_label_idx" ON "pull_request_labels" ("label_id");--> statement-breakpoint
48
-CREATE INDEX IF NOT EXISTS "pr_assignees_pr_idx" ON "pull_request_assignees" ("pull_request_id");--> statement-breakpoint
49
-CREATE INDEX IF NOT EXISTS "pr_assignees_user_idx" ON "pull_request_assignees" ("user_id");--> statement-breakpoint
50
-CREATE INDEX IF NOT EXISTS "pr_reviewers_pr_idx" ON "pull_request_reviewers" ("pull_request_id");--> statement-breakpoint
51
-CREATE INDEX IF NOT EXISTS "pr_reviewers_user_idx" ON "pull_request_reviewers" ("user_id");--> statement-breakpoint
52
-
53
-CREATE INDEX IF NOT EXISTS "organizations_display_name_idx" ON "organizations" ("display_name");--> statement-breakpoint
54
-CREATE INDEX IF NOT EXISTS "organization_members_user_idx" ON "organization_members" ("user_id");--> statement-breakpoint
55
-CREATE INDEX IF NOT EXISTS "organization_members_org_role_idx" ON "organization_members" ("organization_id", "role");--> statement-breakpoint
56
-CREATE INDEX IF NOT EXISTS "teams_org_idx" ON "teams" ("organization_id");--> statement-breakpoint
57
-CREATE UNIQUE INDEX IF NOT EXISTS "teams_org_slug_uniq" ON "teams" ("organization_id", "slug");--> statement-breakpoint
58
-CREATE INDEX IF NOT EXISTS "team_members_user_idx" ON "team_members" ("user_id");--> statement-breakpoint
59
-CREATE INDEX IF NOT EXISTS "team_repositories_repo_idx" ON "team_repositories" ("repository_id");--> statement-breakpoint
60
-
61
-CREATE UNIQUE INDEX IF NOT EXISTS "projects_repo_number_uniq" ON "projects" ("repository_id", "number");--> statement-breakpoint
62
-CREATE INDEX IF NOT EXISTS "projects_repo_idx" ON "projects" ("repository_id");--> statement-breakpoint
63
-CREATE INDEX IF NOT EXISTS "project_columns_project_idx" ON "project_columns" ("project_id");--> statement-breakpoint
64
-CREATE INDEX IF NOT EXISTS "project_columns_project_order_idx" ON "project_columns" ("project_id", "order");--> statement-breakpoint
65
-CREATE INDEX IF NOT EXISTS "project_cards_column_idx" ON "project_cards" ("column_id");--> statement-breakpoint
66
-CREATE INDEX IF NOT EXISTS "project_cards_column_order_idx" ON "project_cards" ("column_id", "order");--> statement-breakpoint
67
-CREATE INDEX IF NOT EXISTS "project_cards_content_idx" ON "project_cards" ("content_type", "content_id");--> statement-breakpoint
68
-
69
-CREATE UNIQUE INDEX IF NOT EXISTS "wiki_pages_repo_slug_uniq" ON "wiki_pages" ("repository_id", "slug");--> statement-breakpoint
70
-CREATE INDEX IF NOT EXISTS "wiki_pages_repo_order_idx" ON "wiki_pages" ("repository_id", "order");--> statement-breakpoint
71
-CREATE INDEX IF NOT EXISTS "wiki_pages_parent_idx" ON "wiki_pages" ("parent_id");--> statement-breakpoint
72
-CREATE INDEX IF NOT EXISTS "wiki_revisions_page_created_idx" ON "wiki_revisions" ("page_id", "created_at");--> statement-breakpoint
73
-
74
-CREATE INDEX IF NOT EXISTS "security_scans_repo_idx" ON "security_scans" ("repository_id");--> statement-breakpoint
75
-CREATE INDEX IF NOT EXISTS "security_scans_repo_status_idx" ON "security_scans" ("repository_id", "status");--> statement-breakpoint
76
-CREATE INDEX IF NOT EXISTS "security_vulns_scan_severity_idx" ON "security_vulnerabilities" ("scan_id", "severity");--> statement-breakpoint
77
-CREATE INDEX IF NOT EXISTS "security_vulns_vuln_id_idx" ON "security_vulnerabilities" ("vulnerability_id");--> statement-breakpoint
78
-
79
-ALTER TABLE "webhooks" ADD COLUMN IF NOT EXISTS "secret_ciphertext" text;--> statement-breakpoint
80
-ALTER TABLE "webhooks" ADD COLUMN IF NOT EXISTS "secret_hint" text;--> statement-breakpoint
81
-
82
-CREATE INDEX IF NOT EXISTS "automation_rules_trigger_enabled_idx" ON "automation_rules" ("trigger", "is_enabled");--> statement-breakpoint
83
-CREATE INDEX IF NOT EXISTS "automation_rules_repo_idx" ON "automation_rules" ("repository_id");--> statement-breakpoint
84
-CREATE INDEX IF NOT EXISTS "automation_rules_org_idx" ON "automation_rules" ("organization_id");--> statement-breakpoint
85
-CREATE INDEX IF NOT EXISTS "automation_executions_rule_created_idx" ON "automation_executions" ("rule_id", "created_at");--> statement-breakpoint
86
-CREATE INDEX IF NOT EXISTS "automation_executions_status_idx" ON "automation_executions" ("status");--> statement-breakpoint
87
-
88
-CREATE UNIQUE INDEX IF NOT EXISTS "notification_prefs_user_event_uniq" ON "notification_preferences" ("user_id", "event_type", "repository_id");--> statement-breakpoint
89
-CREATE INDEX IF NOT EXISTS "notification_prefs_repo_idx" ON "notification_preferences" ("repository_id");--> statement-breakpoint
90
-CREATE UNIQUE INDEX IF NOT EXISTS "notification_quiet_hours_user_uniq" ON "notification_quiet_hours" ("user_id");--> statement-breakpoint
91
-CREATE INDEX IF NOT EXISTS "push_subscriptions_user_active_idx" ON "push_subscriptions" ("user_id", "is_active");--> statement-breakpoint
92
-CREATE UNIQUE INDEX IF NOT EXISTS "push_subscriptions_endpoint_uniq" ON "push_subscriptions" ("endpoint");--> statement-breakpoint
93
-CREATE UNIQUE INDEX IF NOT EXISTS "email_digest_settings_user_uniq" ON "email_digest_settings" ("user_id");--> statement-breakpoint
94
-CREATE INDEX IF NOT EXISTS "email_digest_settings_digest_type_idx" ON "email_digest_settings" ("digest_type");--> statement-breakpoint
95
-
96
-CREATE INDEX IF NOT EXISTS "pr_state_defs_repo_order_idx" ON "pr_state_definitions" ("repository_id", "order");--> statement-breakpoint
97
-CREATE INDEX IF NOT EXISTS "pr_state_reviewers_state_idx" ON "pr_state_reviewers" ("state_definition_id");--> statement-breakpoint
98
-CREATE INDEX IF NOT EXISTS "pr_state_reviewers_user_idx" ON "pr_state_reviewers" ("user_id");--> statement-breakpoint
99
-CREATE INDEX IF NOT EXISTS "pr_state_reviewers_team_idx" ON "pr_state_reviewers" ("team_id");--> statement-breakpoint
100
-CREATE INDEX IF NOT EXISTS "pr_state_transitions_pr_created_idx" ON "pr_state_transitions" ("pull_request_id", "created_at");--> statement-breakpoint
101
-
102
-CREATE INDEX IF NOT EXISTS "branch_protection_repo_pattern_idx" ON "branch_protection" ("repository_id", "pattern");--> statement-breakpoint
103
-
104
-CREATE INDEX IF NOT EXISTS "deploy_keys_repo_idx" ON "deploy_keys" ("repository_id");--> statement-breakpoint
105
-CREATE UNIQUE INDEX IF NOT EXISTS "deploy_keys_fingerprint_uniq" ON "deploy_keys" ("fingerprint");--> statement-breakpoint
106
-
107
-CREATE UNIQUE INDEX IF NOT EXISTS "pipeline_runners_token_uniq" ON "pipeline_runners" ("token");--> statement-breakpoint
108
-CREATE INDEX IF NOT EXISTS "pipeline_runners_status_last_seen_idx" ON "pipeline_runners" ("status", "last_seen_at");--> statement-breakpoint
109
-CREATE INDEX IF NOT EXISTS "pipeline_runners_repo_idx" ON "pipeline_runners" ("repository_id");--> statement-breakpoint
110
-
111
-CREATE INDEX IF NOT EXISTS "external_ci_repo_idx" ON "external_ci_integrations" ("repository_id");--> statement-breakpoint
112
-CREATE UNIQUE INDEX IF NOT EXISTS "external_ci_token_hash_uniq" ON "external_ci_integrations" ("token_hash");--> statement-breakpoint
113
-
114
-CREATE INDEX IF NOT EXISTS "auto_merge_rules_repo_enabled_idx" ON "auto_merge_rules" ("repository_id", "is_enabled");--> statement-breakpoint
115
-
116
-CREATE INDEX IF NOT EXISTS "review_requirements_repo_idx" ON "review_requirements" ("repository_id");--> statement-breakpoint
117
-CREATE INDEX IF NOT EXISTS "reviewer_rules_repo_enabled_idx" ON "reviewer_rules" ("repository_id", "is_enabled");--> statement-breakpoint
118
-
119
-CREATE UNIQUE INDEX IF NOT EXISTS "security_policies_repo_uniq" ON "security_policies" ("repository_id");--> statement-breakpoint
120
-
121
-CREATE INDEX IF NOT EXISTS "sso_configs_org_idx" ON "sso_configs" ("organization_id");--> statement-breakpoint
122
-CREATE UNIQUE INDEX IF NOT EXISTS "sso_configs_issuer_uniq" ON "sso_configs" ("issuer");--> statement-breakpoint
123
-
124
-CREATE INDEX IF NOT EXISTS "repo_path_perms_repo_pattern_idx" ON "repository_path_permissions" ("repository_id", "path_pattern");--> statement-breakpoint
125
-CREATE INDEX IF NOT EXISTS "repo_path_perms_user_idx" ON "repository_path_permissions" ("user_id");--> statement-breakpoint
126
-CREATE INDEX IF NOT EXISTS "repo_path_perms_team_idx" ON "repository_path_permissions" ("team_id");--> statement-breakpoint
127
-
128
-CREATE INDEX IF NOT EXISTS "inbox_sections_user_position_idx" ON "inbox_sections" ("user_id", "position");--> statement-breakpoint
129
-CREATE INDEX IF NOT EXISTS "shared_inbox_sections_section_idx" ON "shared_inbox_sections" ("section_id");--> statement-breakpoint
130
-CREATE INDEX IF NOT EXISTS "shared_inbox_sections_user_idx" ON "shared_inbox_sections" ("shared_with_user_id");--> statement-breakpoint
131
-
132
-CREATE INDEX IF NOT EXISTS "custom_field_defs_repo_order_idx" ON "custom_field_definitions" ("repository_id", "order");--> statement-breakpoint
133
-CREATE INDEX IF NOT EXISTS "issue_custom_field_values_issue_idx" ON "issue_custom_field_values" ("issue_id");--> statement-breakpoint
134
-CREATE INDEX IF NOT EXISTS "issue_custom_field_values_field_idx" ON "issue_custom_field_values" ("field_id");--> statement-breakpoint
135
-CREATE UNIQUE INDEX IF NOT EXISTS "issue_custom_field_values_issue_field_uniq" ON "issue_custom_field_values" ("issue_id", "field_id");--> statement-breakpoint
136
-
137
-CREATE INDEX IF NOT EXISTS "issue_statuses_repo_order_idx" ON "issue_statuses" ("repository_id", "order");--> statement-breakpoint
138
-CREATE UNIQUE INDEX IF NOT EXISTS "issue_statuses_repo_name_uniq" ON "issue_statuses" ("repository_id", "name");--> statement-breakpoint
139
-
140
-CREATE INDEX IF NOT EXISTS "custom_roles_org_idx" ON "custom_roles" ("organization_id");--> statement-breakpoint
141
-
142
-CREATE INDEX IF NOT EXISTS "system_config_updated_by_idx" ON "system_config" ("updated_by_id");--> statement-breakpoint
143
-
144
-CREATE UNIQUE INDEX IF NOT EXISTS "pr_metrics_pr_uniq" ON "pr_metrics" ("pull_request_id");--> statement-breakpoint
145
-CREATE INDEX IF NOT EXISTS "pr_metrics_author_created_idx" ON "pr_metrics" ("author_id", "pr_created_at");--> statement-breakpoint
146
-CREATE INDEX IF NOT EXISTS "pr_metrics_repo_created_idx" ON "pr_metrics" ("repository_id", "pr_created_at");--> statement-breakpoint
147
-CREATE UNIQUE INDEX IF NOT EXISTS "review_metrics_user_week_uniq" ON "review_metrics" ("user_id", "week_of");--> statement-breakpoint
148
-CREATE UNIQUE INDEX IF NOT EXISTS "repo_metrics_repo_week_uniq" ON "repo_metrics" ("repository_id", "week_of");--> statement-breakpoint
149
-
150
-CREATE INDEX IF NOT EXISTS "ai_review_rules_repo_enabled_idx" ON "ai_review_rules" ("repository_id", "is_enabled");--> statement-breakpoint
151
-CREATE INDEX IF NOT EXISTS "ai_review_rules_org_idx" ON "ai_review_rules" ("organization_id");--> statement-breakpoint
152
-CREATE INDEX IF NOT EXISTS "ai_codebase_context_repo_idx" ON "ai_codebase_context" ("repository_id");--> statement-breakpoint
153
-
154
-CREATE UNIQUE INDEX IF NOT EXISTS "slack_workspaces_team_id_uniq" ON "slack_workspaces" ("team_id");--> statement-breakpoint
155
-CREATE INDEX IF NOT EXISTS "slack_channel_mappings_repo_idx" ON "slack_channel_mappings" ("repository_id");--> statement-breakpoint
156
-CREATE INDEX IF NOT EXISTS "slack_channel_mappings_workspace_idx" ON "slack_channel_mappings" ("workspace_id");--> statement-breakpoint
157
-CREATE UNIQUE INDEX IF NOT EXISTS "slack_user_mappings_user_workspace_uniq" ON "slack_user_mappings" ("user_id", "workspace_id");--> statement-breakpoint
158
0
▼make-admin.ts
+8-0
1
+import { getDatabase, schema } from "./src/db";
2
+async function main() {
3
+ const db = getDatabase();
4
+ await db.update(schema.users).set({ isAdmin: true });
5
+ console.log("Made all users admin.");
6
+ process.exit(0);
7
+}
8
+main();
0
9
▼out.json
+0-0
0
0
▼package.json
+5-0
56
56
"@dnd-kit/sortable": "^10.0.0",
57
57
"@dnd-kit/utilities": "^3.2.2",
58
58
"@google/genai": "^2.11.0",
59
+ "@iconify-json/file-icons": "^1.2.2",
60
+ "@iconify-json/vscode-icons": "^1.2.67",
59
61
"@libsql/client": "^0.15.15",
60
62
"@mistralai/mistralai": "^2.4.1",
61
63
"@monaco-editor/react": "^4.6.0",
92
94
"@upstash/redis": "^1.36.0",
93
95
"arctic": "^3.7.0",
94
96
"astro": "^4.11.0",
97
+ "astro-icon": "^1.1.5",
95
98
"bcryptjs": "^2.4.3",
96
99
"better-sqlite3": "^11.1.2",
97
100
"bullmq": "^5.65.1",
107
110
"github-markdown-css": "^5.8.1",
108
111
"graphql": "^16.12.0",
109
112
"graphql-yoga": "^5.18.0",
113
+ "highlight.js": "^11.11.1",
110
114
"ioredis": "^5.9.2",
111
115
"isomorphic-dompurify": "^2.35.0",
112
116
"isomorphic-git": "^1.25.10",
135
139
"react-dom": "^18.3.1",
136
140
"react-globe.gl": "^2.37.0",
137
141
"recharts": "^3.7.0",
142
+ "rehype-highlight": "^7.0.2",
138
143
"rehype-sanitize": "^6.0.0",
139
144
"rehype-stringify": "^10.0.1",
140
145
"remark-gfm": "^4.0.1",
141
146
▼protection.json
+27-0
1
+{
2
+ "required_status_checks": {
3
+ "strict": true,
4
+ "contexts": [
5
+ "Quality Gate",
6
+ "Lint & Type Check",
7
+ "Docs Parity",
8
+ "Security Gates",
9
+ "Unit & Integration Tests",
10
+ "E2E Tests (Playwright)",
11
+ "Container Security (Trivy)",
12
+ "SAST (Semgrep)",
13
+ "Performance Gate",
14
+ "Build"
15
+ ]
16
+ },
17
+ "enforce_admins": false,
18
+ "required_pull_request_reviews": {
19
+ "dismiss_stale_reviews": true,
20
+ "require_code_owner_reviews": false,
21
+ "required_approving_review_count": 1
22
+ },
23
+ "restrictions": null,
24
+ "required_linear_history": true,
25
+ "allow_force_pushes": false,
26
+ "allow_deletions": false
27
+}
0
28
▼render.yaml
+108-0
1
+# OpenCodeHub Render Blueprint
2
+# Deploy with: New → Blueprint → Select this repo
3
+#
4
+# Prerequisites:
5
+# 1. Create a free Upstash Redis at https://upstash.com (region: Asia Pacific)
6
+# 2. Copy the Redis URL
7
+#
8
+# After deploy:
9
+# 1. Set REDIS_URL from Upstash in Environment tab
10
+# 2. Update SITE_URL with your actual Render URL
11
+# 3. Visit your URL → /setup to create admin account
12
+
13
+services:
14
+ # ── Web Service (App + API + Git HTTP) ──────────────────────
15
+ - type: web
16
+ name: opencodehub
17
+ runtime: image
18
+ image:
19
+ url: ghcr.io/swadhinbiswas/opencodehub:latest
20
+ plan: free
21
+ region: singapore
22
+ healthCheckPath: /api/health
23
+ envVars:
24
+ # ── Server ──
25
+ - key: NODE_ENV
26
+ value: production
27
+ - key: PORT
28
+ value: "4321"
29
+ - key: HOST
30
+ value: "0.0.0.0"
31
+ - key: SITE_URL
32
+ # TODO: Update after first deploy with your actual Render URL
33
+ # Example: https://opencodehub.onrender.com
34
+ sync: false
35
+
36
+ # ── Database (Render PostgreSQL) ──
37
+ - key: DATABASE_DRIVER
38
+ value: postgres
39
+ - key: DATABASE_URL
40
+ fromDatabase:
41
+ name: opencodehub-db
42
+ property: connectionString
43
+ - key: DATABASE_SSL
44
+ value: "true"
45
+
46
+ # ── Redis (Upstash - Free) ──
47
+ # TODO: Create free Redis at https://upstash.com (region: Singapore)
48
+ # Then paste the Redis URL here:
49
+ - key: REDIS_URL
50
+ sync: false
51
+
52
+ # ── Secrets (Auto-generated) ──
53
+ - key: JWT_SECRET
54
+ generateValue: true
55
+ - key: SESSION_SECRET
56
+ generateValue: true
57
+ - key: INTERNAL_HOOK_SECRET
58
+ generateValue: true
59
+ - key: CRON_SECRET
60
+ generateValue: true
61
+ - key: RUNNER_SECRET
62
+ generateValue: true
63
+ - key: AI_CONFIG_ENCRYPTION_KEY
64
+ generateValue: true
65
+ - key: WORKFLOW_SECRET_ENCRYPTION_KEY
66
+ generateValue: true
67
+
68
+ # ── Storage ──
69
+ - key: STORAGE_TYPE
70
+ value: local
71
+ - key: GIT_REPOS_PATH
72
+ value: /data/repositories
73
+ - key: STORAGE_PATH
74
+ value: /data/storage
75
+
76
+ # ── Features ──
77
+ - key: ENABLE_REGISTRATION
78
+ value: "true"
79
+ - key: ENABLE_WIKI
80
+ value: "true"
81
+ - key: ENABLE_ISSUES
82
+ value: "true"
83
+ - key: ENABLE_ACTIONS
84
+ value: "true"
85
+ - key: ENABLE_AI_ASSISTANT
86
+ value: "true"
87
+
88
+ # ── Rate Limiting ──
89
+ - key: RATE_LIMIT_ENABLED
90
+ value: "true"
91
+
92
+ # ── Logging ──
93
+ - key: LOG_LEVEL
94
+ value: info
95
+
96
+ # Persistent disk for git repos, storage, SSH keys
97
+ disk:
98
+ name: opencodehub-data
99
+ mountPath: /data
100
+ sizeGB: 1
101
+
102
+# ── PostgreSQL Database (Render Free) ────────────────────────
103
+databases:
104
+ - name: opencodehub-db
105
+ plan: free
106
+ databaseName: opencodehub
107
+ user: opencodehub
108
+ region: singapore
0
109
▼scripts/ssh-server.ts
+20-12
7
7
import { uploadRepoToStorage, getStorageRepoPath, isCloudStorage } from "@/lib/git-storage";
8
8
9
9
const DATA_DIR = join(process.cwd(), "data");
10
-const REPOS_DIR = join(DATA_DIR, "repos");
10
+const REPOS_DIR = process.env.GIT_REPOS_PATH || join(DATA_DIR, "repositories");
11
11
const SSH_DIR = join(DATA_DIR, "ssh");
12
-const HOST_KEY_PATH = join(SSH_DIR, "ssh_host_rsa_key");
12
+const HOST_KEY_PATH = process.env.GIT_SSH_HOST_KEY || join(SSH_DIR, "host_key");
13
+const SSH_PORT = parseInt(process.env.GIT_SSH_PORT || "2222", 10);
13
14
14
15
async function main() {
15
- console.log("Starting OpenCodeHub SSH Server...");
16
+ console.log(`Starting OpenCodeHub SSH Server on port ${SSH_PORT}...`);
17
+ console.log(`Repository path: ${REPOS_DIR}`);
16
18
17
19
await startSSHServer({
18
- port: 2222,
20
+ port: SSH_PORT,
19
21
hostKeyPath: HOST_KEY_PATH,
20
22
reposPath: REPOS_DIR,
21
23
authenticateUser: async (_, key) => {
23
25
const db = getDatabase();
24
26
25
27
// Iterate over all keys to find a match
26
- // In a production app, we would index by fingerprint or key type to optimize
27
28
const allKeys = await db.query.sshKeys.findMany();
28
29
29
30
for (const dbKey of allKeys) {
75
76
76
77
if (!repo) return false;
77
78
79
+ // Check if user is admin
80
+ const user = await db.query.users.findFirst({
81
+ where: eq(schema.users.id, userId)
82
+ });
83
+ const isAdmin = user?.isAdmin === true;
84
+
78
85
if (operation === "read") {
79
- return canReadRepo(userId, repo);
86
+ return canReadRepo(userId, repo, { isAdmin });
80
87
} else {
81
- return canWriteRepo(userId, repo);
88
+ return canWriteRepo(userId, repo, { isAdmin });
82
89
}
83
90
},
84
91
onPush: async (userId, repoPath, refs) => {
104
111
105
112
// 2. Trigger Workflows
106
113
import("@/lib/workflows").then(({ triggerRepoWorkflows }) => {
107
- refs.forEach(refLine => {
108
- const [, newSha, refName] = refLine.split(" ");
109
- // Only trigger on branch updates (not deletions or tags for now unless supported)
110
- if (newSha !== "0000000000000000000000000000000000000000" && refName.startsWith("refs/heads/")) {
111
- triggerRepoWorkflows(repo.id, newSha, refName, userId).catch(console.error);
114
+ refs.forEach(refName => {
115
+ // refs contains ref names like "refs/heads/main"
116
+ if (refName.startsWith("refs/heads/")) {
117
+ // We need to get the latest commit SHA for this ref
118
+ // For now, trigger with a placeholder - the workflow engine will resolve it
119
+ triggerRepoWorkflows(repo.id, "HEAD", refName, userId).catch(console.error);
112
120
}
113
121
});
114
122
});
115
123
▼scripts/worker.ts
+5-4
44
44
const db = getDatabase();
45
45
46
46
try {
47
- // 1. Reclaim stale jobs (stuck in "processing" too long)
47
+ // 1. Reclaim stale jobs (stuck in "running" too long)
48
48
try {
49
49
const staleThreshold = new Date(Date.now() - STALE_JOB_TIMEOUT_MS);
50
50
const staleItems = await db.query.mergeQueueItems.findMany({
51
- where: eq(schema.mergeQueueItems.status, "processing"),
51
+ where: eq(schema.mergeQueueItems.status, "running"),
52
52
columns: { id: true, startedAt: true },
53
53
});
54
54
for (const item of staleItems) {
57
57
.update(schema.mergeQueueItems)
58
58
.set({ status: "queued" })
59
59
.where(eq(schema.mergeQueueItems.id, item.id));
60
+ logger.warn({ itemId: item.id }, "Reclaimed stale queue item");
60
61
}
61
62
}
62
63
} catch {
169
170
logger.info({ signal }, "Worker shutting down gracefully...");
170
171
healthServer.close();
171
172
172
- // Give in-flight jobs up to 30s to complete
173
- await sleep(2000);
173
+ // Give in-flight jobs up to 10s to complete
174
+ await sleep(10_000);
174
175
logger.info("Worker shutdown complete.");
175
176
process.exit(0);
176
177
};
177
178
▼src/components/ThemeToggle.tsx
+68-37
1
-import { Monitor, Moon, Sun } from "lucide-react";
1
+"use client";
2
+
3
+import { Monitor, Moon, Sun, Palette } from "lucide-react";
2
4
import { useEffect, useState } from "react";
5
+import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+} from "@/components/ui/dropdown-menu";
3
11
4
-type Theme = "light" | "dark" | "system";
12
+type Theme = "light" | "dark" | "system" | "github-dark" | "dracula" | "rose-pine" | "tokyo-night";
5
13
6
14
function getStoredTheme(): Theme {
7
15
if (typeof window === "undefined") return "dark";
8
16
return (localStorage.getItem("theme") as Theme) || "system";
9
17
}
10
18
11
-function resolveTheme(theme: Theme): "light" | "dark" {
12
- if (theme === "system") {
13
- return window.matchMedia("(prefers-color-scheme: dark)").matches
14
- ? "dark"
15
- : "light";
16
- }
17
- return theme;
18
-}
19
-
20
19
function applyTheme(theme: Theme) {
21
- const resolved = resolveTheme(theme);
22
20
const html = document.documentElement;
23
- if (resolved === "dark") {
24
- html.classList.add("dark");
25
- } else {
21
+ const isSystemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
22
+ const activeTheme = theme === "system" ? (isSystemDark ? "dark" : "light") : theme;
23
+
24
+ if (activeTheme === "light") {
26
25
html.classList.remove("dark");
26
+ html.removeAttribute("data-theme");
27
+ } else {
28
+ html.classList.add("dark");
29
+ if (activeTheme !== "dark" && activeTheme !== "system") {
30
+ html.setAttribute("data-theme", activeTheme);
31
+ } else {
32
+ html.removeAttribute("data-theme");
33
+ }
27
34
}
28
35
}
29
36
30
37
export function ThemeToggle() {
31
- const [theme, setTheme] = useState<Theme>("dark"); // SSR default
38
+ const [theme, setTheme] = useState<Theme>("dark");
32
39
33
40
useEffect(() => {
34
41
const stored = getStoredTheme();
35
42
setTheme(stored);
36
43
applyTheme(stored);
37
44
38
- // Listen for system changes
39
45
const mq = window.matchMedia("(prefers-color-scheme: dark)");
40
46
const handler = () => {
41
47
if (getStoredTheme() === "system") {
46
52
return () => mq.removeEventListener("change", handler);
47
53
}, []);
48
54
49
- const cycleTheme = () => {
50
- const order: Theme[] = ["light", "dark", "system"];
51
- const idx = order.indexOf(theme);
52
- const next = order[(idx + 1) % order.length];
53
- setTheme(next);
54
- localStorage.setItem("theme", next);
55
- applyTheme(next);
55
+ const changeTheme = (newTheme: Theme) => {
56
+ setTheme(newTheme);
57
+ localStorage.setItem("theme", newTheme);
58
+ applyTheme(newTheme);
56
59
};
57
60
58
- const Icon = theme === "dark" ? Moon : theme === "light" ? Sun : Monitor;
59
- const label =
60
- theme === "dark" ? "Dark" : theme === "light" ? "Light" : "System";
61
-
62
61
return (
63
- <button
64
- onClick={cycleTheme}
65
- className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
66
- title={`Theme: ${label}. Click to cycle.`}
67
- aria-label={`Current theme: ${label}`}
68
- >
69
- <Icon className="h-4 w-4" />
70
- <span className="sr-only">{label}</span>
71
- </button>
62
+ <DropdownMenu>
63
+ <DropdownMenuTrigger asChild>
64
+ <button
65
+ className="inline-flex items-center justify-center h-9 w-9 rounded-full border border-border/50 bg-background transition-all hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
66
+ aria-label="Toggle theme"
67
+ >
68
+ <Palette className="h-4 w-4" />
69
+ <span className="sr-only">Toggle theme</span>
70
+ </button>
71
+ </DropdownMenuTrigger>
72
+ <DropdownMenuContent align="end" className="w-48 bg-popover border-border/50 backdrop-blur-xl">
73
+ <DropdownMenuItem onClick={() => changeTheme("light")} className="cursor-pointer">
74
+ <Sun className="mr-2 h-4 w-4" />
75
+ <span>Light</span>
76
+ </DropdownMenuItem>
77
+ <DropdownMenuItem onClick={() => changeTheme("dark")} className="cursor-pointer">
78
+ <Moon className="mr-2 h-4 w-4" />
79
+ <span>Default Dark</span>
80
+ </DropdownMenuItem>
81
+ <DropdownMenuItem onClick={() => changeTheme("github-dark")} className="cursor-pointer">
82
+ <span className="mr-2 h-4 w-4 rounded-full bg-card border border-border/80"></span>
83
+ <span>GitHub Dark</span>
84
+ </DropdownMenuItem>
85
+ <DropdownMenuItem onClick={() => changeTheme("dracula")} className="cursor-pointer">
86
+ <span className="mr-2 h-4 w-4 rounded-full bg-[#282a36] border border-border/80"></span>
87
+ <span>Dracula</span>
88
+ </DropdownMenuItem>
89
+ <DropdownMenuItem onClick={() => changeTheme("rose-pine")} className="cursor-pointer">
90
+ <span className="mr-2 h-4 w-4 rounded-full bg-[#191724] border border-border/80"></span>
91
+ <span>Rosé Pine</span>
92
+ </DropdownMenuItem>
93
+ <DropdownMenuItem onClick={() => changeTheme("tokyo-night")} className="cursor-pointer">
94
+ <span className="mr-2 h-4 w-4 rounded-full bg-[#1a1b26] border border-border/80"></span>
95
+ <span>Tokyo Night</span>
96
+ </DropdownMenuItem>
97
+ <DropdownMenuItem onClick={() => changeTheme("system")} className="cursor-pointer border-t mt-1 pt-1">
98
+ <Monitor className="mr-2 h-4 w-4" />
99
+ <span>System</span>
100
+ </DropdownMenuItem>
101
+ </DropdownMenuContent>
102
+ </DropdownMenu>
72
103
);
73
104
}
74
105
75
106
▼src/components/actions/PipelineVisualizer.tsx
+2-2
126
126
};
127
127
128
128
const getNodeStyles = (status: string, conclusion: string | null, isActive: boolean) => {
129
- let base = "bg-white/5 border-white/10 text-slate-300 hover:bg-white/10";
129
+ let base = "bg-secondary border-border text-slate-300 hover:bg-secondary/80";
130
130
let shadow = "";
131
131
132
132
if (status === "queued") {
152
152
return (
153
153
<div
154
154
ref={containerRef}
155
- className="relative w-full overflow-x-auto overflow-y-hidden p-8 flex gap-20 min-h-[300px] rounded-2xl border border-white/10 bg-[#06090e] shadow-2xl"
155
+ className="relative w-full overflow-x-auto overflow-y-hidden p-8 flex gap-20 min-h-[300px] rounded-2xl border border-border bg-[#06090e] shadow-2xl"
156
156
style={{
157
157
backgroundImage: `
158
158
radial-gradient(circle at 50% 0%, rgba(59, 130, 246, 0.05) 0%, transparent 70%),
159
159
▼src/components/admin/AdminDashboard.tsx
+4-4
28
28
if (this.state.hasError) {
29
29
return (
30
30
<div className="w-full h-full flex items-center justify-center">
31
- <div className="text-center text-white/50">
31
+ <div className="text-center text-foreground/50">
32
32
<div className="text-6xl mb-4">🌐</div>
33
33
<p className="text-sm">3D Globe unavailable</p>
34
34
<p className="text-xs mt-1 opacity-50">WebGL may not be supported</p>
43
43
const GlobeViz = lazy(() => import("./GlobeViz").catch(() => ({
44
44
default: () => (
45
45
<div className="w-full h-full flex items-center justify-center">
46
- <div className="text-center text-white/50">
46
+ <div className="text-center text-foreground/50">
47
47
<div className="text-6xl mb-4">🌐</div>
48
48
<p className="text-sm">Globe failed to load</p>
49
49
</div>
88
88
<GlobeErrorBoundary>
89
89
<Suspense fallback={
90
90
<div className="w-full h-full flex items-center justify-center">
91
- <div className="text-white/30 text-sm">Loading globe...</div>
91
+ <div className="text-foreground/30 text-sm">Loading globe...</div>
92
92
</div>
93
93
}>
94
94
<GlobeViz activities={data.activityLog || []} />
139
139
{/* Right Column: Log Stream */}
140
140
<div className="col-span-3 flex flex-col gap-4 pointer-events-auto overflow-hidden">
141
141
<div className="flex justify-end shrink-0">
142
- <GitGraph className="h-8 w-8 text-white opacity-30 hover:opacity-70 transition-opacity cursor-pointer" />
142
+ <GitGraph className="h-8 w-8 text-foreground opacity-30 hover:opacity-70 transition-opacity cursor-pointer" />
143
143
</div>
144
144
145
145
<div className="glass-panel rounded-xl flex-1 overflow-hidden min-h-0 relative">
146
146
▼src/components/admin/AlertBanner.tsx
+3-3
90
90
</span>
91
91
92
92
{/* Timestamp */}
93
- <span className="text-xs text-gray-500 font-mono">
93
+ <span className="text-xs text-muted-foreground font-mono">
94
94
{alert.timestamp}
95
95
</span>
96
96
97
97
{/* Dismiss button */}
98
98
<button
99
99
onClick={() => setDismissedIds(prev => new Set([...prev, alert.id]))}
100
- className="p-1 rounded hover:bg-white/10 transition-colors"
100
+ className="p-1 rounded hover:bg-secondary/80 transition-colors"
101
101
>
102
- <X className="w-3 h-3 text-gray-500 hover:text-gray-300" />
102
+ <X className="w-3 h-3 text-muted-foreground hover:text-muted-foreground" />
103
103
</button>
104
104
</div>
105
105
</div>
106
106
▼src/components/admin/BottomPanel.tsx
+6-6
100
100
</span>
101
101
</div>
102
102
</div>
103
- <div className="text-xs text-gray-500 mt-3 flex items-center gap-1.5">
103
+ <div className="text-xs text-muted-foreground mt-3 flex items-center gap-1.5">
104
104
<Globe className="h-3 w-3" />
105
105
Global repository statistics (Last 24h)
106
106
</div>
185
185
<div className="text-5xl font-mono font-light bg-gradient-to-r from-purple-300 via-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
186
186
<AnimatedNumber value={stats.reviewStats?.count || 156} />
187
187
</div>
188
- <div className="text-xs text-gray-500 mt-3 flex items-center gap-1.5">
188
+ <div className="text-xs text-muted-foreground mt-3 flex items-center gap-1.5">
189
189
<Sparkles className="h-3 w-3 text-purple-400/60" />
190
190
Active participants in code review
191
191
</div>
234
234
{u?.avatar ? (
235
235
<img src={u.avatar} className="h-full w-full object-cover" />
236
236
) : (
237
- <span className="text-sm font-bold text-white">{String.fromCharCode(65 + i)}</span>
237
+ <span className="text-sm font-bold text-foreground">{String.fromCharCode(65 + i)}</span>
238
238
)}
239
239
</motion.div>
240
240
))}
260
260
<div className="space-y-3 max-w-4xl w-full">
261
261
{(stats.languages || []).map((lang: any, i: number) => (
262
262
<div key={lang.name} className="flex items-center gap-4">
263
- <div className="w-24 text-sm text-gray-300 font-mono">{lang.name}</div>
264
- <div className="flex-1 h-2 bg-white/5 rounded-full overflow-hidden">
263
+ <div className="w-24 text-sm text-muted-foreground font-mono">{lang.name}</div>
264
+ <div className="flex-1 h-2 bg-secondary rounded-full overflow-hidden">
265
265
<motion.div
266
266
initial={{ width: 0 }}
267
267
animate={{ width: `${lang.percentage}%` }}
273
273
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
274
274
</motion.div>
275
275
</div>
276
- <div className="w-12 text-sm text-gray-400 text-right font-mono">{lang.percentage}%</div>
276
+ <div className="w-12 text-sm text-muted-foreground text-right font-mono">{lang.percentage}%</div>
277
277
</div>
278
278
))}
279
279
</div>
280
280
▼src/components/admin/GlobeViz.tsx
+2-2
21
21
if (this.state.hasError) {
22
22
return (
23
23
<div className="w-full h-full flex items-center justify-center">
24
- <div className="text-center text-white/50">
24
+ <div className="text-center text-foreground/50">
25
25
<div className="text-6xl mb-4">🌐</div>
26
26
<p className="text-sm">Globe unavailable</p>
27
27
</div>
178
178
if (!mounted || webglSupported === null) {
179
179
return (
180
180
<div className="w-full h-full flex items-center justify-center">
181
- <div className="text-white/30 text-sm">Loading globe...</div>
181
+ <div className="text-foreground/30 text-sm">Loading globe...</div>
182
182
</div>
183
183
);
184
184
}
185
185
▼src/components/admin/LiveLogStream.tsx
+8-8
23
23
case "fork": return <GitFork className={`${iconClass} text-purple-400`} />;
24
24
case "star": return <Star className={`${iconClass} text-yellow-400`} />;
25
25
case "comment": return <MessageSquare className={`${iconClass} text-cyan-400`} />;
26
- default: return <Radio className={`${iconClass} text-gray-400`} />;
26
+ default: return <Radio className={`${iconClass} text-muted-foreground`} />;
27
27
}
28
28
};
29
29
34
34
case "fork": return "text-purple-400";
35
35
case "star": return "text-yellow-400";
36
36
case "comment": return "text-cyan-400";
37
- default: return "text-gray-400";
37
+ default: return "text-muted-foreground";
38
38
}
39
39
};
40
40
74
74
<h3 className="text-sm font-semibold bg-gradient-to-r from-gray-100 to-gray-400 bg-clip-text text-transparent">
75
75
Data Stream
76
76
</h3>
77
- <div className="text-[10px] text-gray-500 uppercase tracking-wider">Real-Time</div>
77
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wider">Real-Time</div>
78
78
</div>
79
79
</div>
80
- <div className="flex items-center gap-2 text-xs text-gray-500 font-mono">
80
+ <div className="flex items-center gap-2 text-xs text-muted-foreground font-mono">
81
81
<Clock className="w-3 h-3" />
82
82
{currentTime}
83
83
</div>
96
96
className={`relative overflow-hidden rounded-lg bg-gradient-to-r ${getTypeBg(log.type)} p-2`}
97
97
>
98
98
{/* Timestamp */}
99
- <div className="text-[10px] text-gray-600 font-mono mb-1">
99
+ <div className="text-[10px] text-muted-foreground font-mono mb-1">
100
100
{log.timestamp}
101
101
</div>
102
102
110
110
{/* Details */}
111
111
<div className="flex-1 min-w-0 text-xs">
112
112
<div className="flex flex-wrap gap-x-1.5 items-center">
113
- <span className="text-gray-600 font-mono">#{log.id.substring(0, 7)}</span>
114
- <span className="text-gray-600">:</span>
113
+ <span className="text-muted-foreground font-mono">#{log.id.substring(0, 7)}</span>
114
+ <span className="text-muted-foreground">:</span>
115
115
<span className={`font-semibold ${getTypeColor(log.type)}`}>
116
116
{log.type.toUpperCase().replace("_", " ")}
117
117
</span>
118
118
</div>
119
- <div className="text-gray-400 truncate mt-0.5">
119
+ <div className="text-muted-foreground truncate mt-0.5">
120
120
{log.message}
121
121
</div>
122
122
</div>
123
123
▼src/components/admin/QuickStatsRow.tsx
+2-2
94
94
{/* Glow effect */}
95
95
<div className={`absolute -inset-1 ${widget.bgGlow} rounded-xl blur-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300`} />
96
96
97
- <div className="relative glass-panel rounded-xl px-4 py-3 flex items-center gap-3 hover:border-white/10 transition-colors cursor-default">
97
+ <div className="relative glass-panel rounded-xl px-4 py-3 flex items-center gap-3 hover:border-border transition-colors cursor-default">
98
98
{/* Icon with gradient background */}
99
99
<div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${widget.color} p-[1px]`}>
100
100
<div className="w-full h-full rounded-lg bg-[#0a0a15] flex items-center justify-center">
107
107
<div className={`text-xl font-bold font-mono bg-gradient-to-r ${widget.color} bg-clip-text text-transparent`}>
108
108
<AnimatedNumber value={value} />
109
109
</div>
110
- <div className="text-[10px] uppercase tracking-wider text-gray-500">
110
+ <div className="text-[10px] uppercase tracking-wider text-muted-foreground">
111
111
{widget.label}
112
112
</div>
113
113
</div>
114
114
▼src/components/admin/RecentActivity.tsx
+7-7
29
29
<div className="glass-panel rounded-xl p-4 w-72">
30
30
<div className="flex items-center gap-2 mb-4">
31
31
<Clock className="w-4 h-4 text-cyan-400" />
32
- <h3 className="text-sm font-semibold text-white">Recent Activity</h3>
32
+ <h3 className="text-sm font-semibold text-foreground">Recent Activity</h3>
33
33
</div>
34
- <div className="text-center py-4 text-gray-500 text-sm">
34
+ <div className="text-center py-4 text-muted-foreground text-sm">
35
35
No recent activity
36
36
</div>
37
37
</div>
46
46
>
47
47
<Clock className="w-4 h-4 text-cyan-400" />
48
48
</motion.div>
49
- <h3 className="text-sm font-semibold text-white">Recent Activity</h3>
49
+ <h3 className="text-sm font-semibold text-foreground">Recent Activity</h3>
50
50
</div>
51
51
52
52
<div className="space-y-3">
68
68
69
69
{/* Content */}
70
70
<div className="flex-1 min-w-0">
71
- <div className="text-xs text-gray-300 truncate">
72
- <span className="font-medium text-white">{activity.user}</span>
73
- <span className="text-gray-500"> {activity.action} </span>
71
+ <div className="text-xs text-muted-foreground truncate">
72
+ <span className="font-medium text-foreground">{activity.user}</span>
73
+ <span className="text-muted-foreground"> {activity.action} </span>
74
74
<span className="text-cyan-400">{activity.target}</span>
75
75
</div>
76
- <div className="text-[10px] text-gray-600 mt-0.5">
76
+ <div className="text-[10px] text-muted-foreground mt-0.5">
77
77
{activity.time}
78
78
</div>
79
79
</div>
80
80
▼src/components/admin/SystemStatus.tsx
+7-7
73
73
<div className="absolute inset-0 flex flex-col items-center justify-center">
74
74
<Icon className={`w-4 h-4 mb-1`} style={{ color: gradientColors[0] }} />
75
75
<motion.span
76
- className="text-2xl font-bold font-mono text-white"
76
+ className="text-2xl font-bold font-mono text-foreground"
77
77
initial={{ opacity: 0 }}
78
78
animate={{ opacity: 1 }}
79
79
transition={{ delay: 0.5 }}
80
80
>
81
81
{value}%
82
82
</motion.span>
83
- <span className="text-[10px] text-gray-400 uppercase tracking-wider mt-0.5">{label}</span>
83
+ <span className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">{label}</span>
84
84
</div>
85
85
</motion.div>
86
86
);
120
120
<div className="relative glass-panel px-6 py-4 rounded-2xl flex items-center justify-between gap-6 text-xs">
121
121
{/* Storage Linear Bar */}
122
122
<div className="flex-1">
123
- <div className="flex justify-between mb-1.5 text-gray-400">
123
+ <div className="flex justify-between mb-1.5 text-muted-foreground">
124
124
<span className="flex items-center gap-1.5">
125
125
<HardDrive className="w-3 h-3 text-emerald-400" />
126
126
<span className="text-emerald-400/80">Storage</span>
127
127
</span>
128
128
<span className="text-emerald-400 font-mono">{status.storageUsage}%</span>
129
129
</div>
130
- <div className="h-2 bg-white/5 rounded-full overflow-hidden">
130
+ <div className="h-2 bg-secondary rounded-full overflow-hidden">
131
131
<motion.div
132
132
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-cyan-500"
133
133
initial={{ width: 0 }}
141
141
142
142
{/* Uptime */}
143
143
<div className="flex flex-col items-center">
144
- <span className="text-gray-500 uppercase tracking-widest text-[10px] mb-1">Uptime</span>
144
+ <span className="text-muted-foreground uppercase tracking-widest text-[10px] mb-1">Uptime</span>
145
145
<div className="flex items-center gap-1.5">
146
146
<Activity className="w-3 h-3 text-blue-400" />
147
- <span className="font-mono text-white font-medium">{status.uptime}</span>
147
+ <span className="font-mono text-foreground font-medium">{status.uptime}</span>
148
148
</div>
149
149
</div>
150
150
152
152
153
153
{/* Runners */}
154
154
<div className="flex flex-col items-center">
155
- <span className="text-gray-500 uppercase tracking-widest text-[10px] mb-1">Runners</span>
155
+ <span className="text-muted-foreground uppercase tracking-widest text-[10px] mb-1">Runners</span>
156
156
<div className="flex items-center gap-2">
157
157
<motion.span
158
158
className="relative flex h-2.5 w-2.5"
159
159
▼src/components/admin/TrendingDevelopers.tsx
+4-4
17
17
if (rank === 1) return <Flame className="w-4 h-4 text-orange-400" />;
18
18
if (rank === 2) return <Star className="w-4 h-4 text-yellow-400" />;
19
19
if (rank === 3) return <TrendingUp className="w-4 h-4 text-green-400" />;
20
- return <span className="text-xs font-mono text-gray-500 w-4 text-center">{rank}</span>;
20
+ return <span className="text-xs font-mono text-muted-foreground w-4 text-center">{rank}</span>;
21
21
};
22
22
23
23
const getGradient = (index: number) => {
56
56
{/* Hover glow effect */}
57
57
<div className={`absolute -inset-1 bg-gradient-to-r ${getGradient(i)} rounded-lg opacity-0 group-hover:opacity-20 blur-md transition-opacity duration-300`} />
58
58
59
- <div className="relative flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition-colors">
59
+ <div className="relative flex items-center gap-3 p-2 rounded-lg hover:bg-accent transition-colors">
60
60
{/* Rank */}
61
61
<div className="w-6 flex justify-center">
62
62
{getRankIcon(dev.rank)}
65
65
{/* Avatar with gradient border */}
66
66
<div className={`relative h-10 w-10 rounded-full p-[2px] bg-gradient-to-br ${getGradient(i)}`}>
67
67
<div className="h-full w-full rounded-full bg-[#0a0a15] flex items-center justify-center">
68
- <span className="text-sm font-bold text-white">
68
+ <span className="text-sm font-bold text-foreground">
69
69
{dev.name[0].toUpperCase()}
70
70
</span>
71
71
</div>
73
73
74
74
{/* Info */}
75
75
<div className="flex-1 min-w-0">
76
- <div className="text-sm text-gray-200 font-medium group-hover:text-white transition-colors truncate">
76
+ <div className="text-sm text-gray-200 font-medium group-hover:text-foreground transition-colors truncate">
77
77
{dev.name}
78
78
</div>
79
79
<div className="flex items-center gap-2 mt-1">
80
80
▼src/components/dashboard/ActivityStream.tsx
+4-4
28
28
];
29
29
30
30
return (
31
- <div className={cn("w-full relative overflow-hidden rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur-xl transition-all hover:bg-white/10 hover:border-white/20", className)}>
31
+ <div className={cn("w-full relative overflow-hidden rounded-xl border border-border bg-secondary p-6 backdrop-blur-xl transition-all hover:bg-secondary/80 hover:border-border/80", className)}>
32
32
{/* Subtle background glow effect using CSS gradients */}
33
33
<div className="absolute -top-24 -right-24 w-48 h-48 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none" />
34
34
<div className="absolute -bottom-24 -left-24 w-48 h-48 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
79
79
<div className="flex items-center -space-x-4 pl-4">
80
80
{users.slice(0, 5).map((user, i) => (
81
81
<div key={user.id} className="relative group transition-transform hover:-translate-y-1 hover:z-20">
82
- <Avatar className="w-12 h-12 border-2 border-[#09090b] ring-1 ring-white/10 transition-shadow group-hover:ring-indigo-500/50">
82
+ <Avatar className="w-12 h-12 border-2 border-[#09090b] ring-1 ring-border transition-shadow group-hover:ring-indigo-500/50">
83
83
<AvatarImage src={user.avatarUrl} alt={user.username} className="object-cover" />
84
84
<AvatarFallback className="text-xs bg-zinc-900 font-medium text-zinc-400">
85
85
{user.username.slice(0, 2).toUpperCase()}
86
86
</AvatarFallback>
87
87
</Avatar>
88
88
{/* Tooltip on hover */}
89
- <div className="absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/90 text-white text-[10px] px-2 py-1 rounded whitespace-nowrap pointer-events-none border border-white/10">
89
+ <div className="absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/90 text-foreground text-[10px] px-2 py-1 rounded whitespace-nowrap pointer-events-none border border-border">
90
90
@{user.username}
91
91
</div>
92
92
</div>
93
93
))}
94
94
{users.length > 5 && (
95
95
<div className="relative z-0 group">
96
- <div className="w-12 h-12 rounded-full bg-[#09090b] border-2 border-[#09090b] ring-1 ring-white/10 flex items-center justify-center text-xs font-medium text-zinc-400 group-hover:border-indigo-500/50 transition-colors">
96
+ <div className="w-12 h-12 rounded-full bg-[#09090b] border-2 border-[#09090b] ring-1 ring-border flex items-center justify-center text-xs font-medium text-zinc-400 group-hover:border-indigo-500/50 transition-colors">
97
97
+{users.length - 5}
98
98
</div>
99
99
</div>
100
100
▼src/components/dashboard/StackCard.astro
+7-7
27
27
const hasFailures = stack.prs.some(pr => pr.status === 'failure' || pr.status === 'error');
28
28
const isDraft = stack.prs.some(pr => pr.status === 'draft');
29
29
30
-const statusColor = isFullyMerged ? "text-green-400" : hasFailures ? "text-red-400" : isDraft ? "text-gray-400" : "text-blue-400";
30
+const statusColor = isFullyMerged ? "text-green-400" : hasFailures ? "text-red-400" : isDraft ? "text-muted-foreground" : "text-blue-400";
31
31
const statusBorder = isFullyMerged ? "border-green-500/30" : hasFailures ? "border-red-500/30" : isDraft ? "border-gray-500/30" : "border-blue-500/30";
32
32
const statusBg = isFullyMerged ? "bg-green-500/10" : hasFailures ? "bg-red-500/10" : isDraft ? "bg-gray-500/10" : "bg-blue-500/10";
33
33
---
34
34
35
35
<div class="relative group">
36
36
<div class="absolute -inset-[1px] bg-gradient-to-r from-blue-500/20 via-cyan-500/20 to-purple-500/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity" />
37
- <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-5 hover:bg-white/[0.02] transition-colors">
37
+ <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-5 hover:bg-accent/50 transition-colors">
38
38
39
39
<!-- Header -->
40
40
<div class="flex items-center justify-between mb-4">
41
41
<div>
42
42
<div class="flex items-center gap-2">
43
43
<span class={`text-sm font-semibold ${statusColor}`}>{stack.name}</span>
44
- <span class="text-xs text-gray-500">in {stack.repoOwner}/{stack.repoName}</span>
44
+ <span class="text-xs text-muted-foreground">in {stack.repoOwner}/{stack.repoName}</span>
45
45
</div>
46
- <div class="text-xs text-gray-600 mt-0.5">
46
+ <div class="text-xs text-muted-foreground mt-0.5">
47
47
Updated {new Date(stack.updatedAt).toLocaleDateString()}
48
48
</div>
49
49
</div>
67
67
? "bg-green-500 border-green-500"
68
68
: pr.isClosed
69
69
? "bg-red-500 border-red-500"
70
- : "bg-[#0d1117] border-blue-500"
70
+ : "bg-card border-blue-500"
71
71
}`} />
72
72
73
73
<!-- Content -->
74
74
<div class="flex-1 min-w-0 -mt-0.5">
75
- <a href={pr.url} class="block hover:underline truncate text-sm font-medium text-gray-300 hover:text-blue-400 decoration-blue-400/50">
75
+ <a href={pr.url} class="block hover:underline truncate text-sm font-medium text-muted-foreground hover:text-blue-400 decoration-blue-400/50">
76
76
{pr.title}
77
77
</a>
78
- <div class="flex items-center gap-2 text-xs text-gray-500">
78
+ <div class="flex items-center gap-2 text-xs text-muted-foreground">
79
79
<span class="font-mono">#{pr.number}</span>
80
80
<span>•</span>
81
81
<span class={
82
82
▼src/components/dashboard/UserAvatar.tsx
+1-1
13
13
return (
14
14
<Avatar className={className}>
15
15
<AvatarImage src={user.avatarUrl || undefined} alt={user.username} />
16
- <AvatarFallback className="bg-gradient-to-br from-cyan-500 to-blue-600 text-white text-xl">
16
+ <AvatarFallback className="bg-gradient-to-br from-cyan-500 to-blue-600 text-foreground text-xl">
17
17
{user.username[0].toUpperCase()}
18
18
</AvatarFallback>
19
19
</Avatar>
20
20
▼src/components/diff/DiffView.tsx
+3-3
271
271
272
272
{/* Diff lines */}
273
273
{!collapsedFiles.has(fileIdx) && (
274
- <div className="overflow-x-auto bg-[#0d1117] text-[#c9d1d9] text-sm font-mono">
274
+ <div className="overflow-x-auto bg-card text-[#c9d1d9] text-sm font-mono">
275
275
{file.isBinary ? (
276
- <div className="py-8 text-center text-gray-400">
276
+ <div className="py-8 text-center text-muted-foreground">
277
277
Binary file not shown.
278
278
</div>
279
279
) : (
305
305
? "bg-[#0a3519] text-green-600"
306
306
: line.type === "del"
307
307
? "bg-[#3d0f18] text-red-600"
308
- : "text-gray-600";
308
+ : "text-muted-foreground";
309
309
310
310
const issueForLine =
311
311
line.newLine && codeQualityIssues
312
312
▼src/components/home/FeatureDemosSection.tsx
+4-2
394
394
{/* Description */}
395
395
<div>
396
396
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${currentDemo.color} mb-4`}>
397
- <currentDemo.icon className="h-8 w-8 text-white" />
397
+ <currentDemo.icon className="h-8 w-8 text-foreground" />
398
398
</div>
399
399
<h3 className="text-2xl font-bold mb-2">{currentDemo.title}</h3>
400
400
<p className="text-muted-foreground mb-6">{currentDemo.description}</p>
401
401
<a
402
- href="/docs"
402
+ href="https://docs.opencodehub.space/"
403
+ target="_blank"
404
+ rel="noopener noreferrer"
403
405
className="inline-flex items-center gap-2 text-sm text-primary hover:text-primary/80 transition-colors">
404
406
Learn more
405
407
<ArrowRight className="h-4 w-4" />
406
408
▼src/components/home/HeroSection.tsx
+9-9
94
94
initial={{ opacity: 0, y: 20 }}
95
95
animate={{ opacity: 1, y: 0 }}
96
96
transition={{ duration: 0.5, delay: 0.4 }}
97
- className="flex items-center justify-center gap-8 mt-16"
97
+ className="flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 md:gap-8 mt-12 sm:mt-16"
98
98
>
99
99
<div className="flex items-center gap-2 text-muted-foreground">
100
100
<GitBranch className="h-5 w-5 text-primary" />
101
- <span className="text-2xl font-bold text-foreground">100%</span>
102
- <span className="text-sm">Open Source</span>
101
+ <span className="text-xl sm:text-2xl font-bold text-foreground">100%</span>
102
+ <span className="text-xs sm:text-sm">Open Source</span>
103
103
</div>
104
- <div className="w-px h-8 bg-border" />
104
+ <div className="hidden sm:block w-px h-8 bg-border" />
105
105
<div className="flex items-center gap-2 text-muted-foreground">
106
106
<Zap className="h-5 w-5 text-yellow-500" />
107
- <span className="text-2xl font-bold text-foreground">Graphite</span>
108
- <span className="text-sm">Features</span>
107
+ <span className="text-xl sm:text-2xl font-bold text-foreground">Graphite</span>
108
+ <span className="text-xs sm:text-sm">Features</span>
109
109
</div>
110
- <div className="w-px h-8 bg-border" />
110
+ <div className="hidden sm:block w-px h-8 bg-border" />
111
111
<div className="flex items-center gap-2 text-muted-foreground">
112
112
<Star className="h-5 w-5 text-orange-500" />
113
- <span className="text-2xl font-bold text-foreground">AI</span>
114
- <span className="text-sm">Powered Reviews</span>
113
+ <span className="text-xl sm:text-2xl font-bold text-foreground">AI</span>
114
+ <span className="text-xs sm:text-sm">Powered Reviews</span>
115
115
</div>
116
116
</motion.div>
117
117
</div>
118
118
▼src/components/ide/WebIDE.tsx
+24-24
199
199
<div
200
200
className={cn(
201
201
"flex items-center gap-1.5 px-2 py-0.5 hover:bg-[#2a2d2e] cursor-pointer text-[13px] select-none",
202
- activeFile === node.path && "bg-[#37373d] text-white"
202
+ activeFile === node.path && "bg-[#37373d] text-foreground"
203
203
)}
204
204
style={{ paddingLeft: `${depth * 12 + 4}px` }}
205
205
onClick={(e) => node.type === "file" ? handleOpenFile(node.path) : toggleDir(node, e)}
250
250
<div className="flex flex-col h-screen w-full bg-[#1e1e1e] text-[#cccccc] font-sans overflow-hidden select-none">
251
251
{/* VS Code Title Bar */}
252
252
<div className="h-8 w-full bg-[#3c3c3c] flex items-center px-3 border-b border-[#2d2d2d] shrink-0 text-xs">
253
- <div className="flex items-center gap-1 text-gray-300">
253
+ <div className="flex items-center gap-1 text-muted-foreground">
254
254
<img src="/logo.svg" alt="logo" className="h-4 w-4 mr-2 opacity-90 brightness-200" />
255
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">File</span>
256
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Edit</span>
257
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Selection</span>
258
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">View</span>
259
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Go</span>
260
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Run</span>
261
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Terminal</span>
262
- <span className="cursor-default hover:bg-white/10 px-2 py-1 rounded transition-colors">Help</span>
255
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">File</span>
256
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Edit</span>
257
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Selection</span>
258
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">View</span>
259
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Go</span>
260
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Run</span>
261
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Terminal</span>
262
+ <span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Help</span>
263
263
</div>
264
- <div className="flex-1 flex justify-center text-gray-400 font-medium">
264
+ <div className="flex-1 flex justify-center text-muted-foreground font-medium">
265
265
{activeFileData ? `${activeFileData.path.split("/").pop()} - ` : ""}{repo} - OpenCodeHub
266
266
</div>
267
267
<div className="w-48 flex justify-end items-center gap-2">
279
279
if (activeTab === "explorer" && isSidebarOpen) setIsSidebarOpen(false);
280
280
else { setActiveTab("explorer"); setIsSidebarOpen(true); }
281
281
}}
282
- className={cn("relative p-2 rounded-md hover:text-white transition-colors", activeTab === "explorer" && isSidebarOpen ? "text-white" : "text-[#858585]")}
282
+ className={cn("relative p-2 rounded-md hover:text-foreground transition-colors", activeTab === "explorer" && isSidebarOpen ? "text-foreground" : "text-[#858585]")}
283
283
title="Explorer"
284
284
>
285
285
{activeTab === "explorer" && isSidebarOpen && <div className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-full bg-[#007fd4]" />}
290
290
if (activeTab === "search" && isSidebarOpen) setIsSidebarOpen(false);
291
291
else { setActiveTab("search"); setIsSidebarOpen(true); }
292
292
}}
293
- className={cn("relative p-2 rounded-md hover:text-white transition-colors", activeTab === "search" && isSidebarOpen ? "text-white" : "text-[#858585]")}
293
+ className={cn("relative p-2 rounded-md hover:text-foreground transition-colors", activeTab === "search" && isSidebarOpen ? "text-foreground" : "text-[#858585]")}
294
294
title="Search"
295
295
>
296
296
{activeTab === "search" && isSidebarOpen && <div className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-full bg-[#007fd4]" />}
301
301
if (activeTab === "scm" && isSidebarOpen) setIsSidebarOpen(false);
302
302
else { setActiveTab("scm"); setIsSidebarOpen(true); }
303
303
}}
304
- className={cn("relative p-2 rounded-md hover:text-white transition-colors", activeTab === "scm" && isSidebarOpen ? "text-white" : "text-[#858585]")}
304
+ className={cn("relative p-2 rounded-md hover:text-foreground transition-colors", activeTab === "scm" && isSidebarOpen ? "text-foreground" : "text-[#858585]")}
305
305
title="Source Control"
306
306
>
307
307
{activeTab === "scm" && isSidebarOpen && <div className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-full bg-[#007fd4]" />}
308
308
<GitCommit className="h-6 w-6 stroke-[1.5px]" />
309
309
{modifiedFiles.length > 0 && (
310
- <span className="absolute bottom-1 right-1 h-4 w-4 bg-[#007fd4] rounded-full text-[9px] flex items-center justify-center text-white font-bold">
310
+ <span className="absolute bottom-1 right-1 h-4 w-4 bg-[#007fd4] rounded-full text-[9px] flex items-center justify-center text-foreground font-bold">
311
311
{modifiedFiles.length}
312
312
</span>
313
313
)}
314
314
</button>
315
315
316
316
<div className="mt-auto flex flex-col gap-4">
317
- <a href={`/${owner}/${repo}`} className="p-2 text-[#858585] hover:text-white" title="Back to Repo">
317
+ <a href={`/${owner}/${repo}`} className="p-2 text-[#858585] hover:text-foreground" title="Back to Repo">
318
318
<Home className="h-6 w-6 stroke-[1.5px]" />
319
319
</a>
320
- <button className="p-2 text-[#858585] hover:text-white" title="Settings">
320
+ <button className="p-2 text-[#858585] hover:text-foreground" title="Settings">
321
321
<Settings className="h-6 w-6 stroke-[1.5px]" />
322
322
</button>
323
323
</div>
343
343
</div>
344
344
<div className="mt-1">
345
345
{loading ? (
346
- <div className="px-6 py-4 text-xs text-gray-500 flex items-center gap-2">
346
+ <div className="px-6 py-4 text-xs text-muted-foreground flex items-center gap-2">
347
347
<Loader2 className="h-3 w-3 animate-spin" /> Loading...
348
348
</div>
349
349
) : (
354
354
)}
355
355
356
356
{activeTab === "search" && (
357
- <div className="px-4 py-2 text-sm text-gray-500">
357
+ <div className="px-4 py-2 text-sm text-muted-foreground">
358
358
Search is not available in the web editor.
359
359
</div>
360
360
)}
372
372
<button
373
373
onClick={handleCommit}
374
374
disabled={committing || modifiedFiles.length === 0 || !commitMessage.trim()}
375
- className="w-full bg-[#0e639c] hover:bg-[#1177bb] disabled:opacity-50 disabled:cursor-not-allowed text-white text-[13px] py-1 rounded flex items-center justify-center gap-2 transition-colors"
375
+ className="w-full bg-[#0e639c] hover:bg-[#1177bb] disabled:opacity-50 disabled:cursor-not-allowed text-foreground text-[13px] py-1 rounded flex items-center justify-center gap-2 transition-colors"
376
376
>
377
377
{committing && <Loader2 className="h-3 w-3 animate-spin" />}
378
378
Commit & Push
385
385
<ChevronDown className="h-3.5 w-3.5" />
386
386
<span>Changes</span>
387
387
</div>
388
- <span className="bg-[#4d4d4d] text-white px-1.5 rounded-full text-[10px]">{modifiedFiles.length}</span>
388
+ <span className="bg-[#4d4d4d] text-foreground px-1.5 rounded-full text-[10px]">{modifiedFiles.length}</span>
389
389
</div>
390
390
<div className="mt-1">
391
391
{modifiedFiles.map(f => (
426
426
<span className={cn("truncate flex-1 font-medium", f.isModified && "italic")}>
427
427
{filename}
428
428
</span>
429
- <div className="shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-white/10">
429
+ <div className="shrink-0 w-5 h-5 flex items-center justify-center rounded hover:bg-secondary/80">
430
430
{f.isModified ? (
431
431
<div className={cn("w-2.5 h-2.5 rounded-full bg-white", isActive ? "opacity-100" : "opacity-50")} />
432
432
) : (
447
447
<div className="h-6 flex items-center px-4 text-[12px] text-[#cccccc] bg-[#1e1e1e] shadow-sm z-10 border-b border-[#252526]">
448
448
{activeFileData.path.split("/").map((part, i, arr) => (
449
449
<React.Fragment key={i}>
450
- <span className="cursor-pointer hover:text-white font-medium opacity-80 transition-opacity">{part}</span>
450
+ <span className="cursor-pointer hover:text-foreground font-medium opacity-80 transition-opacity">{part}</span>
451
451
{i < arr.length - 1 && <ChevronRight className="h-3 w-3 mx-1 opacity-50" />}
452
452
</React.Fragment>
453
453
))}
499
499
</div>
500
500
501
501
{/* Status Bar */}
502
- <div className="h-[22px] shrink-0 bg-[#007acc] text-white flex items-center justify-between px-2 text-[11px] font-medium tracking-wide">
502
+ <div className="h-[22px] shrink-0 bg-[#007acc] text-foreground flex items-center justify-between px-2 text-[11px] font-medium tracking-wide">
503
503
<div className="flex items-center gap-1">
504
504
<div className="flex items-center gap-1 cursor-pointer hover:bg-white/20 px-2 h-full transition-colors">
505
505
<GitBranch className="h-3 w-3" />
506
506
▼src/components/inbox/KeyboardReviewInbox.tsx
+1-1
35
35
}, [handleKeyDown]);
36
36
37
37
return (
38
- <div className="flex flex-col h-full bg-slate-950 text-white font-mono">
38
+ <div className="flex flex-col h-full bg-slate-950 text-foreground font-mono">
39
39
<div className="p-4 border-b border-slate-800">
40
40
<h2 className="text-xl font-bold">Reviewing: {initialPr.title}</h2>
41
41
<div className="text-xs text-slate-400 mt-2 flex gap-4">
42
42
▼src/components/issues/IssueDetail.tsx
+29-29
216
216
<motion.div
217
217
initial={{ opacity: 0, y: -20 }}
218
218
animate={{ opacity: 1, y: 0 }}
219
- className="mb-8 pb-6 border-b border-white/10"
219
+ className="mb-8 pb-6 border-b border-border"
220
220
>
221
221
<div className="flex items-start justify-between gap-4 mb-4">
222
222
<div className="flex items-center gap-3">
223
- <h1 className="text-2xl md:text-3xl font-bold text-white">
223
+ <h1 className="text-2xl md:text-3xl font-bold text-foreground">
224
224
{issue.title}
225
225
</h1>
226
- <span className="text-2xl md:text-3xl text-gray-500 font-light">
226
+ <span className="text-2xl md:text-3xl text-muted-foreground font-light">
227
227
#{issue.number}
228
228
</span>
229
229
</div>
244
244
<motion.button
245
245
whileHover={{ scale: 1.05 }}
246
246
whileTap={{ scale: 0.95 }}
247
- className="flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 transition-all"
247
+ className="flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 transition-all"
248
248
>
249
249
<Edit3 className="h-4 w-4" />
250
250
Edit
258
258
<motion.div
259
259
initial={{ scale: 0.9 }}
260
260
animate={{ scale: 1 }}
261
- className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium text-white border"
261
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium text-foreground border"
262
262
style={{
263
263
backgroundColor: `${issue.status.color}20`,
264
264
borderColor: `${issue.status.color}40`,
290
290
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
291
291
: issue.type === 'task'
292
292
? "bg-blue-500/10 text-blue-400 border-blue-500/20"
293
- : "bg-gray-500/10 text-gray-400 border-gray-500/20"
293
+ : "bg-gray-500/10 text-muted-foreground border-gray-500/20"
294
294
}`}>
295
295
{issue.type === 'epic' && <Layers className="h-3.5 w-3.5" />}
296
296
{issue.type === 'task' && <CheckSquare className="h-3.5 w-3.5" />}
299
299
</div>
300
300
</div>
301
301
302
- <span className="text-gray-400">
303
- <span className="font-medium text-white hover:text-cyan-400 cursor-pointer transition-colors">
302
+ <span className="text-muted-foreground">
303
+ <span className="font-medium text-foreground hover:text-cyan-400 cursor-pointer transition-colors">
304
304
{issue.author.username}
305
305
</span>
306
306
{" "}opened this issue {timeAgo(issue.createdAt)}
323
323
>
324
324
<div className="relative">
325
325
<div className="absolute -inset-[1px] bg-gradient-to-b from-emerald-500/10 to-transparent rounded-xl opacity-50" />
326
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
327
- <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
326
+ <div className="relative rounded-xl border border-border bg-card/60 p-4">
327
+ <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
328
328
<Link2 className="h-4 w-4 text-emerald-400" />
329
329
Linked Issues
330
330
</h3>
331
331
{loadingCrossRepo ? (
332
- <div className="text-xs text-gray-500">Loading...</div>
332
+ <div className="text-xs text-muted-foreground">Loading...</div>
333
333
) : crossRepoLinks.length === 0 ? (
334
- <div className="text-xs text-gray-500">No linked issues</div>
334
+ <div className="text-xs text-muted-foreground">No linked issues</div>
335
335
) : (
336
336
<div className="space-y-2">
337
337
{crossRepoLinks.map((link) => (
338
338
<a
339
339
key={link.id}
340
340
href={`/${link.repository.owner}/${link.repository.name}/issues/${link.issue.number}`}
341
- className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-white/5 px-3 py-2 text-xs text-gray-300 hover:bg-white/10"
341
+ className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-secondary px-3 py-2 text-xs text-muted-foreground hover:bg-secondary/80"
342
342
>
343
343
<span>
344
344
{link.repository.owner}/{link.repository.name}#{link.issue.number} {link.issue.title}
345
345
</span>
346
- <span className="text-[10px] uppercase text-gray-500">
346
+ <span className="text-[10px] uppercase text-muted-foreground">
347
347
{link.linkType}
348
348
</span>
349
349
</a>
357
357
value={crossRepoTarget}
358
358
onChange={(e) => setCrossRepoTarget(e.target.value)}
359
359
placeholder="owner/repo#123"
360
- className="flex-1 rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-white"
360
+ className="flex-1 rounded-md border border-border bg-secondary px-2 py-1 text-xs text-foreground"
361
361
/>
362
362
<select
363
363
value={crossRepoType}
364
364
onChange={(e) => setCrossRepoType(e.target.value)}
365
- className="rounded-md border border-white/10 bg-white/5 px-2 py-1 text-xs text-white"
365
+ className="rounded-md border border-border bg-secondary px-2 py-1 text-xs text-foreground"
366
366
>
367
367
<option value="relates">Relates</option>
368
368
<option value="blocks">Blocks</option>
385
385
</div>
386
386
<div className="relative">
387
387
<div className="absolute -inset-[1px] bg-gradient-to-b from-cyan-500/10 to-transparent rounded-xl opacity-50" />
388
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
389
- <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
388
+ <div className="relative rounded-xl border border-border bg-card/60 p-4">
389
+ <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
390
390
<GitBranch className="h-4 w-4 text-cyan-400" />
391
391
Linked Pull Requests
392
392
</h3>
393
393
{loadingLinks ? (
394
- <div className="text-xs text-gray-500">Loading...</div>
394
+ <div className="text-xs text-muted-foreground">Loading...</div>
395
395
) : linkedPRs.length === 0 ? (
396
- <div className="text-xs text-gray-500">No linked pull requests</div>
396
+ <div className="text-xs text-muted-foreground">No linked pull requests</div>
397
397
) : (
398
398
<div className="space-y-2">
399
399
{linkedPRs.map((link) => (
400
400
<a
401
401
key={link.id}
402
402
href={`/${repoOwner}/${repoName}/pulls/${link.pullRequest.number}`}
403
- className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-white/5 px-3 py-2 text-xs text-gray-300 hover:bg-white/10"
403
+ className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-secondary px-3 py-2 text-xs text-muted-foreground hover:bg-secondary/80"
404
404
>
405
405
<span>
406
406
#{link.pullRequest.number} {link.pullRequest.title}
407
407
</span>
408
- <span className="text-[10px] uppercase text-gray-500">
408
+ <span className="text-[10px] uppercase text-muted-foreground">
409
409
{link.linkType}
410
410
</span>
411
411
</a>
418
418
{(issue.type === 'epic' || (issue.children && issue.children.length > 0)) && (
419
419
<div className="relative">
420
420
<div className="absolute -inset-[1px] bg-gradient-to-b from-purple-500/10 to-transparent rounded-xl opacity-50" />
421
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
421
+ <div className="relative rounded-xl border border-border bg-card/60 p-4">
422
422
<div className="flex items-center justify-between mb-3">
423
- <h3 className="text-sm font-semibold text-white flex items-center gap-2">
423
+ <h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
424
424
<ListTodo className="h-4 w-4 text-purple-400" />
425
425
{issue.type === 'epic' ? 'Child Issues' : 'Sub-tasks'}
426
426
</h3>
427
427
{issue.children && issue.children.length > 0 && (
428
- <span className="text-xs bg-white/10 px-2 py-0.5 rounded-full text-gray-300">
428
+ <span className="text-xs bg-secondary/80 px-2 py-0.5 rounded-full text-muted-foreground">
429
429
{issue.children.filter(c => c.state === 'closed').length} / {issue.children.length}
430
430
</span>
431
431
)}
433
433
434
434
{/* Progress Bar */}
435
435
{issue.children && issue.children.length > 0 && (
436
- <div className="h-1.5 w-full bg-white/10 rounded-full mb-3 overflow-hidden">
436
+ <div className="h-1.5 w-full bg-secondary/80 rounded-full mb-3 overflow-hidden">
437
437
<div
438
438
className="h-full bg-purple-500 transition-all duration-500"
439
439
style={{
448
448
<a
449
449
key={child.number}
450
450
href={`/${repoOwner}/${repoName}/issues/${child.number}`}
451
- className="flex items-start gap-2 p-2 rounded-lg hover:bg-white/5 transition-colors group"
451
+ className="flex items-start gap-2 p-2 rounded-lg hover:bg-accent transition-colors group"
452
452
>
453
453
<div className="mt-0.5">
454
454
{child.state === "open" ? (
458
458
)}
459
459
</div>
460
460
<div className="text-sm min-w-0">
461
- <div className="text-gray-300 group-hover:text-cyan-400 truncate transition-colors">
461
+ <div className="text-muted-foreground group-hover:text-cyan-400 truncate transition-colors">
462
462
{child.title}
463
463
</div>
464
464
</div>
468
468
{issue.type === 'epic' && (
469
469
<a
470
470
href={`/${repoOwner}/${repoName}/issues/new?parent=${issue.number}`}
471
- className="flex items-center gap-2 p-2 rounded-lg hover:bg-white/5 text-sm text-gray-400 hover:text-cyan-400 transition-colors mt-2 border border-dashed border-white/10 hover:border-cyan-500/30 justify-center"
471
+ className="flex items-center gap-2 p-2 rounded-lg hover:bg-accent text-sm text-muted-foreground hover:text-cyan-400 transition-colors mt-2 border border-dashed border-border hover:border-cyan-500/30 justify-center"
472
472
>
473
473
<ListTodo className="h-3.5 w-3.5" />
474
474
Add sub-task
475
475
▼src/components/issues/IssuesList.tsx
+24-24
117
117
<div className="relative flex-1 max-w-xl group">
118
118
<div className="absolute -inset-0.5 bg-gradient-to-r from-cyan-500/20 via-purple-500/20 to-pink-500/20 rounded-xl blur opacity-0 group-focus-within:opacity-100 transition-opacity" />
119
119
<div className="relative">
120
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
120
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
121
121
<input
122
122
type="search"
123
123
placeholder="Search all issues..."
124
124
value={searchQuery}
125
125
onChange={(e) => setSearchQuery(e.target.value)}
126
- className="w-full rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
126
+ className="w-full rounded-xl border border-border bg-secondary backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
127
127
/>
128
128
</div>
129
129
</div>
134
134
onClick={() => setGroupByEpic(!groupByEpic)}
135
135
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${groupByEpic
136
136
? "bg-purple-500/20 border-purple-500/50 text-purple-300"
137
- : "border-white/10 bg-white/5 text-gray-300 hover:bg-white/10"
137
+ : "border-border bg-secondary text-muted-foreground hover:bg-secondary/80"
138
138
}`}
139
139
>
140
140
<Layers className="h-4 w-4" />
145
145
href={`/${repoOwner}/${repoName}/labels`}
146
146
whileHover={{ scale: 1.02 }}
147
147
whileTap={{ scale: 0.98 }}
148
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
148
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
149
149
>
150
150
<Tag className="h-4 w-4" />
151
151
<span className="hidden sm:inline">Labels</span>
155
155
href={`/${repoOwner}/${repoName}/milestones`}
156
156
whileHover={{ scale: 1.02 }}
157
157
whileTap={{ scale: 0.98 }}
158
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
158
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
159
159
>
160
160
<Calendar className="h-4 w-4" />
161
161
<span className="hidden sm:inline">Milestones</span>
165
165
href={`/${repoOwner}/${repoName}/issues/new`}
166
166
whileHover={{ scale: 1.05 }}
167
167
whileTap={{ scale: 0.95 }}
168
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-white shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
168
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-foreground shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
169
169
>
170
170
<Plus className="h-4 w-4" />
171
171
<span className="hidden sm:inline">New Issue</span>
183
183
{/* Gradient border effect */}
184
184
<div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/30 via-purple-500/20 to-pink-500/30 rounded-xl blur-sm opacity-50" />
185
185
186
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden">
186
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden">
187
187
{/* Filter Header */}
188
188
<div className="flex items-center justify-between gap-4 border-b border-white/5 bg-white/[0.02] px-4 py-3">
189
189
<div className="flex items-center gap-1">
191
191
onClick={() => setFilter("open")}
192
192
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "open"
193
193
? "bg-green-500/10 text-green-400 border border-green-500/30"
194
- : "text-gray-400 hover:text-white hover:bg-white/5"
194
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
195
195
}`}
196
196
>
197
197
<AlertCircle className="h-4 w-4" />
201
201
onClick={() => setFilter("closed")}
202
202
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "closed"
203
203
? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
204
- : "text-gray-400 hover:text-white hover:bg-white/5"
204
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
205
205
}`}
206
206
>
207
207
<CheckCircle2 className="h-4 w-4" />
209
209
</button>
210
210
</div>
211
211
212
- <div className="flex items-center gap-3 text-sm text-gray-500">
213
- <button className="flex items-center gap-1 hover:text-white transition-colors">
212
+ <div className="flex items-center gap-3 text-sm text-muted-foreground">
213
+ <button className="flex items-center gap-1 hover:text-foreground transition-colors">
214
214
<ArrowUpDown className="h-3.5 w-3.5" />
215
215
Sort
216
216
</button>
229
229
animate={{ opacity: 1, x: 0 }}
230
230
exit={{ opacity: 0, x: 20 }}
231
231
transition={{ delay: index * 0.03 }}
232
- className="flex items-start gap-3 p-4 hover:bg-white/[0.02] transition-colors group relative"
232
+ className="flex items-start gap-3 p-4 hover:bg-accent/50 transition-colors group relative"
233
233
>
234
234
{/* Expand/Collapse for Epics */}
235
235
{groupByEpic && issue.type === 'epic' && issue.children && issue.children.length > 0 && (
236
236
<button
237
237
onClick={(e) => toggleEpic(issue.id, e)}
238
- className="absolute left-1 top-4 p-1 text-gray-500 hover:text-white rounded hover:bg-white/10"
238
+ className="absolute left-1 top-4 p-1 text-muted-foreground hover:text-foreground rounded hover:bg-secondary/80"
239
239
>
240
240
{expandedEpics[issue.id] ? (
241
241
<ChevronDown className="h-4 w-4" />
261
261
</div>
262
262
263
263
{/* Type Icon */}
264
- <div className="mt-0.5 text-gray-500">
264
+ <div className="mt-0.5 text-muted-foreground">
265
265
{issue.type === "epic" ? (
266
266
<Layers className="h-5 w-5 text-purple-400" />
267
267
) : issue.type === "task" ? (
274
274
{/* Content */}
275
275
<div className="flex-1 min-w-0">
276
276
<div className="flex items-center gap-2 mb-1">
277
- <span className="font-semibold text-white group-hover:text-cyan-400 transition-colors truncate">
277
+ <span className="font-semibold text-foreground group-hover:text-cyan-400 transition-colors truncate">
278
278
{issue.title}
279
279
</span>
280
280
{/* Labels */}
292
292
</span>
293
293
))}
294
294
</div>
295
- <div className="flex items-center gap-2 text-xs text-gray-500">
295
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
296
296
<span>#{issue.number}</span>
297
297
<span>•</span>
298
298
<span>opened {timeAgo(issue.createdAt)}</span>
305
305
className="h-4 w-4 rounded-full"
306
306
/>
307
307
) : (
308
- <div className="h-4 w-4 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center text-[8px] text-white font-bold">
308
+ <div className="h-4 w-4 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center text-[8px] text-foreground font-bold">
309
309
{issue.author.username[0].toUpperCase()}
310
310
</div>
311
311
)}
318
318
319
319
{/* Comment Count */}
320
320
{issue.commentCount > 0 && (
321
- <div className="flex items-center gap-1 text-xs text-gray-500 group-hover:text-gray-400 transition-colors">
321
+ <div className="flex items-center gap-1 text-xs text-muted-foreground group-hover:text-muted-foreground transition-colors">
322
322
<MessageSquare className="h-4 w-4" />
323
323
{issue.commentCount}
324
324
</div>
332
332
<a
333
333
key={child.number}
334
334
href={`/${repoOwner}/${repoName}/issues/${child.number}`}
335
- className="flex items-center gap-3 py-3 pr-4 pl-16 hover:bg-white/[0.02] transition-colors border-b last:border-0 border-white/5 group/child"
335
+ className="flex items-center gap-3 py-3 pr-4 pl-16 hover:bg-accent/50 transition-colors border-b last:border-0 border-white/5 group/child"
336
336
>
337
337
<div className="mt-0.5">
338
338
{child.state === "open" ? (
344
344
<div className="mt-0.5 text-blue-400">
345
345
<CheckSquare className="h-4 w-4" />
346
346
</div>
347
- <span className="text-sm text-gray-300 group-hover/child:text-cyan-400 transition-colors truncate">
347
+ <span className="text-sm text-muted-foreground group-hover/child:text-cyan-400 transition-colors truncate">
348
348
{child.title}
349
349
</span>
350
- <span className="text-xs text-gray-600 ml-auto">#{child.number}</span>
350
+ <span className="text-xs text-muted-foreground ml-auto">#{child.number}</span>
351
351
</a>
352
352
))}
353
353
</div>
364
364
<Sparkles className="h-12 w-12 text-cyan-400" />
365
365
<div className="absolute inset-0 bg-cyan-500 blur-xl opacity-30" />
366
366
</div>
367
- <h3 className="text-lg font-semibold text-white mb-2">
367
+ <h3 className="text-lg font-semibold text-foreground mb-2">
368
368
{searchQuery ? "No matching issues" : "Welcome to issues!"}
369
369
</h3>
370
- <p className="text-gray-500 mb-6 max-w-md mx-auto">
370
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
371
371
{searchQuery
372
372
? "Try adjusting your search or filter to find what you're looking for."
373
373
: "Issues are used to track todos, bugs, feature requests, and more."}
377
377
href={`/${repoOwner}/${repoName}/issues/new`}
378
378
whileHover={{ scale: 1.05 }}
379
379
whileTap={{ scale: 0.95 }}
380
- className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 text-sm font-medium text-white shadow-lg shadow-cyan-500/25"
380
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 text-sm font-medium text-foreground shadow-lg shadow-cyan-500/25"
381
381
>
382
382
<Plus className="h-4 w-4" />
383
383
Create the first issue
384
384
▼src/components/kanban/Card.tsx
+1-1
68
68
e.stopPropagation();
69
69
deleteCard(card.id);
70
70
}}
71
- className="opacity-0 group-hover:opacity-100 p-1 text-gray-500 hover:text-red-400 transition-opacity rounded"
71
+ className="opacity-0 group-hover:opacity-100 p-1 text-muted-foreground hover:text-red-400 transition-opacity rounded"
72
72
>
73
73
<Trash2 className="w-3 h-3" />
74
74
</button>
75
75
▼src/components/kanban/Column.tsx
+5-5
67
67
<div
68
68
ref={setNodeRef}
69
69
style={style}
70
- className="bg-[#161b22] w-[350px] h-[500px] max-h-[500px] rounded-md flex flex-col border border-gray-800"
70
+ className="bg-accent w-[350px] h-[500px] max-h-[500px] rounded-md flex flex-col border border-gray-800"
71
71
>
72
72
<div
73
73
{...attributes}
74
74
{...listeners}
75
75
onClick={() => { }}
76
- className="bg-[#0d1117] text-md h-[60px] cursor-grab rounded-md rounded-b-none p-3 font-bold border-b border-gray-800 flex items-center justify-between"
76
+ className="bg-card text-md h-[60px] cursor-grab rounded-md rounded-b-none p-3 font-bold border-b border-gray-800 flex items-center justify-between"
77
77
>
78
78
<div className="flex gap-2 text-gray-200">
79
79
{column.name}
80
- <span className="flex justify-center items-center bg-gray-800 px-2 py-1 text-sm rounded-full text-gray-400">
80
+ <span className="flex justify-center items-center bg-gray-800 px-2 py-1 text-sm rounded-full text-muted-foreground">
81
81
{column.cards.length}
82
82
</span>
83
83
</div>
104
104
<div className="flex gap-2 items-center p-2">
105
105
<input
106
106
autoFocus
107
- className="bg-[#0d1117] border border-gray-700 rounded p-2 w-full text-white"
107
+ className="bg-card border border-gray-700 rounded p-2 w-full text-foreground"
108
108
placeholder="Card content..."
109
109
value={newCardNote}
110
110
onChange={(e) => setNewCardNote(e.target.value)}
114
114
}}
115
115
/>
116
116
<button onClick={handleCreateCard} className="text-green-400 font-bold p-2">Add</button>
117
- <button onClick={() => setIsCreating(false)} className="text-gray-500 p-2">X</button>
117
+ <button onClick={() => setIsCreating(false)} className="text-muted-foreground p-2">X</button>
118
118
</div>
119
119
)}
120
120
</div>
121
121
▼src/components/layout/Header.astro
+112-146
1
1
---
2
-import { GitBranch, Search, Bell, Plus, Menu } from "lucide-react"
2
+import { Search, Bell, Plus, Menu } from "lucide-react"
3
3
import { GlobalSearch } from "@/components/search/GlobalSearch"
4
4
import { ThemeToggle } from "@/components/ThemeToggle"
5
5
6
6
const user = Astro.locals.user;
7
+const currentPath = Astro.url.pathname;
7
8
---
8
9
9
-<header class="sticky top-0 z-50 w-full border-b border-white/5 bg-background/60 backdrop-blur-xl supports-[backdrop-filter]:bg-background/40" role="banner">
10
- <div class="container h-16 flex items-center justify-between gap-4">
10
+<header class="sticky top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl" role="banner">
11
+ <div class="container h-14 flex items-center justify-between gap-4">
11
12
<!-- Logo -->
12
- <a href="/" class="flex items-center gap-2 transition-opacity hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-md" aria-label="OpenCodeHub home">
13
- <img src="/logo-dark.png" alt="OpenCodeHub" class="h-9 w-auto dark:hidden" />
14
- <img src="/logo-light.png" alt="OpenCodeHub" class="h-9 w-auto hidden dark:block" />
13
+ <a href="/" class="flex-shrink-0 transition-opacity hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-md" aria-label="OpenCodeHub home">
14
+ <img src="/logo-dark.png" alt="OpenCodeHub" class="h-7 w-auto dark:hidden" />
15
+ <img src="/logo-light.png" alt="OpenCodeHub" class="h-7 w-auto hidden dark:block" />
15
16
</a>
16
17
17
18
<!-- Navigation -->
18
- <nav class="hidden md:flex items-center gap-1 text-sm font-medium" aria-label="Main navigation">
19
- <a href="/explore" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
20
- Explore
19
+ <nav class="hidden md:flex items-center gap-1 text-[13px] font-medium" aria-label="Main navigation">
20
+ <a href="/pulls" class:list={[
21
+ "px-3 py-1.5 rounded-md transition-colors",
22
+ currentPath.startsWith("/pulls") ? "text-foreground font-semibold" : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
23
+ ]}>
24
+ Pull requests
21
25
</a>
22
- <a href="/issues" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
26
+ <a href="/issues" class:list={[
27
+ "px-3 py-1.5 rounded-md transition-colors",
28
+ currentPath.startsWith("/issues") ? "text-foreground font-semibold" : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
29
+ ]}>
23
30
Issues
24
31
</a>
25
- <a href="/pulls" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
26
- Pulls
27
- </a>
28
- <a href="/actions" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
29
- Actions
30
- </a>
31
- <a href="/cli" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
32
- CLI
33
- </a>
34
- <a href="/docs/" class="px-3 py-2 rounded-md text-muted-foreground transition-all hover:text-foreground hover:bg-muted/50">
35
- Docs
36
- </a>
37
- <div class="mx-2 h-4 w-px bg-border/50"></div>
38
- <a href="/admin" class="flex items-center gap-2 px-3 py-2 rounded-md text-purple-700 dark:text-purple-300 transition-all hover:text-purple-800 dark:hover:text-purple-200 hover:bg-purple-500/10">
39
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
40
- <span>Mission Control</span>
32
+ <a href="/explore" class:list={[
33
+ "px-3 py-1.5 rounded-md transition-colors",
34
+ currentPath.startsWith("/explore") ? "text-foreground font-semibold" : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
35
+ ]}>
36
+ Explore
41
37
</a>
42
38
</nav>
43
39
44
40
<!-- Search -->
45
- <div class="hidden md:flex flex-1 max-w-sm" role="search" aria-label="Site search">
41
+ <div class="hidden md:flex flex-1 max-w-md mx-4" role="search" aria-label="Site search">
46
42
<div class="relative w-full group">
47
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
43
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground group-hover:text-primary transition-colors duration-200" />
48
44
<input
49
45
type="search"
50
- placeholder="Search..."
46
+ placeholder="Type / to search"
51
47
aria-label="Search repositories, users, and code"
52
- class="w-full h-9 rounded-full border border-border/50 bg-secondary/30 px-9 text-sm text-foreground placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/50 transition-all"
48
+ class="w-full h-8 rounded-md border border-border/60 bg-muted/40 pl-9 pr-12 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 focus:bg-background transition-all duration-200"
53
49
/>
54
- <kbd class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 hidden h-5 select-none items-center gap-1 rounded bg-background/50 px-1.5 font-mono text-[10px] font-medium opacity-50 sm:flex border border-border/50">
55
- <span class="text-xs">⌘</span>K
50
+ <kbd class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 hidden h-5 select-none items-center gap-1 rounded border border-border/60 bg-background px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:flex opacity-60">
51
+ /
56
52
</kbd>
57
53
</div>
58
54
</div>
59
55
60
56
<!-- Actions -->
61
- <div class="flex items-center gap-3">
57
+ <div class="flex items-center gap-2">
62
58
{user ? (
63
59
<>
64
60
<!-- Create New -->
65
61
<div class="relative" id="create-menu">
66
62
<button
67
- class="flex h-9 w-9 items-center justify-center rounded-full border border-border/50 bg-background transition-all hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
63
+ class="flex h-8 w-8 items-center justify-center rounded-md border border-border/50 bg-background transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
68
64
aria-label="Create new"
69
65
aria-haspopup="true"
70
66
aria-expanded="false"
72
68
>
73
69
<Plus className="h-4 w-4" />
74
70
</button>
75
- <div id="create-dropdown" class="hidden absolute right-0 mt-2 w-48 rounded-lg border bg-popover p-1 shadow-lg animate-in fade-in zoom-in-95 duration-200" role="menu" aria-label="Create new menu">
76
- <a href="/new" class="flex items-center rounded-md px-2 py-2 text-sm hover:bg-accent transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20" role="menuitem">
71
+ <div id="create-dropdown" class="hidden absolute right-0 mt-1 w-56 rounded-lg border bg-popover p-1 shadow-lg" role="menu" aria-label="Create new menu">
72
+ <a href="/new" class="flex items-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors" role="menuitem">
73
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
77
74
New Repository
78
75
</a>
79
- <a href="/new/import" class="flex items-center rounded-md px-2 py-2 text-sm hover:bg-accent transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20" role="menuitem">
76
+ <a href="/new/import" class="flex items-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors" role="menuitem">
77
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
80
78
Import Repository
81
79
</a>
82
80
</div>
85
83
<!-- Notifications -->
86
84
<a
87
85
href="/notifications"
88
- class="flex h-9 w-9 items-center justify-center rounded-full border border-border/50 bg-background transition-all hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 relative"
86
+ class="relative flex h-8 w-8 items-center justify-center rounded-md border border-border/50 bg-background transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
89
87
aria-label="Notifications"
90
88
>
91
89
<Bell className="h-4 w-4" />
92
- <span class="absolute -top-0.5 -right-0.5 flex h-3 w-3 items-center justify-center rounded-full bg-red-500">
93
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
94
- <span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
95
- </span>
96
90
</a>
97
91
98
92
<!-- User Menu -->
99
93
<div class="relative" id="user-menu">
100
94
<button
101
- class="flex h-9 w-9 items-center justify-center rounded-full bg-muted border border-border/50 ring-offset-2 hover:ring-2 hover:ring-primary/20 overflow-hidden transition-all focus:outline-none focus:ring-2 focus:ring-primary/20"
95
+ class="flex h-8 w-8 items-center justify-center rounded-full bg-muted border border-border/50 overflow-hidden transition-all hover:ring-2 hover:ring-primary/20 focus:outline-none focus:ring-2 focus:ring-primary/20"
102
96
aria-label="User menu"
103
97
aria-haspopup="true"
104
98
aria-expanded="false"
107
101
{user.avatarUrl ? (
108
102
<img src={user.avatarUrl} alt={user.username} class="h-full w-full object-cover" />
109
103
) : (
110
- <span class="text-sm font-medium">{user.username.charAt(0).toUpperCase()}</span>
104
+ <span class="text-xs font-medium">{user.username.charAt(0).toUpperCase()}</span>
111
105
)}
112
106
</button>
113
- <div id="user-dropdown" class="hidden absolute right-0 mt-2 w-64 rounded-xl border bg-popover p-2 shadow-xl animate-in fade-in zoom-in-95 duration-200" role="menu" aria-label="User menu">
114
- <div class="px-3 py-2.5">
115
- <p class="font-semibold text-sm text-foreground">{user.displayName || user.username}</p>
107
+ <div id="user-dropdown" class="hidden absolute right-0 mt-1 w-56 rounded-lg border bg-popover p-1 shadow-xl" role="menu" aria-label="User menu">
108
+ <div class="px-3 py-2 border-b border-border/50 mb-1">
109
+ <p class="font-semibold text-sm text-foreground truncate">{user.displayName || user.username}</p>
116
110
<p class="text-xs text-muted-foreground truncate">@{user.username}</p>
117
111
</div>
118
- <div class="h-px bg-border/50 my-1"></div>
119
- <a href="/profile" class="flex items-center rounded-lg px-3 py-2 text-sm hover:bg-accent transition-colors">
120
- Your Profile
112
+ <a href={`/${user.username}`} class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors" role="menuitem">
113
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
114
+ Your profile
121
115
</a>
122
- <a href="/repositories" class="flex items-center rounded-lg px-3 py-2 text-sm hover:bg-accent transition-colors">
123
- Your Repositories
116
+ <a href="/repositories" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors" role="menuitem">
117
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
118
+ Your repositories
124
119
</a>
125
- <a href="/dashboard" class="flex items-center rounded-lg px-3 py-2 text-sm hover:bg-accent transition-colors">
126
- Dashboard
127
- </a>
128
- <a href="/stars" class="flex items-center rounded-lg px-3 py-2 text-sm hover:bg-accent transition-colors">
129
- Your Stars
120
+ <a href="/stars" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors" role="menuitem">
121
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
122
+ Your stars
130
123
</a>
131
124
<div class="h-px bg-border/50 my-1"></div>
132
- <a href="/settings" class="flex items-center rounded-lg px-3 py-2 text-sm hover:bg-accent transition-colors">
125
+ <a href="/dashboard" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors" role="menuitem">
126
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
127
+ Dashboard
128
+ </a>
129
+ <a href="/settings" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors" role="menuitem">
130
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
133
131
Settings
134
132
</a>
135
133
<div class="h-px bg-border/50 my-1"></div>
136
134
<form action="/api/auth/logout" method="POST">
137
- <button type="submit" class="w-full flex items-center rounded-lg px-3 py-2 text-sm hover:bg-red-500/10 text-red-500 transition-colors">
138
- Sign Out
135
+ <button type="submit" class="w-full flex items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent text-left transition-colors">
136
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
137
+ Sign out
139
138
</button>
140
139
</form>
141
140
</div>
143
142
</>
144
143
) : (
145
144
<div class="flex items-center gap-2">
146
- <a href="/login" class="inline-flex items-center justify-center rounded-full text-sm font-medium h-9 px-5 hover:bg-muted transition-colors">
145
+ <a href="/login" class="inline-flex items-center justify-center rounded-md text-sm font-medium h-8 px-3 border border-border/50 hover:bg-accent transition-colors">
147
146
Sign in
148
147
</a>
149
- <a href="/register" class="inline-flex items-center justify-center rounded-full text-sm font-medium h-9 px-5 bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90 hover:scale-105 active:scale-95 transition-all duration-300">
148
+ <a href="/register" class="inline-flex items-center justify-center rounded-md text-sm font-medium h-8 px-3 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
150
149
Sign up
151
150
</a>
152
151
</div>
157
156
158
157
<!-- Mobile Menu -->
159
158
<button
160
- class="flex h-9 w-9 items-center justify-center rounded-full border border-border/50 bg-background transition-all hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 md:hidden"
159
+ class="flex h-8 w-8 items-center justify-center rounded-md border border-border/50 bg-background transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 md:hidden"
161
160
aria-label="Menu"
162
161
id="mobile-menu-btn"
163
162
>
168
167
169
168
<!-- Mobile Navigation -->
170
169
<div id="mobile-nav" class="hidden border-t md:hidden bg-background/95 backdrop-blur-xl" role="dialog" aria-label="Mobile navigation menu">
171
- <nav class="container py-4 space-y-1" aria-label="Mobile navigation">
172
- <a href="/explore" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
173
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
174
- Explore
170
+ <nav class="container py-3 space-y-0.5" aria-label="Mobile navigation">
171
+ <a href="/pulls" class:list={[
172
+ "flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors",
173
+ currentPath.startsWith("/pulls") ? "bg-accent text-foreground" : "hover:bg-accent"
174
+ ]}>
175
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><line x1="6" y1="9" x2="6" y2="21"/></svg>
176
+ Pull requests
175
177
</a>
176
- <a href="/issues" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
178
+ <a href="/issues" class:list={[
179
+ "flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors",
180
+ currentPath.startsWith("/issues") ? "bg-accent text-foreground" : "hover:bg-accent"
181
+ ]}>
177
182
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
178
183
Issues
179
184
</a>
180
- <a href="/pulls" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
181
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><line x1="6" y1="9" x2="6" y2="21"/></svg>
182
- Pull Requests
183
- </a>
184
- <a href="/actions" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
185
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
185
+ <a href="/actions" class:list={[
186
+ "flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors",
187
+ currentPath.startsWith("/actions") ? "bg-accent text-foreground" : "hover:bg-accent"
188
+ ]}>
189
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
186
190
Actions
187
191
</a>
188
- <a href="/cli" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
189
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
190
- CLI
191
- </a>
192
- <a href="/docs/" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium hover:bg-accent rounded-lg transition-colors">
193
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
194
- Documentation
195
- </a>
196
- <a href="/admin" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium text-purple-400 hover:bg-purple-500/10 rounded-lg transition-colors">
197
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
198
- Mission Control
192
+ <a href="/explore" class:list={[
193
+ "flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors",
194
+ currentPath.startsWith("/explore") ? "bg-accent text-foreground" : "hover:bg-accent"
195
+ ]}>
196
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
197
+ Explore
199
198
</a>
200
199
201
200
<div class="h-px bg-border/50 my-2"></div>
202
201
203
202
{user ? (
204
- <div class="space-y-1">
205
- <div class="flex items-center gap-3 px-3 py-2">
206
- {user.avatarUrl ? (
207
- <img src={user.avatarUrl} alt={user.username} class="h-8 w-8 rounded-full" />
208
- ) : (
209
- <div class="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-sm font-medium">
210
- {user.username.charAt(0).toUpperCase()}
211
- </div>
212
- )}
213
- <div>
214
- <p class="text-sm font-medium">{user.displayName || user.username}</p>
215
- <p class="text-xs text-muted-foreground">@{user.username}</p>
216
- </div>
217
- </div>
218
- <a href="/profile" class="block px-3 py-2 text-sm hover:bg-accent rounded-lg transition-colors">Profile</a>
219
- <a href="/repositories" class="block px-3 py-2 text-sm hover:bg-accent rounded-lg transition-colors">Repositories</a>
220
- <a href="/settings" class="block px-3 py-2 text-sm hover:bg-accent rounded-lg transition-colors">Settings</a>
221
- <a href="/notifications" class="block px-3 py-2 text-sm hover:bg-accent rounded-lg transition-colors">Notifications</a>
203
+ <div class="space-y-0.5">
204
+ <a href={`/${user.username}`} class="flex items-center gap-3 px-3 py-2 text-sm font-medium hover:bg-accent rounded-md transition-colors">
205
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
206
+ Your profile
207
+ </a>
208
+ <a href="/repositories" class="flex items-center gap-3 px-3 py-2 text-sm font-medium hover:bg-accent rounded-md transition-colors">
209
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
210
+ Your repositories
211
+ </a>
212
+ <a href="/settings" class="flex items-center gap-3 px-3 py-2 text-sm font-medium hover:bg-accent rounded-md transition-colors">
213
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
214
+ Settings
215
+ </a>
222
216
<form action="/api/auth/logout" method="POST" class="mt-1">
223
- <button type="submit" class="w-full text-left px-3 py-2 text-sm text-red-500 hover:bg-red-500/10 rounded-lg transition-colors">
224
- Sign Out
217
+ <button type="submit" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium hover:bg-accent rounded-md transition-colors text-left">
218
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
219
+ Sign out
225
220
</button>
226
221
</form>
227
222
</div>
228
223
) : (
229
224
<div class="flex gap-2 px-3 py-2">
230
- <a href="/login" class="flex-1 inline-flex items-center justify-center rounded-lg text-sm font-medium h-10 border hover:bg-accent transition-colors">
225
+ <a href="/login" class="flex-1 inline-flex items-center justify-center rounded-md text-sm font-medium h-9 border hover:bg-accent transition-colors">
231
226
Sign in
232
227
</a>
233
- <a href="/register" class="flex-1 inline-flex items-center justify-center rounded-lg text-sm font-medium h-10 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
228
+ <a href="/register" class="flex-1 inline-flex items-center justify-center rounded-md text-sm font-medium h-9 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
234
229
Sign up
235
230
</a>
236
231
</div>
237
232
)}
238
-
239
- <div class="pt-2">
240
- <button
241
- class="w-full flex items-center h-10 rounded-lg border border-input bg-background/50 px-3 text-sm text-muted-foreground text-left focus:outline-none focus:ring-2 focus:ring-primary/20"
242
- id="mobile-search-trigger"
243
- aria-label="Open search"
244
- >
245
- <Search className="mr-2 h-4 w-4" />
246
- Search...
247
- </button>
248
- </div>
249
233
</nav>
250
234
</div>
251
235
253
237
</header>
254
238
255
239
<script>
256
- // Dropdown toggle functionality with keyboard support
257
240
function setupDropdown(triggerId: string, dropdownId: string) {
258
241
const trigger = document.getElementById(triggerId)
259
242
const dropdown = document.getElementById(dropdownId)
264
247
function openMenu() {
265
248
dropdown!.classList.remove('hidden')
266
249
button?.setAttribute('aria-expanded', 'true')
267
- // Focus first menu item
268
250
const firstItem = dropdown!.querySelector<HTMLElement>('[role="menuitem"], a, button')
269
251
firstItem?.focus()
270
252
}
279
261
button.addEventListener('click', (e) => {
280
262
e.stopPropagation()
281
263
const isOpen = !dropdown.classList.contains('hidden')
282
- if (isOpen) {
283
- closeMenu()
284
- } else {
285
- openMenu()
286
- }
264
+ if (isOpen) closeMenu()
265
+ else openMenu()
287
266
})
288
-
289
- // Keyboard: Enter/Space opens, Escape closes
290
267
button.addEventListener('keydown', (e) => {
291
- if (e.key === 'Escape') {
292
- closeMenu()
293
- }
268
+ if (e.key === 'Escape') closeMenu()
294
269
})
295
270
}
296
271
297
- // Keyboard navigation inside dropdown
298
272
dropdown.addEventListener('keydown', (e) => {
299
273
const items = Array.from(dropdown.querySelectorAll<HTMLElement>('a, button[type="submit"]'))
300
274
const current = document.activeElement as HTMLElement
314
288
items[(idx - 1 + items.length) % items.length]?.focus()
315
289
break
316
290
case 'Tab':
317
- // Allow tab to naturally move focus; close menu
318
291
closeMenu()
319
292
break
320
293
case 'Home':
332
305
setupDropdown('create-menu', 'create-dropdown')
333
306
setupDropdown('user-menu', 'user-dropdown')
334
307
335
- // Mobile menu toggle with aria-expanded
336
308
const mobileBtn = document.getElementById('mobile-menu-btn')
337
309
const mobileNav = document.getElementById('mobile-nav')
338
310
345
317
mobileBtn.setAttribute('aria-expanded', String(!isHidden))
346
318
})
347
319
348
- // Close mobile menu on Escape
349
320
mobileNav.addEventListener('keydown', (e) => {
350
321
if (e.key === 'Escape') {
351
322
mobileNav.classList.add('hidden')
355
326
})
356
327
}
357
328
358
- // Close dropdowns when clicking outside
359
329
document.addEventListener('click', () => {
360
330
document.querySelectorAll('[id$="-dropdown"]').forEach(el => {
361
331
el.classList.add('hidden')
362
332
})
363
- // Reset aria-expanded on all dropdown triggers
364
333
document.querySelectorAll('[aria-haspopup="true"]').forEach(btn => {
365
334
btn.setAttribute('aria-expanded', 'false')
366
335
})
367
336
})
368
337
369
- // Global keyboard shortcut: Escape closes any open dropdown
370
338
document.addEventListener('keydown', (e) => {
371
339
if (e.key === 'Escape') {
372
340
document.querySelectorAll('[id$="-dropdown"]').forEach(el => {
377
345
document.querySelectorAll('[aria-haspopup="true"]').forEach(btn => {
378
346
btn.setAttribute('aria-expanded', 'false')
379
347
})
380
- // Also close mobile nav
381
348
if (mobileNav && !mobileNav.classList.contains('hidden')) {
382
349
mobileNav.classList.add('hidden')
383
350
mobileBtn?.setAttribute('aria-expanded', 'false')
385
352
}
386
353
})
387
354
388
- // Search Triggers
389
355
const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement
390
356
const mobileSearchTrigger = document.getElementById('mobile-search-trigger')
391
357
392
358
function openSearch() {
393
- window.dispatchEvent(new CustomEvent('open-global-search'));
359
+ window.dispatchEvent(new CustomEvent('open-global-search'));
394
360
}
395
361
396
362
if (searchInput) {
397
- searchInput.addEventListener('click', (e) => {
398
- e.preventDefault();
399
- searchInput.blur(); // Don't actually focus
400
- openSearch();
401
- });
363
+ searchInput.addEventListener('click', (e) => {
364
+ e.preventDefault();
365
+ searchInput.blur();
366
+ openSearch();
367
+ });
402
368
}
403
369
404
370
if (mobileSearchTrigger) {
405
- mobileSearchTrigger.addEventListener('click', openSearch);
371
+ mobileSearchTrigger.addEventListener('click', openSearch);
406
372
}
407
373
</script>
408
374
▼src/components/milestones/MilestonesList.tsx
+19-19
70
70
className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"
71
71
>
72
72
<div>
73
- <h1 className="text-2xl font-bold text-white flex items-center gap-3">
73
+ <h1 className="text-2xl font-bold text-foreground flex items-center gap-3">
74
74
<div className="p-2 rounded-lg bg-gradient-to-br from-purple-500/20 to-pink-500/20 border border-purple-500/30">
75
75
<Target className="h-6 w-6 text-purple-400" />
76
76
</div>
77
77
Milestones
78
78
</h1>
79
- <p className="text-gray-500 text-sm mt-1">
79
+ <p className="text-muted-foreground text-sm mt-1">
80
80
Track progress and manage project goals
81
81
</p>
82
82
</div>
85
85
href={`/${repoOwner}/${repoName}/milestones/new`}
86
86
whileHover={{ scale: 1.02 }}
87
87
whileTap={{ scale: 0.98 }}
88
- className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all"
88
+ className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-foreground shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all"
89
89
>
90
90
<Plus className="h-4 w-4" />
91
91
New Milestone
103
103
onClick={() => setFilter("open")}
104
104
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all ${filter === "open"
105
105
? "bg-green-500/10 text-green-400 border border-green-500/30"
106
- : "text-gray-400 hover:text-white hover:bg-white/5"
106
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
107
107
}`}
108
108
>
109
109
<AlertCircle className="h-4 w-4" />
113
113
onClick={() => setFilter("closed")}
114
114
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all ${filter === "closed"
115
115
? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
116
- : "text-gray-400 hover:text-white hover:bg-white/5"
116
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
117
117
}`}
118
118
>
119
119
<CheckCircle2 className="h-4 w-4" />
137
137
{/* Gradient border */}
138
138
<div className="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50 group-hover:opacity-100 transition-opacity" />
139
139
140
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-5">
140
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-5">
141
141
<div className="flex items-start justify-between gap-4">
142
142
<div className="flex-1">
143
143
{/* Title */}
144
144
<a
145
145
href={`/${repoOwner}/${repoName}/milestones/${milestone.id}`}
146
- className="text-lg font-semibold text-white hover:text-purple-400 transition-colors"
146
+ className="text-lg font-semibold text-foreground hover:text-purple-400 transition-colors"
147
147
>
148
148
{milestone.title}
149
149
</a>
150
150
151
151
{/* Description */}
152
152
{milestone.description && (
153
- <p className="text-sm text-gray-400 mt-1 line-clamp-2">
153
+ <p className="text-sm text-muted-foreground mt-1 line-clamp-2">
154
154
{milestone.description}
155
155
</p>
156
156
)}
157
157
158
158
{/* Meta Info */}
159
- <div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
159
+ <div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
160
160
{milestone.dueDate && (
161
161
<span className="flex items-center gap-1">
162
162
<Calendar className="h-3.5 w-3.5" />
176
176
<div className="relative">
177
177
<button
178
178
onClick={() => setShowMenu(showMenu === milestone.id ? null : milestone.id)}
179
- className="p-2 rounded-lg hover:bg-white/10 transition-colors"
179
+ className="p-2 rounded-lg hover:bg-secondary/80 transition-colors"
180
180
>
181
- <MoreHorizontal className="h-4 w-4 text-gray-500" />
181
+ <MoreHorizontal className="h-4 w-4 text-muted-foreground" />
182
182
</button>
183
183
184
184
{showMenu === milestone.id && (
185
185
<motion.div
186
186
initial={{ opacity: 0, scale: 0.95 }}
187
187
animate={{ opacity: 1, scale: 1 }}
188
- className="absolute right-0 top-full mt-1 w-40 rounded-lg border border-white/10 bg-[#161b22] shadow-xl z-10"
188
+ className="absolute right-0 top-full mt-1 w-40 rounded-lg border border-border bg-accent shadow-xl z-10"
189
189
>
190
190
<div className="p-1">
191
- <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md">
191
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:bg-accent rounded-md">
192
192
<Edit3 className="h-4 w-4" />
193
193
Edit
194
194
</button>
195
- <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md">
195
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:bg-accent rounded-md">
196
196
<Archive className="h-4 w-4" />
197
197
Close
198
198
</button>
208
208
209
209
{/* Progress Section */}
210
210
<div className="mt-4">
211
- <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
211
+ <div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
212
212
<span className="flex items-center gap-3">
213
213
<span className="flex items-center gap-1 text-green-400">
214
214
<CheckCircle2 className="h-3.5 w-3.5" />
226
226
</div>
227
227
228
228
{/* Progress Bar */}
229
- <div className="h-2 bg-white/5 rounded-full overflow-hidden">
229
+ <div className="h-2 bg-secondary rounded-full overflow-hidden">
230
230
<motion.div
231
231
initial={{ width: 0 }}
232
232
animate={{ width: `${milestone.progress}%` }}
248
248
<Milestone className="h-16 w-16 text-purple-400" />
249
249
<div className="absolute inset-0 bg-purple-500 blur-xl opacity-30" />
250
250
</div>
251
- <h3 className="text-xl font-semibold text-white mb-2">
251
+ <h3 className="text-xl font-semibold text-foreground mb-2">
252
252
{filter === "closed" ? "No closed milestones" : "No milestones yet"}
253
253
</h3>
254
- <p className="text-gray-500 mb-6 max-w-md mx-auto">
254
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
255
255
{filter === "closed"
256
256
? "Closed milestones will appear here."
257
257
: "Milestones help you track progress on groups of issues and pull requests."}
261
261
href={`/${repoOwner}/${repoName}/milestones/new`}
262
262
whileHover={{ scale: 1.05 }}
263
263
whileTap={{ scale: 0.95 }}
264
- className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25"
264
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-foreground shadow-lg shadow-purple-500/25"
265
265
>
266
266
<Plus className="h-4 w-4" />
267
267
Create a milestone
268
268
▼src/components/milestones/NewMilestoneForm.tsx
+15-15
61
61
>
62
62
<a
63
63
href={`/${repoOwner}/${repoName}/milestones`}
64
- className="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors mb-4"
64
+ className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
65
65
>
66
66
<ArrowLeft className="h-4 w-4" />
67
67
Back to milestones
68
68
</a>
69
69
70
- <h1 className="text-2xl font-bold text-white flex items-center gap-3">
70
+ <h1 className="text-2xl font-bold text-foreground flex items-center gap-3">
71
71
<div className="p-2 rounded-lg bg-gradient-to-br from-purple-500/20 to-pink-500/20 border border-purple-500/30">
72
72
<Target className="h-6 w-6 text-purple-400" />
73
73
</div>
74
74
New Milestone
75
75
</h1>
76
- <p className="text-gray-500 text-sm mt-2">
76
+ <p className="text-muted-foreground text-sm mt-2">
77
77
Create a milestone to track progress on a collection of issues and pull requests.
78
78
</p>
79
79
</motion.div>
89
89
{/* Title */}
90
90
<div className="relative">
91
91
<div className="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50" />
92
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
93
- <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
92
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6">
93
+ <label className="block text-sm font-medium text-foreground mb-2 flex items-center gap-2">
94
94
<FileText className="h-4 w-4 text-purple-400" />
95
95
Title *
96
96
</label>
100
100
onChange={(e) => setTitle(e.target.value)}
101
101
placeholder="Milestone title"
102
102
required
103
- className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all"
103
+ className="w-full rounded-lg border border-border bg-secondary px-4 py-3 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all"
104
104
/>
105
105
</div>
106
106
</div>
108
108
{/* Due Date */}
109
109
<div className="relative">
110
110
<div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-blue-500/20 rounded-xl opacity-50" />
111
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
112
- <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
111
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6">
112
+ <label className="block text-sm font-medium text-foreground mb-2 flex items-center gap-2">
113
113
<Calendar className="h-4 w-4 text-cyan-400" />
114
114
Due date (optional)
115
115
</label>
117
117
type="date"
118
118
value={dueDate}
119
119
onChange={(e) => setDueDate(e.target.value)}
120
- className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all [color-scheme:dark]"
120
+ className="w-full rounded-lg border border-border bg-secondary px-4 py-3 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all [color-scheme:dark]"
121
121
/>
122
- <p className="text-xs text-gray-500 mt-2">
122
+ <p className="text-xs text-muted-foreground mt-2">
123
123
Set a target date for this milestone to track deadlines.
124
124
</p>
125
125
</div>
128
128
{/* Description */}
129
129
<div className="relative">
130
130
<div className="absolute -inset-[1px] bg-gradient-to-r from-green-500/20 via-transparent to-emerald-500/20 rounded-xl opacity-50" />
131
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
132
- <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
131
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6">
132
+ <label className="block text-sm font-medium text-foreground mb-2 flex items-center gap-2">
133
133
<FileText className="h-4 w-4 text-green-400" />
134
134
Description (optional)
135
135
</label>
138
138
onChange={(e) => setDescription(e.target.value)}
139
139
placeholder="Describe the goals of this milestone..."
140
140
rows={4}
141
- className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20 transition-all resize-none"
141
+ className="w-full rounded-lg border border-border bg-secondary px-4 py-3 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20 transition-all resize-none"
142
142
/>
143
143
</div>
144
144
</div>
147
147
<div className="flex items-center justify-end gap-3 pt-4">
148
148
<a
149
149
href={`/${repoOwner}/${repoName}/milestones`}
150
- className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg border border-white/10 bg-white/5 text-sm font-medium text-gray-300 hover:bg-white/10 transition-all"
150
+ className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg border border-border bg-secondary text-sm font-medium text-muted-foreground hover:bg-secondary/80 transition-all"
151
151
>
152
152
<X className="h-4 w-4" />
153
153
Cancel
157
157
disabled={!title.trim() || isSubmitting}
158
158
whileHover={{ scale: 1.02 }}
159
159
whileTap={{ scale: 0.98 }}
160
- className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
160
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-foreground shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
161
161
>
162
162
<Save className="h-4 w-4" />
163
163
{isSubmitting ? "Creating..." : "Create Milestone"}
164
164
▼src/components/pr/AIReviewPanel.tsx
+1-1
298
298
<button
299
299
onClick={() => handleApply(suggestion.id)}
300
300
disabled={applyingId === suggestion.id}
301
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 transition-colors"
301
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-green-600 text-foreground rounded-md hover:bg-green-700 disabled:opacity-50 transition-colors"
302
302
>
303
303
{applyingId === suggestion.id ? (
304
304
<Loader2 className="h-4 w-4 animate-spin" />
305
305
▼src/components/pr/StackVisualization.tsx
+1-1
57
57
return <GitMerge className="h-4 w-4 text-purple-500" />;
58
58
}
59
59
if (entry.pr.isDraft) {
60
- return <FileEdit className="h-4 w-4 text-gray-400" />;
60
+ return <FileEdit className="h-4 w-4 text-muted-foreground" />;
61
61
}
62
62
if (entry.pr.state === "open") {
63
63
return <GitPullRequest className="h-4 w-4 text-green-500" />;
64
64
▼src/components/pulls/CodeOwnersEnforcement.tsx
+14-14
34
34
35
35
if (isLoading) {
36
36
return (
37
- <div className="mt-4 border rounded-xl overflow-hidden bg-[#0d1117]/80 backdrop-blur-md shadow-sm border-white/10 animate-pulse">
38
- <div className="bg-white/[0.02] p-3 border-b border-white/10 flex items-center">
39
- <div className="h-4 w-32 bg-white/10 rounded"></div>
37
+ <div className="mt-4 border rounded-xl overflow-hidden bg-card/80 backdrop-blur-md shadow-sm border-border animate-pulse">
38
+ <div className="bg-white/[0.02] p-3 border-b border-border flex items-center">
39
+ <div className="h-4 w-32 bg-secondary/80 rounded"></div>
40
40
</div>
41
41
<div className="p-4 space-y-3">
42
- <div className="h-3 w-3/4 bg-white/5 rounded"></div>
43
- <div className="h-3 w-1/2 bg-white/5 rounded"></div>
42
+ <div className="h-3 w-3/4 bg-secondary rounded"></div>
43
+ <div className="h-3 w-1/2 bg-secondary rounded"></div>
44
44
</div>
45
45
</div>
46
46
);
48
48
49
49
if (error || !data) {
50
50
return (
51
- <div className="mt-4 border rounded-xl overflow-hidden bg-[#0d1117]/80 backdrop-blur-md shadow-sm border-red-500/20">
51
+ <div className="mt-4 border rounded-xl overflow-hidden bg-card/80 backdrop-blur-md shadow-sm border-red-500/20">
52
52
<div className="bg-red-500/5 p-3 flex items-center gap-2 text-sm text-red-400">
53
53
<AlertTriangle className="h-4 w-4" />
54
54
Failed to load CODEOWNERS enforcement: {error}
69
69
<div className={`mt-4 border rounded-xl overflow-hidden backdrop-blur-md shadow-sm ${
70
70
enforced
71
71
? (isSatisfied ? "border-green-500/20 bg-green-500/[0.02]" : "border-amber-500/20 bg-amber-500/[0.02]")
72
- : "border-white/10 bg-[#0d1117]/80"
72
+ : "border-border bg-card/80"
73
73
}`}>
74
- <div className="bg-white/[0.02] p-3 border-b border-white/10 flex items-center justify-between">
74
+ <div className="bg-white/[0.02] p-3 border-b border-border flex items-center justify-between">
75
75
<div className="text-sm font-semibold flex items-center gap-2 text-gray-200">
76
76
<div className={`p-1.5 rounded-md ${
77
77
enforced ? (isSatisfied ? "bg-green-500/10 text-green-400" : "bg-amber-500/10 text-amber-400") : "bg-purple-500/10 text-purple-400"
87
87
{isSatisfied ? "Satisfied" : "Pending"}
88
88
</span>
89
89
) : (
90
- <span className="text-xs px-2 py-0.5 rounded-full border bg-white/5 border-white/10 text-gray-400 font-medium">
90
+ <span className="text-xs px-2 py-0.5 rounded-full border bg-secondary border-border text-muted-foreground font-medium">
91
91
Optional
92
92
</span>
93
93
)}
99
99
{rules.map((rule: any, idx: number) => {
100
100
const ruleSatisfied = rule.missingApprovals === 0;
101
101
return (
102
- <div key={idx} className="bg-white/5 rounded-lg border border-white/5 p-3 flex flex-col gap-2">
102
+ <div key={idx} className="bg-secondary rounded-lg border border-white/5 p-3 flex flex-col gap-2">
103
103
<div className="flex items-center justify-between">
104
- <div className="font-mono text-xs text-gray-300 break-all bg-black/30 px-2 py-1 rounded">
104
+ <div className="font-mono text-xs text-muted-foreground break-all bg-black/30 px-2 py-1 rounded">
105
105
{rule.pattern}
106
106
</div>
107
107
{ruleSatisfied ? (
110
110
<Clock className="h-4 w-4 text-amber-400 shrink-0" />
111
111
)}
112
112
</div>
113
- <div className="text-xs text-gray-400 flex items-center gap-1.5 flex-wrap">
113
+ <div className="text-xs text-muted-foreground flex items-center gap-1.5 flex-wrap">
114
114
<span>Required:</span>
115
115
{rule.owners.map((owner: string) => (
116
- <span key={owner} className="flex items-center gap-1 text-gray-300 font-medium">
116
+ <span key={owner} className="flex items-center gap-1 text-muted-foreground font-medium">
117
117
<div className="h-4 w-4 rounded-full bg-blue-500/20 flex items-center justify-center text-[8px] text-blue-300">
118
118
{owner.replace('@', '')[0]?.toUpperCase()}
119
119
</div>
131
131
})}
132
132
</div>
133
133
) : (
134
- <div className="text-sm text-gray-400 text-center py-2">
134
+ <div className="text-sm text-muted-foreground text-center py-2">
135
135
No CODEOWNERS rules apply to these changes.
136
136
</div>
137
137
)}
138
138
▼src/components/pulls/CustomPRState.tsx
+12-12
81
81
: null;
82
82
83
83
return (
84
- <div className="w-full max-w-sm rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-md shadow-2xl overflow-hidden mb-6">
85
- <div className="p-4 border-b border-white/10 bg-white/[0.02]">
84
+ <div className="w-full max-w-sm rounded-xl border border-border bg-card/80 backdrop-blur-md shadow-2xl overflow-hidden mb-6">
85
+ <div className="p-4 border-b border-border bg-white/[0.02]">
86
86
<div className="flex items-center justify-between">
87
87
<div className="text-sm font-semibold flex items-center gap-2 text-gray-200">
88
88
<div className="p-1.5 rounded-md bg-blue-500/10">
99
99
<select
100
100
value={selectedState}
101
101
onChange={(e) => setSelectedState(e.target.value)}
102
- className="w-full h-9 rounded-lg border border-white/10 bg-white/5 px-3 text-sm text-gray-200 focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 appearance-none outline-none transition-all cursor-pointer"
102
+ className="w-full h-9 rounded-lg border border-border bg-secondary px-3 text-sm text-gray-200 focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 appearance-none outline-none transition-all cursor-pointer"
103
103
>
104
104
<option value="builtin:open">Open</option>
105
105
<option value="builtin:closed">Closed</option>
109
109
</option>
110
110
))}
111
111
</select>
112
- <ChevronDown className="absolute right-3 top-2.5 h-4 w-4 text-gray-500 pointer-events-none" />
112
+ <ChevronDown className="absolute right-3 top-2.5 h-4 w-4 text-muted-foreground pointer-events-none" />
113
113
</div>
114
114
<button
115
115
onClick={handleApply}
116
116
disabled={isApplying || (initialStateId === selectedState.replace("custom:", ""))}
117
- className="h-9 px-4 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-w-[70px]"
117
+ className="h-9 px-4 rounded-lg bg-blue-500 hover:bg-blue-600 text-foreground text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-w-[70px]"
118
118
>
119
119
{isApplying ? (
120
- <div className="h-4 w-4 rounded-full border-2 border-white/20 border-t-white animate-spin" />
120
+ <div className="h-4 w-4 rounded-full border-2 border-border/80 border-t-white animate-spin" />
121
121
) : (
122
122
"Apply"
123
123
)}
133
133
exit={{ opacity: 0, y: -5 }}
134
134
className="rounded-lg border border-white/5 bg-white/[0.02] p-3 space-y-3"
135
135
>
136
- <div className="text-xs text-gray-400">
136
+ <div className="text-xs text-muted-foreground">
137
137
<span className="font-medium text-gray-200">{selectedStateObj.displayName}</span>
138
138
{selectedStateObj.description && <span className="ml-2">— {selectedStateObj.description}</span>}
139
139
</div>
157
157
</div>
158
158
159
159
{isLoadingPolicy ? (
160
- <div className="text-xs text-gray-500 flex items-center gap-2">
160
+ <div className="text-xs text-muted-foreground flex items-center gap-2">
161
161
<div className="h-3 w-3 rounded-full border-2 border-gray-500 border-t-transparent animate-spin" />
162
162
Loading reviewer policy...
163
163
</div>
164
164
) : policyData ? (
165
165
<div className="text-xs space-y-1 mt-2 pt-2 border-t border-white/5">
166
- <div className="text-gray-300">
167
- Required approvals: <span className="text-white font-medium">{policyData.approvedRequired || 0}/{policyData.totalRequired || 0}</span>
166
+ <div className="text-muted-foreground">
167
+ Required approvals: <span className="text-foreground font-medium">{policyData.approvedRequired || 0}/{policyData.totalRequired || 0}</span>
168
168
</div>
169
169
{policyData.reviewers && policyData.reviewers.length > 0 && (
170
- <div className="text-gray-400">
170
+ <div className="text-muted-foreground">
171
171
Waiting on: {policyData.reviewers.filter((r: any) => !r.hasApproved).map((r: any) => r.username).join(", ")}
172
172
</div>
173
173
)}
180
180
initial={{ opacity: 0, y: 5 }}
181
181
animate={{ opacity: 1, y: 0 }}
182
182
exit={{ opacity: 0, y: -5 }}
183
- className="text-xs text-gray-500 flex items-start gap-1.5 px-1"
183
+ className="text-xs text-muted-foreground flex items-start gap-1.5 px-1"
184
184
>
185
185
<Globe className="h-4 w-4 shrink-0" />
186
186
<span>{selectedState === "builtin:closed" ? "Transitioning to Closed will close this pull request." : "Standard pull request state."}</span>
187
187
▼src/components/pulls/PullRequestsList.tsx
+33-33
88
88
89
89
function getPRIcon(state: string, isDraft?: boolean) {
90
90
if (isDraft) {
91
- return { icon: GitPullRequest, color: "text-gray-400", bg: "bg-gray-500/10" };
91
+ return { icon: GitPullRequest, color: "text-muted-foreground", bg: "bg-gray-500/10" };
92
92
}
93
93
switch (state) {
94
94
case "open":
98
98
case "closed":
99
99
return { icon: XCircle, color: "text-red-400", bg: "bg-red-500/10" };
100
100
default:
101
- return { icon: GitPullRequest, color: "text-gray-400", bg: "bg-gray-500/10" };
101
+ return { icon: GitPullRequest, color: "text-muted-foreground", bg: "bg-gray-500/10" };
102
102
}
103
103
}
104
104
270
270
<div className="relative flex-1 max-w-xl group">
271
271
<div className="absolute -inset-0.5 bg-gradient-to-r from-green-500/20 via-purple-500/20 to-pink-500/20 rounded-xl blur opacity-0 group-focus-within:opacity-100 transition-opacity" />
272
272
<div className="relative">
273
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
273
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
274
274
<input
275
275
type="search"
276
276
placeholder="Search all pull requests..."
277
277
value={searchQuery}
278
278
onChange={(e) => setSearchQuery(e.target.value)}
279
- className="w-full rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20 transition-all"
279
+ className="w-full rounded-xl border border-border bg-secondary backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20 transition-all"
280
280
/>
281
281
</div>
282
282
</div>
287
287
href={`/${repoOwner}/${repoName}/labels`}
288
288
whileHover={{ scale: 1.02 }}
289
289
whileTap={{ scale: 0.98 }}
290
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
290
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
291
291
>
292
292
<Tag className="h-4 w-4" />
293
293
Labels
296
296
href={`/${repoOwner}/${repoName}/milestones`}
297
297
whileHover={{ scale: 1.02 }}
298
298
whileTap={{ scale: 0.98 }}
299
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
299
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
300
300
>
301
301
<Calendar className="h-4 w-4" />
302
302
Milestones
305
305
href={`/${repoOwner}/${repoName}/compare`}
306
306
whileHover={{ scale: 1.05 }}
307
307
whileTap={{ scale: 0.95 }}
308
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-white shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
308
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-foreground shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
309
309
>
310
310
<Plus className="h-4 w-4" />
311
311
New Pull Request
320
320
transition={{ delay: 0.1 }}
321
321
className="relative"
322
322
>
323
- <div className="mb-4 rounded-xl border border-white/10 bg-white/[0.03] p-4">
323
+ <div className="mb-4 rounded-xl border border-border bg-white/[0.03] p-4">
324
324
<div className="flex flex-wrap items-center justify-between gap-3">
325
325
<div>
326
- <p className="text-sm font-medium text-white">Dependency Workflow</p>
327
- <p className="text-xs text-gray-400">
326
+ <p className="text-sm font-medium text-foreground">Dependency Workflow</p>
327
+ <p className="text-xs text-muted-foreground">
328
328
Detect cross-PR dependencies, suggest stack order, and apply it directly.
329
329
</p>
330
330
</div>
358
358
type="button"
359
359
onClick={applySuggestedOrder}
360
360
disabled={isApplying || !suggestion || suggestion.cycles.length > 0}
361
- className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-blue-600 to-cyan-600 px-3 py-1.5 text-xs font-medium text-white disabled:opacity-60"
361
+ className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-blue-600 to-cyan-600 px-3 py-1.5 text-xs font-medium text-foreground disabled:opacity-60"
362
362
>
363
363
{isApplying ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="h-3.5 w-3.5" />}
364
364
Apply As Stack
376
376
type="button"
377
377
onClick={bulkMergeSelected}
378
378
disabled={isBulkMerging || selectedPrIds.length === 0}
379
- className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-emerald-600 to-green-600 px-3 py-1.5 text-xs font-medium text-white disabled:opacity-60"
379
+ className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-emerald-600 to-green-600 px-3 py-1.5 text-xs font-medium text-foreground disabled:opacity-60"
380
380
>
381
381
{isBulkMerging ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <GitMerge className="h-3.5 w-3.5" />}
382
382
Bulk Merge Selected
384
384
</div>
385
385
</div>
386
386
{graph && (
387
- <div className="mt-3 text-xs text-gray-400">
387
+ <div className="mt-3 text-xs text-muted-foreground">
388
388
Graph: {graph.nodes.length} PRs, {graph.edges.length} edges (
389
389
{graph.edges.filter((edge) => edge.type === "files").length} file conflicts)
390
390
</div>
391
391
)}
392
392
{suggestion && (
393
- <div className="mt-2 text-xs text-gray-300">
393
+ <div className="mt-2 text-xs text-muted-foreground">
394
394
Suggested:{" "}
395
395
{suggestion.order.map((id) => {
396
396
const pr = prById.get(id);
399
399
</div>
400
400
)}
401
401
{workflowMsg && (
402
- <div className="mt-2 text-xs text-gray-400">{workflowMsg}</div>
402
+ <div className="mt-2 text-xs text-muted-foreground">{workflowMsg}</div>
403
403
)}
404
404
</div>
405
405
{/* Gradient border effect */}
406
406
<div className="absolute -inset-[1px] bg-gradient-to-r from-green-500/30 via-purple-500/20 to-pink-500/30 rounded-xl blur-sm opacity-50" />
407
407
408
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden">
408
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden">
409
409
{/* Filter Header */}
410
410
<div className="flex items-center justify-between gap-4 border-b border-white/5 bg-white/[0.02] px-4 py-3">
411
411
<div className="flex items-center gap-1">
413
413
onClick={() => setFilter("open")}
414
414
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "open"
415
415
? "bg-green-500/10 text-green-400 border border-green-500/30"
416
- : "text-gray-400 hover:text-white hover:bg-white/5"
416
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
417
417
}`}
418
418
>
419
419
<GitPullRequest className="h-4 w-4" />
423
423
onClick={() => setFilter("closed")}
424
424
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "closed"
425
425
? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
426
- : "text-gray-400 hover:text-white hover:bg-white/5"
426
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
427
427
}`}
428
428
>
429
429
<CheckCircle2 className="h-4 w-4" />
431
431
</button>
432
432
</div>
433
433
434
- <div className="flex items-center gap-3 text-sm text-gray-500">
435
- <button className="flex items-center gap-1 hover:text-white transition-colors">
434
+ <div className="flex items-center gap-3 text-sm text-muted-foreground">
435
+ <button className="flex items-center gap-1 hover:text-foreground transition-colors">
436
436
<User className="h-3.5 w-3.5" />
437
437
Author
438
438
</button>
439
- <button className="flex items-center gap-1 hover:text-white transition-colors">
439
+ <button className="flex items-center gap-1 hover:text-foreground transition-colors">
440
440
<Tag className="h-3.5 w-3.5" />
441
441
Label
442
442
</button>
443
- <button className="flex items-center gap-1 hover:text-white transition-colors">
443
+ <button className="flex items-center gap-1 hover:text-foreground transition-colors">
444
444
<ArrowUpDown className="h-3.5 w-3.5" />
445
445
Sort
446
446
</button>
461
461
animate={{ opacity: 1, x: 0 }}
462
462
exit={{ opacity: 0, x: 20 }}
463
463
transition={{ delay: index * 0.03 }}
464
- className="flex items-start gap-3 p-4 hover:bg-white/[0.02] transition-colors group"
464
+ className="flex items-start gap-3 p-4 hover:bg-accent/50 transition-colors group"
465
465
>
466
466
{pr.state === "open" && (
467
467
<button
474
474
}}
475
475
className={`mt-1 h-4 w-4 rounded border ${selectedPrIds.includes(pr.id)
476
476
? "border-green-400 bg-green-500/20"
477
- : "border-white/20 bg-transparent"
477
+ : "border-border/80 bg-transparent"
478
478
}`}
479
479
/>
480
480
)}
489
489
{/* Content */}
490
490
<div className="flex-1 min-w-0">
491
491
<div className="flex items-center gap-2 mb-1 flex-wrap">
492
- <span className="font-semibold text-white group-hover:text-green-400 transition-colors">
492
+ <span className="font-semibold text-foreground group-hover:text-green-400 transition-colors">
493
493
{pr.title}
494
494
</span>
495
495
{pr.isDraft && (
496
- <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-gray-400 border border-gray-500/30">
496
+ <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-muted-foreground border border-gray-500/30">
497
497
Draft
498
498
</span>
499
499
)}
512
512
</span>
513
513
))}
514
514
</div>
515
- <div className="flex items-center gap-2 text-xs text-gray-500 flex-wrap">
515
+ <div className="flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
516
516
<span>#{pr.number}</span>
517
517
<span>•</span>
518
518
<span>opened {timeAgo(pr.createdAt)}</span>
525
525
className="h-4 w-4 rounded-full"
526
526
/>
527
527
) : (
528
- <div className="h-4 w-4 rounded-full bg-gradient-to-br from-green-400 to-emerald-500 flex items-center justify-center text-[8px] text-white font-bold">
528
+ <div className="h-4 w-4 rounded-full bg-gradient-to-br from-green-400 to-emerald-500 flex items-center justify-center text-[8px] text-foreground font-bold">
529
529
{pr.author.username[0].toUpperCase()}
530
530
</div>
531
531
)}
536
536
{pr.sourceBranch && pr.targetBranch && (
537
537
<>
538
538
<span>•</span>
539
- <span className="flex items-center gap-1 text-gray-600">
539
+ <span className="flex items-center gap-1 text-muted-foreground">
540
540
<GitBranch className="h-3 w-3" />
541
541
<code className="text-purple-400">{pr.sourceBranch}</code>
542
542
<span>→</span>
549
549
550
550
{/* Comment Count */}
551
551
{pr.commentCount > 0 && (
552
- <div className="flex items-center gap-1 text-xs text-gray-500 group-hover:text-gray-400 transition-colors">
552
+ <div className="flex items-center gap-1 text-xs text-muted-foreground group-hover:text-muted-foreground transition-colors">
553
553
<MessageSquare className="h-4 w-4" />
554
554
{pr.commentCount}
555
555
</div>
567
567
<GitPullRequest className="h-12 w-12 text-green-400" />
568
568
<div className="absolute inset-0 bg-green-500 blur-xl opacity-30" />
569
569
</div>
570
- <h3 className="text-lg font-semibold text-white mb-2">
570
+ <h3 className="text-lg font-semibold text-foreground mb-2">
571
571
{searchQuery ? "No matching pull requests" : "Welcome to pull requests!"}
572
572
</h3>
573
- <p className="text-gray-500 mb-6 max-w-md mx-auto">
573
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
574
574
{searchQuery
575
575
? "Try adjusting your search or filter to find what you're looking for."
576
576
: "Pull requests help you collaborate on code with others. When you're ready, you can merge your code into the main branch."}
580
580
href={`/${repoOwner}/${repoName}/compare`}
581
581
whileHover={{ scale: 1.05 }}
582
582
whileTap={{ scale: 0.95 }}
583
- className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-white shadow-lg shadow-green-500/25"
583
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-foreground shadow-lg shadow-green-500/25"
584
584
>
585
585
<Plus className="h-4 w-4" />
586
586
Create the first pull request
587
587
▼src/components/repo/FileExplorer.astro
+39-50
1
1
---
2
-import { FileText, Folder } from "lucide-react";
2
+import { Icon } from "astro-icon/components";
3
3
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
4
4
5
5
interface Props {
25
25
const filePath = path ? `${path}/${file.name}` : file.name;
26
26
return `/${owner}/${repo}/${type}/${branch}/${filePath}`;
27
27
};
28
+
29
+const getIconName = (fileName: string) => {
30
+ const ext = fileName.split(".").pop()?.toLowerCase();
31
+ const lowerName = fileName.toLowerCase();
32
+
33
+ if (lowerName === "package.json") return "vscode-icons:file-type-node";
34
+ if (lowerName === "tsconfig.json") return "vscode-icons:file-type-tsconfig";
35
+ if (lowerName === "dockerfile" || ext === "dockerignore") return "vscode-icons:file-type-docker";
36
+ if (lowerName === ".gitignore") return "vscode-icons:file-type-git";
37
+
38
+ if (ext === "md") return "vscode-icons:file-type-markdown";
39
+ if (ext === "json") return "vscode-icons:file-type-json";
40
+ if (ext === "js" || ext === "cjs" || ext === "mjs") return "vscode-icons:file-type-js";
41
+ if (ext === "jsx") return "vscode-icons:file-type-reactjs";
42
+ if (ext === "ts") return "vscode-icons:file-type-typescript";
43
+ if (ext === "tsx") return "vscode-icons:file-type-reactts";
44
+ if (ext === "html" || ext === "htm") return "vscode-icons:file-type-html";
45
+ if (ext === "css") return "vscode-icons:file-type-css";
46
+ if (ext === "scss" || ext === "sass") return "vscode-icons:file-type-sass";
47
+ if (ext === "py") return "vscode-icons:file-type-python";
48
+ if (ext === "go") return "vscode-icons:file-type-go";
49
+ if (ext === "rs") return "vscode-icons:file-type-rust";
50
+ if (ext === "java") return "vscode-icons:file-type-java";
51
+ if (ext === "c") return "vscode-icons:file-type-c";
52
+ if (ext === "cpp" || ext === "cc" || ext === "cxx") return "vscode-icons:file-type-cpp";
53
+ if (ext === "h" || ext === "hpp") return "vscode-icons:file-type-cplusplus";
54
+ if (ext === "sh" || ext === "bash") return "vscode-icons:file-type-shell";
55
+ if (ext === "yaml" || ext === "yml") return "vscode-icons:file-type-yaml";
56
+ if (ext === "toml") return "vscode-icons:file-type-toml";
57
+ if (ext === "xml") return "vscode-icons:file-type-xml";
58
+ if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "gif" || ext === "svg" || ext === "webp") return "vscode-icons:file-type-image";
59
+ if (ext === "astro") return "vscode-icons:file-type-astro";
60
+ if (ext === "vue") return "vscode-icons:file-type-vue";
61
+
62
+ return "vscode-icons:default-file";
63
+};
28
64
---
29
65
30
66
<div class="w-full">
55
91
<tr class="hover:bg-muted/50 transition-colors group">
56
92
<td class="p-4 py-2.5 w-10">
57
93
{file.type === "directory" ? (
58
- <svg
59
- class="h-4 w-4 text-blue-500 fill-blue-500"
60
- viewBox="0 0 16 16"
61
- fill="currentColor"
62
- >
63
- <path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z" />
64
- </svg>
94
+ <Icon name="vscode-icons:default-folder" class="h-4 w-4" />
65
95
) : (
66
- (() => {
67
- const ext = file.name.split(".").pop()?.toLowerCase();
68
- let iconClass = "text-muted-foreground";
69
- if (
70
- ["js", "jsx", "ts", "tsx", "cjs", "mjs"].includes(
71
- ext || "",
72
- )
73
- )
74
- iconClass = "text-yellow-500";
75
- else if (["html", "htm"].includes(ext || ""))
76
- iconClass = "text-orange-600";
77
- else if (
78
- ["css", "scss", "sass", "less"].includes(ext || "")
79
- )
80
- iconClass = "text-blue-600";
81
- else if (
82
- ["json", "yml", "yaml", "toml"].includes(ext || "")
83
- )
84
- iconClass = "text-yellow-600";
85
- else if (["md", "txt"].includes(ext || ""))
86
- iconClass = "text-gray-400";
87
- else if (
88
- ["png", "jpg", "jpeg", "gif", "svg", "webp"].includes(
89
- ext || "",
90
- )
91
- )
92
- iconClass = "text-purple-500";
93
- else if (["py"].includes(ext || ""))
94
- iconClass = "text-blue-500";
95
- else if (["go"].includes(ext || ""))
96
- iconClass = "text-cyan-500";
97
- else if (["rs"].includes(ext || ""))
98
- iconClass = "text-orange-500";
99
- else if (["java"].includes(ext || ""))
100
- iconClass = "text-red-500";
101
- else if (["c", "cpp", "h", "hpp"].includes(ext || ""))
102
- iconClass = "text-blue-700";
103
- else if (["sh", "bash", "zsh"].includes(ext || ""))
104
- iconClass = "text-green-500";
105
-
106
- return <FileText className={`h-4 w-4 ${iconClass}`} />;
107
- })()
96
+ <Icon name={getIconName(file.name)} class="h-4 w-4" />
108
97
)}
109
98
</td>
110
99
<td class="p-4 py-2.5">
111
100
▼src/components/repo/RepoActions.tsx
+50-5
27
27
isLoggedIn
28
28
}: RepoActionsProps) {
29
29
const [isForking, setIsForking] = React.useState(false);
30
+ const [isStarred, setIsStarred] = React.useState(false);
31
+ const [currentStarCount, setCurrentStarCount] = React.useState(stars);
32
+ const [isStarring, setIsStarring] = React.useState(false);
33
+
34
+ React.useEffect(() => {
35
+ const starred = localStorage.getItem(`starred_${owner}_${repo}`);
36
+ if (starred === "true") {
37
+ setIsStarred(true);
38
+ }
39
+ }, [owner, repo]);
40
+
41
+ const handleStar = async () => {
42
+ if (isStarring) return;
43
+ setIsStarring(true);
44
+
45
+ const newStarredState = !isStarred;
46
+ try {
47
+ const response = await fetch(`/api/repos/${owner}/${repo}/star`, {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({ action: newStarredState ? "star" : "unstar" }),
51
+ });
52
+
53
+ if (response.ok) {
54
+ setIsStarred(newStarredState);
55
+ setCurrentStarCount(prev => newStarredState ? prev + 1 : prev - 1);
56
+
57
+ if (newStarredState) {
58
+ localStorage.setItem(`starred_${owner}_${repo}`, "true");
59
+ } else {
60
+ localStorage.removeItem(`starred_${owner}_${repo}`);
61
+ }
62
+ }
63
+ } catch (error) {
64
+ console.error("Error starring repo:", error);
65
+ } finally {
66
+ setIsStarring(false);
67
+ }
68
+ };
30
69
31
70
const handleFork = async () => {
32
71
if (!isLoggedIn) {
108
147
<Button
109
148
variant="ghost"
110
149
size="sm"
111
- className="rounded-none border-r px-3 hover:bg-muted"
150
+ onClick={handleStar}
151
+ disabled={isStarring}
152
+ className={`rounded-none border-r px-3 hover:bg-muted ${isStarred ? 'text-amber-500' : ''}`}
112
153
>
113
- <Star className="mr-2 h-4 w-4 text-muted-foreground" />
114
- Star
115
- <span className="ml-2 rounded-full bg-muted text-muted-foreground px-2 py-0.5 text-xs font-medium">
116
- {stars}
154
+ {isStarring ? (
155
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
156
+ ) : (
157
+ <Star className={`mr-2 h-4 w-4 ${isStarred ? 'fill-amber-500' : 'text-muted-foreground'}`} />
158
+ )}
159
+ {isStarred ? 'Starred' : 'Star'}
160
+ <span className="ml-2 rounded-full bg-muted text-foreground px-2 py-0.5 text-xs font-medium">
161
+ {currentStarCount}
117
162
</span>
118
163
</Button>
119
164
<Button
120
165
▼src/components/repo/RepoControls.tsx
+1-1
113
113
{/* Clone Button */}
114
114
<Popover open={openClone} onOpenChange={setOpenClone}>
115
115
<PopoverTrigger asChild>
116
- <Button className="gap-2 bg-green-600 hover:bg-green-700 text-white">
116
+ <Button className="gap-2 bg-green-600 hover:bg-green-700 text-foreground">
117
117
<Code className="h-4 w-4" />
118
118
Code
119
119
<ChevronDown className="h-4 w-4" />
120
120
▼src/components/repo/RepoHeader.astro
+8-2
129
129
];
130
130
---
131
131
132
-<div class="mb-6">
132
+<div class="mb-6 -mx-4 px-4 sm:mx-0 sm:px-0">
133
+ <div class="relative overflow-hidden rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-muted/20 shadow-sm backdrop-blur-xl">
134
+ <!-- Decorative glow -->
135
+ <div class="absolute -top-24 -right-24 w-96 h-96 bg-primary/10 rounded-full blur-3xl opacity-50 pointer-events-none" />
136
+ <div class="absolute -bottom-24 -left-24 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl opacity-50 pointer-events-none" />
137
+
133
138
<!-- Top Section: Repo Name & Action Buttons -->
134
139
<div
135
- class="flex flex-col lg:flex-row lg:items-start justify-between gap-4 py-8 border-b"
140
+ class="relative flex flex-col lg:flex-row lg:items-start justify-between gap-4 py-8 px-6 border-b border-border/50"
136
141
>
137
142
<!-- Repo Info -->
138
143
<div class="space-y-4 max-w-3xl">
282
287
}
283
288
</nav>
284
289
</div>
290
+ </div>
285
291
</div>
286
292
▼src/components/settings/MFASetup.tsx
+8-8
77
77
if (step === "initial") {
78
78
return (
79
79
<div>
80
- <p className="text-gray-400 text-sm mb-4">
80
+ <p className="text-muted-foreground text-sm mb-4">
81
81
Add an extra layer of security to your account by enabling two-factor authentication.
82
82
</p>
83
83
<button
84
84
type="button"
85
85
onClick={startSetup}
86
86
disabled={loading}
87
- className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 text-sm font-medium text-white shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all disabled:opacity-70 disabled:cursor-not-allowed"
87
+ className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-gradient-to-r from-orange-500 to-red-600 text-sm font-medium text-foreground shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all disabled:opacity-70 disabled:cursor-not-allowed"
88
88
>
89
89
<Shield className="h-4 w-4" />
90
90
{loading ? "Initializing..." : "Enable 2FA"}
98
98
<div className="bg-card/50 rounded-lg p-4 border border-border">
99
99
<ol className="list-decimal list-inside space-y-4 text-sm text-foreground">
100
100
<li className="pl-2">
101
- <span className="font-medium text-white">Scan QR Code</span>
101
+ <span className="font-medium text-foreground">Scan QR Code</span>
102
102
<p className="text-muted-foreground mt-1 ml-4">
103
103
Open your authenticator app (Google Authenticator, Authy, etc.) and scan this code.
104
104
</p>
107
107
</div>
108
108
</li>
109
109
<li className="pl-2">
110
- <span className="font-medium text-white">Or enter code manually</span>
110
+ <span className="font-medium text-foreground">Or enter code manually</span>
111
111
<p className="text-muted-foreground mt-1 ml-4 mb-2">
112
112
If you can't scan the code, enter this secret key into your app.
113
113
</p>
117
117
</code>
118
118
<button
119
119
onClick={copySecret}
120
- className="p-1.5 hover:bg-muted rounded-md transition-colors text-muted-foreground hover:text-white"
120
+ className="p-1.5 hover:bg-muted rounded-md transition-colors text-muted-foreground hover:text-foreground"
121
121
title="Copy secret"
122
122
>
123
123
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
125
125
</div>
126
126
</li>
127
127
<li className="pl-2">
128
- <span className="font-medium text-white">Verify Code</span>
128
+ <span className="font-medium text-foreground">Verify Code</span>
129
129
<div className="mt-2 ml-4 flex gap-3 max-w-sm">
130
130
<input
131
131
type="text"
132
132
value={token}
133
133
onChange={(e) => setToken(e.target.value.replace(/[^0-9]/g, "").slice(0, 6))}
134
134
placeholder="000 000"
135
- className="flex-1 rounded-lg border border-input bg-background/50 px-4 py-2 text-white placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 text-center font-mono tracking-widest text-lg"
135
+ className="flex-1 rounded-lg border border-input bg-background/50 px-4 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 text-center font-mono tracking-widest text-lg"
136
136
maxLength={6}
137
137
/>
138
138
<button
149
149
150
150
<button
151
151
onClick={() => setStep("initial")}
152
- className="text-sm text-muted-foreground hover:text-white transition-colors"
152
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
153
153
>
154
154
Cancel setup
155
155
</button>
156
156
▼src/components/settings/TokensManager.tsx
+30-30
123
123
{/* Header */}
124
124
<div className="flex items-center justify-between">
125
125
<div>
126
- <h3 className="text-xl font-bold text-white flex items-center gap-3">
126
+ <h3 className="text-xl font-bold text-foreground flex items-center gap-3">
127
127
<div className="p-2 rounded-lg bg-gradient-to-br from-yellow-500/20 to-orange-500/20 border border-yellow-500/30">
128
128
<Key className="h-5 w-5 text-yellow-400" />
129
129
</div>
130
130
Access Tokens
131
131
</h3>
132
- <p className="text-gray-500 text-sm mt-2 ml-12">
132
+ <p className="text-muted-foreground text-sm mt-2 ml-12">
133
133
Tokens for API and Git over HTTPS authentication.
134
134
</p>
135
135
</div>
136
136
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
137
137
<DialogTrigger asChild>
138
- <Button className="bg-gradient-to-r from-yellow-500 to-orange-600 hover:from-yellow-600 hover:to-orange-700 text-white shadow-lg shadow-yellow-500/25 border-0">
138
+ <Button className="bg-gradient-to-r from-yellow-500 to-orange-600 hover:from-yellow-600 hover:to-orange-700 text-foreground shadow-lg shadow-yellow-500/25 border-0">
139
139
Generate Token
140
140
</Button>
141
141
</DialogTrigger>
142
- <DialogContent className="bg-[#0d1117] border-white/10 text-white sm:max-w-md">
142
+ <DialogContent className="bg-card border-border text-foreground sm:max-w-md">
143
143
<DialogHeader>
144
144
<DialogTitle className="flex items-center gap-2">
145
145
<Ticket className="h-5 w-5 text-yellow-400" />
146
146
Generate New Token
147
147
</DialogTitle>
148
- <DialogDescription className="text-gray-400">
148
+ <DialogDescription className="text-muted-foreground">
149
149
Create a new personal access token.
150
150
</DialogDescription>
151
151
</DialogHeader>
152
152
<form onSubmit={handleCreate} className="space-y-4 py-4">
153
153
<div className="space-y-2">
154
- <label className="text-sm font-medium text-gray-300">Note</label>
154
+ <label className="text-sm font-medium text-muted-foreground">Note</label>
155
155
<Input
156
156
placeholder="e.g. CI/CD Pipeline"
157
- className="bg-white/5 border-white/10 text-white placeholder:text-gray-600 focus-visible:ring-yellow-500/50"
157
+ className="bg-secondary border-border text-foreground placeholder:text-muted-foreground focus-visible:ring-yellow-500/50"
158
158
value={name}
159
159
onChange={(e) => setName(e.target.value)}
160
160
required
161
161
/>
162
162
</div>
163
163
<div className="space-y-2">
164
- <label className="text-sm font-medium text-gray-300">Expiration</label>
164
+ <label className="text-sm font-medium text-muted-foreground">Expiration</label>
165
165
<select
166
- className="flex h-10 w-full rounded-md border border-white/10 bg-[#1a1f2e] px-3 py-2 text-sm text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-500/50 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
166
+ className="flex h-10 w-full rounded-md border border-border bg-[#1a1f2e] px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-500/50 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
167
167
value={expiresIn}
168
168
onChange={(e) => setExpiresIn(e.target.value)}
169
169
>
175
175
</select>
176
176
</div>
177
177
<DialogFooter>
178
- <Button variant="ghost" type="button" onClick={() => setIsCreateOpen(false)} className="text-gray-400 hover:text-white hover:bg-white/10">
178
+ <Button variant="ghost" type="button" onClick={() => setIsCreateOpen(false)} className="text-muted-foreground hover:text-foreground hover:bg-secondary/80">
179
179
Cancel
180
180
</Button>
181
- <Button type="submit" disabled={loading} className="bg-yellow-500 hover:bg-yellow-600 text-white">
181
+ <Button type="submit" disabled={loading} className="bg-yellow-500 hover:bg-yellow-600 text-foreground">
182
182
{loading ? <RotateCw className="h-4 w-4 animate-spin mr-2" /> : null}
183
183
Generate
184
184
</Button>
190
190
191
191
{/* Success Modal */}
192
192
<Dialog open={isSuccessOpen} onOpenChange={setIsSuccessOpen}>
193
- <DialogContent className="bg-[#0d1117] border-white/10 text-white sm:max-w-md text-center">
193
+ <DialogContent className="bg-card border-border text-foreground sm:max-w-md text-center">
194
194
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-500/20 mb-2">
195
195
<Check className="h-6 w-6 text-green-400" />
196
196
</div>
197
197
<DialogHeader>
198
198
<DialogTitle className="text-center text-xl">Token Generated!</DialogTitle>
199
- <DialogDescription className="text-center text-gray-400">
199
+ <DialogDescription className="text-center text-muted-foreground">
200
200
Copy your token now. You won't see it again!
201
201
</DialogDescription>
202
202
</DialogHeader>
203
- <div className="flex items-center gap-2 bg-white/5 rounded-lg p-3 border border-white/10 mt-4">
204
- <code className="flex-1 bg-transparent text-sm text-white font-mono break-all text-left">
203
+ <div className="flex items-center gap-2 bg-secondary rounded-lg p-3 border border-border mt-4">
204
+ <code className="flex-1 bg-transparent text-sm text-foreground font-mono break-all text-left">
205
205
{newToken}
206
206
</code>
207
- <Button size="icon" variant="ghost" onClick={copyToken} className="hover:bg-white/10 shrink-0">
208
- {copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4 text-gray-400" />}
207
+ <Button size="icon" variant="ghost" onClick={copyToken} className="hover:bg-secondary/80 shrink-0">
208
+ {copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4 text-muted-foreground" />}
209
209
</Button>
210
210
</div>
211
211
<DialogFooter className="sm:justify-center mt-6">
212
- <Button onClick={() => setIsSuccessOpen(false)} className="w-full bg-green-600 hover:bg-green-700 text-white">
212
+ <Button onClick={() => setIsSuccessOpen(false)} className="w-full bg-green-600 hover:bg-green-700 text-foreground">
213
213
I have copied it
214
214
</Button>
215
215
</DialogFooter>
219
219
{/* List */}
220
220
<div className="relative group/list">
221
221
<div className="absolute -inset-[1px] bg-gradient-to-r from-yellow-500/20 via-transparent to-orange-500/20 rounded-xl opacity-50" />
222
- <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden">
222
+ <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden">
223
223
{tokens.length > 0 ? (
224
224
<div className="divide-y divide-white/5">
225
225
{tokens.map((token) => (
226
226
<div
227
227
key={token.id}
228
- className="flex items-center justify-between p-4 hover:bg-white/[0.02] transition-colors group"
228
+ className="flex items-center justify-between p-4 hover:bg-accent/50 transition-colors group"
229
229
>
230
230
<div className="flex items-center gap-3">
231
231
<div className="p-2 rounded-lg bg-yellow-500/10">
232
232
<Key className="h-5 w-5 text-yellow-400" />
233
233
</div>
234
234
<div>
235
- <p className="font-medium text-white">{token.name}</p>
236
- <div className="flex items-center gap-3 text-xs text-gray-500 mt-1">
235
+ <p className="font-medium text-foreground">{token.name}</p>
236
+ <div className="flex items-center gap-3 text-xs text-muted-foreground mt-1">
237
237
<span>
238
238
Created {new Date(token.createdAt).toLocaleDateString()}
239
239
</span>
243
243
"px-2 py-0.5 rounded-full",
244
244
new Date(token.expiresAt) < new Date()
245
245
? "bg-red-500/10 text-red-400"
246
- : "bg-gray-500/10 text-gray-400"
246
+ : "bg-gray-500/10 text-muted-foreground"
247
247
)}
248
248
>
249
249
{new Date(token.expiresAt) < new Date()
266
266
Delete
267
267
</Button>
268
268
</AlertDialogTrigger>
269
- <AlertDialogContent className="bg-[#0d1117] border-white/10 text-white">
269
+ <AlertDialogContent className="bg-card border-border text-foreground">
270
270
<AlertDialogHeader>
271
271
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
272
- <AlertDialogDescription className="text-gray-400">
272
+ <AlertDialogDescription className="text-muted-foreground">
273
273
This action cannot be undone. This will permanently delete the token
274
274
"{token.name}" and any applications using it will lose access.
275
275
</AlertDialogDescription>
276
276
</AlertDialogHeader>
277
277
<AlertDialogFooter>
278
- <AlertDialogCancel className="bg-transparent border-white/10 text-white hover:bg-white/5 hover:text-white">Cancel</AlertDialogCancel>
279
- <AlertDialogAction onClick={handleDelete} className="bg-red-600 text-white hover:bg-red-700 border-0">
278
+ <AlertDialogCancel className="bg-transparent border-border text-foreground hover:bg-accent hover:text-foreground">Cancel</AlertDialogCancel>
279
+ <AlertDialogAction onClick={handleDelete} className="bg-red-600 text-foreground hover:bg-red-700 border-0">
280
280
Delete Token
281
281
</AlertDialogAction>
282
282
</AlertDialogFooter>
291
291
<Ticket className="h-16 w-16 text-yellow-400" />
292
292
<div className="absolute inset-0 bg-yellow-500 blur-2xl opacity-20" />
293
293
</div>
294
- <h3 className="text-lg font-semibold text-white mb-2">
294
+ <h3 className="text-lg font-semibold text-foreground mb-2">
295
295
No tokens yet
296
296
</h3>
297
- <p className="text-gray-500 mb-4">
297
+ <p className="text-muted-foreground mb-4">
298
298
Generate a token to access the API.
299
299
</p>
300
- <Button onClick={() => setIsCreateOpen(true)} className="bg-gradient-to-r from-yellow-500 to-orange-600 text-white">
300
+ <Button onClick={() => setIsCreateOpen(true)} className="bg-gradient-to-r from-yellow-500 to-orange-600 text-foreground">
301
301
Generate your first token
302
302
</Button>
303
303
</div>
304
304
▼src/components/setup/SetupWizard.tsx
+49-49
85
85
];
86
86
87
87
return (
88
- <div className="w-full max-w-3xl mx-auto rounded-3xl overflow-hidden shadow-[0_0_80px_-20px_rgba(139,92,246,0.3)] bg-[#0d1117]/80 backdrop-blur-xl border border-white/10 flex flex-col min-h-[600px]">
88
+ <div className="w-full max-w-3xl mx-auto rounded-3xl overflow-hidden shadow-[0_0_80px_-20px_rgba(139,92,246,0.3)] bg-card/80 backdrop-blur-xl border border-border flex flex-col min-h-[600px]">
89
89
90
90
{/* Progress Header */}
91
91
<div className="flex border-b border-white/5 bg-white/[0.02]">
96
96
return (
97
97
<div key={s.num} className={cn(
98
98
"flex-1 p-4 flex items-center justify-center border-b-2 transition-all duration-300",
99
- isActive ? "border-violet-500 text-white" : isPast ? "border-green-500 text-green-400" : "border-transparent text-gray-500"
99
+ isActive ? "border-violet-500 text-foreground" : isPast ? "border-green-500 text-green-400" : "border-transparent text-muted-foreground"
100
100
)}>
101
101
<div className="flex items-center gap-2">
102
102
<div className={cn(
103
103
"w-6 h-6 rounded-full flex items-center justify-center text-xs",
104
- isActive ? "bg-violet-500 text-white" : isPast ? "bg-green-500 text-black" : "bg-white/10"
104
+ isActive ? "bg-violet-500 text-foreground" : isPast ? "bg-green-500 text-black" : "bg-secondary/80"
105
105
)}>
106
106
{isPast ? <Check className="w-4 h-4" /> : s.num}
107
107
</div>
125
125
{step === 1 && (
126
126
<motion.div key="step1" variants={stepVariants} initial="hidden" animate="visible" exit="exit" className="space-y-8">
127
127
<div>
128
- <h2 className="text-2xl font-bold mb-2 text-white">Configure Database</h2>
129
- <p className="text-gray-400">Select the storage backend for OpenCodeHub.</p>
128
+ <h2 className="text-2xl font-bold mb-2 text-foreground">Configure Database</h2>
129
+ <p className="text-muted-foreground">Select the storage backend for OpenCodeHub.</p>
130
130
</div>
131
131
132
132
<div className="grid md:grid-cols-2 gap-4">
134
134
onClick={() => setDbType("sqlite")}
135
135
className={cn(
136
136
"relative p-6 rounded-2xl cursor-pointer transition-all duration-300 border-2 group hover:-translate-y-1",
137
- dbType === "sqlite" ? "border-violet-500 bg-violet-500/10" : "border-white/10 bg-white/5 hover:border-white/20"
137
+ dbType === "sqlite" ? "border-violet-500 bg-violet-500/10" : "border-border bg-secondary hover:border-border/80"
138
138
)}
139
139
>
140
140
<div className="absolute top-4 right-4">
141
- <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", dbType === "sqlite" ? "border-violet-500" : "border-white/20 group-hover:border-white/40")}>
141
+ <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", dbType === "sqlite" ? "border-violet-500" : "border-border/80 group-hover:border-white/40")}>
142
142
{dbType === "sqlite" && <div className="w-2.5 h-2.5 bg-violet-500 rounded-full" />}
143
143
</div>
144
144
</div>
145
- <Database className={cn("w-8 h-8 mb-4", dbType === "sqlite" ? "text-violet-400" : "text-gray-400")} />
146
- <h3 className="text-lg font-semibold text-white mb-1">Local (SQLite)</h3>
147
- <p className="text-sm text-gray-400 leading-relaxed">Zero-configuration standalone setup. Ideal for local dev, single VPS, or NAS deployments.</p>
145
+ <Database className={cn("w-8 h-8 mb-4", dbType === "sqlite" ? "text-violet-400" : "text-muted-foreground")} />
146
+ <h3 className="text-lg font-semibold text-foreground mb-1">Local (SQLite)</h3>
147
+ <p className="text-sm text-muted-foreground leading-relaxed">Zero-configuration standalone setup. Ideal for local dev, single VPS, or NAS deployments.</p>
148
148
</div>
149
149
150
150
<div
151
151
onClick={() => setDbType("postgres")}
152
152
className={cn(
153
153
"relative p-6 rounded-2xl cursor-pointer transition-all duration-300 border-2 group hover:-translate-y-1",
154
- dbType === "postgres" ? "border-violet-500 bg-violet-500/10" : "border-white/10 bg-white/5 hover:border-white/20"
154
+ dbType === "postgres" ? "border-violet-500 bg-violet-500/10" : "border-border bg-secondary hover:border-border/80"
155
155
)}
156
156
>
157
157
<div className="absolute top-4 right-4">
158
- <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", dbType === "postgres" ? "border-violet-500" : "border-white/20 group-hover:border-white/40")}>
158
+ <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", dbType === "postgres" ? "border-violet-500" : "border-border/80 group-hover:border-white/40")}>
159
159
{dbType === "postgres" && <div className="w-2.5 h-2.5 bg-violet-500 rounded-full" />}
160
160
</div>
161
161
</div>
162
- <Server className={cn("w-8 h-8 mb-4", dbType === "postgres" ? "text-violet-400" : "text-gray-400")} />
163
- <h3 className="text-lg font-semibold text-white mb-1">PostgreSQL</h3>
164
- <p className="text-sm text-gray-400 leading-relaxed">High-performance production database. Required for multi-node setups and large teams.</p>
162
+ <Server className={cn("w-8 h-8 mb-4", dbType === "postgres" ? "text-violet-400" : "text-muted-foreground")} />
163
+ <h3 className="text-lg font-semibold text-foreground mb-1">PostgreSQL</h3>
164
+ <p className="text-sm text-muted-foreground leading-relaxed">High-performance production database. Required for multi-node setups and large teams.</p>
165
165
</div>
166
166
</div>
167
167
168
168
{dbType === "postgres" && (
169
169
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
170
- <Label className="text-gray-300">Connection String</Label>
170
+ <Label className="text-muted-foreground">Connection String</Label>
171
171
<Input
172
172
placeholder="postgresql://user:password@localhost:5432/opencodehub"
173
173
value={dbUrl}
174
174
onChange={(e) => setDbUrl(e.target.value)}
175
- className="bg-black/50 border-white/10 focus:border-violet-500 focus:ring-violet-500 h-12 text-base"
175
+ className="bg-black/50 border-border focus:border-violet-500 focus:ring-violet-500 h-12 text-base"
176
176
/>
177
177
</motion.div>
178
178
)}
182
182
{step === 2 && (
183
183
<motion.div key="step2" variants={stepVariants} initial="hidden" animate="visible" exit="exit" className="space-y-8">
184
184
<div>
185
- <h2 className="text-2xl font-bold mb-2 text-white">Performance Layer</h2>
186
- <p className="text-gray-400">Configure caching to accelerate repository indexing and API requests.</p>
185
+ <h2 className="text-2xl font-bold mb-2 text-foreground">Performance Layer</h2>
186
+ <p className="text-muted-foreground">Configure caching to accelerate repository indexing and API requests.</p>
187
187
</div>
188
188
189
189
<div className="grid md:grid-cols-2 gap-4">
191
191
onClick={() => setCacheType("local")}
192
192
className={cn(
193
193
"relative p-6 rounded-2xl cursor-pointer transition-all duration-300 border-2 group hover:-translate-y-1",
194
- cacheType === "local" ? "border-emerald-500 bg-emerald-500/10" : "border-white/10 bg-white/5 hover:border-white/20"
194
+ cacheType === "local" ? "border-emerald-500 bg-emerald-500/10" : "border-border bg-secondary hover:border-border/80"
195
195
)}
196
196
>
197
197
<div className="absolute top-4 right-4">
198
- <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", cacheType === "local" ? "border-emerald-500" : "border-white/20 group-hover:border-white/40")}>
198
+ <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", cacheType === "local" ? "border-emerald-500" : "border-border/80 group-hover:border-white/40")}>
199
199
{cacheType === "local" && <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full" />}
200
200
</div>
201
201
</div>
202
- <Database className={cn("w-8 h-8 mb-4", cacheType === "local" ? "text-emerald-400" : "text-gray-400")} />
203
- <h3 className="text-lg font-semibold text-white mb-1">Local Memory</h3>
204
- <p className="text-sm text-gray-400 leading-relaxed">Fast on-device caching. Recommended for single-instance deployments.</p>
202
+ <Database className={cn("w-8 h-8 mb-4", cacheType === "local" ? "text-emerald-400" : "text-muted-foreground")} />
203
+ <h3 className="text-lg font-semibold text-foreground mb-1">Local Memory</h3>
204
+ <p className="text-sm text-muted-foreground leading-relaxed">Fast on-device caching. Recommended for single-instance deployments.</p>
205
205
</div>
206
206
207
207
<div
208
208
onClick={() => setCacheType("redis")}
209
209
className={cn(
210
210
"relative p-6 rounded-2xl cursor-pointer transition-all duration-300 border-2 group hover:-translate-y-1",
211
- cacheType === "redis" ? "border-emerald-500 bg-emerald-500/10" : "border-white/10 bg-white/5 hover:border-white/20"
211
+ cacheType === "redis" ? "border-emerald-500 bg-emerald-500/10" : "border-border bg-secondary hover:border-border/80"
212
212
)}
213
213
>
214
214
<div className="absolute top-4 right-4">
215
- <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", cacheType === "redis" ? "border-emerald-500" : "border-white/20 group-hover:border-white/40")}>
215
+ <div className={cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", cacheType === "redis" ? "border-emerald-500" : "border-border/80 group-hover:border-white/40")}>
216
216
{cacheType === "redis" && <div className="w-2.5 h-2.5 bg-emerald-500 rounded-full" />}
217
217
</div>
218
218
</div>
219
- <Server className={cn("w-8 h-8 mb-4", cacheType === "redis" ? "text-emerald-400" : "text-gray-400")} />
220
- <h3 className="text-lg font-semibold text-white mb-1">External Redis</h3>
221
- <p className="text-sm text-gray-400 leading-relaxed">Distributed caching. Essential for High Availability and horizontal scaling.</p>
219
+ <Server className={cn("w-8 h-8 mb-4", cacheType === "redis" ? "text-emerald-400" : "text-muted-foreground")} />
220
+ <h3 className="text-lg font-semibold text-foreground mb-1">External Redis</h3>
221
+ <p className="text-sm text-muted-foreground leading-relaxed">Distributed caching. Essential for High Availability and horizontal scaling.</p>
222
222
</div>
223
223
</div>
224
224
225
225
{cacheType === "redis" && (
226
226
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className="space-y-3">
227
- <Label className="text-gray-300">Redis URL</Label>
227
+ <Label className="text-muted-foreground">Redis URL</Label>
228
228
<Input
229
229
placeholder="redis://localhost:6379"
230
230
value={redisUrl}
231
231
onChange={(e) => setRedisUrl(e.target.value)}
232
- className="bg-black/50 border-white/10 focus:border-emerald-500 focus:ring-emerald-500 h-12 text-base"
232
+ className="bg-black/50 border-border focus:border-emerald-500 focus:ring-emerald-500 h-12 text-base"
233
233
/>
234
234
</motion.div>
235
235
)}
239
239
{step === 3 && (
240
240
<motion.div key="step3" variants={stepVariants} initial="hidden" animate="visible" exit="exit" className="space-y-8">
241
241
<div>
242
- <h2 className="text-2xl font-bold mb-2 text-white">Create Administrator</h2>
243
- <p className="text-gray-400">Set up the initial master account for platform administration.</p>
242
+ <h2 className="text-2xl font-bold mb-2 text-foreground">Create Administrator</h2>
243
+ <p className="text-muted-foreground">Set up the initial master account for platform administration.</p>
244
244
</div>
245
245
246
- <div className="space-y-5 bg-white/5 p-6 rounded-2xl border border-white/10">
246
+ <div className="space-y-5 bg-secondary p-6 rounded-2xl border border-border">
247
247
<div className="space-y-2">
248
- <Label className="text-gray-300">Username</Label>
248
+ <Label className="text-muted-foreground">Username</Label>
249
249
<div className="relative">
250
- <User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
250
+ <User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
251
251
<Input
252
252
value={adminUser.username}
253
253
onChange={(e) => setAdminUser({...adminUser, username: e.target.value})}
254
- className="pl-10 bg-black/50 border-white/10 focus:border-blue-500 h-11"
254
+ className="pl-10 bg-black/50 border-border focus:border-blue-500 h-11"
255
255
placeholder="admin"
256
256
/>
257
257
</div>
258
258
</div>
259
259
260
260
<div className="space-y-2">
261
- <Label className="text-gray-300">Email Address</Label>
261
+ <Label className="text-muted-foreground">Email Address</Label>
262
262
<div className="relative">
263
- <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
263
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
264
264
<Input
265
265
type="email"
266
266
value={adminUser.email}
267
267
onChange={(e) => setAdminUser({...adminUser, email: e.target.value})}
268
- className="pl-10 bg-black/50 border-white/10 focus:border-blue-500 h-11"
268
+ className="pl-10 bg-black/50 border-border focus:border-blue-500 h-11"
269
269
placeholder="[email protected]"
270
270
/>
271
271
</div>
273
273
274
274
<div className="grid grid-cols-2 gap-4">
275
275
<div className="space-y-2">
276
- <Label className="text-gray-300">Password</Label>
276
+ <Label className="text-muted-foreground">Password</Label>
277
277
<div className="relative">
278
- <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
278
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
279
279
<Input
280
280
type="password"
281
281
value={adminUser.password}
282
282
onChange={(e) => setAdminUser({...adminUser, password: e.target.value})}
283
- className="pl-10 bg-black/50 border-white/10 focus:border-blue-500 h-11"
283
+ className="pl-10 bg-black/50 border-border focus:border-blue-500 h-11"
284
284
placeholder="••••••••"
285
285
/>
286
286
</div>
287
287
</div>
288
288
289
289
<div className="space-y-2">
290
- <Label className="text-gray-300">Confirm Password</Label>
290
+ <Label className="text-muted-foreground">Confirm Password</Label>
291
291
<div className="relative">
292
- <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
292
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
293
293
<Input
294
294
type="password"
295
295
value={adminUser.confirmPassword}
296
296
onChange={(e) => setAdminUser({...adminUser, confirmPassword: e.target.value})}
297
- className="pl-10 bg-black/50 border-white/10 focus:border-blue-500 h-11"
297
+ className="pl-10 bg-black/50 border-border focus:border-blue-500 h-11"
298
298
placeholder="••••••••"
299
299
/>
300
300
</div>
309
309
{/* Footer Navigation */}
310
310
<div className="p-6 bg-black/20 border-t border-white/5 flex items-center justify-between">
311
311
{step > 1 ? (
312
- <Button variant="ghost" onClick={handleBack} className="text-gray-400 hover:text-white hover:bg-white/5">
312
+ <Button variant="ghost" onClick={handleBack} className="text-muted-foreground hover:text-foreground hover:bg-accent">
313
313
Back
314
314
</Button>
315
315
) : (
321
321
Continue <ArrowRight className="w-4 h-4 ml-2" />
322
322
</Button>
323
323
) : (
324
- <Button onClick={handleSubmit} disabled={loading} className="bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 text-white h-11 px-8 rounded-xl font-medium shadow-[0_0_30px_rgba(139,92,246,0.5)] border-0 transition-all">
324
+ <Button onClick={handleSubmit} disabled={loading} className="bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 text-foreground h-11 px-8 rounded-xl font-medium shadow-[0_0_30px_rgba(139,92,246,0.5)] border-0 transition-all">
325
325
{loading ? (
326
326
<span className="flex items-center">
327
- <svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
327
+ <svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-foreground" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
328
328
Initializing Platform...
329
329
</span>
330
330
) : (
331
331
▼src/components/ui/animated-tooltip.tsx
+2-2
62
62
>
63
63
<div className="absolute inset-x-10 z-30 w-[20%] -bottom-px bg-gradient-to-r from-transparent via-emerald-500 to-transparent h-px" />
64
64
<div className="absolute left-10 w-[40%] z-30 -bottom-px bg-gradient-to-r from-transparent via-sky-500 to-transparent h-px" />
65
- <div className="font-bold text-white relative z-30 text-base">
65
+ <div className="font-bold text-foreground relative z-30 text-base">
66
66
{item.name}
67
67
</div>
68
68
{item.designation && (
69
- <div className="text-white text-xs">{item.designation}</div>
69
+ <div className="text-foreground text-xs">{item.designation}</div>
70
70
)}
71
71
</motion.div>
72
72
)}
73
73
▼src/components/ui/button.tsx
+1-1
17
17
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
18
ghost: "hover:bg-accent hover:text-accent-foreground",
19
19
link: "text-primary underline-offset-4 hover:underline",
20
- success: "bg-green-600 text-white hover:bg-green-700",
20
+ success: "bg-green-600 text-foreground hover:bg-green-700",
21
21
},
22
22
size: {
23
23
default: "h-10 px-4 py-2",
24
24
▼src/components/ui/typewriter-effect.tsx
+2-2
54
54
initial={{}}
55
55
key={`char-${index}`}
56
56
className={cn(
57
- `dark:text-white text-black opacity-0 hidden`,
57
+ `dark:text-foreground text-black opacity-0 hidden`,
58
58
word.className
59
59
)}
60
60
>
126
126
{word.text.map((char, index) => (
127
127
<span
128
128
key={`char-${index}`}
129
- className={cn(`dark:text-white text-black`, word.className)}
129
+ className={cn(`dark:text-foreground text-black`, word.className)}
130
130
>
131
131
{char}
132
132
</span>
133
133
▼src/db/index.ts
+2-0
1
+import { config } from "dotenv";
2
+config({ override: true });
1
3
import { logger } from "@/lib/logger";
2
4
import { createClient } from "@libsql/client";
3
5
import Database from "better-sqlite3";
4
6
▼src/layouts/BaseLayout.astro
+39-5
27
27
<script is:inline>
28
28
(function() {
29
29
var t = localStorage.getItem('theme') || 'system';
30
- var dark = t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
31
- if (dark) document.documentElement.classList.add('dark');
32
- else document.documentElement.classList.remove('dark');
30
+ var isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
31
+ var theme = t === 'system' ? (isSystemDark ? 'dark' : 'light') : t;
32
+
33
+ if (theme === 'light') {
34
+ document.documentElement.classList.remove('dark');
35
+ document.documentElement.removeAttribute('data-theme');
36
+ } else {
37
+ document.documentElement.classList.add('dark');
38
+ if (theme !== 'dark' && theme !== 'system') {
39
+ document.documentElement.setAttribute('data-theme', theme);
40
+ } else {
41
+ document.documentElement.removeAttribute('data-theme');
42
+ }
43
+ }
33
44
})();
34
45
</script>
35
46
<!-- Preload fonts -->
49
60
Skip to main content
50
61
</a>
51
62
63
+ <!-- Premium Background Effects -->
64
+ <div class="fixed inset-0 z-[-1] h-full w-full bg-background dark:bg-slate-950">
65
+ <div class="absolute bottom-0 left-0 right-0 top-0 bg-[linear-gradient(to_right,#4f4f4f2e_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f2e_1px,transparent_1px)] bg-[size:14px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]"></div>
66
+ <div class="absolute top-0 z-[-2] h-screen w-screen dark:bg-slate-950 dark:bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))]"></div>
67
+ </div>
68
+
52
69
<div class="relative flex min-h-screen flex-col">
53
70
<Header />
54
71
<main id="main-content" class="flex-1" role="main" tabindex="-1">
64
81
Built with ❤️ using OpenCodeHub. Self-hosted and open source.
65
82
</p>
66
83
<nav class="flex items-center gap-4 text-sm text-muted-foreground" aria-label="Footer navigation">
67
- <a href="/docs" class="hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-sm">Documentation</a>
84
+ <a href="https://docs.opencodehub.space/" target="_blank" rel="noopener noreferrer" class="hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-sm">Documentation</a>
68
85
<a href="/api" class="hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-sm">API</a>
69
86
<a
70
87
href="https://github.com/swadhinbiswas/OpencodeHub"
122
139
}
123
140
124
141
.container {
125
- @apply mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl;
142
+ @apply mx-auto w-full max-w-7xl px-4 sm:px-5 lg:px-6;
143
+ }
144
+
145
+ /* Responsive text utilities */
146
+ .text-balance {
147
+ text-wrap: balance;
148
+ }
149
+
150
+ /* Smooth scroll */
151
+ html {
152
+ scroll-behavior: smooth;
153
+ }
154
+
155
+ /* Better mobile tap targets */
156
+ @media (max-width: 640px) {
157
+ button, a, input, textarea, select {
158
+ min-height: 44px;
159
+ }
126
160
}
127
161
</style>
128
162
▼src/layouts/SettingsLayout.astro
+70-59
3
3
4
4
const { title } = Astro.props;
5
5
6
+const user = Astro.locals.user;
7
+
6
8
const navigation = [
7
- { name: "Profile", href: "/settings/profile", icon: "user", color: "cyan" },
8
- { name: "Account", href: "/settings/account", icon: "lock", color: "purple" },
9
- { name: "Security", href: "/settings/security", icon: "shield", color: "orange" },
10
- { name: "SSH Keys", href: "/settings/ssh-keys", icon: "key", color: "green" },
11
- { name: "GPG Keys", href: "/settings/gpg-keys", icon: "lock", color: "pink" },
12
- { name: "Access Tokens", href: "/settings/tokens", icon: "ticket", color: "yellow" },
9
+ { name: "Public profile", href: "/settings/profile" },
10
+ { name: "Account", href: "/settings/account" },
11
+ { name: "Password and authentication", href: "/settings/security" },
12
+ { name: "SSH and GPG keys", href: "/settings/ssh-keys" },
13
+ { name: "Personal access tokens", href: "/settings/tokens" },
13
14
];
14
15
15
-const icons: Record<string, string> = {
16
- user: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
17
- lock: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z",
18
- shield: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
19
- key: "M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z",
20
- ticket: "M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z",
21
-};
22
-
23
-const colors: Record<string, { active: string; text: string }> = {
24
- cyan: { active: "bg-cyan-500/10 border-cyan-500/30", text: "text-cyan-400" },
25
- purple: { active: "bg-purple-500/10 border-purple-500/30", text: "text-purple-400" },
26
- orange: { active: "bg-orange-500/10 border-orange-500/30", text: "text-orange-400" },
27
- green: { active: "bg-green-500/10 border-green-500/30", text: "text-green-400" },
28
- pink: { active: "bg-pink-500/10 border-pink-500/30", text: "text-pink-400" },
29
- yellow: { active: "bg-yellow-500/10 border-yellow-500/30", text: "text-yellow-400" },
30
-};
16
+const repoNavigation = [
17
+ { name: "Repositories", href: "/repositories" },
18
+ { name: "Organizations", href: "/orgs" },
19
+ { name: "Notifications", href: "/notifications" },
20
+];
31
21
32
22
const currentPath = Astro.url.pathname;
33
23
---
34
24
35
25
<BaseLayout title={title}>
36
- <div class="container mx-auto px-4 py-8">
37
- <div class="flex flex-col lg:flex-row gap-8">
38
- <!-- Sidebar -->
39
- <aside class="w-full lg:w-64 shrink-0">
40
- <div class="relative">
41
- <div class="absolute -inset-[1px] bg-gradient-to-b from-cyan-500/20 to-transparent rounded-xl opacity-50"></div>
42
- <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-4">
43
- <h2 class="text-sm font-semibold text-gray-400 mb-4 flex items-center gap-2">
44
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
46
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
47
- </svg>
48
- Settings
49
- </h2>
50
- <nav class="space-y-1">
51
- {navigation.map((item) => {
52
- const isActive = currentPath === item.href;
53
- const colorSet = colors[item.color];
54
- return (
55
- <a
56
- href={item.href}
57
- class:list={[
58
- "flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-all",
59
- isActive
60
- ? `${colorSet.active} ${colorSet.text} border`
61
- : "text-gray-400 hover:text-white hover:bg-white/5",
62
- ]}
63
- >
64
- <svg class={`h-4 w-4 ${isActive ? colorSet.text : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={icons[item.icon]}></path>
66
- </svg>
67
- {item.name}
68
- </a>
69
- );
70
- })}
71
- </nav>
26
+ <div class="border-b border-border bg-background">
27
+ <div class="max-w-[1280px] mx-auto px-4 sm:px-6 lg:px-8">
28
+ <div class="flex items-center gap-3 py-3 sm:py-4">
29
+ {user?.avatarUrl ? (
30
+ <img src={user.avatarUrl} alt="" class="w-7 h-7 sm:w-8 sm:h-8 rounded-full" />
31
+ ) : (
32
+ <div class="w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-secondary flex items-center justify-center text-xs sm:text-sm font-medium text-muted-foreground">
33
+ {user?.username?.[0]?.toUpperCase()}
72
34
</div>
35
+ )}
36
+ <div>
37
+ <h1 class="text-base sm:text-xl font-semibold text-foreground">{user?.username}</h1>
73
38
</div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="max-w-[1280px] mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
44
+ <div class="flex flex-col lg:flex-row gap-6 lg:gap-8">
45
+ <!-- Sidebar - horizontal on mobile, vertical on desktop -->
46
+ <aside class="w-full lg:w-[260px] shrink-0">
47
+ <!-- Mobile: horizontal scroll -->
48
+ <nav class="flex lg:flex-col gap-1 overflow-x-auto no-scrollbar pb-2 lg:pb-0">
49
+ <div class="hidden lg:block text-xs font-semibold text-foreground uppercase tracking-wider mb-2 px-3">Personal settings</div>
50
+ {navigation.map((item) => {
51
+ const isActive = currentPath === item.href;
52
+ return (
53
+ <a
54
+ href={item.href}
55
+ class:list={[
56
+ "whitespace-nowrap px-3 py-1.5 text-sm rounded-md transition-colors",
57
+ isActive
58
+ ? "font-semibold text-foreground bg-accent"
59
+ : "text-[#58a6ff] hover:text-[#79c0ff] hover:bg-accent/50"
60
+ ]}
61
+ >
62
+ {item.name}
63
+ </a>
64
+ );
65
+ })}
66
+ <div class="hidden lg:block h-px bg-border my-3"></div>
67
+ <div class="hidden lg:block text-xs font-semibold text-foreground uppercase tracking-wider mb-2 px-3">Repository settings</div>
68
+ {repoNavigation.map((item) => {
69
+ const isActive = currentPath === item.href;
70
+ return (
71
+ <a
72
+ href={item.href}
73
+ class:list={[
74
+ "whitespace-nowrap px-3 py-1.5 text-sm rounded-md transition-colors",
75
+ isActive
76
+ ? "font-semibold text-foreground bg-accent"
77
+ : "text-[#58a6ff] hover:text-[#79c0ff] hover:bg-accent/50"
78
+ ]}
79
+ >
80
+ {item.name}
81
+ </a>
82
+ );
83
+ })}
84
+ </nav>
74
85
</aside>
75
86
76
87
<!-- Content -->
77
- <div class="flex-1">
88
+ <div class="flex-1 min-w-0">
78
89
<slot />
79
90
</div>
80
91
</div>
81
92
▼src/lib/auth.ts
+3-3
154
154
/**
155
155
* Generate SSH key fingerprint
156
156
*/
157
-export function generateSSHFingerprint(publicKey: string): string {
158
- const crypto = require("crypto");
157
+export async function generateSSHFingerprint(publicKey: string): Promise<string> {
158
+ const nodeCrypto = await import("crypto");
159
159
// Extract the key data (ignore type and comment)
160
160
const parts = publicKey.trim().split(" ");
161
161
const keyData = parts.length >= 2 ? parts[1] : parts[0];
162
162
const buffer = Buffer.from(keyData, "base64");
163
- const hash = crypto.createHash("sha256").update(buffer).digest("base64");
163
+ const hash = nodeCrypto.createHash("sha256").update(buffer).digest("base64");
164
164
return `SHA256:${hash.replace(/=+$/, "")}`;
165
165
}
166
166
167
167
▼src/lib/ci-gates.ts
+41-4
9
9
import { logger } from "./logger";
10
10
import { repositories } from "@/db/schema/repositories";
11
11
12
+/**
13
+ * Safe expression evaluator — replaces dangerous `new Function()`.
14
+ * Only supports simple property comparisons on the PR object.
15
+ * Allowed patterns: pr.property === "value", pr.property !== "value",
16
+ * pr.property == value, pr.property >= value, etc.
17
+ */
18
+function evaluateSafeExpression(script: string, pr: any): boolean {
19
+ // Only allow simple comparisons on pr.* properties
20
+ const allowedPattern = /^pr\.\w+\s*(===|!==|==|!=|>=|<=|>|<)\s*(?:"[^"]*"|'[^']*'|\d+)$/;
21
+ const normalized = script.trim();
22
+
23
+ if (!allowedPattern.test(normalized)) {
24
+ logger.warn({ script: normalized }, "Rejected unsafe gate expression");
25
+ return false;
26
+ }
27
+
28
+ // Parse the expression
29
+ const match = normalized.match(/^(pr\.(\w+))\s*(===|!==|==|!=|>=|<=|>|<)\s*(?:"([^"]*)"|'([^']*)'|(\d+))$/);
30
+ if (!match) return false;
31
+
32
+ const [, , prop, operator, strVal1, strVal2, numVal] = match;
33
+ const propValue = pr[prop];
34
+ const compareVal = strVal1 ?? strVal2 ?? (numVal ? Number(numVal) : undefined);
35
+
36
+ switch (operator) {
37
+ case "===": return propValue === compareVal;
38
+ case "!==": return propValue !== compareVal;
39
+ case "==": return propValue == compareVal;
40
+ case "!=": return propValue != compareVal;
41
+ case ">=": return Number(propValue) >= Number(compareVal);
42
+ case "<=": return Number(propValue) <= Number(compareVal);
43
+ case ">": return Number(propValue) > Number(compareVal);
44
+ case "<": return Number(propValue) < Number(compareVal);
45
+ default: return false;
46
+ }
47
+}
48
+
12
49
/**
13
50
* Required status checks for merging
14
51
*/
334
371
}
335
372
336
373
case "custom": {
337
- // Execute custom condition script
374
+ // Execute custom condition script — SAFELY
375
+ // Custom scripts are restricted to simple boolean expressions
376
+ // that reference PR properties. No arbitrary code execution.
338
377
if (gate.conditionScript) {
339
378
try {
340
- // Sandbox execution would be needed in production
341
- const fn = new Function("pr", gate.conditionScript);
342
- const passed = fn(pr);
379
+ const passed = evaluateSafeExpression(gate.conditionScript, pr);
343
380
return {
344
381
passed: Boolean(passed),
345
382
gateName: gate.name,
346
383
▼src/lib/git-server.ts
+12-3
130
130
.then(() => callback())
131
131
.catch(callback);
132
132
} else {
133
- this.storagePass?.write(chunk);
134
- this.indexPass?.write(chunk);
135
- callback();
133
+ try {
134
+ const storageOk = this.storagePass?.write(chunk) ?? true;
135
+ const indexOk = this.indexPass?.write(chunk) ?? true;
136
+ // If either stream signals backpressure, we still accept the chunk
137
+ // but log a warning. The streams will buffer internally.
138
+ if (!storageOk || !indexOk) {
139
+ // Backpressure detected - the streams will buffer
140
+ }
141
+ callback();
142
+ } catch (err) {
143
+ callback(err instanceof Error ? err : new Error(String(err)));
144
+ }
136
145
}
137
146
}
138
147
139
148
▼src/lib/git-storage.ts
+9-3
87
87
const relativePath = obj.key.replace(storagePath, "").replace(/^\//, "");
88
88
if (!relativePath) continue;
89
89
90
+ // Security: reject path traversal attempts
91
+ if (relativePath.includes("..") || relativePath.startsWith("/")) {
92
+ logger.warn({ relativePath }, "Rejected path traversal in storage key");
93
+ continue;
94
+ }
95
+
90
96
logger.info(`[downloadRepoFromStorage] storagePath="${storagePath}", obj.key="${obj.key}", relativePath="${relativePath}"`);
91
97
92
98
const localFilePath = join(localPath, relativePath);
267
273
}
268
274
// Create target directory
269
275
mkdirSync(localPath, { recursive: true });
270
- const { execSync } = await import('child_process');
271
- // Copy directory CONTENTS (using /. to copy contents, not the directory itself)
272
- execSync(`cp -R "${fallbackPath}/." "${localPath}/"`, { stdio: 'inherit' });
276
+ const { execFileSync } = await import('child_process');
277
+ // Copy directory CONTENTS using execFileSync to prevent command injection
278
+ execFileSync("cp", ["-R", `${fallbackPath}/.`, `${localPath}/`], { stdio: 'inherit' });
273
279
logger.info(`[acquireRepo] Successfully copied from fallback`);
274
280
} else {
275
281
logger.error(`[acquireRepo] No fallback found at: ${fallbackPath}`);
276
282
▼src/lib/markdown.ts
+11-2
1
-import rehypeSanitize from "rehype-sanitize";
1
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
2
2
import rehypeStringify from "rehype-stringify";
3
+import rehypeHighlight from "rehype-highlight";
3
4
import remarkGfm from "remark-gfm";
4
5
import remarkParse from "remark-parse";
5
6
import remarkRehype from "remark-rehype";
67
68
.use(remarkGfm)
68
69
.use(remarkCrossRepoLinks) // Add custom plugin
69
70
.use(remarkRehype)
70
- .use(rehypeSanitize)
71
+ .use(rehypeHighlight) // Code highlighting
72
+ .use(rehypeSanitize, {
73
+ ...defaultSchema,
74
+ attributes: {
75
+ ...defaultSchema.attributes,
76
+ code: [...(defaultSchema.attributes?.code || []), "className"],
77
+ span: [...(defaultSchema.attributes?.span || []), "className"],
78
+ },
79
+ })
71
80
.use(rehypeStringify)
72
81
.process(content);
73
82
74
83
▼src/lib/pipeline.ts
+55-10
13
13
import { generateId } from "./utils";
14
14
import { logger } from "@/lib/logger";
15
15
16
+/**
17
+ * Filter sensitive environment variables to prevent leaking secrets to workflows.
18
+ */
19
+function filterSensitiveEnvVars(env: Record<string, string | undefined>): Record<string, string> {
20
+ const sensitiveKeys = new Set([
21
+ "JWT_SECRET", "SESSION_SECRET", "INTERNAL_HOOK_SECRET", "CRON_SECRET",
22
+ "RUNNER_SECRET", "AI_CONFIG_ENCRYPTION_KEY", "WORKFLOW_SECRET_ENCRYPTION_KEY",
23
+ "DATABASE_URL", "DATABASE_AUTH_TOKEN", "POSTGRES_PASSWORD",
24
+ "REDIS_PASSWORD", "SMTP_PASSWORD", "SESSION_SECRET",
25
+ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY",
26
+ "BYTEZ_API_KEY", "OPENROUTER_API_KEY", "TOGETHER_API_KEY", "GOOGLE_AI_API_KEY",
27
+ "EXTERNAL_AGENT_API_KEY", "EXTERNAL_AGENT_CALLBACK_SECRET",
28
+ "BACKUP_ENCRYPTION_KEY", "STORAGE_ACCESS_KEY_ID", "STORAGE_SECRET_ACCESS_KEY",
29
+ "GRAFANA_API_KEY", "GRAFANA_INSTANCE_ID",
30
+ ]);
31
+ const result: Record<string, string> = {};
32
+ for (const [key, value] of Object.entries(env)) {
33
+ if (value !== undefined && !sensitiveKeys.has(key)) {
34
+ result[key] = value;
35
+ }
36
+ }
37
+ return result;
38
+}
39
+
16
40
// Types
17
41
export interface WorkflowConfig {
18
42
name: string;
420
444
graphql_url:
421
445
process.env.GRAPHQL_URL || "http://localhost:4321/graphql",
422
446
},
423
- env: { ...process.env, ...workflow.env } as Record<string, string>,
447
+ env: { ...filterSensitiveEnvVars(process.env), ...workflow.env } as Record<string, string>,
424
448
job: { status: "in_progress" },
425
449
steps: {},
426
450
runner: {
1020
1044
}
1021
1045
1022
1046
/**
1023
- * Collect output from Docker stream
1047
+ * Collect output from Docker stream.
1048
+ * Docker multiplexed streams have an 8-byte header per frame:
1049
+ * [0] = stream type (1=stdout, 2=stderr)
1050
+ * [1-3] = padding
1051
+ * [4-7] = frame size (big-endian uint32)
1052
+ * A single data event may contain multiple frames or partial frames.
1024
1053
*/
1025
1054
private async collectOutput(
1026
1055
stream: NodeJS.ReadableStream,
1028
1057
): Promise<string> {
1029
1058
return new Promise((resolve, reject) => {
1030
1059
let output = "";
1031
- stream.on("data", (chunk) => {
1032
- // Docker multiplexes stdout/stderr in stream
1033
- // Skip first 8 bytes (header) for each chunk
1034
- const data = chunk.slice(8).toString();
1035
- output += data;
1036
- this.emit("output", data);
1037
- if (logContext) {
1038
- this.emit("log", { ...logContext, data, timestamp: new Date() });
1060
+ let buffer = Buffer.alloc(0);
1061
+
1062
+ stream.on("data", (chunk: Buffer) => {
1063
+ buffer = Buffer.concat([buffer, chunk]);
1064
+
1065
+ // Process complete frames from the buffer
1066
+ while (buffer.length >= 8) {
1067
+ const frameSize = buffer.readUInt32BE(4);
1068
+ const totalFrameSize = 8 + frameSize;
1069
+
1070
+ if (buffer.length < totalFrameSize) {
1071
+ break; // Wait for more data
1072
+ }
1073
+
1074
+ // Extract frame payload (skip 8-byte header)
1075
+ const framePayload = buffer.slice(8, totalFrameSize);
1076
+ buffer = buffer.slice(totalFrameSize);
1077
+
1078
+ const data = framePayload.toString();
1079
+ output += data;
1080
+ this.emit("output", data);
1081
+ if (logContext) {
1082
+ this.emit("log", { ...logContext, data, timestamp: new Date() });
1083
+ }
1039
1084
}
1040
1085
});
1041
1086
stream.on("end", () => resolve(output));
1042
1087
▼src/lib/realtime.ts
+8-2
353
353
repositories: string[] = []
354
354
): Response {
355
355
let connectionId: string;
356
+ let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
356
357
357
358
const stream = new ReadableStream<Uint8Array>({
358
359
start(controller) {
359
360
connectionId = registerConnection(userId, controller, repositories);
360
361
361
362
// Send heartbeat every 30 seconds to keep connection alive
362
- const heartbeatInterval = setInterval(() => {
363
+ heartbeatInterval = setInterval(() => {
363
364
try {
364
365
controller.enqueue(new TextEncoder().encode(": heartbeat\n\n"));
365
366
} catch {
366
- clearInterval(heartbeatInterval);
367
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
367
368
}
368
369
}, 30000);
369
370
},
370
371
cancel() {
372
+ // Clear heartbeat interval to prevent memory leak
373
+ if (heartbeatInterval) {
374
+ clearInterval(heartbeatInterval);
375
+ heartbeatInterval = null;
376
+ }
371
377
if (connectionId) {
372
378
unregisterConnection(connectionId);
373
379
}
374
380
▼src/lib/security-advanced.ts
+2-3
4
4
*/
5
5
6
6
import { getDatabase, schema } from "@/db";
7
+import * as nodeCrypto from "crypto";
7
8
import {
8
9
ipAllowLists,
9
10
licenseScans,
577
578
algorithm: string,
578
579
): boolean {
579
580
try {
580
- const crypto = require("crypto");
581
-
582
581
// Extract the SignatureValue
583
582
const sigValueMatch = xml.match(
584
583
/<ds:SignatureValue[^>]*>([\s\S]*?)<\/ds:SignatureValue>/,
607
606
"RSA-SHA512": "RSA-SHA512",
608
607
};
609
608
610
- const verifier = crypto.createVerify(algoMap[algorithm] || "RSA-SHA256");
609
+ const verifier = nodeCrypto.createVerify(algoMap[algorithm] || "RSA-SHA256");
611
610
verifier.update(signedInfoMatch[0]);
612
611
613
612
const signatureBytes = Buffer.from(
614
613
▼src/lib/ssh.ts
+19-4
19
19
maxFailures: number;
20
20
windowMs: number;
21
21
blockMs: number;
22
+ maxEntries: number;
22
23
map: Map<
23
24
string,
24
25
{ failures: number; firstFailure: number; blockedUntil: number }
27
28
maxFailures: 5,
28
29
windowMs: 60_000, // 1-minute window
29
30
blockMs: 5 * 60_000, // 5-minute block after threshold
31
+ maxEntries: 10_000, // Max tracked IPs to prevent memory leak
30
32
map: new Map(),
31
33
};
32
34
55
57
const entry = SSH_RATE_LIMIT.map.get(ip);
56
58
57
59
if (!entry || now - entry.firstFailure > SSH_RATE_LIMIT.windowMs) {
60
+ // Evict oldest entries if map is full
61
+ if (SSH_RATE_LIMIT.map.size >= SSH_RATE_LIMIT.maxEntries) {
62
+ let oldestKey = "";
63
+ let oldestTime = Infinity;
64
+ for (const [key, val] of SSH_RATE_LIMIT.map) {
65
+ if (val.firstFailure < oldestTime) {
66
+ oldestTime = val.firstFailure;
67
+ oldestKey = key;
68
+ }
69
+ }
70
+ if (oldestKey) SSH_RATE_LIMIT.map.delete(oldestKey);
71
+ }
72
+
58
73
SSH_RATE_LIMIT.map.set(ip, {
59
74
failures: 1,
60
75
firstFailure: now,
408
423
/**
409
424
* Parse SSH public key to get type and fingerprint
410
425
*/
411
-export function parseSSHPublicKey(key: string): {
426
+export async function parseSSHPublicKey(key: string): Promise<{
412
427
type: string;
413
428
fingerprint: string;
414
429
comment?: string;
415
-} | null {
430
+} | null> {
416
431
const parts = key.trim().split(" ");
417
432
if (parts.length < 2) return null;
418
433
421
436
const comment = parts.slice(2).join(" ") || undefined;
422
437
423
438
// Generate fingerprint
424
- const crypto = require("crypto");
439
+ const nodeCrypto = await import("crypto");
425
440
const buffer = Buffer.from(data, "base64");
426
- const hash = crypto.createHash("sha256").update(buffer).digest("base64");
441
+ const hash = nodeCrypto.createHash("sha256").update(buffer).digest("base64");
427
442
const fingerprint = `SHA256:${hash.replace(/=+$/, "")}`;
428
443
429
444
return { type, fingerprint, comment };
430
445
▼src/lib/storage.ts
+7-4
145
145
146
146
if (options?.range) {
147
147
const fd = await fs.open(fullPath, "r");
148
- const buffer = Buffer.alloc(options.range.end - options.range.start + 1);
149
- await fd.read(buffer, 0, buffer.length, options.range.start);
150
- await fd.close();
151
- return buffer;
148
+ try {
149
+ const buffer = Buffer.alloc(options.range.end - options.range.start + 1);
150
+ await fd.read(buffer, 0, buffer.length, options.range.start);
151
+ return buffer;
152
+ } finally {
153
+ await fd.close();
154
+ }
152
155
}
153
156
154
157
return fs.readFile(fullPath);
155
158
▼src/lib/webhooks.ts
+17-13
5
5
6
6
import { getDatabase, schema } from "@/db";
7
7
import crypto from "crypto";
8
-import { and, eq } from "drizzle-orm";
8
+import { and, eq, sql } from "drizzle-orm";
9
9
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
10
10
import { logger } from "./logger";
11
11
import { validateWebhookUrl } from "./url-validator";
127
127
const controller = new AbortController();
128
128
const timeout = setTimeout(() => controller.abort(), 10_000);
129
129
130
- const response = await fetch(webhook.url, {
131
- method: "POST",
132
- headers,
133
- body: JSON.stringify(payload),
134
- signal: controller.signal,
135
- });
136
- clearTimeout(timeout);
130
+ let response: Response;
131
+ try {
132
+ response = await fetch(webhook.url, {
133
+ method: "POST",
134
+ headers,
135
+ body: JSON.stringify(payload),
136
+ signal: controller.signal,
137
+ });
138
+ } finally {
139
+ clearTimeout(timeout);
140
+ }
137
141
138
142
const durationMs = Date.now() - startTime;
139
- const responseBody = await response.text();
143
+ const responseBody = await response!.text();
140
144
141
145
// Log delivery
142
146
await db.insert(schema.webhookDeliveries).values({
154
158
),
155
159
});
156
160
157
- // Update webhook stats
161
+ // Update webhook stats (atomic increment to prevent race conditions)
158
162
await db
159
163
.update(schema.webhooks)
160
164
.set({
161
- deliveryCount: (webhook.deliveryCount || 0) + 1,
165
+ deliveryCount: sql`COALESCE(${schema.webhooks.deliveryCount}, 0) + 1`,
162
166
lastDeliveryStatus: response.ok ? "success" : "failure",
163
167
lastDeliveryAt: new Date(),
164
168
})
178
182
durationMs,
179
183
});
180
184
181
- // Update webhook stats
185
+ // Update webhook stats (atomic increment)
182
186
await db
183
187
.update(schema.webhooks)
184
188
.set({
185
- deliveryCount: (webhook.deliveryCount || 0) + 1,
189
+ deliveryCount: sql`COALESCE(${schema.webhooks.deliveryCount}, 0) + 1`,
186
190
lastDeliveryStatus: "failure",
187
191
lastDeliveryAt: new Date(),
188
192
})
189
193