diff --git a/.env.example b/.env.example
index f558db8..d567701 100644
--- a/.env.example
+++ b/.env.example
@@ -6,3 +6,7 @@ VAULT_NAMESPACE=admin # (옵션) 기업용/클라우드 사용 시 필요
# API Path Example (KV Engine v2)
# 예: secret/data/anvil-hosting -> VAULT_SECRET_PATH=secret/data/anvil-hosting
VAULT_SECRET_PATH=secret/data/production
+
+# Cloudflare Pages Functions (API Proxy)
+# API Key for Worker authentication
+WORKER_API_KEY=your-api-key-here
diff --git a/CLAUDE.md b/CLAUDE.md
index a0f8cd6..e23821f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,12 +19,23 @@ No build step required - deploy directly.
## Architecture
-Multi-file static website:
+Multi-file static website with serverless API proxy:
- `index.html` - 메인 랜딩 페이지 (~1000줄)
- `style.css` - Tailwind CSS 빌드 결과
- `fonts.css` - 시스템 폰트 정의
- `app.js` - Alpine.js 앱 로직 (Server Launcher, Pricing Table 등)
- `terms.html`, `privacy.html`, `sla.html` - 법적 페이지
+- `functions/` - Cloudflare Pages Functions (API proxy)
+
+### API Endpoints (Pages Functions)
+
+| Method | Path | Description |
+|--------|------|-------------|
+| GET | `/api/health` | Health check |
+| GET | `/api/instances` | Query VM instances |
+| POST | `/api/recommend` | Tech stack recommendations |
+
+**Proxy Target**: https://cloud-instances-api.kappa-d8e.workers.dev
### Tech Stack
- Tailwind CSS v4 (로컬 빌드, `style.css`)
@@ -48,5 +59,16 @@ Navigation → Hero (Telegram Bot Demo) → Features (`#features`) → Automatio
## External Integrations
- **Telegram Bot**: @AnvilForgeBot (서버 생성, 도메인 등록)
+- **Worker API**: cloud-instances-api (VM pricing aggregator)
- **Credentials**: Stored in Vault at https://vault.anvil.it.com
- **Registrant Info Source**: npm-linode-1 server (`/home/admin/namecheap_api/.env`)
+
+## Environment Configuration
+
+### Secrets (set via Cloudflare Dashboard or CLI)
+```bash
+wrangler pages secret put WORKER_API_KEY
+```
+
+### Variables (wrangler.toml)
+- `WORKER_API_URL` - Worker API base URL
diff --git a/README.md b/README.md
index 474b387..736822f 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,50 @@
-# Anvil Hosting - Cloudflare Pages
+# Anvil Hosting
-공식 웹사이트: https://hosting.anvil.it.com
+Static HTML 마케팅 웹사이트 - Cloudflare Pages 배포
+
+**URL**: https://hosting.anvil.it.com
## 배포
```bash
-cd /Users/kaffa/anvil-hosting
wrangler pages deploy . --project-name anvil-hosting
```
-## 구성
+빌드 단계 없이 바로 배포.
-- **로고**: 3D 메탈릭 모루 + 대장간 망치 + 불꽃 (SVG)
-- **서비스 순서**: 도메인 → DDoS → 해외서버 → 웹호스팅
-- **푸터 사업자 정보**: namecheap-api 등록자 정보와 동일
+## 아키텍처
+
+### 파일 구조
+- `index.html` - 메인 랜딩 페이지
+- `style.css` - Tailwind CSS v4 빌드 결과
+- `fonts.css` - 시스템 폰트 정의
+- `app.js` - Alpine.js 앱 로직 (Server Launcher, Pricing Table 등)
+- `terms.html`, `privacy.html`, `sla.html` - 법적 페이지
+
+### Tech Stack
+- Tailwind CSS v4 (로컬 빌드)
+- Alpine.js 3.14.3 (CDN with SRI)
+- 시스템 폰트 (-apple-system, Apple SD Gothic Neo, Malgun Gothic)
+
+### 디자인 시스템
+- 다크 테마 (배경: #0a0f1a)
+- 글래스모피즘 효과 (glass-card, glass-panel)
+- 메시 그라디언트 배경
+- Color palette: brand-* (sky blue 계열), purple, green, red
+
+## 페이지 섹션
+
+Navigation → Hero (Telegram Bot Demo) → Features → Automation → Infrastructure → Domain → Pricing → Footer
+
+## 주요 컴포넌트
+
+- **Server Launcher Modal**: Alpine.js 기반 서버 생성 마법사
+- **Pricing Table**: 리전별 (Global/Seoul) 동적 요금표
+- **Telegram Bot Demo**: Hero 섹션 대화형 데모
+
+## 외부 연동
+
+- **Telegram Bot**: @AnvilForgeBot (서버 생성, 도메인 등록)
## 등록자 정보 소스
diff --git a/app/index.html b/app/index.html
index aa91a12..0e982ef 100644
--- a/app/index.html
+++ b/app/index.html
@@ -23,27 +23,50 @@
-
-
-
-
+
+
+
+
+
+
+
-
🔒
-
텔레그램 전용 페이지
-
이 페이지는 텔레그램 미니앱에서만 접근할 수 있습니다.
-
-
- 텔레그램에서 열기
-
+
+
A
+
+
Anvil Hosting
+
텔레그램 계정으로 로그인하여
서버를 관리하세요
+
+
+
+
+
+ 로그인하면 이용약관 및
+ 개인정보처리방침에 동의하는 것으로 간주됩니다.
+
+
+
+
-
-
+
+
@@ -53,11 +76,27 @@
A
Anvil
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
@@ -68,10 +107,10 @@
-
- 안녕하세요, 님
+
+ 안녕하세요, 님
-
+
대시보드
@@ -102,6 +141,11 @@
class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center"
x-text="unreadCount">
+
@@ -254,6 +298,124 @@
+
+
+
+
+
+
클라우드 인스턴스
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ❌
+
+
+
+
+
+
+
+
+ 개 인스턴스
+
+
+
+
+
+
+ vCPU:
+ RAM:
+ Storage:
+
+
+
+
+
+
+
+
🔍
+
검색 버튼을 눌러 인스턴스를 조회하세요
+
+
+
+
diff --git a/functions/README.md b/functions/README.md
new file mode 100644
index 0000000..932aa4f
--- /dev/null
+++ b/functions/README.md
@@ -0,0 +1,88 @@
+# Cloudflare Pages Functions
+
+API proxy endpoints for cloud-instances-api Worker.
+
+## Endpoints
+
+| Method | Path | Description |
+|--------|------|-------------|
+| GET | `/api/health` | Health check |
+| GET | `/api/instances` | Query VM instances with filters |
+| POST | `/api/recommend` | Tech stack recommendations |
+
+## Configuration
+
+### Environment Variables
+
+Set in Cloudflare Pages dashboard or via CLI:
+
+```bash
+wrangler pages secret put WORKER_API_KEY
+```
+
+**Required Secrets:**
+- `WORKER_API_KEY` - API key for Worker authentication
+
+**Configured Variables (wrangler.toml):**
+- `WORKER_API_URL` - Worker API base URL
+
+## Local Development
+
+1. Install dependencies:
+```bash
+npm install
+```
+
+2. Create `.env` file:
+```bash
+WORKER_API_KEY=your-api-key-here
+```
+
+3. Run local dev server:
+```bash
+npx wrangler pages dev . --port 8788
+```
+
+## Deployment
+
+```bash
+wrangler pages deploy . --project-name anvil-hosting
+```
+
+## CORS Configuration
+
+All endpoints allow requests from:
+- Origin: `https://hosting.anvil.it.com`
+- Methods: `GET, POST, OPTIONS`
+- Headers: `Content-Type`
+
+## Architecture
+
+```
+Client Request
+ ↓
+/api/{endpoint} (Pages Function)
+ ↓
+functions/_shared/proxy.ts (CORS + Auth)
+ ↓
+cloud-instances-api.kappa-d8e.workers.dev (Worker)
+ ↓
+Response with CORS headers
+```
+
+## Error Handling
+
+All errors return JSON with CORS headers:
+
+```json
+{
+ "success": false,
+ "error": "Error message",
+ "details": "Optional error details"
+}
+```
+
+Status codes:
+- `400` - Invalid request
+- `500` - Internal error
+- `503` - Worker API unavailable
diff --git a/functions/_shared/proxy.ts b/functions/_shared/proxy.ts
new file mode 100644
index 0000000..840920d
--- /dev/null
+++ b/functions/_shared/proxy.ts
@@ -0,0 +1,102 @@
+/**
+ * Shared proxy utilities for Cloudflare Pages Functions
+ * Handles CORS, error responses, and Worker API forwarding
+ */
+
+export interface Env {
+ WORKER_API_KEY: string;
+ WORKER_API_URL: string;
+ DB: D1Database;
+}
+
+export interface ErrorResponse {
+ success: false;
+ error: string;
+ details?: any;
+}
+
+const CORS_HEADERS = {
+ 'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type',
+} as const;
+
+/**
+ * Create CORS preflight response
+ */
+export function createCorsPreflightResponse(): Response {
+ return new Response(null, {
+ status: 204,
+ headers: CORS_HEADERS,
+ });
+}
+
+/**
+ * Create error response with CORS headers
+ */
+export function createErrorResponse(
+ error: string,
+ status: number = 500,
+ details?: any
+): Response {
+ const body: ErrorResponse = {
+ success: false,
+ error,
+ ...(details && { details }),
+ };
+
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: {
+ ...CORS_HEADERS,
+ 'Content-Type': 'application/json',
+ },
+ });
+}
+
+/**
+ * Proxy request to Worker API with authentication
+ */
+export async function proxyToWorker(
+ env: Env,
+ path: string,
+ options: RequestInit = {}
+): Promise {
+ const workerUrl = `${env.WORKER_API_URL}${path}`;
+
+ try {
+ const response = await fetch(workerUrl, {
+ ...options,
+ headers: {
+ ...options.headers,
+ 'X-API-Key': env.WORKER_API_KEY,
+ },
+ });
+
+ // Clone response to add CORS headers
+ const body = await response.text();
+ return new Response(body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: {
+ ...CORS_HEADERS,
+ 'Content-Type': response.headers.get('Content-Type') || 'application/json',
+ },
+ });
+ } catch (error) {
+ console.error(`[Proxy] Failed to fetch ${workerUrl}:`, error);
+ return createErrorResponse(
+ 'Failed to connect to API server',
+ 503,
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
+
+/**
+ * Build query string from URL search params
+ */
+export function buildQueryString(searchParams: URLSearchParams): string {
+ const params = searchParams.toString();
+ return params ? `?${params}` : '';
+}
diff --git a/functions/api/health.ts b/functions/api/health.ts
new file mode 100644
index 0000000..d16d8fb
--- /dev/null
+++ b/functions/api/health.ts
@@ -0,0 +1,17 @@
+/**
+ * Health check endpoint
+ * GET /api/health → Worker GET /health
+ */
+
+import { type PagesFunction } from '@cloudflare/workers-types';
+import { Env, createCorsPreflightResponse, proxyToWorker } from '../_shared/proxy';
+
+export const onRequestGet: PagesFunction = async ({ env }) => {
+ return proxyToWorker(env, '/health', {
+ method: 'GET',
+ });
+};
+
+export const onRequestOptions: PagesFunction = async () => {
+ return createCorsPreflightResponse();
+};
diff --git a/functions/api/instances.ts b/functions/api/instances.ts
new file mode 100644
index 0000000..b11b411
--- /dev/null
+++ b/functions/api/instances.ts
@@ -0,0 +1,25 @@
+/**
+ * Instances query endpoint
+ * GET /api/instances → Worker GET /instances
+ */
+
+import { type PagesFunction } from '@cloudflare/workers-types';
+import {
+ Env,
+ createCorsPreflightResponse,
+ proxyToWorker,
+ buildQueryString,
+} from '../_shared/proxy';
+
+export const onRequestGet: PagesFunction = async ({ request, env }) => {
+ const url = new URL(request.url);
+ const queryString = buildQueryString(url.searchParams);
+
+ return proxyToWorker(env, `/instances${queryString}`, {
+ method: 'GET',
+ });
+};
+
+export const onRequestOptions: PagesFunction = async () => {
+ return createCorsPreflightResponse();
+};
diff --git a/functions/api/pricing.ts b/functions/api/pricing.ts
new file mode 100644
index 0000000..5ae0ea5
--- /dev/null
+++ b/functions/api/pricing.ts
@@ -0,0 +1,205 @@
+/**
+ * Pricing endpoint - Direct D1 query (no rate limiting)
+ * GET /api/pricing → D1 cloud-instances-db
+ *
+ * Supported regions:
+ * - Tokyo, Osaka, Singapore: Linode
+ * - Seoul: Vultr
+ */
+
+import { type PagesFunction } from '@cloudflare/workers-types';
+import { Env, createCorsPreflightResponse } from '../_shared/proxy';
+
+interface InstanceRow {
+ instance_id: string;
+ instance_name: string;
+ vcpu: number;
+ memory_mb: number;
+ storage_gb: number | null;
+ transfer_tb: number | null;
+ provider_name: string;
+ region_name: string;
+ region_code: string;
+ monthly_price: number;
+ hourly_price: number | null;
+ monthly_price_krw: number | null;
+ hourly_price_krw: number | null;
+ // GPU fields
+ has_gpu?: number;
+ gpu_count?: number;
+ gpu_type?: string;
+}
+
+const CORS_HEADERS = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type',
+};
+
+export const onRequestGet: PagesFunction = async ({ env }) => {
+ try {
+ // 1. 일반 인스턴스 쿼리 - Linode(Tokyo, Osaka, Singapore) + Vultr(Seoul)
+ const regularSql = `
+ SELECT
+ it.instance_id,
+ it.instance_name,
+ it.vcpu,
+ it.memory_mb,
+ it.storage_gb,
+ it.transfer_tb,
+ p.name as provider_name,
+ r.region_name,
+ r.region_code,
+ pr.monthly_price,
+ pr.hourly_price,
+ pr.monthly_price_krw,
+ pr.hourly_price_krw,
+ 0 as has_gpu,
+ 0 as gpu_count,
+ NULL as gpu_type
+ FROM instance_types it
+ JOIN providers p ON it.provider_id = p.id
+ JOIN pricing pr ON it.id = pr.instance_type_id
+ JOIN regions r ON pr.region_id = r.id
+ WHERE pr.available = 1
+ AND it.instance_id NOT LIKE '%-sc1'
+ AND (
+ (p.name = 'linode' AND (
+ r.region_name LIKE '%Tokyo%' OR
+ r.region_name LIKE '%Osaka%' OR
+ r.region_code LIKE '%jp-osa%' OR
+ r.region_name LIKE '%Singapore%'
+ ))
+ OR
+ (p.name = 'vultr' AND (
+ r.region_name LIKE '%Seoul%' OR
+ r.region_code LIKE '%icn%'
+ ))
+ )
+ ORDER BY pr.monthly_price ASC
+ `;
+
+ // 2. GPU 인스턴스 쿼리 - Linode(Tokyo) + Vultr(Seoul)
+ const gpuSql = `
+ SELECT
+ gi.instance_id,
+ gi.instance_name,
+ gi.vcpu,
+ gi.memory_mb,
+ gi.storage_gb,
+ gi.transfer_tb,
+ p.name as provider_name,
+ r.region_name,
+ r.region_code,
+ gp.monthly_price,
+ gp.hourly_price,
+ gp.monthly_price_krw,
+ gp.hourly_price_krw,
+ 1 as has_gpu,
+ gi.gpu_count,
+ gi.gpu_type
+ FROM gpu_instances gi
+ JOIN providers p ON gi.provider_id = p.id
+ JOIN gpu_pricing gp ON gi.id = gp.gpu_instance_id
+ JOIN regions r ON gp.region_id = r.id
+ WHERE gp.available = 1
+ AND (
+ (p.name = 'linode' AND (
+ r.region_name LIKE '%Tokyo%' OR
+ r.region_name LIKE '%Osaka%' OR
+ r.region_code LIKE '%jp-osa%'
+ ))
+ OR
+ (p.name = 'vultr' AND (
+ r.region_name LIKE '%Seoul%' OR
+ r.region_code LIKE '%icn%'
+ ))
+ )
+ ORDER BY gp.monthly_price ASC
+ `;
+
+ const [regularResult, gpuResult] = await Promise.all([
+ env.DB.prepare(regularSql).all(),
+ env.DB.prepare(gpuSql).all(),
+ ]);
+
+ // Transform to frontend format
+ const transformRow = (row: InstanceRow) => ({
+ id: row.instance_id,
+ instance_name: row.instance_name,
+ vcpu: row.vcpu,
+ memory_mb: row.memory_mb,
+ storage_gb: row.storage_gb,
+ transfer_tb: row.transfer_tb,
+ provider: { name: row.provider_name },
+ region: {
+ region_name: row.region_name,
+ region_code: row.region_code
+ },
+ pricing: {
+ monthly_price: row.monthly_price,
+ hourly_price: row.hourly_price,
+ monthly_price_krw: row.monthly_price_krw,
+ hourly_price_krw: row.hourly_price_krw,
+ },
+ has_gpu: row.has_gpu === 1,
+ gpu_count: row.gpu_count || 0,
+ gpu_type: row.gpu_type || null,
+ });
+
+ const regularInstances = regularResult.results.map(transformRow);
+ const gpuInstances = gpuResult.results.map(transformRow);
+ const instances = [...regularInstances, ...gpuInstances];
+
+ // Count by region
+ const regionCounts: Record = {};
+ for (const inst of instances) {
+ const name = inst.region.region_name.toLowerCase();
+ if (name.includes('tokyo')) regionCounts['tokyo'] = (regionCounts['tokyo'] || 0) + 1;
+ else if (name.includes('osaka')) regionCounts['osaka'] = (regionCounts['osaka'] || 0) + 1;
+ else if (name.includes('singapore')) regionCounts['singapore'] = (regionCounts['singapore'] || 0) + 1;
+ else if (name.includes('seoul')) regionCounts['seoul'] = (regionCounts['seoul'] || 0) + 1;
+ }
+
+ // GPU counts
+ const gpuCounts = {
+ 'gpu-japan': gpuInstances.filter(i =>
+ i.region.region_name.toLowerCase().includes('tokyo') ||
+ i.region.region_name.toLowerCase().includes('osaka')
+ ).length,
+ 'gpu-korea': gpuInstances.filter(i =>
+ i.region.region_name.toLowerCase().includes('seoul')
+ ).length,
+ };
+
+ return new Response(JSON.stringify({
+ success: true,
+ total: instances.length,
+ region_counts: regionCounts,
+ gpu_counts: gpuCounts,
+ instances,
+ }), {
+ headers: {
+ ...CORS_HEADERS,
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'public, max-age=300', // 5분 캐시
+ },
+ });
+ } catch (error) {
+ console.error('[Pricing] D1 query error:', error);
+ return new Response(JSON.stringify({
+ success: false,
+ error: error instanceof Error ? error.message : 'Database query failed',
+ }), {
+ status: 500,
+ headers: {
+ ...CORS_HEADERS,
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+};
+
+export const onRequestOptions: PagesFunction = async () => {
+ return createCorsPreflightResponse();
+};
diff --git a/functions/api/recommend.ts b/functions/api/recommend.ts
new file mode 100644
index 0000000..769d62d
--- /dev/null
+++ b/functions/api/recommend.ts
@@ -0,0 +1,47 @@
+/**
+ * Recommendation endpoint
+ * POST /api/recommend → Worker POST /recommend
+ */
+
+import { type PagesFunction } from '@cloudflare/workers-types';
+import {
+ Env,
+ createCorsPreflightResponse,
+ createErrorResponse,
+ proxyToWorker,
+} from '../_shared/proxy';
+
+export const onRequestPost: PagesFunction = async ({ request, env }) => {
+ try {
+ // Read request body
+ const body = await request.text();
+
+ // Validate JSON
+ if (body) {
+ try {
+ JSON.parse(body);
+ } catch {
+ return createErrorResponse('Invalid JSON in request body', 400);
+ }
+ }
+
+ return proxyToWorker(env, '/recommend', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body,
+ });
+ } catch (error) {
+ console.error('[Recommend] Failed to process request:', error);
+ return createErrorResponse(
+ 'Failed to process recommendation request',
+ 500,
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+};
+
+export const onRequestOptions: PagesFunction = async () => {
+ return createCorsPreflightResponse();
+};
diff --git a/index.html b/index.html
index 0bef204..a7ab9dc 100644
--- a/index.html
+++ b/index.html
@@ -243,6 +243,268 @@
+
+
+
+
+
+
+
+
+
+
+
+ 투명한 가격 정책
+
+
합리적인 요금제
+
전 세계 4개 리전에서 제공되는 최적의 인프라 요금
+
(월간 기준, VAT 포함)
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- 투명한 가격 정책
-
-
합리적인 요금제
-
전 세계 4개 리전에서 제공되는 최적의 인프라 요금
-
(월간 기준, VAT 포함)
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/package-lock.json b/package-lock.json
index f1662a7..64616a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,19 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
+ "@cloudflare/workers-types": "^4.20250110.0",
"@tailwindcss/cli": "^4.1.18",
- "tailwindcss": "^4.1.18"
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.7.3"
}
},
+ "node_modules/@cloudflare/workers-types": {
+ "version": "4.20260122.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260122.0.tgz",
+ "integrity": "sha512-ktjHXwjsjxFqsnCb9YQj9l12i6yfK5klBggKddogQ4KT3GCaU3Kq6IAkd4bGZSZrgwwbCkhAuhTzz4a3llA97g==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0"
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1050,6 +1059,20 @@
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
}
}
}
diff --git a/package.json b/package.json
index 4c48266..0beb11c 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
+ "@cloudflare/workers-types": "^4.20250110.0",
"@tailwindcss/cli": "^4.1.18",
- "tailwindcss": "^4.1.18"
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.7.3"
}
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f83210e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "lib": ["ES2022"],
+ "types": ["@cloudflare/workers-types"],
+ "moduleResolution": "node",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["functions/**/*"],
+ "exclude": ["node_modules"]
+}
diff --git a/wrangler.toml b/wrangler.toml
new file mode 100644
index 0000000..ac46c3b
--- /dev/null
+++ b/wrangler.toml
@@ -0,0 +1,16 @@
+name = "anvil-hosting"
+compatibility_date = "2024-01-01"
+pages_build_output_dir = "."
+
+[vars]
+WORKER_API_URL = "https://cloud-instances-api.kappa-d8e.workers.dev"
+
+# D1 Database binding (cloud-instances-db)
+[[d1_databases]]
+binding = "DB"
+database_name = "cloud-instances-db"
+database_id = "bbcb472d-b25e-4e48-b6ea-112f9fffb4a8"
+
+# Environment variables for local development
+# Production secrets should be set via:
+# wrangler pages secret put WORKER_API_KEY