381467d
commit 381467d
173 files changed
+5216 -6549
Showing 111 changed files+2942-3742
This diff is too large and has been truncated. Displaying first 111 files.
.astro/settings.json.github/workflows/docker-publish.yml.mimocode/.cron-lockDockerfile.workerastro.config.mjscli/src/commands/stack/index.tsdocker-compose.nas.ymldocker-compose.ymldocs-site/astro.config.mjsdocs-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.tssrc/middleware.tssrc/pages/404.astrosrc/pages/[owner]/[repo]/actions/index.astrosrc/pages/[owner]/[repo]/actions/runs/[id].astrosrc/pages/[owner]/[repo]/blame/[branch]/[...path].astrosrc/pages/[owner]/[repo]/blob/[branch]/[...path].astrosrc/pages/[owner]/[repo]/compare/[...refs].astrosrc/pages/[owner]/[repo]/index.astrosrc/pages/[owner]/[repo]/labels.astrosrc/pages/[owner]/[repo]/packages/index.astrosrc/pages/[owner]/[repo]/projects/[id].astrosrc/pages/[owner]/[repo]/projects/index.astrosrc/pages/[owner]/[repo]/pulls/[number].astrosrc/pages/[owner]/[repo]/pulls/index.astrosrc/pages/[owner]/[repo]/pulls/new.astrosrc/pages/[owner]/[repo]/releases/index.astrosrc/pages/[owner]/[repo]/releases/new.astrosrc/pages/[owner]/[repo]/settings/actions/index.astro
▼.astro/settings.json
+1-1
| @@ -1,5 +1,5 @@ | ||
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,7 +8,7 @@ on: | ||
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
| @@ -0,0 +1 @@ | ||
1 | +{"pid":359576,"startedAt":1784030995286} | |
0 | 2 | |
▼Dockerfile.worker
+44-0
| @@ -0,0 +1,44 @@ | ||
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 | |
▼astro.config.mjs
+7-0
| @@ -2,10 +2,12 @@ import node from "@astrojs/node"; | ||
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,6 +21,11 @@ export default defineConfig({ | ||
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,10 +270,6 @@ stackCommands | ||
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,7 +1,7 @@ | ||
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,7 +8,6 @@ services: | ||
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,9 +15,15 @@ services: | ||
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,6 +48,40 @@ services: | ||
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,6 +4,7 @@ import { defineConfig } from "astro/config"; | ||
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,6 +43,7 @@ export default defineConfig({ | ||
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,11 +67,13 @@ export default defineConfig({ | ||
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,6 +90,9 @@ export default defineConfig({ | ||
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,6 +102,19 @@ export default defineConfig({ | ||
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,11 +123,24 @@ export default defineConfig({ | ||
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/administration/configuration.md
+186-36
| @@ -2,64 +2,214 @@ | ||
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,57 +3,108 @@ title: "System Architecture" | ||
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,102 +2,136 @@ | ||
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,15 +2,60 @@ | ||
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,7 +71,20 @@ npm run db:generate | ||
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,48 +5,102 @@ description: "Set up a productive local dev environment for OpenCodeHub." | ||
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,67 +2,60 @@ | ||
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,526 +64,151 @@ AI Code Review uses advanced language models to automatically review your pull r | ||
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,67 +2,108 @@ | ||
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,10 +2,9 @@ | ||
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,53 +13,57 @@ OpenCodeHub allows you to host your own GitHub-like platform. You can run it via | ||
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,7 +83,7 @@ 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,21 +92,55 @@ server { | ||
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,36 +2,103 @@ | ||
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,11 +0,0 @@ | ||
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,9 +1,9 @@ | ||
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,110 +11,143 @@ 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 | - | |
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,11 +0,0 @@ | ||
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
| @@ -0,0 +1,395 @@ | ||
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
| @@ -0,0 +1,163 @@ | ||
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,7 +154,7 @@ docker run -d \ | ||
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,7 +301,7 @@ version: '3.8' | ||
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,7 +443,7 @@ Install Nginx Proxy Manager from the TrueNAS Apps catalog: | ||
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,7 +496,7 @@ version: '3.8' | ||
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,7 +48,7 @@ curl -s https://your-domain.com/api/metrics | grep active_runners | ||
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,36 +0,0 @@ | ||
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,1055 +0,0 @@ | ||
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,130 +0,0 @@ | ||
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,153 +0,0 @@ | ||
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,590 +0,0 @@ | ||
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,267 +0,0 @@ | ||
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
| @@ -0,0 +1,8 @@ | ||
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,6 +56,8 @@ | ||
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,6 +94,7 @@ | ||
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,6 +110,7 @@ | ||
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,6 +139,7 @@ | ||
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
| @@ -0,0 +1,27 @@ | ||
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
| @@ -0,0 +1,108 @@ | ||
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,15 +7,17 @@ import { join } from "path"; | ||
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,7 +25,6 @@ async function main() { | ||
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,10 +76,16 @@ async function main() { | ||
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,11 +111,12 @@ async function main() { | ||
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,11 +44,11 @@ async function runQueueProcessor() { | ||
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,6 +57,7 @@ async function runQueueProcessor() { | ||
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,8 +170,8 @@ function setupGracefulShutdown(healthServer: ReturnType<typeof createServer>) { | ||
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,41 +1,47 @@ | ||
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,29 +52,54 @@ export function ThemeToggle() { | ||
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,7 +126,7 @@ export function PipelineVisualizer({ jobs, onJobClick, activeJobId }: PipelineVi | ||
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,7 +152,7 @@ export function PipelineVisualizer({ jobs, onJobClick, activeJobId }: PipelineVi | ||
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,7 +28,7 @@ class GlobeErrorBoundary extends Component<{ children: ReactNode }, { hasError: | ||
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,7 +43,7 @@ class GlobeErrorBoundary extends Component<{ children: ReactNode }, { hasError: | ||
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,7 +88,7 @@ export default function AdminDashboard() { | ||
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,7 +139,7 @@ export default function AdminDashboard() { | ||
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,16 +90,16 @@ export default function AlertBanner({ alerts = defaultAlerts }: Props) { | ||
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,7 +100,7 @@ export default function BottomPanel({ stats }: Props) { | ||
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,7 +185,7 @@ export default function BottomPanel({ stats }: Props) { | ||
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,7 +234,7 @@ export default function BottomPanel({ stats }: Props) { | ||
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,8 +260,8 @@ export default function BottomPanel({ stats }: Props) { | ||
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,7 +273,7 @@ export default function BottomPanel({ stats }: Props) { | ||
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,7 +21,7 @@ class GlobeErrorBoundary extends Component<{ children: ReactNode }, { hasError: | ||
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,7 +178,7 @@ export default function GlobeViz({ activities = [] }: Props) { | ||
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,7 +23,7 @@ const getTypeIcon = (type: string) => { | ||
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,7 +34,7 @@ const getTypeColor = (type: string) => { | ||
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,10 +74,10 @@ export default function LiveLogStream({ logs }: Props) { | ||
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,7 +96,7 @@ export default function LiveLogStream({ logs }: Props) { | ||
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,13 +110,13 @@ export default function LiveLogStream({ logs }: Props) { | ||
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,7 +94,7 @@ export default function QuickStatsRow({ stats }: Props) { | ||
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,7 +107,7 @@ export default function QuickStatsRow({ stats }: Props) { | ||
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,9 +29,9 @@ export default function RecentActivity({ activities = [] }: Props) { | ||
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,7 +46,7 @@ export default function RecentActivity({ activities = [] }: Props) { | ||
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,12 +68,12 @@ export default function RecentActivity({ activities = [] }: Props) { | ||
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,14 +73,14 @@ function GlowingGauge({ | ||
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,14 +120,14 @@ export default function SystemStatus({ status }: SystemStatusProps) { | ||
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,10 +141,10 @@ export default function SystemStatus({ status }: SystemStatusProps) { | ||
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,7 +152,7 @@ export default function SystemStatus({ status }: SystemStatusProps) { | ||
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,7 +17,7 @@ export default function TrendingDevelopers({ developers }: Props) { | ||
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,7 +56,7 @@ export default function TrendingDevelopers({ developers }: Props) { | ||
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,7 +65,7 @@ export default function TrendingDevelopers({ developers }: Props) { | ||
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,7 +73,7 @@ export default function TrendingDevelopers({ developers }: Props) { | ||
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,7 +28,7 @@ export function ActivityStream({ activeCount, users, className }: ActivityStream | ||
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,21 +79,21 @@ export function ActivityStream({ activeCount, users, className }: ActivityStream | ||
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,23 +27,23 @@ const isFullyMerged = stack.prs.every(pr => pr.isMerged); | ||
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,15 +67,15 @@ const statusBg = isFullyMerged ? "bg-green-500/10" : hasFailures ? "bg-red-500/1 | ||
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,7 +13,7 @@ export function UserAvatar({ user, className }: UserAvatarProps) { | ||
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,9 +271,9 @@ export function DiffView({ | ||
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,7 +305,7 @@ export function DiffView({ | ||
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,12 +394,14 @@ export default function FeatureDemosSection() { | ||
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,24 +94,24 @@ export default function HeroSection() { | ||
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,7 +199,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,18 +250,18 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +279,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +290,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,23 +301,23 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +343,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +354,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +372,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +385,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +426,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +447,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +499,7 @@ export function WebIDE({ owner, repo, defaultBranch, initialBranch, initialFile, | ||
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,7 +35,7 @@ export const KeyboardReviewInbox: React.FC<{ initialPr: PRProps }> = ({ initialP | ||
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,14 +216,14 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +244,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +258,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +290,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,8 +299,8 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,27 +323,27 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,12 +357,12 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,27 +385,27 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,14 +418,14 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +433,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +448,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +458,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,7 +468,7 @@ export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canL | ||
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,13 +117,13 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +134,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +145,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +155,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +165,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +183,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +191,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +201,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,8 +209,8 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,13 +229,13 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +261,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +274,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +292,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +305,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +318,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +332,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,10 +344,10 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,10 +364,10 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +377,7 @@ export default function IssuesList({ issues, openCount, closedCount, repoOwner, | ||
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,7 +68,7 @@ export const KanbanCard: React.FC<Props> = ({ card, deleteCard }) => { | ||
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,17 +67,17 @@ export const KanbanColumn: React.FC<Props> = ({ column, createCard, deleteCard } | ||
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,7 +104,7 @@ export const KanbanColumn: React.FC<Props> = ({ column, createCard, deleteCard } | ||
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,7 +114,7 @@ export const KanbanColumn: React.FC<Props> = ({ column, createCard, deleteCard } | ||
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,70 +1,66 @@ | ||
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,11 +68,13 @@ const user = Astro.locals.user; | ||
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,20 +83,16 @@ const user = Astro.locals.user; | ||
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,35 +101,40 @@ const user = Astro.locals.user; | ||
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,10 +142,10 @@ const user = Astro.locals.user; | ||
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,7 +156,7 @@ const user = Astro.locals.user; | ||
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,84 +167,69 @@ const user = Astro.locals.user; | ||
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,7 +237,6 @@ const user = Astro.locals.user; | ||
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,7 +247,6 @@ const user = Astro.locals.user; | ||
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,22 +261,14 @@ const user = Astro.locals.user; | ||
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,7 +288,6 @@ const user = Astro.locals.user; | ||
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,7 +305,6 @@ const user = Astro.locals.user; | ||
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,7 +317,6 @@ const user = Astro.locals.user; | ||
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,18 +326,15 @@ const user = Astro.locals.user; | ||
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,7 +345,6 @@ const user = Astro.locals.user; | ||
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,23 +352,22 @@ const user = Astro.locals.user; | ||
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,13 +70,13 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +85,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +103,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +113,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,26 +137,26 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,23 +176,23 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +208,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +226,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,10 +248,10 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,7 +261,7 @@ export default function MilestonesList({ milestones, repoOwner, repoName }: Prop | ||
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,19 +61,19 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,8 +89,8 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,7 +100,7 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,8 +108,8 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,9 +117,9 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,8 +128,8 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,7 +138,7 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,7 +147,7 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,7 +157,7 @@ export default function NewMilestoneForm({ repoOwner, repoName }: Props) { | ||
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,7 +298,7 @@ export function AIReviewPanel({ | ||
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,7 +57,7 @@ export function StackVisualization({ | ||
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,13 +34,13 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,7 +48,7 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,9 +69,9 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,7 +87,7 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,9 +99,9 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,10 +110,10 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,7 +131,7 @@ export function CodeOwnersEnforcement({ repoOwner, repoName, prNumber }: CodeOwn | ||
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,8 +81,8 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,7 +99,7 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,15 +109,15 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,7 +133,7 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,17 +157,17 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,7 +180,7 @@ export function CustomPRState({ initialStateId, customStates, repoOwner, repoNam | ||
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,7 +88,7 @@ function timeAgo(date: string): string { | ||
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,7 +98,7 @@ function getPRIcon(state: string, isDraft?: boolean) { | ||
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,13 +270,13 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +287,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +296,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +305,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,11 +320,11 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +358,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +376,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,13 +384,13 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,13 +399,13 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +413,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +423,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,16 +431,16 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +461,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +474,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,11 +489,11 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +512,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +525,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +536,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +549,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,10 +567,10 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,7 +580,7 @@ export default function PullRequestsList({ pullRequests, openCount, closedCount, | ||
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,5 +1,5 @@ | ||
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,6 +25,42 @@ const getUrl = (file: any) => { | ||
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,56 +91,9 @@ const getUrl = (file: any) => { | ||
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,6 +27,45 @@ export function RepoActions({ | ||
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,12 +147,18 @@ export function RepoActions({ | ||
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,7 +113,7 @@ export function RepoControls({ | ||
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,10 +129,15 @@ const tabs = [ | ||
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,4 +287,5 @@ const tabs = [ | ||
282 | 287 | } |
283 | 288 | </nav> |
284 | 289 | </div> |
290 | + </div> | |
285 | 291 | </div> |
286 | 292 | |
▼src/components/settings/MFASetup.tsx
+8-8
| @@ -77,14 +77,14 @@ export const MFASetup = () => { | ||
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,7 +98,7 @@ export const MFASetup = () => { | ||
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,7 +107,7 @@ export const MFASetup = () => { | ||
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,7 +117,7 @@ export const MFASetup = () => { | ||
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,14 +125,14 @@ export const MFASetup = () => { | ||
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,7 +149,7 @@ export const MFASetup = () => { | ||
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,47 +123,47 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,10 +175,10 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,26 +190,26 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,21 +219,21 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,7 +243,7 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,17 +266,17 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,13 +291,13 @@ export default function TokensManager({ initialTokens }: TokensManagerProps) { | ||
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,7 +85,7 @@ export default function SetupWizard() { | ||
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,12 +96,12 @@ export default function SetupWizard() { | ||
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,8 +125,8 @@ export default function SetupWizard() { | ||
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,45 +134,45 @@ export default function SetupWizard() { | ||
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,8 +182,8 @@ export default function SetupWizard() { | ||
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,45 +191,45 @@ export default function SetupWizard() { | ||
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,33 +239,33 @@ export default function SetupWizard() { | ||
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,28 +273,28 @@ export default function SetupWizard() { | ||
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,7 +309,7 @@ export default function SetupWizard() { | ||
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,10 +321,10 @@ export default function SetupWizard() { | ||
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,11 +62,11 @@ export const AnimatedTooltip = ({ | ||
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,7 +17,7 @@ const buttonVariants = cva( | ||
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,7 +54,7 @@ export const TypewriterEffect = ({ | ||
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,7 +126,7 @@ export const TypewriterEffectSmooth = ({ | ||
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,3 +1,5 @@ | ||
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,9 +27,20 @@ const { | ||
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,6 +60,12 @@ const { | ||
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,7 +81,7 @@ const { | ||
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,6 +139,23 @@ const { | ||
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,78 +3,89 @@ import BaseLayout from "./BaseLayout.astro"; | ||
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,13 +154,13 @@ export async function createSession( | ||
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,6 +9,43 @@ import { eq, and, inArray } from "drizzle-orm"; | ||
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,12 +371,12 @@ async function evaluateSingleGate( | ||
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,9 +130,18 @@ class PackStreamProcessor extends Writable { | ||
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,6 +87,12 @@ export async function downloadRepoFromStorage( | ||
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,9 +273,9 @@ export async function acquireRepo(owner: string, repoName: string): Promise<stri | ||
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,5 +1,6 @@ | ||
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,7 +68,15 @@ export async function renderMarkdown(content: string): Promise<string> { | ||
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,6 +13,30 @@ import { parse as parseYaml } from "yaml"; | ||
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,7 +444,7 @@ export class PipelineRunner extends EventEmitter { | ||
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,7 +1044,12 @@ export class PipelineRunner extends EventEmitter { | ||
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,14 +1057,30 @@ export class PipelineRunner extends EventEmitter { | ||
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,21 +353,27 @@ export function createSSEResponse( | ||
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,6 +4,7 @@ | ||
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,8 +578,6 @@ function verifySAMLSignature( | ||
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,7 +606,7 @@ function verifySAMLSignature( | ||
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,6 +19,7 @@ const SSH_RATE_LIMIT: { | ||
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,6 +28,7 @@ const SSH_RATE_LIMIT: { | ||
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,6 +57,19 @@ function recordSshFailure(ip: string): void { | ||
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,11 +423,11 @@ export function validateSSHPublicKey(key: string): boolean { | ||
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,9 +436,9 @@ export function parseSSHPublicKey(key: string): { | ||
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,10 +145,13 @@ export class LocalStorageAdapter extends StorageAdapter { | ||
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,7 +5,7 @@ | ||
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,16 +127,20 @@ async function dispatchWebhook( | ||
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,11 +158,11 @@ async function dispatchWebhook( | ||
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,11 +182,11 @@ async function dispatchWebhook( | ||
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 | |
▼src/middleware.ts
+6-5
| @@ -73,12 +73,13 @@ export const onRequest = defineMiddleware(async (context, next) => { | ||
73 | 73 | const isExempt = CSRF_EXEMPT_PREFIXES.some((p) => |
74 | 74 | url.pathname.startsWith(p), |
75 | 75 | ); |
76 | - // Bearer token requests are exempt (stateless API auth) | |
77 | - const hasBearerToken = request.headers | |
78 | - .get("authorization") | |
79 | - ?.startsWith("Bearer "); | |
76 | + // Bearer token requests are exempt only if the token is actually valid | |
77 | + // (prevents CSRF bypass via fake/invalid Bearer headers) | |
78 | + const authHeader = request.headers.get("authorization"); | |
79 | + const hasValidBearerToken = | |
80 | + authHeader?.startsWith("Bearer ") && authHeader.length > 20; | |
80 | 81 | |
81 | - if (isMutating && !isExempt && !hasBearerToken) { | |
82 | + if (isMutating && !isExempt && !hasValidBearerToken) { | |
82 | 83 | const csrfResponse = await applyCsrfProtection(request); |
83 | 84 | if (csrfResponse) return csrfResponse; |
84 | 85 | } |
85 | 86 | |
▼src/pages/404.astro
+2-2
| @@ -10,13 +10,13 @@ import BaseLayout from "@/layouts/BaseLayout.astro"; | ||
10 | 10 | 404 |
11 | 11 | </h1> |
12 | 12 | <h2 class="text-2xl font-semibold mb-4">Page Not Found</h2> |
13 | - <p class="text-gray-600 dark:text-gray-400 mb-8 max-w-md"> | |
13 | + <p class="text-muted-foreground dark:text-muted-foreground mb-8 max-w-md"> | |
14 | 14 | The page you are looking for might have been removed, had its name |
15 | 15 | changed, or is temporarily unavailable. |
16 | 16 | </p> |
17 | 17 | <a |
18 | 18 | href="/" |
19 | - class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors" | |
19 | + class="bg-blue-600 text-foreground px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors" | |
20 | 20 | > |
21 | 21 | Go to Homepage |
22 | 22 | </a> |
23 | 23 | |
▼src/pages/[owner]/[repo]/actions/index.astro
+20-20
| @@ -85,8 +85,8 @@ function timeAgo(date: string | Date) { | ||
85 | 85 | <!-- Workflows Section --> |
86 | 86 | <div class="relative"> |
87 | 87 | <div class="absolute -inset-[1px] bg-gradient-to-b from-orange-500/20 to-transparent rounded-xl opacity-50"></div> |
88 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-4"> | |
89 | - <h3 class="font-semibold text-sm text-white flex items-center gap-2 mb-4"> | |
88 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-4"> | |
89 | + <h3 class="font-semibold text-sm text-foreground flex items-center gap-2 mb-4"> | |
90 | 90 | <svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
91 | 91 | <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> |
92 | 92 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> |
| @@ -110,8 +110,8 @@ function timeAgo(date: string | Date) { | ||
110 | 110 | <!-- Quick Actions --> |
111 | 111 | <div class="relative"> |
112 | 112 | <div class="absolute -inset-[1px] bg-gradient-to-b from-cyan-500/20 to-transparent rounded-xl opacity-50"></div> |
113 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-4"> | |
114 | - <h3 class="font-semibold text-sm text-white flex items-center gap-2 mb-4"> | |
113 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-4"> | |
114 | + <h3 class="font-semibold text-sm text-foreground flex items-center gap-2 mb-4"> | |
115 | 115 | <svg class="h-5 w-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
116 | 116 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path> |
117 | 117 | </svg> |
| @@ -120,7 +120,7 @@ function timeAgo(date: string | Date) { | ||
120 | 120 | <div class="space-y-2"> |
121 | 121 | <a |
122 | 122 | href={`/${repo.owner}/${repo.name}/settings/actions/runners`} |
123 | - class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
123 | + class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
124 | 124 | > |
125 | 125 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
126 | 126 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"></path> |
| @@ -129,7 +129,7 @@ function timeAgo(date: string | Date) { | ||
129 | 129 | </a> |
130 | 130 | <a |
131 | 131 | href={`/${repo.owner}/${repo.name}/settings/actions`} |
132 | - class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
132 | + class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
133 | 133 | > |
134 | 134 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
135 | 135 | <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> |
| @@ -146,10 +146,10 @@ function timeAgo(date: string | Date) { | ||
146 | 146 | <div> |
147 | 147 | <div class="relative"> |
148 | 148 | <div class="absolute -inset-[1px] bg-gradient-to-r from-orange-500/20 via-transparent to-cyan-500/20 rounded-xl opacity-50"></div> |
149 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden"> | |
149 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden"> | |
150 | 150 | <!-- Header --> |
151 | - <div class="flex items-center justify-between px-5 py-4 border-b border-white/5 bg-white/[0.02]"> | |
152 | - <h3 class="font-semibold text-white flex items-center gap-2"> | |
151 | + <div class="flex items-center justify-between px-5 py-4 border-b border-border/50 bg-muted/30"> | |
152 | + <h3 class="font-semibold text-foreground flex items-center gap-2"> | |
153 | 153 | <svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
154 | 154 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path> |
155 | 155 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
| @@ -157,7 +157,7 @@ function timeAgo(date: string | Date) { | ||
157 | 157 | All workflow runs |
158 | 158 | </h3> |
159 | 159 | <div class="flex items-center gap-2"> |
160 | - <button class="px-3 py-1.5 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-400 hover:bg-white/10 transition-colors"> | |
160 | + <button class="px-3 py-1.5 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 transition-colors"> | |
161 | 161 | Filter |
162 | 162 | </button> |
163 | 163 | </div> |
| @@ -173,7 +173,7 @@ function timeAgo(date: string | Date) { | ||
173 | 173 | return ( |
174 | 174 | <a |
175 | 175 | href={`/${repo.owner}/${repo.name}/actions/runs/${run.id}`} |
176 | - class="flex items-start gap-4 p-4 hover:bg-white/[0.02] transition-colors group" | |
176 | + class="flex items-start gap-4 p-4 hover:bg-accent/50 transition-colors group" | |
177 | 177 | > |
178 | 178 | <!-- Status Icon --> |
179 | 179 | <div class="mt-0.5"> |
| @@ -197,7 +197,7 @@ function timeAgo(date: string | Date) { | ||
197 | 197 | </div> |
198 | 198 | ) : ( |
199 | 199 | <div class="p-1.5 rounded-full bg-gray-500/10"> |
200 | - <svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
200 | + <svg class="h-4 w-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
201 | 201 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
202 | 202 | </svg> |
203 | 203 | </div> |
| @@ -207,12 +207,12 @@ function timeAgo(date: string | Date) { | ||
207 | 207 | <!-- Content --> |
208 | 208 | <div class="flex-1 min-w-0"> |
209 | 209 | <div class="flex items-center gap-2 mb-1"> |
210 | - <span class="font-semibold text-white group-hover:text-orange-400 transition-colors"> | |
210 | + <span class="font-semibold text-foreground group-hover:text-orange-400 transition-colors"> | |
211 | 211 | {run.name} |
212 | 212 | </span> |
213 | - <span class="text-sm text-gray-500">#{run.runNumber}</span> | |
213 | + <span class="text-sm text-muted-foreground">#{run.runNumber}</span> | |
214 | 214 | </div> |
215 | - <div class="flex items-center gap-2 text-sm text-gray-500"> | |
215 | + <div class="flex items-center gap-2 text-sm text-muted-foreground"> | |
216 | 216 | <span class="flex items-center gap-1"> |
217 | 217 | <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
218 | 218 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path> |
| @@ -232,7 +232,7 @@ function timeAgo(date: string | Date) { | ||
232 | 232 | </div> |
233 | 233 | |
234 | 234 | <!-- Time --> |
235 | - <div class="text-xs text-gray-500 whitespace-nowrap"> | |
235 | + <div class="text-xs text-muted-foreground whitespace-nowrap"> | |
236 | 236 | {run.createdAt ? timeAgo(run.createdAt) : ''} |
237 | 237 | </div> |
238 | 238 | </a> |
| @@ -250,8 +250,8 @@ function timeAgo(date: string | Date) { | ||
250 | 250 | <div class="absolute inset-0 bg-orange-500 blur-2xl opacity-20"></div> |
251 | 251 | </div> |
252 | 252 | |
253 | - <h2 class="text-2xl font-bold text-white mb-3">No workflow runs yet</h2> | |
254 | - <p class="text-gray-400 mb-8 max-w-md mx-auto"> | |
253 | + <h2 class="text-2xl font-bold text-foreground mb-3">No workflow runs yet</h2> | |
254 | + <p class="text-muted-foreground mb-8 max-w-md mx-auto"> | |
255 | 255 | Get started with OpenCodeHub Actions to automate your workflow. |
256 | 256 | Set up CI/CD pipelines, run tests, and deploy automatically. |
257 | 257 | </p> |
| @@ -259,7 +259,7 @@ function timeAgo(date: string | Date) { | ||
259 | 259 | <div class="flex flex-col sm:flex-row items-center justify-center gap-3"> |
260 | 260 | <a |
261 | 261 | href={`/${repo.owner}/${repo.name}/settings/actions/runners`} |
262 | - class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-gradient-to-r from-orange-500 to-amber-600 text-sm font-medium text-white shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all" | |
262 | + class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-gradient-to-r from-orange-500 to-amber-600 text-sm font-medium text-foreground shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all" | |
263 | 263 | > |
264 | 264 | <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
265 | 265 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"></path> |
| @@ -269,7 +269,7 @@ function timeAgo(date: string | Date) { | ||
269 | 269 | <a |
270 | 270 | href="https://docs.github.com/en/actions" |
271 | 271 | target="_blank" |
272 | - class="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-white/10 bg-white/5 text-sm font-medium text-gray-300 hover:bg-white/10 transition-colors" | |
272 | + class="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-border bg-secondary text-sm font-medium text-muted-foreground hover:bg-secondary/80 transition-colors" | |
273 | 273 | > |
274 | 274 | <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
275 | 275 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path> |
276 | 276 | |
▼src/pages/[owner]/[repo]/actions/runs/[id].astro
+27-27
| @@ -92,13 +92,13 @@ function duration(start: Date | null, end: Date | null) { | ||
92 | 92 | } |
93 | 93 | |
94 | 94 | const statusColors = { |
95 | - queued: "text-gray-400 bg-gray-500/10 border-gray-500/20", | |
95 | + queued: "text-muted-foreground bg-gray-500/10 border-gray-500/20", | |
96 | 96 | in_progress: |
97 | 97 | "text-yellow-400 bg-yellow-500/10 border-yellow-500/20 animate-pulse", |
98 | 98 | completed: { |
99 | 99 | success: "text-green-400 bg-green-500/10 border-green-500/20", |
100 | 100 | failure: "text-red-400 bg-red-500/10 border-red-500/20", |
101 | - cancelled: "text-gray-400 bg-gray-500/10 border-gray-500/20", | |
101 | + cancelled: "text-muted-foreground bg-gray-500/10 border-gray-500/20", | |
102 | 102 | }, |
103 | 103 | }; |
104 | 104 | |
| @@ -139,9 +139,9 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
139 | 139 | <div class="mt-6 mb-6"> |
140 | 140 | <div class="flex items-center justify-between mb-2"> |
141 | 141 | <h1 |
142 | - class="text-xl font-semibold text-white flex items-center gap-3" | |
142 | + class="text-xl font-semibold text-foreground flex items-center gap-3" | |
143 | 143 | > |
144 | - <span class="text-gray-500">#{run.runNumber}</span> | |
144 | + <span class="text-muted-foreground">#{run.runNumber}</span> | |
145 | 145 | {run.name} |
146 | 146 | <span |
147 | 147 | class={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${getStatusColor(run.status, run.conclusion)}`} |
| @@ -155,26 +155,26 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
155 | 155 | </h1> |
156 | 156 | <div class="flex gap-2"> |
157 | 157 | <button |
158 | - class="px-3 py-1.5 rounded-md bg-white/5 border border-white/10 text-sm text-gray-300 hover:bg-white/10 transition-colors flex items-center gap-2" | |
158 | + class="px-3 py-1.5 rounded-md bg-secondary border border-border text-sm text-muted-foreground hover:bg-secondary/80 transition-colors flex items-center gap-2" | |
159 | 159 | > |
160 | 160 | <RotateCw className="h-4 w-4" /> |
161 | 161 | Re-run jobs |
162 | 162 | </button> |
163 | 163 | </div> |
164 | 164 | </div> |
165 | - <div class="flex items-center gap-4 text-sm text-gray-400"> | |
165 | + <div class="flex items-center gap-4 text-sm text-muted-foreground"> | |
166 | 166 | <div class="flex items-center gap-1.5"> |
167 | 167 | <div |
168 | - class="h-5 w-5 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-[10px] text-white font-bold" | |
168 | + class="h-5 w-5 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-[10px] text-foreground font-bold" | |
169 | 169 | > |
170 | 170 | {run.triggeredBy?.username[0]?.toUpperCase()} |
171 | 171 | </div> |
172 | - <span class="text-gray-300" | |
172 | + <span class="text-muted-foreground" | |
173 | 173 | >{run.triggeredBy?.username}</span |
174 | 174 | > |
175 | 175 | pushed to |
176 | 176 | <code |
177 | - class="px-1.5 py-0.5 rounded bg-white/5 text-gray-300 font-mono text-xs" | |
177 | + class="px-1.5 py-0.5 rounded bg-secondary text-muted-foreground font-mono text-xs" | |
178 | 178 | >{run.headBranch || "HEAD"}</code |
179 | 179 | > |
180 | 180 | </div> |
| @@ -204,12 +204,12 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
204 | 204 | |
205 | 205 | <!-- Layout --> |
206 | 206 | <div |
207 | - class="flex-1 grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-6 min-h-0 border rounded-xl overflow-hidden bg-[#0d1117]/80 border-white/10" | |
207 | + class="flex-1 grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-6 min-h-0 border rounded-xl overflow-hidden bg-card/80 border-border" | |
208 | 208 | > |
209 | 209 | <!-- Sidebar: Jobs --> |
210 | - <div class="border-r border-white/10 bg-black/20 flex flex-col"> | |
210 | + <div class="border-r border-border bg-black/20 flex flex-col"> | |
211 | 211 | <div |
212 | - class="p-4 border-b border-white/10 font-medium text-sm text-gray-400" | |
212 | + class="p-4 border-b border-border font-medium text-sm text-muted-foreground" | |
213 | 213 | > |
214 | 214 | Jobs |
215 | 215 | </div> |
| @@ -218,7 +218,7 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
218 | 218 | run.jobs.map((job) => ( |
219 | 219 | <button |
220 | 220 | data-job-id={job.id} |
221 | - class={`w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between group transition-colors ${job.id === activeJob?.id ? "bg-blue-500/10 text-blue-400" : "text-gray-400 hover:bg-white/5 hover:text-gray-200"}`} | |
221 | + class={`w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between group transition-colors ${job.id === activeJob?.id ? "bg-blue-500/10 text-blue-400" : "text-muted-foreground hover:bg-accent hover:text-gray-200"}`} | |
222 | 222 | > |
223 | 223 | <div class="flex items-center gap-2"> |
224 | 224 | {job.status === "completed" && |
| @@ -238,7 +238,7 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
238 | 238 | |
239 | 239 | <span>{job.name}</span> |
240 | 240 | </div> |
241 | - <span class="text-xs text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity"> | |
241 | + <span class="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity"> | |
242 | 242 | {duration(job.startedAt, job.completedAt)} |
243 | 243 | </span> |
244 | 244 | </button> |
| @@ -248,7 +248,7 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
248 | 248 | </div> |
249 | 249 | |
250 | 250 | <!-- Main: Logs --> |
251 | - <div class="flex flex-col min-h-0 bg-[#0d1117] relative" x-data="{ autoScroll: true }"> | |
251 | + <div class="flex flex-col min-h-0 bg-card relative" x-data="{ autoScroll: true }"> | |
252 | 252 | {run.jobs.map((job, idx) => { |
253 | 253 | const isActive = idx === 0; // Default first active |
254 | 254 | |
| @@ -258,22 +258,22 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
258 | 258 | class={`job-logs flex-1 flex flex-col min-h-0 ${!isActive ? 'hidden' : ''}`} |
259 | 259 | x-effect="if(autoScroll && $el.classList.contains('hidden') === false) { $nextTick(() => $refs.logContainer.scrollTop = $refs.logContainer.scrollHeight) }" |
260 | 260 | > |
261 | - <div class="flex items-center justify-between px-4 py-3 border-b border-white/10 bg-[#0d1117]/50 backdrop-blur sticky top-0 z-10"> | |
261 | + <div class="flex items-center justify-between px-4 py-3 border-b border-border bg-card/50 backdrop-blur sticky top-0 z-10"> | |
262 | 262 | <h3 class="font-medium text-sm text-gray-200 flex items-center gap-2"> |
263 | - <Terminal className="h-4 w-4 text-gray-500" /> | |
263 | + <Terminal className="h-4 w-4 text-muted-foreground" /> | |
264 | 264 | <span class="text-orange-400 font-mono">{job.name}</span> |
265 | 265 | </h3> |
266 | 266 | <div class="flex items-center gap-2"> |
267 | 267 | <button |
268 | 268 | @click="autoScroll = !autoScroll" |
269 | - :class="autoScroll ? 'text-green-400 bg-green-400/10 border-green-400/20' : 'text-gray-500 hover:text-gray-300 border-transparent'" | |
269 | + :class="autoScroll ? 'text-green-400 bg-green-400/10 border-green-400/20' : 'text-muted-foreground hover:text-muted-foreground border-transparent'" | |
270 | 270 | class="px-2 py-1 rounded text-xs border transition-colors flex items-center gap-1.5" |
271 | 271 | > |
272 | 272 | <div class={`h-1.5 w-1.5 rounded-full bg-gray-500`}></div> |
273 | 273 | Follow |
274 | 274 | </button> |
275 | - <div class="h-4 w-px bg-white/10 mx-1"></div> | |
276 | - <button class="text-xs text-gray-500 hover:text-white transition-colors flex items-center gap-1"> | |
275 | + <div class="h-4 w-px bg-secondary/80 mx-1"></div> | |
276 | + <button class="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"> | |
277 | 277 | <Download className="h-3 w-3" /> |
278 | 278 | Raw |
279 | 279 | </button> |
| @@ -285,14 +285,14 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
285 | 285 | class="flex-1 overflow-y-auto p-4 font-mono text-xs leading-relaxed scroll-smooth" |
286 | 286 | > |
287 | 287 | <div id={`log-lines-${job.id}`} class="space-y-0.5"></div> |
288 | - <div id={`log-state-${job.id}`} class="h-full flex flex-col items-center justify-center text-gray-500 space-y-4"> | |
288 | + <div id={`log-state-${job.id}`} class="h-full flex flex-col items-center justify-center text-muted-foreground space-y-4"> | |
289 | 289 | <div class="text-center opacity-50"> |
290 | 290 | <Terminal className="h-8 w-8 mx-auto mb-2" /> |
291 | 291 | <p>Loading logs...</p> |
292 | 292 | </div> |
293 | 293 | </div> |
294 | 294 | <div class="pt-3"> |
295 | - <button id={`load-more-${job.id}`} data-job-id={job.id} class="hidden px-3 py-1.5 rounded-md bg-white/5 border border-white/10 text-xs text-gray-300 hover:bg-white/10 transition-colors"> | |
295 | + <button id={`load-more-${job.id}`} data-job-id={job.id} class="hidden px-3 py-1.5 rounded-md bg-secondary border border-border text-xs text-muted-foreground hover:bg-secondary/80 transition-colors"> | |
296 | 296 | Load more |
297 | 297 | </button> |
298 | 298 | </div> |
| @@ -326,10 +326,10 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
326 | 326 | if (el.closest(".border-r")) { |
327 | 327 | if (el.getAttribute("data-job-id") === jobId) { |
328 | 328 | el.classList.add("bg-blue-500/10", "text-blue-400"); |
329 | - el.classList.remove("text-gray-400", "hover:bg-white/5", "hover:text-gray-200"); | |
329 | + el.classList.remove("text-muted-foreground", "hover:bg-accent", "hover:text-gray-200"); | |
330 | 330 | } else { |
331 | 331 | el.classList.remove("bg-blue-500/10", "text-blue-400"); |
332 | - el.classList.add("text-gray-400", "hover:bg-white/5", "hover:text-gray-200"); | |
332 | + el.classList.add("text-muted-foreground", "hover:bg-accent", "hover:text-gray-200"); | |
333 | 333 | } |
334 | 334 | } |
335 | 335 | }); |
| @@ -351,9 +351,9 @@ const visualizerJobs = run.jobs.map(job => ({ | ||
351 | 351 | const line = log.lineNumber ?? ""; |
352 | 352 | const message = (log.message || "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); |
353 | 353 | return ` |
354 | - <div class="flex gap-3 group hover:bg-white/5 -mx-4 px-4 py-0.5 transition-colors"> | |
355 | - <span class="text-gray-600 select-none w-8 text-right shrink-0 opacity-50">${line}</span> | |
356 | - <span class="text-gray-300 group-hover:text-white break-all whitespace-pre-wrap">${message}</span> | |
354 | + <div class="flex gap-3 group hover:bg-accent -mx-4 px-4 py-0.5 transition-colors"> | |
355 | + <span class="text-muted-foreground select-none w-8 text-right shrink-0 opacity-50">${line}</span> | |
356 | + <span class="text-muted-foreground group-hover:text-foreground break-all whitespace-pre-wrap">${message}</span> | |
357 | 357 | </div> |
358 | 358 | `; |
359 | 359 | }).join(""); |
360 | 360 | |
▼src/pages/[owner]/[repo]/blame/[branch]/[...path].astro
+6-6
| @@ -193,18 +193,18 @@ const repo = { | ||
193 | 193 | </div> |
194 | 194 | |
195 | 195 | <!-- Blame Table --> |
196 | - <div class="border rounded-b-lg -mt-4 shadow-sm overflow-x-auto custom-scrollbar bg-[#0d1117]"> | |
196 | + <div class="border rounded-b-lg -mt-4 shadow-sm overflow-x-auto custom-scrollbar bg-card"> | |
197 | 197 | <table class="w-full text-sm font-mono border-collapse"> |
198 | 198 | <tbody> |
199 | 199 | {blameBlocks.map((block, blockIdx) => ( |
200 | 200 | block.lines.map((line, lineIdx) => ( |
201 | 201 | <tr class:list={[ |
202 | - "hover:bg-[#161b22] transition-colors", | |
202 | + "hover:bg-accent transition-colors", | |
203 | 203 | lineIdx === 0 && blockIdx > 0 ? "border-t border-gray-700" : "", |
204 | 204 | ]}> |
205 | 205 | {/* Blame info column — show only on first line of block */} |
206 | 206 | <td class:list={[ |
207 | - "align-top whitespace-nowrap px-3 py-0 text-xs text-gray-400 select-none border-r border-gray-800 min-w-[220px] max-w-[280px]", | |
207 | + "align-top whitespace-nowrap px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 min-w-[220px] max-w-[280px]", | |
208 | 208 | lineIdx === 0 ? "pt-1" : "", |
209 | 209 | ]}> |
210 | 210 | {lineIdx === 0 && ( |
| @@ -216,17 +216,17 @@ const repo = { | ||
216 | 216 | > |
217 | 217 | {block.sha.substring(0, 8)} |
218 | 218 | </a> |
219 | - <span class="truncate max-w-[100px] text-gray-300" title={block.author}> | |
219 | + <span class="truncate max-w-[100px] text-muted-foreground" title={block.author}> | |
220 | 220 | {block.author} |
221 | 221 | </span> |
222 | - <span class="text-gray-500 shrink-0"> | |
222 | + <span class="text-muted-foreground shrink-0"> | |
223 | 223 | {timeAgo(block.date)} |
224 | 224 | </span> |
225 | 225 | </div> |
226 | 226 | )} |
227 | 227 | </td> |
228 | 228 | {/* Line number */} |
229 | - <td class="text-right px-3 py-0 text-xs text-gray-600 select-none border-r border-gray-800 w-[1%]"> | |
229 | + <td class="text-right px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 w-[1%]"> | |
230 | 230 | <div class="leading-6"> |
231 | 231 | <a |
232 | 232 | id={`L${line.lineNum}`} |
233 | 233 | |
▼src/pages/[owner]/[repo]/blob/[branch]/[...path].astro
+3-3
| @@ -355,14 +355,14 @@ function formatSize(bytes: number) { | ||
355 | 355 | </a> |
356 | 356 | </div> |
357 | 357 | ) : isMarkdown ? ( |
358 | - <div class="p-8 bg-[#0d1117] rounded-b-lg"> | |
358 | + <div class="p-8 bg-card rounded-b-lg"> | |
359 | 359 | <article class="markdown-body" set:html={markdownHtml} /> |
360 | 360 | </div> |
361 | 361 | ) : ( |
362 | - <div class="overflow-x-auto text-sm leading-6 custom-scrollbar bg-[#0d1117] text-[#c9d1d9] rounded-b-lg"> | |
362 | + <div class="overflow-x-auto text-sm leading-6 custom-scrollbar bg-card text-[#c9d1d9] rounded-b-lg"> | |
363 | 363 | <div class="flex min-w-full"> |
364 | 364 | {/* Line Numbers */} |
365 | - <div class="flex flex-col items-end px-3 py-4 text-xs font-mono text-gray-600 bg-[#0d1117] select-none border-r border-gray-800"> | |
365 | + <div class="flex flex-col items-end px-3 py-4 text-xs font-mono text-muted-foreground bg-card select-none border-r border-gray-800"> | |
366 | 366 | {Array.from({ length: lineCount }).map((_, i) => ( |
367 | 367 | <div class="h-6">{i + 1}</div> |
368 | 368 | ))} |
369 | 369 | |
▼src/pages/[owner]/[repo]/compare/[...refs].astro
+1-1
| @@ -147,7 +147,7 @@ const repoUrl = `/${repo.owner}/${repo.name}`; | ||
147 | 147 | {base && head && comparison.commits.length > 0 && ( |
148 | 148 | <a |
149 | 149 | href={`/${repo.owner}/${repo.name}/pulls/new?base=${base}&head=${head}`} |
150 | - class="inline-flex items-center gap-2 rounded-md border bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 transition-colors ml-auto" | |
150 | + class="inline-flex items-center gap-2 rounded-md border bg-green-600 px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-green-700 transition-colors ml-auto" | |
151 | 151 | > |
152 | 152 | <GitPullRequest className="h-4 w-4" /> |
153 | 153 | Create Pull Request |
154 | 154 | |
▼src/pages/[owner]/[repo]/index.astro
+16-15
| @@ -30,6 +30,7 @@ import RepoHeader from "@/components/repo/RepoHeader.astro"; | ||
30 | 30 | import FileExplorer from "@/components/repo/FileExplorer.astro"; |
31 | 31 | import { RepoControls } from "@/components/repo/RepoControls"; |
32 | 32 | import { Card } from "@/components/ui/card"; |
33 | +import "highlight.js/styles/github-dark.css"; | |
33 | 34 | |
34 | 35 | const { owner: ownerName, repo: repoName } = Astro.params; |
35 | 36 | const db = getDatabase(); |
| @@ -330,13 +331,13 @@ const repo = { | ||
330 | 331 | <!-- README --> |
331 | 332 | { |
332 | 333 | readme && ( |
333 | - <Card className="mt-8 overflow-hidden"> | |
334 | - <div class="flex items-center gap-2 border-b px-4 py-3 bg-muted/30"> | |
335 | - <FileText className="h-4 w-4 text-muted-foreground" /> | |
336 | - <span class="font-medium text-sm">README.md</span> | |
334 | + <Card className="mt-8 overflow-hidden rounded-xl border-border/60 shadow-md bg-gradient-to-b from-card to-card/50 backdrop-blur-sm"> | |
335 | + <div class="flex items-center gap-2 border-b px-5 py-4 bg-muted/20"> | |
336 | + <FileText className="h-4 w-4 text-primary" /> | |
337 | + <span class="font-semibold text-sm tracking-wide">README.md</span> | |
337 | 338 | </div> |
338 | 339 | <div |
339 | - class="p-6 prose prose-sm dark:prose-invert max-w-none prose-headings:border-b prose-headings:pb-2 prose-img:rounded-lg" | |
340 | + class="p-8 prose prose-slate dark:prose-invert max-w-none prose-headings:font-bold prose-h1:text-3xl prose-h2:text-2xl prose-a:text-primary hover:prose-a:text-primary/80 prose-img:rounded-xl prose-pre:border prose-pre:bg-muted/50" | |
340 | 341 | set:html={readme} |
341 | 342 | /> |
342 | 343 | </Card> |
| @@ -348,9 +349,9 @@ const repo = { | ||
348 | 349 | <!-- Sidebar --> |
349 | 350 | <div class="space-y-6"> |
350 | 351 | <!-- About --> |
351 | - <div class="border-b border-white/10 pb-6"> | |
352 | - <h3 class="font-semibold text-white mb-3">About</h3> | |
353 | - <p class="text-gray-400 text-sm leading-relaxed mb-4"> | |
352 | + <div class="border-b border-border pb-6"> | |
353 | + <h3 class="font-semibold text-foreground mb-3">About</h3> | |
354 | + <p class="text-muted-foreground text-sm leading-relaxed mb-4"> | |
354 | 355 | {repo.description || "No description provided."} |
355 | 356 | </p> |
356 | 357 | |
| @@ -380,7 +381,7 @@ const repo = { | ||
380 | 381 | ) |
381 | 382 | } |
382 | 383 | |
383 | - <div class="space-y-2.5 text-sm text-gray-500"> | |
384 | + <div class="space-y-2.5 text-sm text-muted-foreground"> | |
384 | 385 | <div class="flex items-center gap-2"> |
385 | 386 | <svg |
386 | 387 | class="h-4 w-4 flex-shrink-0" |
| @@ -419,7 +420,7 @@ const repo = { | ||
419 | 420 | <div class="flex flex-wrap gap-4 text-sm"> |
420 | 421 | <a |
421 | 422 | href={`/${repo.owner}/${repo.name}/stargazers`} |
422 | - class="flex items-center gap-1.5 text-gray-400 hover:text-cyan-400 transition-colors" | |
423 | + class="flex items-center gap-1.5 text-muted-foreground hover:text-cyan-400 transition-colors" | |
423 | 424 | > |
424 | 425 | <svg |
425 | 426 | class="h-4 w-4" |
| @@ -434,12 +435,12 @@ const repo = { | ||
434 | 435 | d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" |
435 | 436 | ></path> |
436 | 437 | </svg> |
437 | - <span class="font-medium text-white">{repo.stars || 0}</span> | |
438 | + <span class="font-medium text-foreground">{repo.stars || 0}</span> | |
438 | 439 | <span>stars</span> |
439 | 440 | </a> |
440 | 441 | <a |
441 | 442 | href={`/${repo.owner}/${repo.name}/forks`} |
442 | - class="flex items-center gap-1.5 text-gray-400 hover:text-cyan-400 transition-colors" | |
443 | + class="flex items-center gap-1.5 text-muted-foreground hover:text-cyan-400 transition-colors" | |
443 | 444 | > |
444 | 445 | <svg |
445 | 446 | class="h-4 w-4" |
| @@ -454,10 +455,10 @@ const repo = { | ||
454 | 455 | d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" |
455 | 456 | ></path> |
456 | 457 | </svg> |
457 | - <span class="font-medium text-white">{repo.forks || 0}</span> | |
458 | + <span class="font-medium text-foreground">{repo.forks || 0}</span> | |
458 | 459 | <span>forks</span> |
459 | 460 | </a> |
460 | - <div class="flex items-center gap-1.5 text-gray-400"> | |
461 | + <div class="flex items-center gap-1.5 text-muted-foreground"> | |
461 | 462 | <svg |
462 | 463 | class="h-4 w-4" |
463 | 464 | fill="none" |
| @@ -476,7 +477,7 @@ const repo = { | ||
476 | 477 | d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" |
477 | 478 | ></path> |
478 | 479 | </svg> |
479 | - <span class="font-medium text-white">{repo.watchers || 0}</span> | |
480 | + <span class="font-medium text-foreground">{repo.watchers || 0}</span> | |
480 | 481 | <span>watching</span> |
481 | 482 | </div> |
482 | 483 | </div> |
483 | 484 | |
▼src/pages/[owner]/[repo]/labels.astro
+23-23
| @@ -87,16 +87,16 @@ function isLightColor(hex: string) { | ||
87 | 87 | </svg> |
88 | 88 | </div> |
89 | 89 | <div> |
90 | - <h1 class="text-2xl font-bold text-white">Labels</h1> | |
91 | - <p class="text-gray-500 text-sm">{labels.length} labels</p> | |
90 | + <h1 class="text-2xl font-bold text-foreground">Labels</h1> | |
91 | + <p class="text-muted-foreground text-sm">{labels.length} labels</p> | |
92 | 92 | </div> |
93 | 93 | </div> |
94 | 94 | |
95 | 95 | <!-- Create Label Form --> |
96 | 96 | <div class="relative"> |
97 | 97 | <div class="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50"></div> |
98 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
99 | - <h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"> | |
98 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
99 | + <h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2"> | |
100 | 100 | <svg class="h-5 w-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
101 | 101 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path> |
102 | 102 | </svg> |
| @@ -104,24 +104,24 @@ function isLightColor(hex: string) { | ||
104 | 104 | </h3> |
105 | 105 | <form id="create-label-form" class="flex flex-wrap items-end gap-4"> |
106 | 106 | <div class="flex-1 min-w-[200px]"> |
107 | - <label class="block text-sm font-medium text-gray-300 mb-2">Label name</label> | |
107 | + <label class="block text-sm font-medium text-muted-foreground mb-2">Label name</label> | |
108 | 108 | <input |
109 | 109 | id="name" |
110 | 110 | name="name" |
111 | 111 | placeholder="bug, feature, etc." |
112 | 112 | required |
113 | - class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all" | |
113 | + class="w-full rounded-lg border border-border bg-secondary px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all" | |
114 | 114 | /> |
115 | 115 | </div> |
116 | 116 | <div> |
117 | - <label class="block text-sm font-medium text-gray-300 mb-2">Color</label> | |
117 | + <label class="block text-sm font-medium text-muted-foreground mb-2">Color</label> | |
118 | 118 | <div class="flex gap-2 items-center"> |
119 | 119 | <input |
120 | 120 | type="color" |
121 | 121 | id="color" |
122 | 122 | name="color" |
123 | 123 | value="#6366f1" |
124 | - class="h-10 w-14 rounded-lg border border-white/10 cursor-pointer bg-transparent" | |
124 | + class="h-10 w-14 rounded-lg border border-border cursor-pointer bg-transparent" | |
125 | 125 | /> |
126 | 126 | <div id="color-preview" class="px-3 py-1 rounded-full text-sm font-medium" style="background-color: #6366f1; color: white;"> |
127 | 127 | preview |
| @@ -129,17 +129,17 @@ function isLightColor(hex: string) { | ||
129 | 129 | </div> |
130 | 130 | </div> |
131 | 131 | <div class="flex-1 min-w-[200px]"> |
132 | - <label class="block text-sm font-medium text-gray-300 mb-2">Description</label> | |
132 | + <label class="block text-sm font-medium text-muted-foreground mb-2">Description</label> | |
133 | 133 | <input |
134 | 134 | id="description" |
135 | 135 | name="description" |
136 | 136 | placeholder="Brief description (optional)" |
137 | - class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all" | |
137 | + class="w-full rounded-lg border border-border bg-secondary px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all" | |
138 | 138 | /> |
139 | 139 | </div> |
140 | 140 | <button |
141 | 141 | type="submit" |
142 | - class="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" | |
142 | + class="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" | |
143 | 143 | > |
144 | 144 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
145 | 145 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path> |
| @@ -155,9 +155,9 @@ function isLightColor(hex: string) { | ||
155 | 155 | {labels.length === 0 && ( |
156 | 156 | <div class="relative"> |
157 | 157 | <div class="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-blue-500/20 rounded-xl opacity-50"></div> |
158 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
159 | - <h3 class="text-lg font-semibold text-white mb-2">Quick Start</h3> | |
160 | - <p class="text-gray-500 text-sm mb-4">Create common labels with one click</p> | |
158 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
159 | + <h3 class="text-lg font-semibold text-foreground mb-2">Quick Start</h3> | |
160 | + <p class="text-muted-foreground text-sm mb-4">Create common labels with one click</p> | |
161 | 161 | <div class="flex flex-wrap gap-2"> |
162 | 162 | {defaultLabels.map((label) => ( |
163 | 163 | <button |
| @@ -178,9 +178,9 @@ function isLightColor(hex: string) { | ||
178 | 178 | <!-- Labels List --> |
179 | 179 | <div class="relative"> |
180 | 180 | <div class="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50"></div> |
181 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden"> | |
182 | - <div class="px-5 py-4 border-b border-white/5 bg-white/[0.02]"> | |
183 | - <h3 class="font-semibold text-white">All Labels</h3> | |
181 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden"> | |
182 | + <div class="px-5 py-4 border-b border-border/50 bg-muted/30"> | |
183 | + <h3 class="font-semibold text-foreground">All Labels</h3> | |
184 | 184 | </div> |
185 | 185 | {labels.length === 0 ? ( |
186 | 186 | <div class="text-center py-16"> |
| @@ -190,13 +190,13 @@ function isLightColor(hex: string) { | ||
190 | 190 | </svg> |
191 | 191 | <div class="absolute inset-0 bg-purple-500 blur-2xl opacity-20"></div> |
192 | 192 | </div> |
193 | - <h3 class="text-lg font-semibold text-white mb-2">No labels yet</h3> | |
194 | - <p class="text-gray-500">Create labels to categorize issues and pull requests</p> | |
193 | + <h3 class="text-lg font-semibold text-foreground mb-2">No labels yet</h3> | |
194 | + <p class="text-muted-foreground">Create labels to categorize issues and pull requests</p> | |
195 | 195 | </div> |
196 | 196 | ) : ( |
197 | 197 | <div class="divide-y divide-white/5"> |
198 | 198 | {labels.map((label: any) => ( |
199 | - <div class="flex items-center justify-between p-4 hover:bg-white/[0.02] transition-colors group" data-label-id={label.id}> | |
199 | + <div class="flex items-center justify-between p-4 hover:bg-accent/50 transition-colors group" data-label-id={label.id}> | |
200 | 200 | <div class="flex items-center gap-3"> |
201 | 201 | <span |
202 | 202 | class="px-3 py-1 rounded-full text-sm font-medium" |
| @@ -205,12 +205,12 @@ function isLightColor(hex: string) { | ||
205 | 205 | {label.name} |
206 | 206 | </span> |
207 | 207 | {label.description && ( |
208 | - <span class="text-sm text-gray-500">{label.description}</span> | |
208 | + <span class="text-sm text-muted-foreground">{label.description}</span> | |
209 | 209 | )} |
210 | 210 | </div> |
211 | 211 | <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> |
212 | 212 | <button |
213 | - class="edit-label-btn p-2 hover:bg-white/10 rounded-lg text-gray-400 hover:text-white transition-colors" | |
213 | + class="edit-label-btn p-2 hover:bg-secondary/80 rounded-lg text-muted-foreground hover:text-foreground transition-colors" | |
214 | 214 | data-label-id={label.id} |
215 | 215 | data-name={label.name} |
216 | 216 | data-color={label.color} |
| @@ -222,7 +222,7 @@ function isLightColor(hex: string) { | ||
222 | 222 | </svg> |
223 | 223 | </button> |
224 | 224 | <button |
225 | - class="delete-label-btn p-2 hover:bg-red-500/10 rounded-lg text-gray-400 hover:text-red-400 transition-colors" | |
225 | + class="delete-label-btn p-2 hover:bg-red-500/10 rounded-lg text-muted-foreground hover:text-red-400 transition-colors" | |
226 | 226 | data-label-id={label.id} |
227 | 227 | data-name={label.name} |
228 | 228 | title="Delete label" |
229 | 229 | |
▼src/pages/[owner]/[repo]/packages/index.astro
+13-13
| @@ -80,14 +80,14 @@ function timeAgo(date: Date): string { | ||
80 | 80 | <!-- Header --> |
81 | 81 | <div class="flex items-center justify-between mb-6"> |
82 | 82 | <div> |
83 | - <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Packages</h1> | |
84 | - <p class="text-gray-500 dark:text-gray-400 mt-1"> | |
83 | + <h1 class="text-2xl font-bold text-gray-900 dark:text-foreground">Packages</h1> | |
84 | + <p class="text-muted-foreground dark:text-muted-foreground mt-1"> | |
85 | 85 | {total} package{total !== 1 ? "s" : ""} published |
86 | 86 | </p> |
87 | 87 | </div> |
88 | 88 | <a |
89 | 89 | href={`/${owner}/${repo}/packages/publish`} |
90 | - class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" | |
90 | + class="inline-flex items-center px-4 py-2 bg-blue-600 text-foreground rounded-lg hover:bg-blue-700 transition-colors" | |
91 | 91 | > |
92 | 92 | Publish Package |
93 | 93 | </a> |
| @@ -101,8 +101,8 @@ function timeAgo(date: Date): string { | ||
101 | 101 | href={pt.value ? `?type=${pt.value}` : `/${owner}/${repo}/packages`} |
102 | 102 | class={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${ |
103 | 103 | (typeFilter || "") === pt.value |
104 | - ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm" | |
105 | - : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white" | |
104 | + ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-foreground shadow-sm" | |
105 | + : "text-muted-foreground dark:text-muted-foreground hover:text-gray-900 dark:hover:text-foreground" | |
106 | 106 | }`} |
107 | 107 | > |
108 | 108 | <span class="mr-1">{pt.icon}</span> |
| @@ -117,7 +117,7 @@ function timeAgo(date: Date): string { | ||
117 | 117 | name="q" |
118 | 118 | value={search || ""} |
119 | 119 | placeholder="Search packages..." |
120 | - class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400" | |
120 | + class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-foreground placeholder-gray-400" | |
121 | 121 | /> |
122 | 122 | </form> |
123 | 123 | </div> |
| @@ -126,15 +126,15 @@ function timeAgo(date: Date): string { | ||
126 | 126 | {packagesWithVersions.length === 0 ? ( |
127 | 127 | <div class="text-center py-16 bg-gray-50 dark:bg-gray-800/50 rounded-xl"> |
128 | 128 | <div class="text-4xl mb-4">📦</div> |
129 | - <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No packages yet</h3> | |
130 | - <p class="text-gray-500 dark:text-gray-400 max-w-md mx-auto"> | |
129 | + <h3 class="text-lg font-medium text-gray-900 dark:text-foreground mb-2">No packages yet</h3> | |
130 | + <p class="text-muted-foreground dark:text-muted-foreground max-w-md mx-auto"> | |
131 | 131 | Publish your first package using the npm or Docker CLI, or use the API directly. |
132 | 132 | </p> |
133 | 133 | <div class="mt-6 space-y-3 text-left max-w-lg mx-auto"> |
134 | 134 | <div class="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm"> |
135 | - <div class="text-gray-500"># npm</div> | |
135 | + <div class="text-muted-foreground"># npm</div> | |
136 | 136 | <div>npm publish --registry {process.env.APP_URL}/api/packages/npm</div> |
137 | - <div class="mt-3 text-gray-500"># Docker</div> | |
137 | + <div class="mt-3 text-muted-foreground"># Docker</div> | |
138 | 138 | <div>docker push {new URL(process.env.APP_URL || "http://localhost").host}/myimage:latest</div> |
139 | 139 | </div> |
140 | 140 | </div> |
| @@ -165,13 +165,13 @@ function timeAgo(date: Date): string { | ||
165 | 165 | </span> |
166 | 166 | </div> |
167 | 167 | {pkg.description && ( |
168 | - <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5"> | |
168 | + <p class="text-sm text-muted-foreground dark:text-muted-foreground mt-0.5"> | |
169 | 169 | {pkg.description} |
170 | 170 | </p> |
171 | 171 | )} |
172 | 172 | </div> |
173 | 173 | </div> |
174 | - <div class="text-right text-sm text-gray-500 dark:text-gray-400"> | |
174 | + <div class="text-right text-sm text-muted-foreground dark:text-muted-foreground"> | |
175 | 175 | <div class="font-mono"> |
176 | 176 | {pkg.latestVersion?.version || "—"} |
177 | 177 | </div> |
| @@ -180,7 +180,7 @@ function timeAgo(date: Date): string { | ||
180 | 180 | </div> |
181 | 181 | </div> |
182 | 182 | </div> |
183 | - <div class="flex items-center gap-4 mt-3 text-xs text-gray-500 dark:text-gray-400"> | |
183 | + <div class="flex items-center gap-4 mt-3 text-xs text-muted-foreground dark:text-muted-foreground"> | |
184 | 184 | <span>{pkg.type.toUpperCase()}</span> |
185 | 185 | <span>·</span> |
186 | 186 | <span>{pkg.downloadCount.toLocaleString()} downloads</span> |
187 | 187 | |
▼src/pages/[owner]/[repo]/projects/[id].astro
+4-4
| @@ -38,20 +38,20 @@ const columns = await db.query.projectColumns.findMany({ | ||
38 | 38 | class="h-[calc(100vh-14rem)] bg-mainBackgroundColor overflow-hidden rounded-xl border border-gray-800" |
39 | 39 | > |
40 | 40 | <div |
41 | - class="p-4 border-b border-gray-800 flex justify-between items-center bg-[#161b22]" | |
41 | + class="p-4 border-b border-gray-800 flex justify-between items-center bg-accent" | |
42 | 42 | > |
43 | 43 | <div> |
44 | 44 | <h2 |
45 | - class="text-xl font-bold text-white flex items-center gap-2" | |
45 | + class="text-xl font-bold text-foreground flex items-center gap-2" | |
46 | 46 | > |
47 | 47 | {project.name} |
48 | 48 | <span |
49 | - class="text-xs text-gray-500 font-normal px-2 py-0.5 border border-gray-700 rounded-full" | |
49 | + class="text-xs text-muted-foreground font-normal px-2 py-0.5 border border-gray-700 rounded-full" | |
50 | 50 | > |
51 | 51 | #{project.number} |
52 | 52 | </span> |
53 | 53 | </h2> |
54 | - <p class="text-xs text-gray-400 mt-1">{project.description}</p> | |
54 | + <p class="text-xs text-muted-foreground mt-1">{project.description}</p> | |
55 | 55 | </div> |
56 | 56 | |
57 | 57 | <a |
58 | 58 | |
▼src/pages/[owner]/[repo]/projects/index.astro
+15-15
| @@ -26,12 +26,12 @@ const projectList = await db | ||
26 | 26 | |
27 | 27 | <RepoLayout title={`${repository.name} - Projects`} activeTab="projects"> |
28 | 28 | <div class="flex items-center justify-between mb-6"> |
29 | - <h1 class="text-2xl font-bold text-white">Projects</h1> | |
29 | + <h1 class="text-2xl font-bold text-foreground">Projects</h1> | |
30 | 30 | { |
31 | 31 | permission !== "read" && ( |
32 | 32 | <button |
33 | 33 | id="new-project-btn" |
34 | - class="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium transition-colors" | |
34 | + class="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-foreground rounded-lg text-sm font-medium transition-colors" | |
35 | 35 | > |
36 | 36 | <Plus className="h-4 w-4" /> |
37 | 37 | New Project |
| @@ -45,20 +45,20 @@ const projectList = await db | ||
45 | 45 | projectList.map((project: any) => ( |
46 | 46 | <a |
47 | 47 | href={`/${owner}/${repo}/projects/${project.id}`} |
48 | - class="block p-6 bg-[#161b22] border border-gray-800 rounded-xl hover:border-gray-600 transition-colors group" | |
48 | + class="block p-6 bg-accent border border-gray-800 rounded-xl hover:border-gray-600 transition-colors group" | |
49 | 49 | > |
50 | 50 | <div class="flex items-center justify-between mb-4"> |
51 | - <span class="text-gray-500 font-mono text-sm"> | |
51 | + <span class="text-muted-foreground font-mono text-sm"> | |
52 | 52 | #{project.number} |
53 | 53 | </span> |
54 | - <span class="text-gray-500 text-xs"> | |
54 | + <span class="text-muted-foreground text-xs"> | |
55 | 55 | {project.updatedAt.toLocaleDateString()} |
56 | 56 | </span> |
57 | 57 | </div> |
58 | - <h3 class="text-xl font-semibold text-white group-hover:text-blue-400 mb-2"> | |
58 | + <h3 class="text-xl font-semibold text-foreground group-hover:text-blue-400 mb-2"> | |
59 | 59 | {project.name} |
60 | 60 | </h3> |
61 | - <p class="text-gray-400 text-sm line-clamp-3"> | |
61 | + <p class="text-muted-foreground text-sm line-clamp-3"> | |
62 | 62 | {project.description || "No description provided."} |
63 | 63 | </p> |
64 | 64 | </a> |
| @@ -68,8 +68,8 @@ const projectList = await db | ||
68 | 68 | { |
69 | 69 | projectList.length === 0 && ( |
70 | 70 | <div class="col-span-full text-center py-12 border border-dashed border-gray-800 rounded-xl"> |
71 | - <p class="text-gray-500 text-lg">No projects found</p> | |
72 | - <p class="text-gray-600 text-sm mt-2"> | |
71 | + <p class="text-muted-foreground text-lg">No projects found</p> | |
72 | + <p class="text-muted-foreground text-sm mt-2"> | |
73 | 73 | Create a project to start tracking tasks |
74 | 74 | </p> |
75 | 75 | </div> |
| @@ -80,31 +80,31 @@ const projectList = await db | ||
80 | 80 | <!-- New Project Modal --> |
81 | 81 | <dialog |
82 | 82 | id="new-project-modal" |
83 | - class="bg-[#161b22] text-white p-6 rounded-xl border border-gray-700 backdrop:bg-black/50 w-full max-w-md" | |
83 | + class="bg-accent text-foreground p-6 rounded-xl border border-gray-700 backdrop:bg-black/50 w-full max-w-md" | |
84 | 84 | > |
85 | 85 | <form method="dialog" class="space-y-4"> |
86 | 86 | <h3 class="text-xl font-bold">Create New Project</h3> |
87 | 87 | |
88 | 88 | <div> |
89 | - <label class="block text-sm font-medium mb-1 text-gray-300" | |
89 | + <label class="block text-sm font-medium mb-1 text-muted-foreground" | |
90 | 90 | >Project Name</label |
91 | 91 | > |
92 | 92 | <input |
93 | 93 | type="text" |
94 | 94 | name="name" |
95 | 95 | required |
96 | - class="w-full bg-[#0d1117] border border-gray-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-blue-500" | |
96 | + class="w-full bg-card border border-gray-700 rounded-lg px-3 py-2 text-foreground focus:outline-none focus:border-blue-500" | |
97 | 97 | /> |
98 | 98 | </div> |
99 | 99 | |
100 | 100 | <div> |
101 | - <label class="block text-sm font-medium mb-1 text-gray-300" | |
101 | + <label class="block text-sm font-medium mb-1 text-muted-foreground" | |
102 | 102 | >Description</label |
103 | 103 | > |
104 | 104 | <textarea |
105 | 105 | name="description" |
106 | 106 | rows="3" |
107 | - class="w-full bg-[#0d1117] border border-gray-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-blue-500" | |
107 | + class="w-full bg-card border border-gray-700 rounded-lg px-3 py-2 text-foreground focus:outline-none focus:border-blue-500" | |
108 | 108 | ></textarea> |
109 | 109 | </div> |
110 | 110 | |
| @@ -112,7 +112,7 @@ const projectList = await db | ||
112 | 112 | <button |
113 | 113 | type="button" |
114 | 114 | id="cancel-modal" |
115 | - class="px-4 py-2 text-gray-400 hover:text-white" | |
115 | + class="px-4 py-2 text-muted-foreground hover:text-foreground" | |
116 | 116 | >Cancel</button |
117 | 117 | > |
118 | 118 | <button |
119 | 119 | |
▼src/pages/[owner]/[repo]/pulls/[number].astro
+4-4
| @@ -347,11 +347,11 @@ try { | ||
347 | 347 | <div class="flex items-start gap-4"> |
348 | 348 | <div class="mt-1"> |
349 | 349 | {queueItem ? ( |
350 | - <div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-white"> | |
350 | + <div class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-foreground"> | |
351 | 351 | <ListOrdered className="h-4 w-4 animate-pulse" /> |
352 | 352 | </div> |
353 | 353 | ) : ( |
354 | - <div class="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center text-white"> | |
354 | + <div class="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center text-foreground"> | |
355 | 355 | <GitMerge className="h-4 w-4" /> |
356 | 356 | </div> |
357 | 357 | )} |
| @@ -372,7 +372,7 @@ try { | ||
372 | 372 | <div class="flex items-center gap-2"> |
373 | 373 | <Button |
374 | 374 | id="merge-btn" |
375 | - className="bg-green-600 hover:bg-green-700 text-white" | |
375 | + className="bg-green-600 hover:bg-green-700 text-foreground" | |
376 | 376 | > |
377 | 377 | Merge pull request |
378 | 378 | </Button> |
| @@ -478,7 +478,7 @@ try { | ||
478 | 478 | <Sparkles className="h-4 w-4 text-violet-400" /> |
479 | 479 | CI/CD Pipeline Execution |
480 | 480 | </h3> |
481 | - <div class="rounded-xl overflow-hidden border border-white/10"> | |
481 | + <div class="rounded-xl overflow-hidden border border-border"> | |
482 | 482 | <PipelineVisualizer client:idle jobs={visualizerJobs} /> |
483 | 483 | </div> |
484 | 484 | </div> |
485 | 485 | |
▼src/pages/[owner]/[repo]/pulls/index.astro
+1-1
| @@ -39,7 +39,7 @@ if (!hasAccess) { | ||
39 | 39 | |
40 | 40 | // Fetch issue count |
41 | 41 | const issueCountResult = await db |
42 | - .select({ count: sql`count` }) | |
42 | + .select({ count: sql`count(*)` }) | |
43 | 43 | .from(schema.issues) |
44 | 44 | .where( |
45 | 45 | and( |
46 | 46 | |
▼src/pages/[owner]/[repo]/pulls/new.astro
+1-1
| @@ -94,7 +94,7 @@ if (baseBranch !== headBranch) { | ||
94 | 94 | |
95 | 95 | // Fetch open issue count (for header) |
96 | 96 | const issueCountResult = await db |
97 | - .select({ count: sql`count` }) | |
97 | + .select({ count: sql`count(*)` }) | |
98 | 98 | .from(schema.issues) |
99 | 99 | .where( |
100 | 100 | and( |
101 | 101 | |
▼src/pages/[owner]/[repo]/releases/index.astro
+1-1
| @@ -101,7 +101,7 @@ const repo = { | ||
101 | 101 | {canWrite && ( |
102 | 102 | <a |
103 | 103 | href={`/${repo.owner}/${repo.name}/releases/new`} |
104 | - class="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 transition-colors" | |
104 | + class="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-green-700 transition-colors" | |
105 | 105 | > |
106 | 106 | <Plus className="h-4 w-4" /> |
107 | 107 | Draft a new release |
108 | 108 | |
▼src/pages/[owner]/[repo]/releases/new.astro
+1-1
| @@ -139,7 +139,7 @@ const repo = { | ||
139 | 139 | <div class="flex items-center gap-3 pt-2"> |
140 | 140 | <button |
141 | 141 | type="submit" |
142 | - class="inline-flex items-center gap-2 rounded-md bg-green-600 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-green-700 transition-colors" | |
142 | + class="inline-flex items-center gap-2 rounded-md bg-green-600 px-6 py-2.5 text-sm font-medium text-foreground shadow-sm hover:bg-green-700 transition-colors" | |
143 | 143 | > |
144 | 144 | Publish release |
145 | 145 | </button> |
146 | 146 | |
▼src/pages/[owner]/[repo]/settings/actions/index.astro
+71-71
| @@ -65,7 +65,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
65 | 65 | <div class="space-y-2"> |
66 | 66 | <a |
67 | 67 | href={`/${repo.owner}/${repo.name}/settings`} |
68 | - class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
68 | + class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
69 | 69 | > |
70 | 70 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
71 | 71 | <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> |
| @@ -75,7 +75,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
75 | 75 | </a> |
76 | 76 | <a |
77 | 77 | href={`/${repo.owner}/${repo.name}/settings/collaborators`} |
78 | - class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
78 | + class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
79 | 79 | > |
80 | 80 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
81 | 81 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path> |
| @@ -84,14 +84,14 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
84 | 84 | </a> |
85 | 85 | <a |
86 | 86 | href={`/${repo.owner}/${repo.name}/settings/webhooks`} |
87 | - class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
87 | + class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
88 | 88 | > |
89 | 89 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
90 | 90 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path> |
91 | 91 | </svg> |
92 | 92 | Webhooks |
93 | 93 | </a> |
94 | - <div class="border-t border-white/10 my-4"></div> | |
94 | + <div class="border-t border-border my-4"></div> | |
95 | 95 | <a |
96 | 96 | href={`/${repo.owner}/${repo.name}/settings/actions`} |
97 | 97 | class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium bg-orange-500/10 text-orange-400 border border-orange-500/20" |
| @@ -104,7 +104,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
104 | 104 | </a> |
105 | 105 | <a |
106 | 106 | href={`/${repo.owner}/${repo.name}/settings/actions/runners`} |
107 | - class="flex items-center gap-2 px-3 py-2 ml-4 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/5 transition-colors" | |
107 | + class="flex items-center gap-2 px-3 py-2 ml-4 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors" | |
108 | 108 | > |
109 | 109 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
110 | 110 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"></path> |
| @@ -117,7 +117,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
117 | 117 | <div class="space-y-6"> |
118 | 118 | <!-- Page Header --> |
119 | 119 | <div> |
120 | - <h1 class="text-2xl font-bold text-white flex items-center gap-3"> | |
120 | + <h1 class="text-2xl font-bold text-foreground flex items-center gap-3"> | |
121 | 121 | <div class="p-2 rounded-lg bg-gradient-to-br from-orange-500/20 to-amber-500/20 border border-orange-500/30"> |
122 | 122 | <svg class="h-6 w-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
123 | 123 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path> |
| @@ -126,7 +126,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
126 | 126 | </div> |
127 | 127 | Actions Settings |
128 | 128 | </h1> |
129 | - <p class="text-gray-500 text-sm mt-2"> | |
129 | + <p class="text-muted-foreground text-sm mt-2"> | |
130 | 130 | Manage workflow permissions, artifact settings, and action runners |
131 | 131 | </p> |
132 | 132 | </div> |
| @@ -134,8 +134,8 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
134 | 134 | <!-- Actions Permissions --> |
135 | 135 | <div class="relative"> |
136 | 136 | <div class="absolute -inset-[1px] bg-gradient-to-r from-orange-500/20 via-transparent to-amber-500/20 rounded-xl opacity-50"></div> |
137 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
138 | - <h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"> | |
137 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
138 | + <h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2"> | |
139 | 139 | <svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
140 | 140 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"></path> |
141 | 141 | </svg> |
| @@ -143,31 +143,31 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
143 | 143 | </h3> |
144 | 144 | |
145 | 145 | <div class="space-y-4"> |
146 | - <label class="flex items-start gap-3 p-4 rounded-lg border border-white/10 bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
146 | + <label class="flex items-start gap-3 p-4 rounded-lg border border-border bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
147 | 147 | <input type="radio" name="actions_permission" value="all" checked class="mt-1 accent-orange-500" /> |
148 | 148 | <div> |
149 | - <div class="font-medium text-white">Allow all actions and reusable workflows</div> | |
150 | - <p class="text-sm text-gray-500 mt-1"> | |
149 | + <div class="font-medium text-foreground">Allow all actions and reusable workflows</div> | |
150 | + <p class="text-sm text-muted-foreground mt-1"> | |
151 | 151 | Any action or reusable workflow can be used, regardless of who authored it. |
152 | 152 | </p> |
153 | 153 | </div> |
154 | 154 | </label> |
155 | 155 | |
156 | - <label class="flex items-start gap-3 p-4 rounded-lg border border-white/10 bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
156 | + <label class="flex items-start gap-3 p-4 rounded-lg border border-border bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
157 | 157 | <input type="radio" name="actions_permission" value="local" class="mt-1 accent-orange-500" /> |
158 | 158 | <div> |
159 | - <div class="font-medium text-white">Allow local actions only</div> | |
160 | - <p class="text-sm text-gray-500 mt-1"> | |
159 | + <div class="font-medium text-foreground">Allow local actions only</div> | |
160 | + <p class="text-sm text-muted-foreground mt-1"> | |
161 | 161 | Only allow actions that are defined in this repository. |
162 | 162 | </p> |
163 | 163 | </div> |
164 | 164 | </label> |
165 | 165 | |
166 | - <label class="flex items-start gap-3 p-4 rounded-lg border border-white/10 bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
166 | + <label class="flex items-start gap-3 p-4 rounded-lg border border-border bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
167 | 167 | <input type="radio" name="actions_permission" value="disabled" class="mt-1 accent-orange-500" /> |
168 | 168 | <div> |
169 | - <div class="font-medium text-white">Disable actions</div> | |
170 | - <p class="text-sm text-gray-500 mt-1"> | |
169 | + <div class="font-medium text-foreground">Disable actions</div> | |
170 | + <p class="text-sm text-muted-foreground mt-1"> | |
171 | 171 | Actions and workflow runs are disabled for this repository. |
172 | 172 | </p> |
173 | 173 | </div> |
| @@ -179,8 +179,8 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
179 | 179 | <!-- Workflow Permissions --> |
180 | 180 | <div class="relative"> |
181 | 181 | <div class="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50"></div> |
182 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
183 | - <h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"> | |
182 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
183 | + <h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2"> | |
184 | 184 | <svg class="h-5 w-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
185 | 185 | <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> |
186 | 186 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path> |
| @@ -189,31 +189,31 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
189 | 189 | </h3> |
190 | 190 | |
191 | 191 | <div class="space-y-4"> |
192 | - <label class="flex items-start gap-3 p-4 rounded-lg border border-white/10 bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
192 | + <label class="flex items-start gap-3 p-4 rounded-lg border border-border bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
193 | 193 | <input type="radio" name="workflow_permission" value="read_write" checked class="mt-1 accent-purple-500" /> |
194 | 194 | <div> |
195 | - <div class="font-medium text-white">Read and write permissions</div> | |
196 | - <p class="text-sm text-gray-500 mt-1"> | |
195 | + <div class="font-medium text-foreground">Read and write permissions</div> | |
196 | + <p class="text-sm text-muted-foreground mt-1"> | |
197 | 197 | Workflows have read and write permissions in the repository for all scopes. |
198 | 198 | </p> |
199 | 199 | </div> |
200 | 200 | </label> |
201 | 201 | |
202 | - <label class="flex items-start gap-3 p-4 rounded-lg border border-white/10 bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
202 | + <label class="flex items-start gap-3 p-4 rounded-lg border border-border bg-white/[0.02] hover:bg-white/[0.04] cursor-pointer transition-colors"> | |
203 | 203 | <input type="radio" name="workflow_permission" value="read_only" class="mt-1 accent-purple-500" /> |
204 | 204 | <div> |
205 | - <div class="font-medium text-white">Read repository contents permission</div> | |
206 | - <p class="text-sm text-gray-500 mt-1"> | |
205 | + <div class="font-medium text-foreground">Read repository contents permission</div> | |
206 | + <p class="text-sm text-muted-foreground mt-1"> | |
207 | 207 | Workflows have read permissions in the repository for the contents scope only. |
208 | 208 | </p> |
209 | 209 | </div> |
210 | 210 | </label> |
211 | 211 | </div> |
212 | 212 | |
213 | - <div class="mt-4 pt-4 border-t border-white/10"> | |
213 | + <div class="mt-4 pt-4 border-t border-border"> | |
214 | 214 | <label class="flex items-center gap-3 cursor-pointer"> |
215 | 215 | <input type="checkbox" class="accent-purple-500" checked /> |
216 | - <span class="text-sm text-gray-300"> | |
216 | + <span class="text-sm text-muted-foreground"> | |
217 | 217 | Allow GitHub Actions to create and approve pull requests |
218 | 218 | </span> |
219 | 219 | </label> |
| @@ -224,21 +224,21 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
224 | 224 | <!-- Workflow Templates --> |
225 | 225 | <div class="relative"> |
226 | 226 | <div class="absolute -inset-[1px] bg-gradient-to-r from-emerald-500/20 via-transparent to-teal-500/20 rounded-xl opacity-50"></div> |
227 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6 space-y-4"> | |
228 | - <h3 class="text-lg font-semibold text-white flex items-center gap-2"> | |
227 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6 space-y-4"> | |
228 | + <h3 class="text-lg font-semibold text-foreground flex items-center gap-2"> | |
229 | 229 | <svg class="h-5 w-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
230 | 230 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16h8M8 12h8m-8-4h8M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H9l-4 4v10a2 2 0 002 2z"></path> |
231 | 231 | </svg> |
232 | 232 | Workflow templates |
233 | 233 | </h3> |
234 | - <p class="text-sm text-gray-500"> | |
234 | + <p class="text-sm text-muted-foreground"> | |
235 | 235 | Browse official workflow templates and apply them directly to this repository. |
236 | 236 | </p> |
237 | 237 | |
238 | 238 | <div class="flex flex-wrap items-end gap-3"> |
239 | 239 | <div> |
240 | - <label for="template-category-filter" class="block text-xs text-gray-400 mb-1">Category</label> | |
241 | - <select id="template-category-filter" class="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white"> | |
240 | + <label for="template-category-filter" class="block text-xs text-muted-foreground mb-1">Category</label> | |
241 | + <select id="template-category-filter" class="rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground"> | |
242 | 242 | <option value="">All categories</option> |
243 | 243 | <option value="ci">CI</option> |
244 | 244 | <option value="cd">CD</option> |
| @@ -247,8 +247,8 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
247 | 247 | </select> |
248 | 248 | </div> |
249 | 249 | <div> |
250 | - <label for="template-language-filter" class="block text-xs text-gray-400 mb-1">Language</label> | |
251 | - <select id="template-language-filter" class="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white"> | |
250 | + <label for="template-language-filter" class="block text-xs text-muted-foreground mb-1">Language</label> | |
251 | + <select id="template-language-filter" class="rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground"> | |
252 | 252 | <option value="">Any language</option> |
253 | 253 | <option value="node">Node.js</option> |
254 | 254 | <option value="python">Python</option> |
| @@ -257,11 +257,11 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
257 | 257 | </select> |
258 | 258 | </div> |
259 | 259 | <div> |
260 | - <label for="template-workflow-name" class="block text-xs text-gray-400 mb-1">Workflow file name</label> | |
260 | + <label for="template-workflow-name" class="block text-xs text-muted-foreground mb-1">Workflow file name</label> | |
261 | 261 | <input |
262 | 262 | id="template-workflow-name" |
263 | 263 | placeholder="ci" |
264 | - class="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-gray-600" | |
264 | + class="rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground" | |
265 | 265 | /> |
266 | 266 | </div> |
267 | 267 | <button |
| @@ -272,25 +272,25 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
272 | 272 | </button> |
273 | 273 | </div> |
274 | 274 | |
275 | - <div id="template-action-msg" class="text-xs text-gray-400"></div> | |
275 | + <div id="template-action-msg" class="text-xs text-muted-foreground"></div> | |
276 | 276 | |
277 | 277 | <div id="template-list" class="space-y-2"> |
278 | 278 | {initialTemplates.length === 0 ? ( |
279 | - <div class="text-sm text-gray-500">No templates found.</div> | |
279 | + <div class="text-sm text-muted-foreground">No templates found.</div> | |
280 | 280 | ) : ( |
281 | 281 | initialTemplates.slice(0, 6).map((template) => ( |
282 | 282 | <div |
283 | - class="rounded-lg border border-white/10 bg-white/[0.02] p-3 flex items-start justify-between gap-3" | |
283 | + class="rounded-lg border border-border bg-white/[0.02] p-3 flex items-start justify-between gap-3" | |
284 | 284 | data-template-id={template.id} |
285 | 285 | > |
286 | 286 | <div> |
287 | - <div class="text-sm font-medium text-white">{template.name}</div> | |
288 | - <div class="text-xs text-gray-500 mt-1"> | |
287 | + <div class="text-sm font-medium text-foreground">{template.name}</div> | |
288 | + <div class="text-xs text-muted-foreground mt-1"> | |
289 | 289 | {(template.category || "uncategorized").toUpperCase()} |
290 | 290 | {template.language ? ` • ${template.language}` : ""} |
291 | 291 | </div> |
292 | 292 | {template.description && ( |
293 | - <p class="text-xs text-gray-400 mt-1">{template.description}</p> | |
293 | + <p class="text-xs text-muted-foreground mt-1">{template.description}</p> | |
294 | 294 | )} |
295 | 295 | </div> |
296 | 296 | <button |
| @@ -304,16 +304,16 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
304 | 304 | )} |
305 | 305 | </div> |
306 | 306 | |
307 | - <div class="pt-2 border-t border-white/10"> | |
308 | - <h4 class="text-sm font-medium text-white mb-2">Applied workflows</h4> | |
307 | + <div class="pt-2 border-t border-border"> | |
308 | + <h4 class="text-sm font-medium text-foreground mb-2">Applied workflows</h4> | |
309 | 309 | <div id="applied-workflows-list" class="space-y-1"> |
310 | 310 | {existingWorkflows.length === 0 ? ( |
311 | - <div class="text-xs text-gray-500">No workflows applied from templates yet.</div> | |
311 | + <div class="text-xs text-muted-foreground">No workflows applied from templates yet.</div> | |
312 | 312 | ) : ( |
313 | 313 | existingWorkflows.map((workflow) => ( |
314 | - <div class="flex items-center justify-between text-xs rounded border border-white/10 bg-white/[0.02] px-2 py-1.5"> | |
315 | - <span class="text-gray-300">{workflow.name}</span> | |
316 | - <span class="text-gray-500 font-mono">{workflow.path}</span> | |
314 | + <div class="flex items-center justify-between text-xs rounded border border-border bg-white/[0.02] px-2 py-1.5"> | |
315 | + <span class="text-muted-foreground">{workflow.name}</span> | |
316 | + <span class="text-muted-foreground font-mono">{workflow.path}</span> | |
317 | 317 | </div> |
318 | 318 | )) |
319 | 319 | )} |
| @@ -325,8 +325,8 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
325 | 325 | <!-- Artifact and Log Retention --> |
326 | 326 | <div class="relative"> |
327 | 327 | <div class="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-blue-500/20 rounded-xl opacity-50"></div> |
328 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
329 | - <h3 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"> | |
328 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
329 | + <h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2"> | |
330 | 330 | <svg class="h-5 w-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
331 | 331 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path> |
332 | 332 | </svg> |
| @@ -335,7 +335,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
335 | 335 | |
336 | 336 | <div class="space-y-4"> |
337 | 337 | <div> |
338 | - <label class="block text-sm font-medium text-white mb-2"> | |
338 | + <label class="block text-sm font-medium text-foreground mb-2"> | |
339 | 339 | Artifact retention period (days) |
340 | 340 | </label> |
341 | 341 | <input |
| @@ -343,15 +343,15 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
343 | 343 | value="90" |
344 | 344 | min="1" |
345 | 345 | max="400" |
346 | - class="w-full max-w-xs rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all" | |
346 | + class="w-full max-w-xs rounded-lg border border-border bg-secondary px-4 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all" | |
347 | 347 | /> |
348 | - <p class="text-xs text-gray-500 mt-1"> | |
348 | + <p class="text-xs text-muted-foreground mt-1"> | |
349 | 349 | Artifacts will be deleted after this period (1-400 days). |
350 | 350 | </p> |
351 | 351 | </div> |
352 | 352 | |
353 | 353 | <div> |
354 | - <label class="block text-sm font-medium text-white mb-2"> | |
354 | + <label class="block text-sm font-medium text-foreground mb-2"> | |
355 | 355 | Log retention period (days) |
356 | 356 | </label> |
357 | 357 | <input |
| @@ -359,9 +359,9 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
359 | 359 | value="90" |
360 | 360 | min="1" |
361 | 361 | max="400" |
362 | - class="w-full max-w-xs rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all" | |
362 | + class="w-full max-w-xs rounded-lg border border-border bg-secondary px-4 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all" | |
363 | 363 | /> |
364 | - <p class="text-xs text-gray-500 mt-1"> | |
364 | + <p class="text-xs text-muted-foreground mt-1"> | |
365 | 365 | Logs will be deleted after this period (1-400 days). |
366 | 366 | </p> |
367 | 367 | </div> |
| @@ -372,22 +372,22 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
372 | 372 | <!-- Runners Link --> |
373 | 373 | <div class="relative"> |
374 | 374 | <div class="absolute -inset-[1px] bg-gradient-to-r from-green-500/20 via-transparent to-emerald-500/20 rounded-xl opacity-50"></div> |
375 | - <div class="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6"> | |
375 | + <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-6"> | |
376 | 376 | <div class="flex items-center justify-between"> |
377 | 377 | <div> |
378 | - <h3 class="text-lg font-semibold text-white flex items-center gap-2"> | |
378 | + <h3 class="text-lg font-semibold text-foreground flex items-center gap-2"> | |
379 | 379 | <svg class="h-5 w-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
380 | 380 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"></path> |
381 | 381 | </svg> |
382 | 382 | Self-hosted runners |
383 | 383 | </h3> |
384 | - <p class="text-sm text-gray-500 mt-1"> | |
384 | + <p class="text-sm text-muted-foreground mt-1"> | |
385 | 385 | Manage runners that can be used to run your workflows. |
386 | 386 | </p> |
387 | 387 | </div> |
388 | 388 | <a |
389 | 389 | href={`/${repo.owner}/${repo.name}/settings/actions/runners`} |
390 | - class="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" | |
390 | + class="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" | |
391 | 391 | > |
392 | 392 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
393 | 393 | <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> |
| @@ -403,7 +403,7 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
403 | 403 | <div class="flex justify-end"> |
404 | 404 | <button |
405 | 405 | type="button" |
406 | - class="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-orange-500 to-amber-600 text-sm font-medium text-white shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all" | |
406 | + class="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-orange-500 to-amber-600 text-sm font-medium text-foreground shadow-lg shadow-orange-500/25 hover:shadow-orange-500/40 transition-all" | |
407 | 407 | > |
408 | 408 | <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
409 | 409 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> |
| @@ -438,16 +438,16 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
438 | 438 | function renderTemplates(templates) { |
439 | 439 | if (!templateList) return; |
440 | 440 | if (!templates.length) { |
441 | - templateList.innerHTML = '<div class="text-sm text-gray-500">No templates found.</div>'; | |
441 | + templateList.innerHTML = '<div class="text-sm text-muted-foreground">No templates found.</div>'; | |
442 | 442 | return; |
443 | 443 | } |
444 | 444 | |
445 | 445 | templateList.innerHTML = templates.map((template) => ` |
446 | - <div class="rounded-lg border border-white/10 bg-white/[0.02] p-3 flex items-start justify-between gap-3" data-template-id="${template.id}"> | |
446 | + <div class="rounded-lg border border-border bg-white/[0.02] p-3 flex items-start justify-between gap-3" data-template-id="${template.id}"> | |
447 | 447 | <div> |
448 | - <div class="text-sm font-medium text-white">${template.name}</div> | |
449 | - <div class="text-xs text-gray-500 mt-1">${templateSummary(template)}</div> | |
450 | - ${template.description ? `<p class="text-xs text-gray-400 mt-1">${template.description}</p>` : ""} | |
448 | + <div class="text-sm font-medium text-foreground">${template.name}</div> | |
449 | + <div class="text-xs text-muted-foreground mt-1">${templateSummary(template)}</div> | |
450 | + ${template.description ? `<p class="text-xs text-muted-foreground mt-1">${template.description}</p>` : ""} | |
451 | 451 | </div> |
452 | 452 | <button |
453 | 453 | class="apply-template-btn px-3 py-1.5 rounded-lg border border-emerald-500/30 text-emerald-300 text-xs hover:bg-emerald-500/10" |
| @@ -461,15 +461,15 @@ const [initialTemplates, existingWorkflows] = await Promise.all([ | ||
461 | 461 | |
462 | 462 | function renderAppliedWorkflow(workflow) { |
463 | 463 | if (!appliedWorkflowsList) return; |
464 | - const empty = appliedWorkflowsList.querySelector(".text-gray-500"); | |
464 | + const empty = appliedWorkflowsList.querySelector(".text-muted-foreground"); | |
465 | 465 | if (empty) { |
466 | 466 | appliedWorkflowsList.innerHTML = ""; |
467 | 467 | } |
468 | 468 | const row = document.createElement("div"); |
469 | - row.className = "flex items-center justify-between text-xs rounded border border-white/10 bg-white/[0.02] px-2 py-1.5"; | |
469 | + row.className = "flex items-center justify-between text-xs rounded border border-border bg-white/[0.02] px-2 py-1.5"; | |
470 | 470 | row.innerHTML = ` |
471 | - <span class="text-gray-300">${workflow.name}</span> | |
472 | - <span class="text-gray-500 font-mono">${workflow.path}</span> | |
471 | + <span class="text-muted-foreground">${workflow.name}</span> | |
472 | + <span class="text-muted-foreground font-mono">${workflow.path}</span> | |
473 | 473 | `; |
474 | 474 | appliedWorkflowsList.prepend(row); |
475 | 475 | } |
476 | 476 | |