Преглед на файлове

feat: Phase 1 MVP — Auth stub + Devices heartbeat + Dashboard shell

- NestJS API: AuthController/Service, JwtStrategy, GoogleStrategy stubs
- Devices module: heartbeat endpoint, API key guard, repo/service/controller
- Web dashboard: Next.js 14 with Tailwind CSS, device list/detail pages
- Docker Compose: postgres + redis + api-server + web-dashboard + worker
- Infrastructure: Dockerfiles, .env.example, drizzle.config.ts
- Git: .gitignore, repo initialized

Stack: NestJS + Next.js + PostgreSQL(Drizzle) + Redis + Bun
kingkong преди 2 месеца
ревизия
607157183a
променени са 100 файла, в които са добавени 4775 реда и са изтрити 0 реда
  1. 27 0
      .env.example
  2. 38 0
      .eslintrc.js
  3. 13 0
      .gitignore
  4. 11 0
      .prettierrc
  5. 4 0
      apps/api-server/.dockerignore
  6. 24 0
      apps/api-server/Dockerfile
  7. 12 0
      apps/api-server/drizzle.config.ts
  8. 51 0
      apps/api-server/package.json
  9. 3 0
      apps/api-server/src/app.module.d.ts
  10. 1 0
      apps/api-server/src/app.module.d.ts.map
  11. 80 0
      apps/api-server/src/app.module.js
  12. 1 0
      apps/api-server/src/app.module.js.map
  13. 27 0
      apps/api-server/src/app.module.ts
  14. 8 0
      apps/api-server/src/common/decorators/api-key.decorator.ts
  15. 24 0
      apps/api-server/src/common/guards/api-key.guard.ts
  16. 5 0
      apps/api-server/src/db/database.module.d.ts
  17. 1 0
      apps/api-server/src/db/database.module.d.ts.map
  18. 110 0
      apps/api-server/src/db/database.module.js
  19. 1 0
      apps/api-server/src/db/database.module.js.map
  20. 24 0
      apps/api-server/src/db/database.module.ts
  21. 2100 0
      apps/api-server/src/db/schema.d.ts
  22. 0 0
      apps/api-server/src/db/schema.d.ts.map
  23. 293 0
      apps/api-server/src/db/schema.js
  24. 0 0
      apps/api-server/src/db/schema.js.map
  25. 324 0
      apps/api-server/src/db/schema.ts
  26. 2 0
      apps/api-server/src/main.d.ts
  27. 1 0
      apps/api-server/src/main.d.ts.map
  28. 23 0
      apps/api-server/src/main.js
  29. 1 0
      apps/api-server/src/main.js.map
  30. 26 0
      apps/api-server/src/main.ts
  31. 3 0
      apps/api-server/src/modules/alerts/alerts.module.d.ts
  32. 1 0
      apps/api-server/src/modules/alerts/alerts.module.d.ts.map
  33. 61 0
      apps/api-server/src/modules/alerts/alerts.module.js
  34. 1 0
      apps/api-server/src/modules/alerts/alerts.module.js.map
  35. 8 0
      apps/api-server/src/modules/alerts/alerts.module.ts
  36. 29 0
      apps/api-server/src/modules/auth/auth.controller.d.ts
  37. 1 0
      apps/api-server/src/modules/auth/auth.controller.d.ts.map
  38. 98 0
      apps/api-server/src/modules/auth/auth.controller.js
  39. 1 0
      apps/api-server/src/modules/auth/auth.controller.js.map
  40. 37 0
      apps/api-server/src/modules/auth/auth.controller.ts
  41. 3 0
      apps/api-server/src/modules/auth/auth.module.d.ts
  42. 1 0
      apps/api-server/src/modules/auth/auth.module.d.ts.map
  43. 76 0
      apps/api-server/src/modules/auth/auth.module.js
  44. 1 0
      apps/api-server/src/modules/auth/auth.module.js.map
  45. 23 0
      apps/api-server/src/modules/auth/auth.module.ts
  46. 14 0
      apps/api-server/src/modules/auth/auth.service.d.ts
  47. 1 0
      apps/api-server/src/modules/auth/auth.service.d.ts.map
  48. 72 0
      apps/api-server/src/modules/auth/auth.service.js
  49. 1 0
      apps/api-server/src/modules/auth/auth.service.js.map
  50. 21 0
      apps/api-server/src/modules/auth/auth.service.ts
  51. 23 0
      apps/api-server/src/modules/auth/strategies/google.strategy.d.ts
  52. 1 0
      apps/api-server/src/modules/auth/strategies/google.strategy.d.ts.map
  53. 77 0
      apps/api-server/src/modules/auth/strategies/google.strategy.js
  54. 1 0
      apps/api-server/src/modules/auth/strategies/google.strategy.js.map
  55. 34 0
      apps/api-server/src/modules/auth/strategies/google.strategy.ts
  56. 15 0
      apps/api-server/src/modules/auth/strategies/jwt.strategy.d.ts
  57. 1 0
      apps/api-server/src/modules/auth/strategies/jwt.strategy.d.ts.map
  58. 73 0
      apps/api-server/src/modules/auth/strategies/jwt.strategy.js
  59. 1 0
      apps/api-server/src/modules/auth/strategies/jwt.strategy.js.map
  60. 26 0
      apps/api-server/src/modules/auth/strategies/jwt.strategy.ts
  61. 3 0
      apps/api-server/src/modules/captures/captures.module.d.ts
  62. 1 0
      apps/api-server/src/modules/captures/captures.module.d.ts.map
  63. 61 0
      apps/api-server/src/modules/captures/captures.module.js
  64. 1 0
      apps/api-server/src/modules/captures/captures.module.js.map
  65. 8 0
      apps/api-server/src/modules/captures/captures.module.ts
  66. 49 0
      apps/api-server/src/modules/devices/devices.controller.ts
  67. 3 0
      apps/api-server/src/modules/devices/devices.module.d.ts
  68. 1 0
      apps/api-server/src/modules/devices/devices.module.d.ts.map
  69. 61 0
      apps/api-server/src/modules/devices/devices.module.js
  70. 1 0
      apps/api-server/src/modules/devices/devices.module.js.map
  71. 11 0
      apps/api-server/src/modules/devices/devices.module.ts
  72. 97 0
      apps/api-server/src/modules/devices/devices.repository.ts
  73. 82 0
      apps/api-server/src/modules/devices/devices.service.ts
  74. 52 0
      apps/api-server/src/modules/devices/dto/heartbeat.dto.ts
  75. 3 0
      apps/api-server/src/modules/orgs/orgs.module.d.ts
  76. 1 0
      apps/api-server/src/modules/orgs/orgs.module.d.ts.map
  77. 61 0
      apps/api-server/src/modules/orgs/orgs.module.js
  78. 1 0
      apps/api-server/src/modules/orgs/orgs.module.js.map
  79. 8 0
      apps/api-server/src/modules/orgs/orgs.module.ts
  80. 3 0
      apps/api-server/src/modules/projects/projects.module.d.ts
  81. 1 0
      apps/api-server/src/modules/projects/projects.module.d.ts.map
  82. 61 0
      apps/api-server/src/modules/projects/projects.module.js
  83. 1 0
      apps/api-server/src/modules/projects/projects.module.js.map
  84. 8 0
      apps/api-server/src/modules/projects/projects.module.ts
  85. 3 0
      apps/api-server/src/modules/videos/videos.module.d.ts
  86. 1 0
      apps/api-server/src/modules/videos/videos.module.d.ts.map
  87. 61 0
      apps/api-server/src/modules/videos/videos.module.js
  88. 1 0
      apps/api-server/src/modules/videos/videos.module.js.map
  89. 8 0
      apps/api-server/src/modules/videos/videos.module.ts
  90. 3 0
      apps/api-server/src/realtime/realtime.module.d.ts
  91. 1 0
      apps/api-server/src/realtime/realtime.module.d.ts.map
  92. 60 0
      apps/api-server/src/realtime/realtime.module.js
  93. 1 0
      apps/api-server/src/realtime/realtime.module.js.map
  94. 7 0
      apps/api-server/src/realtime/realtime.module.ts
  95. 13 0
      apps/api-server/tsconfig.json
  96. 33 0
      apps/device-agent/package.json
  97. 4 0
      apps/web-dashboard/.dockerignore
  98. 24 0
      apps/web-dashboard/Dockerfile
  99. 5 0
      apps/web-dashboard/next-env.d.ts
  100. 31 0
      apps/web-dashboard/package.json

+ 27 - 0
.env.example

@@ -0,0 +1,27 @@
+# Database
+DATABASE_URL=postgres://timelapse:timelapse_dev_password@localhost:5432/timelapse_dev
+
+# Redis
+REDIS_URL=redis://localhost:6379
+
+# Auth
+JWT_SECRET=change-this-to-a-long-random-string-in-production
+REFRESH_TOKEN_SECRET=change-this-to-another-long-random-string-in-production
+
+# CORS
+CORS_ORIGIN=http://localhost:3000
+
+# Google OAuth (optional)
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+GOOGLE_CALLBACK_URL=http://localhost:3001/v1/auth/google/callback
+
+# S3 / Object Storage (optional)
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_REGION=ap-southeast-1
+S3_BUCKET=
+
+# API Server
+PORT=3001
+NODE_ENV=development

+ 38 - 0
.eslintrc.js

@@ -0,0 +1,38 @@
+module.exports = {
+  root: true,
+  ignorePatterns: ['node_modules', 'dist', '*.config.js'],
+  plugins: ['@typescript-eslint'],
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended-type-checked',
+    'plugin:@typescript-eslint/stylistic-type-checked',
+    'prettier',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 'latest',
+    sourceType: 'module',
+    project: ['./tsconfig.json', './apps/*/tsconfig.json', './packages/*/tsconfig.json'],
+  },
+  rules: {
+    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'warn',
+    'no-console': ['warn', { allow: ['warn', 'error'] }],
+  },
+  overrides: [
+    {
+      files: ['apps/web-dashboard/**/*.ts', 'apps/web-dashboard/**/*.tsx'],
+      parserOptions: {
+        project: './apps/web-dashboard/tsconfig.json',
+      },
+    },
+    {
+      files: ['*.test.ts', '*.spec.ts'],
+      rules: {
+        '@typescript-eslint/no-explicit-any': 'off',
+        '@typescript-eslint/no-unused-vars': 'off',
+      },
+    },
+  ],
+};

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+node_modules/
+dist/
+.env
+.env.*
+!.env.example
+*.log
+.DS_Store
+*.local
+.next/
+out/
+bun.lockb
+.claude/
+*.tsbuildinfo

+ 11 - 0
.prettierrc

@@ -0,0 +1,11 @@
+{
+  "semi": false,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "printWidth": 100,
+  "bracketSpacing": true,
+  "arrowParens": "always",
+  "endOfLine": "lf",
+  "plugins": []
+}

+ 4 - 0
apps/api-server/.dockerignore

@@ -0,0 +1,4 @@
+node_modules
+dist
+.env
+*.log

+ 24 - 0
apps/api-server/Dockerfile

@@ -0,0 +1,24 @@
+FROM node:20-alpine AS base
+
+FROM base AS deps
+WORKDIR /app
+COPY package.json packages/*/package.json apps/*/package.json ./
+RUN npm install --workspaces --include-workspace-root
+
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run build --workspace=apps/api-server
+
+FROM base AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+
+COPY --from=builder /app/apps/api-server/dist ./dist
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/packages/shared-types/dist ./packages/shared-types/dist
+COPY --from=builder /app/apps/api-server/package.json ./
+
+EXPOSE 3001
+CMD ["node", "dist/main.js"]

+ 12 - 0
apps/api-server/drizzle.config.ts

@@ -0,0 +1,12 @@
+import { defineConfig } from 'drizzle-kit'
+
+export default defineConfig({
+  schema: './src/db/schema.ts',
+  out: './drizzle',
+  dialect: 'postgresql',
+  dbCredentials: {
+    url: process.env['DATABASE_URL']!,
+  },
+  verbose: true,
+  strict: true,
+})

+ 51 - 0
apps/api-server/package.json

@@ -0,0 +1,51 @@
+{
+  "name": "api-server",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "tsx watch src/main.ts",
+    "build": "tsc",
+    "start": "node dist/main.js",
+    "lint": "eslint src --ext .ts --fix",
+    "typecheck": "tsc --noEmit",
+    "migrate": "drizzle-kit push",
+    "seed": "tsx src/db/seed.ts"
+  },
+  "dependencies": {
+    "@aws-sdk/client-s3": "^3.490.0",
+    "@aws-sdk/s3-request-presigner": "^3.490.0",
+    "@nestjs/common": "^10.3.0",
+    "@nestjs/config": "^4.0.3",
+    "@nestjs/core": "^10.3.0",
+    "@nestjs/jwt": "^10.2.0",
+    "@nestjs/passport": "^10.0.3",
+    "@nestjs/platform-express": "^10.3.0",
+    "@nestjs/platform-socket.io": "^10.3.0",
+    "@nestjs/websockets": "^10.3.0",
+    "@shared/types": "workspace:*",
+    "bcrypt": "^5.1.1",
+    "bullmq": "^5.1.0",
+    "class-transformer": "^0.5.1",
+    "class-validator": "^0.14.1",
+    "drizzle-orm": "^0.29.3",
+    "ioredis": "^5.3.2",
+    "nanoid": "^5.0.4",
+    "passport": "^0.7.0",
+    "passport-google-oauth20": "^2.0.0",
+    "passport-jwt": "^4.0.1",
+    "postgres": "^3.4.3",
+    "socket.io": "^4.6.1",
+    "zod": "^3.22.4"
+  },
+  "devDependencies": {
+    "@types/node": "^20.11.0",
+    "@types/passport-jwt": "^4.0.1",
+    "@types/passport-google-oauth20": "^2.0.14",
+    "@types/bcrypt": "^5.0.2",
+    "typescript": "^5.3.3",
+    "tsx": "^4.7.0",
+    "drizzle-kit": "^0.20.13",
+    "eslint": "^8.56.0",
+    "prettier": "^3.2.0"
+  }
+}

+ 3 - 0
apps/api-server/src/app.module.d.ts

@@ -0,0 +1,3 @@
+export declare class AppModule {
+}
+//# sourceMappingURL=app.module.d.ts.map

+ 1 - 0
apps/api-server/src/app.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"app.module.d.ts","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":"AAYA,qBAca,SAAS;CAAG"}

+ 80 - 0
apps/api-server/src/app.module.js

@@ -0,0 +1,80 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AppModule = void 0;
+const common_1 = require("@nestjs/common");
+const config_1 = require("@nestjs/config");
+const auth_module_1 = require("./modules/auth/auth.module");
+const orgs_module_1 = require("./modules/orgs/orgs.module");
+const projects_module_1 = require("./modules/projects/projects.module");
+const devices_module_1 = require("./modules/devices/devices.module");
+const captures_module_1 = require("./modules/captures/captures.module");
+const videos_module_1 = require("./modules/videos/videos.module");
+const alerts_module_1 = require("./modules/alerts/alerts.module");
+const database_module_1 = require("./db/database.module");
+const realtime_module_1 = require("./realtime/realtime.module");
+let AppModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            imports: [
+                config_1.ConfigModule.forRoot({ isGlobal: true }),
+                database_module_1.DatabaseModule,
+                realtime_module_1.RealtimeModule,
+                auth_module_1.AuthModule,
+                orgs_module_1.OrgsModule,
+                projects_module_1.ProjectsModule,
+                devices_module_1.DevicesModule,
+                captures_module_1.CapturesModule,
+                videos_module_1.VideosModule,
+                alerts_module_1.AlertsModule,
+            ],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var AppModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            AppModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return AppModule = _classThis;
+})();
+exports.AppModule = AppModule;
+//# sourceMappingURL=app.module.js.map

+ 1 - 0
apps/api-server/src/app.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"app.module.js","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;AACvC,2CAA6C;AAC7C,4DAAuD;AACvD,4DAAuD;AACvD,wEAAmE;AACnE,qEAAgE;AAChE,wEAAmE;AACnE,kEAA6D;AAC7D,kEAA6D;AAC7D,0DAAqD;AACrD,gEAA2D;IAgB9C,SAAS;4BAdrB,IAAA,eAAM,EAAC;YACN,OAAO,EAAE;gBACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACxC,gCAAc;gBACd,gCAAc;gBACd,wBAAU;gBACV,wBAAU;gBACV,gCAAc;gBACd,8BAAa;gBACb,gCAAc;gBACd,4BAAY;gBACZ,4BAAY;aACb;SACF,CAAC;;;;;;;;YACF,6KAAyB;;;YAAZ,uDAAS;;;;;AAAT,8BAAS"}

+ 27 - 0
apps/api-server/src/app.module.ts

@@ -0,0 +1,27 @@
+import { Module } from '@nestjs/common'
+import { ConfigModule } from '@nestjs/config'
+import { AuthModule } from './modules/auth/auth.module'
+import { OrgsModule } from './modules/orgs/orgs.module'
+import { ProjectsModule } from './modules/projects/projects.module'
+import { DevicesModule } from './modules/devices/devices.module'
+import { CapturesModule } from './modules/captures/captures.module'
+import { VideosModule } from './modules/videos/videos.module'
+import { AlertsModule } from './modules/alerts/alerts.module'
+import { DatabaseModule } from './db/database.module'
+import { RealtimeModule } from './realtime/realtime.module'
+
+@Module({
+  imports: [
+    ConfigModule.forRoot({ isGlobal: true }),
+    DatabaseModule,
+    RealtimeModule,
+    AuthModule,
+    OrgsModule,
+    ProjectsModule,
+    DevicesModule,
+    CapturesModule,
+    VideosModule,
+    AlertsModule,
+  ],
+})
+export class AppModule {}

+ 8 - 0
apps/api-server/src/common/decorators/api-key.decorator.ts

@@ -0,0 +1,8 @@
+import { createParamDecorator, ExecutionContext } from '@nestjs/common'
+
+/**
+ * Extracts the raw API key attached by ApiKeyGuard.
+ */
+export const ApiKey = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
+  return ctx.switchToHttp().getRequest().apiKey as string
+})

+ 24 - 0
apps/api-server/src/common/guards/api-key.guard.ts

@@ -0,0 +1,24 @@
+import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'
+
+/**
+ * ApiKeyGuard — Phase 1 implementation.
+ *
+ * Extracts `X-API-Key` header and attaches it to the request.
+ * Key validation (hash compare) is done in DevicesService to keep
+ * DB access in the service layer.
+ */
+@Injectable()
+export class ApiKeyGuard implements CanActivate {
+  canActivate(context: ExecutionContext): boolean {
+    const request = context.switchToHttp().getRequest()
+    const apiKey = request.headers['x-api-key']
+
+    if (!apiKey || typeof apiKey !== 'string') {
+      throw new UnauthorizedException('Missing X-API-Key header')
+    }
+
+    // Attach raw key for downstream service validation
+    request.apiKey = apiKey
+    return true
+  }
+}

+ 5 - 0
apps/api-server/src/db/database.module.d.ts

@@ -0,0 +1,5 @@
+import * as schema from './schema';
+export declare const db: import("drizzle-orm/postgres-js").PostgresJsDatabase<typeof schema>;
+export declare class DatabaseModule {
+}
+//# sourceMappingURL=database.module.d.ts.map

+ 1 - 0
apps/api-server/src/db/database.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"database.module.d.ts","sourceRoot":"","sources":["database.module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAA;AAQlC,eAAO,MAAM,EAAE,qEAA8B,CAAA;AAE7C,qBAUa,cAAc;CAAG"}

+ 110 - 0
apps/api-server/src/db/database.module.js

@@ -0,0 +1,110 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+var __importStar = (this && this.__importStar) || (function () {
+    var ownKeys = function(o) {
+        ownKeys = Object.getOwnPropertyNames || function (o) {
+            var ar = [];
+            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+            return ar;
+        };
+        return ownKeys(o);
+    };
+    return function (mod) {
+        if (mod && mod.__esModule) return mod;
+        var result = {};
+        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+        __setModuleDefault(result, mod);
+        return result;
+    };
+})();
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.DatabaseModule = exports.db = void 0;
+const common_1 = require("@nestjs/common");
+const postgres_js_1 = require("drizzle-orm/postgres-js");
+const postgres_1 = __importDefault(require("postgres"));
+const schema = __importStar(require("./schema"));
+const DATABASE_URL = process.env['DATABASE_URL'];
+if (!DATABASE_URL) {
+    console.warn('DATABASE_URL not set — using mock driver for dev');
+}
+const client = DATABASE_URL ? (0, postgres_1.default)(DATABASE_URL, { max: 10 }) : (0, postgres_1.default)('');
+exports.db = (0, postgres_js_1.drizzle)(client, { schema });
+let DatabaseModule = (() => {
+    let _classDecorators = [(0, common_1.Global)(), (0, common_1.Module)({
+            providers: [
+                {
+                    provide: 'DB',
+                    useValue: exports.db,
+                },
+            ],
+            exports: ['DB'],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var DatabaseModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            DatabaseModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return DatabaseModule = _classThis;
+})();
+exports.DatabaseModule = DatabaseModule;
+//# sourceMappingURL=database.module.js.map

+ 1 - 0
apps/api-server/src/db/database.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"database.module.js","sourceRoot":"","sources":["database.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA+C;AAC/C,yDAAiD;AACjD,wDAA+B;AAC/B,iDAAkC;AAElC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AAChD,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,OAAO,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAA;AAClE,CAAC;AAED,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,IAAA,kBAAQ,EAAC,YAAY,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAA,kBAAQ,EAAC,EAAE,CAAC,CAAA;AACnE,QAAA,EAAE,GAAG,IAAA,qBAAO,EAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IAYhC,cAAc;4BAV1B,IAAA,eAAM,GAAE,EACR,IAAA,eAAM,EAAC;YACN,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,IAAI;oBACb,QAAQ,EAAE,UAAE;iBACb;aACF;YACD,OAAO,EAAE,CAAC,IAAI,CAAC;SAChB,CAAC;;;;;;;;YACF,6KAA8B;;;YAAjB,uDAAc;;;;;AAAd,wCAAc"}

+ 24 - 0
apps/api-server/src/db/database.module.ts

@@ -0,0 +1,24 @@
+import { Module, Global } from '@nestjs/common'
+import { drizzle } from 'drizzle-orm/postgres-js'
+import postgres from 'postgres'
+import * as schema from './schema'
+
+const DATABASE_URL = process.env['DATABASE_URL']
+if (!DATABASE_URL) {
+  console.warn('DATABASE_URL not set — using mock driver for dev')
+}
+
+const client = DATABASE_URL ? postgres(DATABASE_URL, { max: 10 }) : postgres('')
+export const db = drizzle(client, { schema })
+
+@Global()
+@Module({
+  providers: [
+    {
+      provide: 'DB',
+      useValue: db,
+    },
+  ],
+  exports: ['DB'],
+})
+export class DatabaseModule {}

+ 2100 - 0
apps/api-server/src/db/schema.d.ts

@@ -0,0 +1,2100 @@
+export declare const orgStatusEnum: import("drizzle-orm/pg-core").PgEnum<["active", "suspended", "trial"]>;
+export declare const projectStatusEnum: import("drizzle-orm/pg-core").PgEnum<["planning", "active", "paused", "completed", "archived"]>;
+export declare const deviceStatusEnum: import("drizzle-orm/pg-core").PgEnum<["offline", "online", "capturing", "uploading", "degraded", "updating", "error"]>;
+export declare const captureStatusEnum: import("drizzle-orm/pg-core").PgEnum<["pending", "uploaded", "processing", "ready", "failed"]>;
+export declare const videoStatusEnum: import("drizzle-orm/pg-core").PgEnum<["pending", "processing", "ready", "failed"]>;
+export declare const alertSeverityEnum: import("drizzle-orm/pg-core").PgEnum<["info", "warning", "error", "critical"]>;
+export declare const alertTypeEnum: import("drizzle-orm/pg-core").PgEnum<["device_offline", "device_error", "storage_full", "upload_failed", "capture_missed", "video_failed", "firmware_update_available"]>;
+export declare const alertStateEnum: import("drizzle-orm/pg-core").PgEnum<["open", "acknowledged", "resolved"]>;
+export declare const userRoleEnum: import("drizzle-orm/pg-core").PgEnum<["super_admin", "org_admin", "project_manager", "viewer"]>;
+export declare const commandResultEnum: import("drizzle-orm/pg-core").PgEnum<["pending", "delivered", "acknowledged", "success", "failed", "timeout"]>;
+export declare const organizations: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "organizations";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "organizations";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        name: import("drizzle-orm/pg-core").PgColumn<{
+            name: "name";
+            tableName: "organizations";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "organizations";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "active" | "suspended" | "trial";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["active", "suspended", "trial"];
+            baseColumn: never;
+        }, {}, {}>;
+        planTier: import("drizzle-orm/pg-core").PgColumn<{
+            name: "plan_tier";
+            tableName: "organizations";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "organizations";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        updatedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "updated_at";
+            tableName: "organizations";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const projects: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "projects";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        name: import("drizzle-orm/pg-core").PgColumn<{
+            name: "name";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        description: import("drizzle-orm/pg-core").PgColumn<{
+            name: "description";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        timezone: import("drizzle-orm/pg-core").PgColumn<{
+            name: "timezone";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        startDate: import("drizzle-orm/pg-core").PgColumn<{
+            name: "start_date";
+            tableName: "projects";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        endDate: import("drizzle-orm/pg-core").PgColumn<{
+            name: "end_date";
+            tableName: "projects";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "active" | "planning" | "paused" | "completed" | "archived";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["planning", "active", "paused", "completed", "archived"];
+            baseColumn: never;
+        }, {}, {}>;
+        captureInterval: import("drizzle-orm/pg-core").PgColumn<{
+            name: "capture_interval";
+            tableName: "projects";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        resolution: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resolution";
+            tableName: "projects";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "projects";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        updatedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "updated_at";
+            tableName: "projects";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "users";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        email: import("drizzle-orm/pg-core").PgColumn<{
+            name: "email";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        name: import("drizzle-orm/pg-core").PgColumn<{
+            name: "name";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        avatarUrl: import("drizzle-orm/pg-core").PgColumn<{
+            name: "avatar_url";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        provider: import("drizzle-orm/pg-core").PgColumn<{
+            name: "provider";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        emailVerified: import("drizzle-orm/pg-core").PgColumn<{
+            name: "email_verified";
+            tableName: "users";
+            dataType: "boolean";
+            columnType: "PgBoolean";
+            data: boolean;
+            driverParam: boolean;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        passwordHash: import("drizzle-orm/pg-core").PgColumn<{
+            name: "password_hash";
+            tableName: "users";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "users";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        updatedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "updated_at";
+            tableName: "users";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const memberships: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "memberships";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "memberships";
+            dataType: "number";
+            columnType: "PgSerial";
+            data: number;
+            driverParam: number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        userId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_id";
+            tableName: "memberships";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "memberships";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        role: import("drizzle-orm/pg-core").PgColumn<{
+            name: "role";
+            tableName: "memberships";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "viewer" | "super_admin" | "org_admin" | "project_manager";
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: ["super_admin", "org_admin", "project_manager", "viewer"];
+            baseColumn: never;
+        }, {}, {}>;
+        invitedBy: import("drizzle-orm/pg-core").PgColumn<{
+            name: "invited_by";
+            tableName: "memberships";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        joinedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "joined_at";
+            tableName: "memberships";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const sessions: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "sessions";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "sessions";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        userId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_id";
+            tableName: "sessions";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        refreshTokenHash: import("drizzle-orm/pg-core").PgColumn<{
+            name: "refresh_token_hash";
+            tableName: "sessions";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        expiresAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "expires_at";
+            tableName: "sessions";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "sessions";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        userAgent: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_agent";
+            tableName: "sessions";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        ipAddress: import("drizzle-orm/pg-core").PgColumn<{
+            name: "ip_address";
+            tableName: "sessions";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const magicLinks: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "magic_links";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "magic_links";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        email: import("drizzle-orm/pg-core").PgColumn<{
+            name: "email";
+            tableName: "magic_links";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        tokenHash: import("drizzle-orm/pg-core").PgColumn<{
+            name: "token_hash";
+            tableName: "magic_links";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        expiresAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "expires_at";
+            tableName: "magic_links";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        usedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "used_at";
+            tableName: "magic_links";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "magic_links";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const devices: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "devices";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        serialNo: import("drizzle-orm/pg-core").PgColumn<{
+            name: "serial_no";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        name: import("drizzle-orm/pg-core").PgColumn<{
+            name: "name";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        apiKeyHash: import("drizzle-orm/pg-core").PgColumn<{
+            name: "api_key_hash";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        firmwareVersion: import("drizzle-orm/pg-core").PgColumn<{
+            name: "firmware_version";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        lastSeenAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "last_seen_at";
+            tableName: "devices";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "offline" | "online" | "capturing" | "uploading" | "degraded" | "updating" | "error";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["offline", "online", "capturing", "uploading", "degraded", "updating", "error"];
+            baseColumn: never;
+        }, {}, {}>;
+        config: import("drizzle-orm/pg-core").PgColumn<{
+            name: "config";
+            tableName: "devices";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: {
+                captureIntervalMinutes: number;
+                resolution: string;
+                quality: number;
+                uploadOnWifiOnly: boolean;
+                nightModeEnabled: boolean;
+                nightModeStart: string;
+                nightModeEnd: string;
+                maxStorageGb: number;
+                heartbeatIntervalSeconds: number;
+                timezone: string;
+            };
+            driverParam: unknown;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        claimCode: import("drizzle-orm/pg-core").PgColumn<{
+            name: "claim_code";
+            tableName: "devices";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "devices";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        updatedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "updated_at";
+            tableName: "devices";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const deviceHeartbeats: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "device_heartbeats";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "device_heartbeats";
+            dataType: "number";
+            columnType: "PgSerial";
+            data: number;
+            driverParam: number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        deviceId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "device_id";
+            tableName: "device_heartbeats";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        heartbeatAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "heartbeat_at";
+            tableName: "device_heartbeats";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        tempC: import("drizzle-orm/pg-core").PgColumn<{
+            name: "temp_c";
+            tableName: "device_heartbeats";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        batteryPct: import("drizzle-orm/pg-core").PgColumn<{
+            name: "battery_pct";
+            tableName: "device_heartbeats";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        storageFreeGb: import("drizzle-orm/pg-core").PgColumn<{
+            name: "storage_free_gb";
+            tableName: "device_heartbeats";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        capturesToday: import("drizzle-orm/pg-core").PgColumn<{
+            name: "captures_today";
+            tableName: "device_heartbeats";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        lastCaptureAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "last_capture_at";
+            tableName: "device_heartbeats";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        networkStatus: import("drizzle-orm/pg-core").PgColumn<{
+            name: "network_status";
+            tableName: "device_heartbeats";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const captures: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "captures";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        deviceId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "device_id";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        capturedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "captured_at";
+            tableName: "captures";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        uploadedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "uploaded_at";
+            tableName: "captures";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        fileKey: import("drizzle-orm/pg-core").PgColumn<{
+            name: "file_key";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        thumbnailKey: import("drizzle-orm/pg-core").PgColumn<{
+            name: "thumbnail_key";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        checksum: import("drizzle-orm/pg-core").PgColumn<{
+            name: "checksum";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        resolution: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resolution";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        fileSizeBytes: import("drizzle-orm/pg-core").PgColumn<{
+            name: "file_size_bytes";
+            tableName: "captures";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        exposureMs: import("drizzle-orm/pg-core").PgColumn<{
+            name: "exposure_ms";
+            tableName: "captures";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        iso: import("drizzle-orm/pg-core").PgColumn<{
+            name: "iso";
+            tableName: "captures";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        aperture: import("drizzle-orm/pg-core").PgColumn<{
+            name: "aperture";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        gpsLat: import("drizzle-orm/pg-core").PgColumn<{
+            name: "gps_lat";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        gpsLng: import("drizzle-orm/pg-core").PgColumn<{
+            name: "gps_lng";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "captures";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "pending" | "uploaded" | "processing" | "ready" | "failed";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["pending", "uploaded", "processing", "ready", "failed"];
+            baseColumn: never;
+        }, {}, {}>;
+        metadata: import("drizzle-orm/pg-core").PgColumn<{
+            name: "metadata";
+            tableName: "captures";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "captures";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const videos: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "videos";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        periodStart: import("drizzle-orm/pg-core").PgColumn<{
+            name: "period_start";
+            tableName: "videos";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        periodEnd: import("drizzle-orm/pg-core").PgColumn<{
+            name: "period_end";
+            tableName: "videos";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        fps: import("drizzle-orm/pg-core").PgColumn<{
+            name: "fps";
+            tableName: "videos";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        resolution: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resolution";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        fileKey: import("drizzle-orm/pg-core").PgColumn<{
+            name: "file_key";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        thumbnailKey: import("drizzle-orm/pg-core").PgColumn<{
+            name: "thumbnail_key";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        durationSec: import("drizzle-orm/pg-core").PgColumn<{
+            name: "duration_sec";
+            tableName: "videos";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "videos";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "pending" | "processing" | "ready" | "failed";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["pending", "processing", "ready", "failed"];
+            baseColumn: never;
+        }, {}, {}>;
+        generatedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "generated_at";
+            tableName: "videos";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        fileSizeBytes: import("drizzle-orm/pg-core").PgColumn<{
+            name: "file_size_bytes";
+            tableName: "videos";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "videos";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const videoJobs: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "video_jobs";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        videoId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "video_id";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        jobType: import("drizzle-orm/pg-core").PgColumn<{
+            name: "job_type";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        params: import("drizzle-orm/pg-core").PgColumn<{
+            name: "params";
+            tableName: "video_jobs";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        status: import("drizzle-orm/pg-core").PgColumn<{
+            name: "status";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        attempts: import("drizzle-orm/pg-core").PgColumn<{
+            name: "attempts";
+            tableName: "video_jobs";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        startedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "started_at";
+            tableName: "video_jobs";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        completedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "completed_at";
+            tableName: "video_jobs";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        errorMsg: import("drizzle-orm/pg-core").PgColumn<{
+            name: "error_msg";
+            tableName: "video_jobs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "video_jobs";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const commands: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "commands";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "commands";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        deviceId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "device_id";
+            tableName: "commands";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        commandType: import("drizzle-orm/pg-core").PgColumn<{
+            name: "command_type";
+            tableName: "commands";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        payload: import("drizzle-orm/pg-core").PgColumn<{
+            name: "payload";
+            tableName: "commands";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        scheduledAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "scheduled_at";
+            tableName: "commands";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        queuedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "queued_at";
+            tableName: "commands";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        deliveredAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "delivered_at";
+            tableName: "commands";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        acknowledgedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "acknowledged_at";
+            tableName: "commands";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        resultStatus: import("drizzle-orm/pg-core").PgColumn<{
+            name: "result_status";
+            tableName: "commands";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "pending" | "failed" | "acknowledged" | "delivered" | "success" | "timeout";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["pending", "delivered", "acknowledged", "success", "failed", "timeout"];
+            baseColumn: never;
+        }, {}, {}>;
+        resultData: import("drizzle-orm/pg-core").PgColumn<{
+            name: "result_data";
+            tableName: "commands";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const alertRules: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "alert_rules";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "alert_rules";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "alert_rules";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        name: import("drizzle-orm/pg-core").PgColumn<{
+            name: "name";
+            tableName: "alert_rules";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        type: import("drizzle-orm/pg-core").PgColumn<{
+            name: "type";
+            tableName: "alert_rules";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "device_offline" | "device_error" | "storage_full" | "upload_failed" | "capture_missed" | "video_failed" | "firmware_update_available";
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: ["device_offline", "device_error", "storage_full", "upload_failed", "capture_missed", "video_failed", "firmware_update_available"];
+            baseColumn: never;
+        }, {}, {}>;
+        condition: import("drizzle-orm/pg-core").PgColumn<{
+            name: "condition";
+            tableName: "alert_rules";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: true;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        cooldownMinutes: import("drizzle-orm/pg-core").PgColumn<{
+            name: "cooldown_minutes";
+            tableName: "alert_rules";
+            dataType: "number";
+            columnType: "PgInteger";
+            data: number;
+            driverParam: string | number;
+            notNull: false;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        enabled: import("drizzle-orm/pg-core").PgColumn<{
+            name: "enabled";
+            tableName: "alert_rules";
+            dataType: "boolean";
+            columnType: "PgBoolean";
+            data: boolean;
+            driverParam: boolean;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        notifyEmail: import("drizzle-orm/pg-core").PgColumn<{
+            name: "notify_email";
+            tableName: "alert_rules";
+            dataType: "boolean";
+            columnType: "PgBoolean";
+            data: boolean;
+            driverParam: boolean;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        notifySms: import("drizzle-orm/pg-core").PgColumn<{
+            name: "notify_sms";
+            tableName: "alert_rules";
+            dataType: "boolean";
+            columnType: "PgBoolean";
+            data: boolean;
+            driverParam: boolean;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        webhookUrl: import("drizzle-orm/pg-core").PgColumn<{
+            name: "webhook_url";
+            tableName: "alert_rules";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "alert_rules";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const alerts: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "alerts";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        deviceId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "device_id";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        type: import("drizzle-orm/pg-core").PgColumn<{
+            name: "type";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "device_offline" | "device_error" | "storage_full" | "upload_failed" | "capture_missed" | "video_failed" | "firmware_update_available";
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: ["device_offline", "device_error", "storage_full", "upload_failed", "capture_missed", "video_failed", "firmware_update_available"];
+            baseColumn: never;
+        }, {}, {}>;
+        severity: import("drizzle-orm/pg-core").PgColumn<{
+            name: "severity";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "error" | "info" | "warning" | "critical";
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: ["info", "warning", "error", "critical"];
+            baseColumn: never;
+        }, {}, {}>;
+        message: import("drizzle-orm/pg-core").PgColumn<{
+            name: "message";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        data: import("drizzle-orm/pg-core").PgColumn<{
+            name: "data";
+            tableName: "alerts";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        state: import("drizzle-orm/pg-core").PgColumn<{
+            name: "state";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgEnumColumn";
+            data: "open" | "acknowledged" | "resolved";
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: ["open", "acknowledged", "resolved"];
+            baseColumn: never;
+        }, {}, {}>;
+        openedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "opened_at";
+            tableName: "alerts";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        acknowledgedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "acknowledged_at";
+            tableName: "alerts";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        acknowledgedBy: import("drizzle-orm/pg-core").PgColumn<{
+            name: "acknowledged_by";
+            tableName: "alerts";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        resolvedAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resolved_at";
+            tableName: "alerts";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const auditLogs: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "audit_logs";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "audit_logs";
+            dataType: "number";
+            columnType: "PgSerial";
+            data: number;
+            driverParam: number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        userId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_id";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        action: import("drizzle-orm/pg-core").PgColumn<{
+            name: "action";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        resourceType: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resource_type";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        resourceId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "resource_id";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        ipAddress: import("drizzle-orm/pg-core").PgColumn<{
+            name: "ip_address";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        userAgent: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_agent";
+            tableName: "audit_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        metadata: import("drizzle-orm/pg-core").PgColumn<{
+            name: "metadata";
+            tableName: "audit_logs";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: false;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "audit_logs";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const activityLogs: import("drizzle-orm/pg-core").PgTableWithColumns<{
+    name: "activity_logs";
+    schema: undefined;
+    columns: {
+        id: import("drizzle-orm/pg-core").PgColumn<{
+            name: "id";
+            tableName: "activity_logs";
+            dataType: "number";
+            columnType: "PgSerial";
+            data: number;
+            driverParam: number;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        orgId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "org_id";
+            tableName: "activity_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        projectId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "project_id";
+            tableName: "activity_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        userId: import("drizzle-orm/pg-core").PgColumn<{
+            name: "user_id";
+            tableName: "activity_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: false;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        eventType: import("drizzle-orm/pg-core").PgColumn<{
+            name: "event_type";
+            tableName: "activity_logs";
+            dataType: "string";
+            columnType: "PgText";
+            data: string;
+            driverParam: string;
+            notNull: true;
+            hasDefault: false;
+            enumValues: [string, ...string[]];
+            baseColumn: never;
+        }, {}, {}>;
+        metadata: import("drizzle-orm/pg-core").PgColumn<{
+            name: "metadata";
+            tableName: "activity_logs";
+            dataType: "json";
+            columnType: "PgJsonb";
+            data: unknown;
+            driverParam: unknown;
+            notNull: false;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+        createdAt: import("drizzle-orm/pg-core").PgColumn<{
+            name: "created_at";
+            tableName: "activity_logs";
+            dataType: "date";
+            columnType: "PgTimestamp";
+            data: Date;
+            driverParam: string;
+            notNull: true;
+            hasDefault: true;
+            enumValues: undefined;
+            baseColumn: never;
+        }, {}, {}>;
+    };
+    dialect: "pg";
+}>;
+export declare const organizationsRelations: import("drizzle-orm").Relations<"organizations", {
+    projects: import("drizzle-orm").Many<"projects">;
+    memberships: import("drizzle-orm").Many<"memberships">;
+}>;
+export declare const projectsRelations: import("drizzle-orm").Relations<"projects", {
+    organization: import("drizzle-orm").One<"organizations", true>;
+    devices: import("drizzle-orm").Many<"devices">;
+    captures: import("drizzle-orm").Many<"captures">;
+    videos: import("drizzle-orm").Many<"videos">;
+    alerts: import("drizzle-orm").Many<"alerts">;
+}>;
+export declare const usersRelations: import("drizzle-orm").Relations<"users", {
+    memberships: import("drizzle-orm").Many<"memberships">;
+    sessions: import("drizzle-orm").Many<"sessions">;
+}>;
+export declare const membershipsRelations: import("drizzle-orm").Relations<"memberships", {
+    user: import("drizzle-orm").One<"users", true>;
+    organization: import("drizzle-orm").One<"organizations", true>;
+}>;
+export declare const devicesRelations: import("drizzle-orm").Relations<"devices", {
+    project: import("drizzle-orm").One<"projects", true>;
+    organization: import("drizzle-orm").One<"organizations", true>;
+    captures: import("drizzle-orm").Many<"captures">;
+    heartbeats: import("drizzle-orm").Many<"device_heartbeats">;
+    commands: import("drizzle-orm").Many<"commands">;
+    alerts: import("drizzle-orm").Many<"alerts">;
+}>;
+export declare const capturesRelations: import("drizzle-orm").Relations<"captures", {
+    project: import("drizzle-orm").One<"projects", true>;
+    device: import("drizzle-orm").One<"devices", true>;
+}>;
+export declare const videosRelations: import("drizzle-orm").Relations<"videos", {
+    project: import("drizzle-orm").One<"projects", true>;
+}>;
+//# sourceMappingURL=schema.d.ts.map

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
apps/api-server/src/db/schema.d.ts.map


+ 293 - 0
apps/api-server/src/db/schema.js

@@ -0,0 +1,293 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.videosRelations = exports.capturesRelations = exports.devicesRelations = exports.membershipsRelations = exports.usersRelations = exports.projectsRelations = exports.organizationsRelations = exports.activityLogs = exports.auditLogs = exports.alerts = exports.alertRules = exports.commands = exports.videoJobs = exports.videos = exports.captures = exports.deviceHeartbeats = exports.devices = exports.magicLinks = exports.sessions = exports.memberships = exports.users = exports.projects = exports.organizations = exports.commandResultEnum = exports.userRoleEnum = exports.alertStateEnum = exports.alertTypeEnum = exports.alertSeverityEnum = exports.videoStatusEnum = exports.captureStatusEnum = exports.deviceStatusEnum = exports.projectStatusEnum = exports.orgStatusEnum = void 0;
+const pg_core_1 = require("drizzle-orm/pg-core");
+const drizzle_orm_1 = require("drizzle-orm");
+// ---- Enums ----
+exports.orgStatusEnum = (0, pg_core_1.pgEnum)('org_status', ['active', 'suspended', 'trial']);
+exports.projectStatusEnum = (0, pg_core_1.pgEnum)('project_status', ['planning', 'active', 'paused', 'completed', 'archived']);
+exports.deviceStatusEnum = (0, pg_core_1.pgEnum)('device_status', ['offline', 'online', 'capturing', 'uploading', 'degraded', 'updating', 'error']);
+exports.captureStatusEnum = (0, pg_core_1.pgEnum)('capture_status', ['pending', 'uploaded', 'processing', 'ready', 'failed']);
+exports.videoStatusEnum = (0, pg_core_1.pgEnum)('video_status', ['pending', 'processing', 'ready', 'failed']);
+exports.alertSeverityEnum = (0, pg_core_1.pgEnum)('alert_severity', ['info', 'warning', 'error', 'critical']);
+exports.alertTypeEnum = (0, pg_core_1.pgEnum)('alert_type', ['device_offline', 'device_error', 'storage_full', 'upload_failed', 'capture_missed', 'video_failed', 'firmware_update_available']);
+exports.alertStateEnum = (0, pg_core_1.pgEnum)('alert_state', ['open', 'acknowledged', 'resolved']);
+exports.userRoleEnum = (0, pg_core_1.pgEnum)('user_role', ['super_admin', 'org_admin', 'project_manager', 'viewer']);
+exports.commandResultEnum = (0, pg_core_1.pgEnum)('command_result_status', ['pending', 'delivered', 'acknowledged', 'success', 'failed', 'timeout']);
+// ---- Tenancy ----
+exports.organizations = (0, pg_core_1.pgTable)('organizations', {
+    id: (0, pg_core_1.text)('id').primaryKey(), // nanoid
+    name: (0, pg_core_1.text)('name').notNull(),
+    status: (0, exports.orgStatusEnum)('status').default('trial').notNull(),
+    planTier: (0, pg_core_1.text)('plan_tier').default('free').notNull(),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+    updatedAt: (0, pg_core_1.timestamp)('updated_at').defaultNow().notNull(),
+});
+exports.projects = (0, pg_core_1.pgTable)('projects', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }).notNull(),
+    name: (0, pg_core_1.text)('name').notNull(),
+    description: (0, pg_core_1.text)('description'),
+    timezone: (0, pg_core_1.text)('timezone').default('Asia/Ho_Chi_Minh').notNull(),
+    startDate: (0, pg_core_1.timestamp)('start_date'),
+    endDate: (0, pg_core_1.timestamp)('end_date'),
+    status: (0, exports.projectStatusEnum)('status').default('planning').notNull(),
+    captureInterval: (0, pg_core_1.integer)('capture_interval').default(60).notNull(), // minutes
+    resolution: (0, pg_core_1.text)('resolution').default('1920x1080').notNull(),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+    updatedAt: (0, pg_core_1.timestamp)('updated_at').defaultNow().notNull(),
+}, (table) => ({
+    orgStatusIdx: (0, pg_core_1.index)('projects_org_status_idx').on(table.orgId, table.status),
+}));
+// ---- Identity ----
+exports.users = (0, pg_core_1.pgTable)('users', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    email: (0, pg_core_1.text)('email').notNull(),
+    name: (0, pg_core_1.text)('name').notNull(),
+    avatarUrl: (0, pg_core_1.text)('avatar_url'),
+    provider: (0, pg_core_1.text)('provider').default('email').notNull(), // 'google' | 'email'
+    emailVerified: (0, pg_core_1.boolean)('email_verified').default(false).notNull(),
+    passwordHash: (0, pg_core_1.text)('password_hash'), // null for OAuth users
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+    updatedAt: (0, pg_core_1.timestamp)('updated_at').defaultNow().notNull(),
+}, (table) => ({
+    emailIdx: (0, pg_core_1.uniqueIndex)('users_email_idx').on(table.email),
+}));
+exports.memberships = (0, pg_core_1.pgTable)('memberships', {
+    id: (0, pg_core_1.serial)('id').primaryKey(),
+    userId: (0, pg_core_1.text)('user_id').references(() => exports.users.id, { onDelete: 'cascade' }).notNull(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }).notNull(),
+    role: (0, exports.userRoleEnum)('role').notNull(),
+    invitedBy: (0, pg_core_1.text)('invited_by').references(() => exports.users.id),
+    joinedAt: (0, pg_core_1.timestamp)('joined_at').defaultNow().notNull(),
+}, (table) => ({
+    userOrgIdx: (0, pg_core_1.uniqueIndex)('memberships_user_org_idx').on(table.userId, table.orgId),
+}));
+exports.sessions = (0, pg_core_1.pgTable)('sessions', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    userId: (0, pg_core_1.text)('user_id').references(() => exports.users.id, { onDelete: 'cascade' }).notNull(),
+    refreshTokenHash: (0, pg_core_1.text)('refresh_token_hash').notNull(),
+    expiresAt: (0, pg_core_1.timestamp)('expires_at').notNull(),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+    userAgent: (0, pg_core_1.text)('user_agent'),
+    ipAddress: (0, pg_core_1.text)('ip_address'),
+});
+exports.magicLinks = (0, pg_core_1.pgTable)('magic_links', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    email: (0, pg_core_1.text)('email').notNull(),
+    tokenHash: (0, pg_core_1.text)('token_hash').notNull(),
+    expiresAt: (0, pg_core_1.timestamp)('expires_at').notNull(),
+    usedAt: (0, pg_core_1.timestamp)('used_at'),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+}, (table) => ({
+    emailExpiresIdx: (0, pg_core_1.index)('magic_links_email_expires_idx').on(table.email, table.expiresAt),
+}));
+// ---- Devices ----
+exports.devices = (0, pg_core_1.pgTable)('devices', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'cascade' }).notNull(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }).notNull(),
+    serialNo: (0, pg_core_1.text)('serial_no').notNull(),
+    name: (0, pg_core_1.text)('name').notNull(),
+    apiKeyHash: (0, pg_core_1.text)('api_key_hash').notNull(),
+    firmwareVersion: (0, pg_core_1.text)('firmware_version'),
+    lastSeenAt: (0, pg_core_1.timestamp)('last_seen_at'),
+    status: (0, exports.deviceStatusEnum)('status').default('offline').notNull(),
+    config: (0, pg_core_1.jsonb)('config').$type().default({
+        captureIntervalMinutes: 60,
+        resolution: '1920x1080',
+        quality: 85,
+        uploadOnWifiOnly: false,
+        nightModeEnabled: false,
+        nightModeStart: '20:00',
+        nightModeEnd: '06:00',
+        maxStorageGb: 64,
+        heartbeatIntervalSeconds: 300,
+        timezone: 'Asia/Ho_Chi_Minh',
+    }).notNull(),
+    claimCode: (0, pg_core_1.text)('claim_code'),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+    updatedAt: (0, pg_core_1.timestamp)('updated_at').defaultNow().notNull(),
+}, (table) => ({
+    projectStatusIdx: (0, pg_core_1.index)('devices_project_status_idx').on(table.projectId, table.status),
+    serialNoIdx: (0, pg_core_1.uniqueIndex)('devices_serial_no_idx').on(table.serialNo),
+}));
+exports.deviceHeartbeats = (0, pg_core_1.pgTable)('device_heartbeats', {
+    id: (0, pg_core_1.serial)('id').primaryKey(),
+    deviceId: (0, pg_core_1.text)('device_id').references(() => exports.devices.id, { onDelete: 'cascade' }).notNull(),
+    heartbeatAt: (0, pg_core_1.timestamp)('heartbeat_at').defaultNow().notNull(),
+    tempC: (0, pg_core_1.integer)('temp_c'),
+    batteryPct: (0, pg_core_1.integer)('battery_pct'),
+    storageFreeGb: (0, pg_core_1.integer)('storage_free_gb'),
+    capturesToday: (0, pg_core_1.integer)('captures_today').default(0),
+    lastCaptureAt: (0, pg_core_1.timestamp)('last_capture_at'),
+    networkStatus: (0, pg_core_1.text)('network_status').default('online'),
+}, (table) => ({
+    deviceHeartbeatIdx: (0, pg_core_1.index)('device_heartbeats_device_idx').on(table.deviceId, table.heartbeatAt),
+}));
+// ---- Captures ----
+exports.captures = (0, pg_core_1.pgTable)('captures', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'cascade' }).notNull(),
+    deviceId: (0, pg_core_1.text)('device_id').references(() => exports.devices.id, { onDelete: 'cascade' }).notNull(),
+    capturedAt: (0, pg_core_1.timestamp)('captured_at').notNull(),
+    uploadedAt: (0, pg_core_1.timestamp)('uploaded_at'),
+    fileKey: (0, pg_core_1.text)('file_key'),
+    thumbnailKey: (0, pg_core_1.text)('thumbnail_key'),
+    checksum: (0, pg_core_1.text)('checksum'),
+    resolution: (0, pg_core_1.text)('resolution'),
+    fileSizeBytes: (0, pg_core_1.integer)('file_size_bytes'),
+    exposureMs: (0, pg_core_1.integer)('exposure_ms'),
+    iso: (0, pg_core_1.integer)('iso'),
+    aperture: (0, pg_core_1.text)('aperture'),
+    gpsLat: (0, pg_core_1.text)('gps_lat'),
+    gpsLng: (0, pg_core_1.text)('gps_lng'),
+    status: (0, exports.captureStatusEnum)('status').default('pending').notNull(),
+    metadata: (0, pg_core_1.jsonb)('metadata').default({}).notNull(),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+}, (table) => ({
+    deviceCapturedIdx: (0, pg_core_1.index)('captures_device_captured_idx').on(table.deviceId, table.capturedAt),
+    projectCapturedIdx: (0, pg_core_1.index)('captures_project_captured_idx').on(table.projectId, table.capturedAt),
+    statusUploadedIdx: (0, pg_core_1.index)('captures_status_uploaded_idx').on(table.status, table.uploadedAt),
+}));
+// ---- Videos ----
+exports.videos = (0, pg_core_1.pgTable)('videos', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'cascade' }).notNull(),
+    periodStart: (0, pg_core_1.timestamp)('period_start').notNull(),
+    periodEnd: (0, pg_core_1.timestamp)('period_end').notNull(),
+    fps: (0, pg_core_1.integer)('fps').default(24).notNull(),
+    resolution: (0, pg_core_1.text)('resolution').default('1920x1080').notNull(),
+    fileKey: (0, pg_core_1.text)('file_key'),
+    thumbnailKey: (0, pg_core_1.text)('thumbnail_key'),
+    durationSec: (0, pg_core_1.integer)('duration_sec'),
+    status: (0, exports.videoStatusEnum)('status').default('pending').notNull(),
+    generatedAt: (0, pg_core_1.timestamp)('generated_at'),
+    fileSizeBytes: (0, pg_core_1.integer)('file_size_bytes'),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+}, (table) => ({
+    projectGeneratedIdx: (0, pg_core_1.index)('videos_project_generated_idx').on(table.projectId, table.generatedAt),
+}));
+exports.videoJobs = (0, pg_core_1.pgTable)('video_jobs', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'cascade' }).notNull(),
+    videoId: (0, pg_core_1.text)('video_id').references(() => exports.videos.id, { onDelete: 'cascade' }),
+    jobType: (0, pg_core_1.text)('job_type').notNull(), // 'daily', 'weekly', 'custom'
+    params: (0, pg_core_1.jsonb)('params').default({}).notNull(),
+    status: (0, pg_core_1.text)('status').default('pending').notNull(),
+    attempts: (0, pg_core_1.integer)('attempts').default(0),
+    startedAt: (0, pg_core_1.timestamp)('started_at'),
+    completedAt: (0, pg_core_1.timestamp)('completed_at'),
+    errorMsg: (0, pg_core_1.text)('error_msg'),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+});
+// ---- Commands ----
+exports.commands = (0, pg_core_1.pgTable)('commands', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    deviceId: (0, pg_core_1.text)('device_id').references(() => exports.devices.id, { onDelete: 'cascade' }).notNull(),
+    commandType: (0, pg_core_1.text)('command_type').notNull(), // DeviceCommandType
+    payload: (0, pg_core_1.jsonb)('payload'),
+    scheduledAt: (0, pg_core_1.timestamp)('scheduled_at'),
+    queuedAt: (0, pg_core_1.timestamp)('queued_at').defaultNow().notNull(),
+    deliveredAt: (0, pg_core_1.timestamp)('delivered_at'),
+    acknowledgedAt: (0, pg_core_1.timestamp)('acknowledged_at'),
+    resultStatus: (0, exports.commandResultEnum)('result_status').default('pending').notNull(),
+    resultData: (0, pg_core_1.jsonb)('result_data'),
+}, (table) => ({
+    deviceQueuedIdx: (0, pg_core_1.index)('commands_device_queued_idx').on(table.deviceId, table.queuedAt),
+    pendingIdx: (0, pg_core_1.index)('commands_pending_idx').on(table.resultStatus, table.scheduledAt),
+}));
+// ---- Alerts ----
+exports.alertRules = (0, pg_core_1.pgTable)('alert_rules', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }).notNull(),
+    name: (0, pg_core_1.text)('name').notNull(),
+    type: (0, exports.alertTypeEnum)('type').notNull(),
+    condition: (0, pg_core_1.jsonb)('condition').notNull(),
+    cooldownMinutes: (0, pg_core_1.integer)('cooldown_minutes').default(15),
+    enabled: (0, pg_core_1.boolean)('enabled').default(true).notNull(),
+    notifyEmail: (0, pg_core_1.boolean)('notify_email').default(false).notNull(),
+    notifySms: (0, pg_core_1.boolean)('notify_sms').default(false).notNull(),
+    webhookUrl: (0, pg_core_1.text)('webhook_url'),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+});
+exports.alerts = (0, pg_core_1.pgTable)('alerts', {
+    id: (0, pg_core_1.text)('id').primaryKey(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }).notNull(),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'set null' }),
+    deviceId: (0, pg_core_1.text)('device_id').references(() => exports.devices.id, { onDelete: 'set null' }),
+    type: (0, exports.alertTypeEnum)('type').notNull(),
+    severity: (0, exports.alertSeverityEnum)('severity').notNull(),
+    message: (0, pg_core_1.text)('message').notNull(),
+    data: (0, pg_core_1.jsonb)('data').default({}).notNull(),
+    state: (0, exports.alertStateEnum)('state').default('open').notNull(),
+    openedAt: (0, pg_core_1.timestamp)('opened_at').defaultNow().notNull(),
+    acknowledgedAt: (0, pg_core_1.timestamp)('acknowledged_at'),
+    acknowledgedBy: (0, pg_core_1.text)('acknowledged_by').references(() => exports.users.id),
+    resolvedAt: (0, pg_core_1.timestamp)('resolved_at'),
+}, (table) => ({
+    orgStateIdx: (0, pg_core_1.index)('alerts_org_state_idx').on(table.orgId, table.state),
+    projectIdx: (0, pg_core_1.index)('alerts_project_idx').on(table.projectId),
+}));
+// ---- Audit ----
+exports.auditLogs = (0, pg_core_1.pgTable)('audit_logs', {
+    id: (0, pg_core_1.serial)('id').primaryKey(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }),
+    userId: (0, pg_core_1.text)('user_id').references(() => exports.users.id, { onDelete: 'set null' }),
+    action: (0, pg_core_1.text)('action').notNull(),
+    resourceType: (0, pg_core_1.text)('resource_type').notNull(),
+    resourceId: (0, pg_core_1.text)('resource_id'),
+    ipAddress: (0, pg_core_1.text)('ip_address'),
+    userAgent: (0, pg_core_1.text)('user_agent'),
+    metadata: (0, pg_core_1.jsonb)('metadata').default({}),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+}, (table) => ({
+    orgCreatedIdx: (0, pg_core_1.index)('audit_logs_org_created_idx').on(table.orgId, table.createdAt),
+}));
+exports.activityLogs = (0, pg_core_1.pgTable)('activity_logs', {
+    id: (0, pg_core_1.serial)('id').primaryKey(),
+    orgId: (0, pg_core_1.text)('org_id').references(() => exports.organizations.id, { onDelete: 'cascade' }),
+    projectId: (0, pg_core_1.text)('project_id').references(() => exports.projects.id, { onDelete: 'set null' }),
+    userId: (0, pg_core_1.text)('user_id').references(() => exports.users.id, { onDelete: 'set null' }),
+    eventType: (0, pg_core_1.text)('event_type').notNull(),
+    metadata: (0, pg_core_1.jsonb)('metadata').default({}),
+    createdAt: (0, pg_core_1.timestamp)('created_at').defaultNow().notNull(),
+}, (table) => ({
+    projectCreatedIdx: (0, pg_core_1.index)('activity_logs_project_created_idx').on(table.projectId, table.createdAt),
+}));
+// ---- Relations ----
+exports.organizationsRelations = (0, drizzle_orm_1.relations)(exports.organizations, ({ many }) => ({
+    projects: many(exports.projects),
+    memberships: many(exports.memberships),
+}));
+exports.projectsRelations = (0, drizzle_orm_1.relations)(exports.projects, ({ one, many }) => ({
+    organization: one(exports.organizations, { fields: [exports.projects.orgId], references: [exports.organizations.id] }),
+    devices: many(exports.devices),
+    captures: many(exports.captures),
+    videos: many(exports.videos),
+    alerts: many(exports.alerts),
+}));
+exports.usersRelations = (0, drizzle_orm_1.relations)(exports.users, ({ many }) => ({
+    memberships: many(exports.memberships),
+    sessions: many(exports.sessions),
+}));
+exports.membershipsRelations = (0, drizzle_orm_1.relations)(exports.memberships, ({ one }) => ({
+    user: one(exports.users, { fields: [exports.memberships.userId], references: [exports.users.id] }),
+    organization: one(exports.organizations, { fields: [exports.memberships.orgId], references: [exports.organizations.id] }),
+}));
+exports.devicesRelations = (0, drizzle_orm_1.relations)(exports.devices, ({ one, many }) => ({
+    project: one(exports.projects, { fields: [exports.devices.projectId], references: [exports.projects.id] }),
+    organization: one(exports.organizations, { fields: [exports.devices.orgId], references: [exports.organizations.id] }),
+    captures: many(exports.captures),
+    heartbeats: many(exports.deviceHeartbeats),
+    commands: many(exports.commands),
+    alerts: many(exports.alerts),
+}));
+exports.capturesRelations = (0, drizzle_orm_1.relations)(exports.captures, ({ one }) => ({
+    project: one(exports.projects, { fields: [exports.captures.projectId], references: [exports.projects.id] }),
+    device: one(exports.devices, { fields: [exports.captures.deviceId], references: [exports.devices.id] }),
+}));
+exports.videosRelations = (0, drizzle_orm_1.relations)(exports.videos, ({ one }) => ({
+    project: one(exports.projects, { fields: [exports.videos.projectId], references: [exports.projects.id] }),
+}));
+//# sourceMappingURL=schema.js.map

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
apps/api-server/src/db/schema.js.map


+ 324 - 0
apps/api-server/src/db/schema.ts

@@ -0,0 +1,324 @@
+import { pgTable, text, timestamp, boolean, integer, jsonb, serial, pgEnum, index, uniqueIndex } from 'drizzle-orm/pg-core'
+import { relations } from 'drizzle-orm'
+
+// ---- Enums ----
+export const orgStatusEnum = pgEnum('org_status', ['active', 'suspended', 'trial'])
+export const projectStatusEnum = pgEnum('project_status', ['planning', 'active', 'paused', 'completed', 'archived'])
+export const deviceStatusEnum = pgEnum('device_status', ['offline', 'online', 'capturing', 'uploading', 'degraded', 'updating', 'error'])
+export const captureStatusEnum = pgEnum('capture_status', ['pending', 'uploaded', 'processing', 'ready', 'failed'])
+export const videoStatusEnum = pgEnum('video_status', ['pending', 'processing', 'ready', 'failed'])
+export const alertSeverityEnum = pgEnum('alert_severity', ['info', 'warning', 'error', 'critical'])
+export const alertTypeEnum = pgEnum('alert_type', ['device_offline', 'device_error', 'storage_full', 'upload_failed', 'capture_missed', 'video_failed', 'firmware_update_available'])
+export const alertStateEnum = pgEnum('alert_state', ['open', 'acknowledged', 'resolved'])
+export const userRoleEnum = pgEnum('user_role', ['super_admin', 'org_admin', 'project_manager', 'viewer'])
+export const commandResultEnum = pgEnum('command_result_status', ['pending', 'delivered', 'acknowledged', 'success', 'failed', 'timeout'])
+
+// ---- Tenancy ----
+export const organizations = pgTable('organizations', {
+  id: text('id').primaryKey(), // nanoid
+  name: text('name').notNull(),
+  status: orgStatusEnum('status').default('trial').notNull(),
+  planTier: text('plan_tier').default('free').notNull(),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+  updatedAt: timestamp('updated_at').defaultNow().notNull(),
+})
+
+export const projects = pgTable('projects', {
+  id: text('id').primaryKey(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
+  name: text('name').notNull(),
+  description: text('description'),
+  timezone: text('timezone').default('Asia/Ho_Chi_Minh').notNull(),
+  startDate: timestamp('start_date'),
+  endDate: timestamp('end_date'),
+  status: projectStatusEnum('status').default('planning').notNull(),
+  captureInterval: integer('capture_interval').default(60).notNull(), // minutes
+  resolution: text('resolution').default('1920x1080').notNull(),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+  updatedAt: timestamp('updated_at').defaultNow().notNull(),
+}, (table) => ({
+  orgStatusIdx: index('projects_org_status_idx').on(table.orgId, table.status),
+}))
+
+// ---- Identity ----
+export const users = pgTable('users', {
+  id: text('id').primaryKey(),
+  email: text('email').notNull(),
+  name: text('name').notNull(),
+  avatarUrl: text('avatar_url'),
+  provider: text('provider').default('email').notNull(), // 'google' | 'email'
+  emailVerified: boolean('email_verified').default(false).notNull(),
+  passwordHash: text('password_hash'), // null for OAuth users
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+  updatedAt: timestamp('updated_at').defaultNow().notNull(),
+}, (table) => ({
+  emailIdx: uniqueIndex('users_email_idx').on(table.email),
+}))
+
+export const memberships = pgTable('memberships', {
+  id: serial('id').primaryKey(),
+  userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
+  role: userRoleEnum('role').notNull(),
+  invitedBy: text('invited_by').references(() => users.id),
+  joinedAt: timestamp('joined_at').defaultNow().notNull(),
+}, (table) => ({
+  userOrgIdx: uniqueIndex('memberships_user_org_idx').on(table.userId, table.orgId),
+}))
+
+export const sessions = pgTable('sessions', {
+  id: text('id').primaryKey(),
+  userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
+  refreshTokenHash: text('refresh_token_hash').notNull(),
+  expiresAt: timestamp('expires_at').notNull(),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+  userAgent: text('user_agent'),
+  ipAddress: text('ip_address'),
+})
+
+export const magicLinks = pgTable('magic_links', {
+  id: text('id').primaryKey(),
+  email: text('email').notNull(),
+  tokenHash: text('token_hash').notNull(),
+  expiresAt: timestamp('expires_at').notNull(),
+  usedAt: timestamp('used_at'),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+}, (table) => ({
+  emailExpiresIdx: index('magic_links_email_expires_idx').on(table.email, table.expiresAt),
+}))
+
+// ---- Devices ----
+export const devices = pgTable('devices', {
+  id: text('id').primaryKey(),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
+  serialNo: text('serial_no').notNull(),
+  name: text('name').notNull(),
+  apiKeyHash: text('api_key_hash').notNull(),
+  firmwareVersion: text('firmware_version'),
+  lastSeenAt: timestamp('last_seen_at'),
+  status: deviceStatusEnum('status').default('offline').notNull(),
+  config: jsonb('config').$type<{
+    captureIntervalMinutes: number
+    resolution: string
+    quality: number
+    uploadOnWifiOnly: boolean
+    nightModeEnabled: boolean
+    nightModeStart: string
+    nightModeEnd: string
+    maxStorageGb: number
+    heartbeatIntervalSeconds: number
+    timezone: string
+  }>().default({
+    captureIntervalMinutes: 60,
+    resolution: '1920x1080',
+    quality: 85,
+    uploadOnWifiOnly: false,
+    nightModeEnabled: false,
+    nightModeStart: '20:00',
+    nightModeEnd: '06:00',
+    maxStorageGb: 64,
+    heartbeatIntervalSeconds: 300,
+    timezone: 'Asia/Ho_Chi_Minh',
+  }).notNull(),
+  claimCode: text('claim_code'),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+  updatedAt: timestamp('updated_at').defaultNow().notNull(),
+}, (table) => ({
+  projectStatusIdx: index('devices_project_status_idx').on(table.projectId, table.status),
+  serialNoIdx: uniqueIndex('devices_serial_no_idx').on(table.serialNo),
+}))
+
+export const deviceHeartbeats = pgTable('device_heartbeats', {
+  id: serial('id').primaryKey(),
+  deviceId: text('device_id').references(() => devices.id, { onDelete: 'cascade' }).notNull(),
+  heartbeatAt: timestamp('heartbeat_at').defaultNow().notNull(),
+  tempC: integer('temp_c'),
+  batteryPct: integer('battery_pct'),
+  storageFreeGb: integer('storage_free_gb'),
+  capturesToday: integer('captures_today').default(0),
+  lastCaptureAt: timestamp('last_capture_at'),
+  networkStatus: text('network_status').default('online'),
+}, (table) => ({
+  deviceHeartbeatIdx: index('device_heartbeats_device_idx').on(table.deviceId, table.heartbeatAt),
+}))
+
+// ---- Captures ----
+export const captures = pgTable('captures', {
+  id: text('id').primaryKey(),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
+  deviceId: text('device_id').references(() => devices.id, { onDelete: 'cascade' }).notNull(),
+  capturedAt: timestamp('captured_at').notNull(),
+  uploadedAt: timestamp('uploaded_at'),
+  fileKey: text('file_key'),
+  thumbnailKey: text('thumbnail_key'),
+  checksum: text('checksum'),
+  resolution: text('resolution'),
+  fileSizeBytes: integer('file_size_bytes'),
+  exposureMs: integer('exposure_ms'),
+  iso: integer('iso'),
+  aperture: text('aperture'),
+  gpsLat: text('gps_lat'),
+  gpsLng: text('gps_lng'),
+  status: captureStatusEnum('status').default('pending').notNull(),
+  metadata: jsonb('metadata').default({}).notNull(),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+}, (table) => ({
+  deviceCapturedIdx: index('captures_device_captured_idx').on(table.deviceId, table.capturedAt),
+  projectCapturedIdx: index('captures_project_captured_idx').on(table.projectId, table.capturedAt),
+  statusUploadedIdx: index('captures_status_uploaded_idx').on(table.status, table.uploadedAt),
+}))
+
+// ---- Videos ----
+export const videos = pgTable('videos', {
+  id: text('id').primaryKey(),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
+  periodStart: timestamp('period_start').notNull(),
+  periodEnd: timestamp('period_end').notNull(),
+  fps: integer('fps').default(24).notNull(),
+  resolution: text('resolution').default('1920x1080').notNull(),
+  fileKey: text('file_key'),
+  thumbnailKey: text('thumbnail_key'),
+  durationSec: integer('duration_sec'),
+  status: videoStatusEnum('status').default('pending').notNull(),
+  generatedAt: timestamp('generated_at'),
+  fileSizeBytes: integer('file_size_bytes'),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+}, (table) => ({
+  projectGeneratedIdx: index('videos_project_generated_idx').on(table.projectId, table.generatedAt),
+}))
+
+export const videoJobs = pgTable('video_jobs', {
+  id: text('id').primaryKey(),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
+  videoId: text('video_id').references(() => videos.id, { onDelete: 'cascade' }),
+  jobType: text('job_type').notNull(), // 'daily', 'weekly', 'custom'
+  params: jsonb('params').default({}).notNull(),
+  status: text('status').default('pending').notNull(),
+  attempts: integer('attempts').default(0),
+  startedAt: timestamp('started_at'),
+  completedAt: timestamp('completed_at'),
+  errorMsg: text('error_msg'),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+})
+
+// ---- Commands ----
+export const commands = pgTable('commands', {
+  id: text('id').primaryKey(),
+  deviceId: text('device_id').references(() => devices.id, { onDelete: 'cascade' }).notNull(),
+  commandType: text('command_type').notNull(), // DeviceCommandType
+  payload: jsonb('payload'),
+  scheduledAt: timestamp('scheduled_at'),
+  queuedAt: timestamp('queued_at').defaultNow().notNull(),
+  deliveredAt: timestamp('delivered_at'),
+  acknowledgedAt: timestamp('acknowledged_at'),
+  resultStatus: commandResultEnum('result_status').default('pending').notNull(),
+  resultData: jsonb('result_data'),
+}, (table) => ({
+  deviceQueuedIdx: index('commands_device_queued_idx').on(table.deviceId, table.queuedAt),
+  pendingIdx: index('commands_pending_idx').on(table.resultStatus, table.scheduledAt),
+}))
+
+// ---- Alerts ----
+export const alertRules = pgTable('alert_rules', {
+  id: text('id').primaryKey(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
+  name: text('name').notNull(),
+  type: alertTypeEnum('type').notNull(),
+  condition: jsonb('condition').notNull(),
+  cooldownMinutes: integer('cooldown_minutes').default(15),
+  enabled: boolean('enabled').default(true).notNull(),
+  notifyEmail: boolean('notify_email').default(false).notNull(),
+  notifySms: boolean('notify_sms').default(false).notNull(),
+  webhookUrl: text('webhook_url'),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+})
+
+export const alerts = pgTable('alerts', {
+  id: text('id').primaryKey(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'set null' }),
+  deviceId: text('device_id').references(() => devices.id, { onDelete: 'set null' }),
+  type: alertTypeEnum('type').notNull(),
+  severity: alertSeverityEnum('severity').notNull(),
+  message: text('message').notNull(),
+  data: jsonb('data').default({}).notNull(),
+  state: alertStateEnum('state').default('open').notNull(),
+  openedAt: timestamp('opened_at').defaultNow().notNull(),
+  acknowledgedAt: timestamp('acknowledged_at'),
+  acknowledgedBy: text('acknowledged_by').references(() => users.id),
+  resolvedAt: timestamp('resolved_at'),
+}, (table) => ({
+  orgStateIdx: index('alerts_org_state_idx').on(table.orgId, table.state),
+  projectIdx: index('alerts_project_idx').on(table.projectId),
+}))
+
+// ---- Audit ----
+export const auditLogs = pgTable('audit_logs', {
+  id: serial('id').primaryKey(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }),
+  userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
+  action: text('action').notNull(),
+  resourceType: text('resource_type').notNull(),
+  resourceId: text('resource_id'),
+  ipAddress: text('ip_address'),
+  userAgent: text('user_agent'),
+  metadata: jsonb('metadata').default({}),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+}, (table) => ({
+  orgCreatedIdx: index('audit_logs_org_created_idx').on(table.orgId, table.createdAt),
+}))
+
+export const activityLogs = pgTable('activity_logs', {
+  id: serial('id').primaryKey(),
+  orgId: text('org_id').references(() => organizations.id, { onDelete: 'cascade' }),
+  projectId: text('project_id').references(() => projects.id, { onDelete: 'set null' }),
+  userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
+  eventType: text('event_type').notNull(),
+  metadata: jsonb('metadata').default({}),
+  createdAt: timestamp('created_at').defaultNow().notNull(),
+}, (table) => ({
+  projectCreatedIdx: index('activity_logs_project_created_idx').on(table.projectId, table.createdAt),
+}))
+
+// ---- Relations ----
+export const organizationsRelations = relations(organizations, ({ many }) => ({
+  projects: many(projects),
+  memberships: many(memberships),
+}))
+
+export const projectsRelations = relations(projects, ({ one, many }) => ({
+  organization: one(organizations, { fields: [projects.orgId], references: [organizations.id] }),
+  devices: many(devices),
+  captures: many(captures),
+  videos: many(videos),
+  alerts: many(alerts),
+}))
+
+export const usersRelations = relations(users, ({ many }) => ({
+  memberships: many(memberships),
+  sessions: many(sessions),
+}))
+
+export const membershipsRelations = relations(memberships, ({ one }) => ({
+  user: one(users, { fields: [memberships.userId], references: [users.id] }),
+  organization: one(organizations, { fields: [memberships.orgId], references: [organizations.id] }),
+}))
+
+export const devicesRelations = relations(devices, ({ one, many }) => ({
+  project: one(projects, { fields: [devices.projectId], references: [projects.id] }),
+  organization: one(organizations, { fields: [devices.orgId], references: [organizations.id] }),
+  captures: many(captures),
+  heartbeats: many(deviceHeartbeats),
+  commands: many(commands),
+  alerts: many(alerts),
+}))
+
+export const capturesRelations = relations(captures, ({ one }) => ({
+  project: one(projects, { fields: [captures.projectId], references: [projects.id] }),
+  device: one(devices, { fields: [captures.deviceId], references: [devices.id] }),
+}))
+
+export const videosRelations = relations(videos, ({ one }) => ({
+  project: one(projects, { fields: [videos.projectId], references: [projects.id] }),
+}))

+ 2 - 0
apps/api-server/src/main.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=main.d.ts.map

+ 1 - 0
apps/api-server/src/main.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":""}

+ 23 - 0
apps/api-server/src/main.js

@@ -0,0 +1,23 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const core_1 = require("@nestjs/core");
+const common_1 = require("@nestjs/common");
+const app_module_1 = require("./app.module");
+async function bootstrap() {
+    const app = await core_1.NestFactory.create(app_module_1.AppModule);
+    app.setGlobalPrefix('v1');
+    app.useGlobalPipes(new common_1.ValidationPipe({
+        whitelist: true,
+        transform: true,
+        forbidNonWhitelisted: true,
+    }));
+    app.enableCors({
+        origin: process.env['CORS_ORIGIN'] || 'http://localhost:3000',
+        credentials: true,
+    });
+    const port = process.env['PORT'] || 3001;
+    await app.listen(port);
+    console.log(`API running on http://localhost:${port}`);
+}
+bootstrap();
+//# sourceMappingURL=main.js.map

+ 1 - 0
apps/api-server/src/main.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";;AAAA,uCAA0C;AAC1C,2CAA+C;AAC/C,6CAAwC;AAExC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAA;IAE/C,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IACzB,GAAG,CAAC,cAAc,CAChB,IAAI,uBAAc,CAAC;QACjB,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,IAAI;KAC3B,CAAC,CACH,CAAA;IACD,GAAG,CAAC,UAAU,CAAC;QACb,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,uBAAuB;QAC7D,WAAW,EAAE,IAAI;KAClB,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAA;IACxC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACtB,OAAO,CAAC,GAAG,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC;AAED,SAAS,EAAE,CAAA"}

+ 26 - 0
apps/api-server/src/main.ts

@@ -0,0 +1,26 @@
+import { NestFactory } from '@nestjs/core'
+import { ValidationPipe } from '@nestjs/common'
+import { AppModule } from './app.module'
+
+async function bootstrap() {
+  const app = await NestFactory.create(AppModule)
+
+  app.setGlobalPrefix('v1')
+  app.useGlobalPipes(
+    new ValidationPipe({
+      whitelist: true,
+      transform: true,
+      forbidNonWhitelisted: true,
+    }),
+  )
+  app.enableCors({
+    origin: process.env['CORS_ORIGIN'] || 'http://localhost:3000',
+    credentials: true,
+  })
+
+  const port = process.env['PORT'] || 3001
+  await app.listen(port)
+  console.log(`API running on http://localhost:${port}`)
+}
+
+bootstrap()

+ 3 - 0
apps/api-server/src/modules/alerts/alerts.module.d.ts

@@ -0,0 +1,3 @@
+export declare class AlertsModule {
+}
+//# sourceMappingURL=alerts.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/alerts/alerts.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"alerts.module.d.ts","sourceRoot":"","sources":["alerts.module.ts"],"names":[],"mappings":"AAEA,qBAKa,YAAY;CAAG"}

+ 61 - 0
apps/api-server/src/modules/alerts/alerts.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AlertsModule = void 0;
+const common_1 = require("@nestjs/common");
+let AlertsModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var AlertsModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            AlertsModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return AlertsModule = _classThis;
+})();
+exports.AlertsModule = AlertsModule;
+//# sourceMappingURL=alerts.module.js.map

+ 1 - 0
apps/api-server/src/modules/alerts/alerts.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"alerts.module.js","sourceRoot":"","sources":["alerts.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,YAAY;4BALxB,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA4B;;;YAAf,uDAAY;;;;;AAAZ,oCAAY"}

+ 8 - 0
apps/api-server/src/modules/alerts/alerts.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  controllers: [],
+  providers: [],
+  exports: [],
+})
+export class AlertsModule {}

+ 29 - 0
apps/api-server/src/modules/auth/auth.controller.d.ts

@@ -0,0 +1,29 @@
+import { AuthService } from './auth.service';
+export declare class AuthController {
+    private readonly authService;
+    constructor(authService: AuthService);
+    health(): {
+        module: string;
+        status: string;
+        phase: string;
+    };
+    me(): {
+        id: string;
+        email: string;
+        name: string;
+        roles: string[];
+    };
+    googleAuth(): {
+        message: string;
+    };
+    googleCallback(): {
+        message: string;
+    };
+    refresh(): {
+        message: string;
+    };
+    logout(): {
+        message: string;
+    };
+}
+//# sourceMappingURL=auth.controller.d.ts.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.controller.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["auth.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,qBACa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,WAAW;IAGrD,MAAM;;;;;IAKN,EAAE;;;;;;IAKF,UAAU;;;IAKV,cAAc;;;IAKd,OAAO;;;IAKP,MAAM;;;CAGP"}

+ 98 - 0
apps/api-server/src/modules/auth/auth.controller.js

@@ -0,0 +1,98 @@
+"use strict";
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AuthController = void 0;
+const common_1 = require("@nestjs/common");
+let AuthController = (() => {
+    let _classDecorators = [(0, common_1.Controller)('auth')];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    let _instanceExtraInitializers = [];
+    let _health_decorators;
+    let _me_decorators;
+    let _googleAuth_decorators;
+    let _googleCallback_decorators;
+    let _refresh_decorators;
+    let _logout_decorators;
+    var AuthController = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            _health_decorators = [(0, common_1.Get)('health')];
+            _me_decorators = [(0, common_1.Get)('me')];
+            _googleAuth_decorators = [(0, common_1.Get)('google')];
+            _googleCallback_decorators = [(0, common_1.Get)('google/callback')];
+            _refresh_decorators = [(0, common_1.Post)('refresh')];
+            _logout_decorators = [(0, common_1.Post)('logout')];
+            __esDecorate(this, null, _health_decorators, { kind: "method", name: "health", static: false, private: false, access: { has: obj => "health" in obj, get: obj => obj.health }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(this, null, _me_decorators, { kind: "method", name: "me", static: false, private: false, access: { has: obj => "me" in obj, get: obj => obj.me }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(this, null, _googleAuth_decorators, { kind: "method", name: "googleAuth", static: false, private: false, access: { has: obj => "googleAuth" in obj, get: obj => obj.googleAuth }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(this, null, _googleCallback_decorators, { kind: "method", name: "googleCallback", static: false, private: false, access: { has: obj => "googleCallback" in obj, get: obj => obj.googleCallback }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(this, null, _refresh_decorators, { kind: "method", name: "refresh", static: false, private: false, access: { has: obj => "refresh" in obj, get: obj => obj.refresh }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(this, null, _logout_decorators, { kind: "method", name: "logout", static: false, private: false, access: { has: obj => "logout" in obj, get: obj => obj.logout }, metadata: _metadata }, null, _instanceExtraInitializers);
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            AuthController = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+        authService = __runInitializers(this, _instanceExtraInitializers);
+        constructor(authService) {
+            this.authService = authService;
+        }
+        health() {
+            return this.authService.getHealth();
+        }
+        me() {
+            return this.authService.getCurrentUser();
+        }
+        googleAuth() {
+            return { message: 'Google OAuth stub endpoint (Phase 1)' };
+        }
+        googleCallback() {
+            return { message: 'Google OAuth callback stub endpoint (Phase 1)' };
+        }
+        refresh() {
+            return { message: 'Refresh token stub endpoint (Phase 1)' };
+        }
+        logout() {
+            return { message: 'Logout stub endpoint (Phase 1)' };
+        }
+    };
+    return AuthController = _classThis;
+})();
+exports.AuthController = AuthController;
+//# sourceMappingURL=auth.controller.js.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.controller.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["auth.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAsD;IAIzC,cAAc;4BAD1B,IAAA,mBAAU,EAAC,MAAM,CAAC;;;;;;;;;;;;;;;kCAIhB,IAAA,YAAG,EAAC,QAAQ,CAAC;8BAKb,IAAA,YAAG,EAAC,IAAI,CAAC;sCAKT,IAAA,YAAG,EAAC,QAAQ,CAAC;0CAKb,IAAA,YAAG,EAAC,iBAAiB,CAAC;mCAKtB,IAAA,aAAI,EAAC,SAAS,CAAC;kCAKf,IAAA,aAAI,EAAC,QAAQ,CAAC;YAxBf,qKAAA,MAAM,6DAEL;YAGD,yJAAA,EAAE,6DAED;YAGD,iLAAA,UAAU,6DAET;YAGD,6LAAA,cAAc,6DAEb;YAGD,wKAAA,OAAO,6DAEN;YAGD,qKAAA,MAAM,6DAEL;YA/BH,6KAgCC;;;YAhCY,uDAAc;;QACI,WAAW,GAD7B,mDAAc;QACzB,YAA6B,WAAwB;YAAxB,gBAAW,GAAX,WAAW,CAAa;QAAG,CAAC;QAGzD,MAAM;YACJ,OAAO,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAA;QACrC,CAAC;QAGD,EAAE;YACA,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,CAAA;QAC1C,CAAC;QAGD,UAAU;YACR,OAAO,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAA;QAC5D,CAAC;QAGD,cAAc;YACZ,OAAO,EAAE,OAAO,EAAE,+CAA+C,EAAE,CAAA;QACrE,CAAC;QAGD,OAAO;YACL,OAAO,EAAE,OAAO,EAAE,uCAAuC,EAAE,CAAA;QAC7D,CAAC;QAGD,MAAM;YACJ,OAAO,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAA;QACtD,CAAC;;;;AA/BU,wCAAc"}

+ 37 - 0
apps/api-server/src/modules/auth/auth.controller.ts

@@ -0,0 +1,37 @@
+import { Controller, Get, Post } from '@nestjs/common'
+import { AuthService } from './auth.service'
+
+@Controller('auth')
+export class AuthController {
+  constructor(private readonly authService: AuthService) {}
+
+  @Get('health')
+  health() {
+    return this.authService.getHealth()
+  }
+
+  @Get('me')
+  me() {
+    return this.authService.getCurrentUser()
+  }
+
+  @Get('google')
+  googleAuth() {
+    return { message: 'Google OAuth stub endpoint (Phase 1)' }
+  }
+
+  @Get('google/callback')
+  googleCallback() {
+    return { message: 'Google OAuth callback stub endpoint (Phase 1)' }
+  }
+
+  @Post('refresh')
+  refresh() {
+    return { message: 'Refresh token stub endpoint (Phase 1)' }
+  }
+
+  @Post('logout')
+  logout() {
+    return { message: 'Logout stub endpoint (Phase 1)' }
+  }
+}

+ 3 - 0
apps/api-server/src/modules/auth/auth.module.d.ts

@@ -0,0 +1,3 @@
+export declare class AuthModule {
+}
+//# sourceMappingURL=auth.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["auth.module.ts"],"names":[],"mappings":"AASA,qBAaa,UAAU;CAAG"}

+ 76 - 0
apps/api-server/src/modules/auth/auth.module.js

@@ -0,0 +1,76 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AuthModule = void 0;
+const common_1 = require("@nestjs/common");
+const jwt_1 = require("@nestjs/jwt");
+const passport_1 = require("@nestjs/passport");
+const auth_controller_1 = require("./auth.controller");
+const auth_service_1 = require("./auth.service");
+const jwt_strategy_1 = require("./strategies/jwt.strategy");
+const google_strategy_1 = require("./strategies/google.strategy");
+const orgs_module_1 = require("../orgs/orgs.module");
+let AuthModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            imports: [
+                passport_1.PassportModule.register({ defaultStrategy: 'jwt' }),
+                jwt_1.JwtModule.register({
+                    secret: process.env['JWT_SECRET'] || 'dev-secret-change-in-production',
+                    signOptions: { expiresIn: '15m' },
+                }),
+                orgs_module_1.OrgsModule,
+            ],
+            controllers: [auth_controller_1.AuthController],
+            providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy, google_strategy_1.GoogleStrategy],
+            exports: [auth_service_1.AuthService, jwt_1.JwtModule],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var AuthModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            AuthModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return AuthModule = _classThis;
+})();
+exports.AuthModule = AuthModule;
+//# sourceMappingURL=auth.module.js.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["auth.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;AACvC,qCAAuC;AACvC,+CAAiD;AACjD,uDAAkD;AAClD,iDAA4C;AAC5C,4DAAuD;AACvD,kEAA6D;AAC7D,qDAAgD;IAenC,UAAU;4BAbtB,IAAA,eAAM,EAAC;YACN,OAAO,EAAE;gBACP,yBAAc,CAAC,QAAQ,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;gBACnD,eAAS,CAAC,QAAQ,CAAC;oBACjB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,iCAAiC;oBACtE,WAAW,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;iBAClC,CAAC;gBACF,wBAAU;aACX;YACD,WAAW,EAAE,CAAC,gCAAc,CAAC;YAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,0BAAW,EAAE,gCAAc,CAAC;YACrD,OAAO,EAAE,CAAC,0BAAW,EAAE,eAAS,CAAC;SAClC,CAAC;;;;;;;;YACF,6KAA0B;;;YAAb,uDAAU;;;;;AAAV,gCAAU"}

+ 23 - 0
apps/api-server/src/modules/auth/auth.module.ts

@@ -0,0 +1,23 @@
+import { Module } from '@nestjs/common'
+import { JwtModule } from '@nestjs/jwt'
+import { PassportModule } from '@nestjs/passport'
+import { AuthController } from './auth.controller'
+import { AuthService } from './auth.service'
+import { JwtStrategy } from './strategies/jwt.strategy'
+import { GoogleStrategy } from './strategies/google.strategy'
+import { OrgsModule } from '../orgs/orgs.module'
+
+@Module({
+  imports: [
+    PassportModule.register({ defaultStrategy: 'jwt' }),
+    JwtModule.register({
+      secret: process.env['JWT_SECRET'] || 'dev-secret-change-in-production',
+      signOptions: { expiresIn: '15m' },
+    }),
+    OrgsModule,
+  ],
+  controllers: [AuthController],
+  providers: [AuthService, JwtStrategy, GoogleStrategy],
+  exports: [AuthService, JwtModule],
+})
+export class AuthModule {}

+ 14 - 0
apps/api-server/src/modules/auth/auth.service.d.ts

@@ -0,0 +1,14 @@
+export declare class AuthService {
+    getHealth(): {
+        module: string;
+        status: string;
+        phase: string;
+    };
+    getCurrentUser(): {
+        id: string;
+        email: string;
+        name: string;
+        roles: string[];
+    };
+}
+//# sourceMappingURL=auth.service.d.ts.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.service.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["auth.service.ts"],"names":[],"mappings":"AAEA,qBACa,WAAW;IACtB,SAAS;;;;;IAQT,cAAc;;;;;;CAQf"}

+ 72 - 0
apps/api-server/src/modules/auth/auth.service.js

@@ -0,0 +1,72 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AuthService = void 0;
+const common_1 = require("@nestjs/common");
+let AuthService = (() => {
+    let _classDecorators = [(0, common_1.Injectable)()];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var AuthService = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            AuthService = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+        getHealth() {
+            return {
+                module: 'auth',
+                status: 'ok',
+                phase: 'phase-1-stub',
+            };
+        }
+        getCurrentUser() {
+            return {
+                id: 'stub-user',
+                email: 'stub@example.com',
+                name: 'Stub User',
+                roles: ['viewer'],
+            };
+        }
+    };
+    return AuthService = _classThis;
+})();
+exports.AuthService = AuthService;
+//# sourceMappingURL=auth.service.js.map

+ 1 - 0
apps/api-server/src/modules/auth/auth.service.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["auth.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA2C;IAG9B,WAAW;4BADvB,IAAA,mBAAU,GAAE;;;;;;;;YACb,6KAiBC;;;YAjBY,uDAAW;;QACtB,SAAS;YACP,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,IAAI;gBACZ,KAAK,EAAE,cAAc;aACtB,CAAA;QACH,CAAC;QAED,cAAc;YACZ,OAAO;gBACL,EAAE,EAAE,WAAW;gBACf,KAAK,EAAE,kBAAkB;gBACzB,IAAI,EAAE,WAAW;gBACjB,KAAK,EAAE,CAAC,QAAQ,CAAC;aAClB,CAAA;QACH,CAAC;;;;AAhBU,kCAAW"}

+ 21 - 0
apps/api-server/src/modules/auth/auth.service.ts

@@ -0,0 +1,21 @@
+import { Injectable } from '@nestjs/common'
+
+@Injectable()
+export class AuthService {
+  getHealth() {
+    return {
+      module: 'auth',
+      status: 'ok',
+      phase: 'phase-1-stub',
+    }
+  }
+
+  getCurrentUser() {
+    return {
+      id: 'stub-user',
+      email: 'stub@example.com',
+      name: 'Stub User',
+      roles: ['viewer'],
+    }
+  }
+}

+ 23 - 0
apps/api-server/src/modules/auth/strategies/google.strategy.d.ts

@@ -0,0 +1,23 @@
+import { Strategy } from 'passport-google-oauth20';
+declare const GoogleStrategy_base: new (...args: any[]) => Strategy;
+export declare class GoogleStrategy extends GoogleStrategy_base {
+    constructor();
+    validate(_accessToken: string, _refreshToken: string, profile: {
+        id: string;
+        displayName?: string;
+        emails?: Array<{
+            value: string;
+        }>;
+        photos?: Array<{
+            value: string;
+        }>;
+    }): Promise<{
+        provider: string;
+        providerId: string;
+        email: string | null;
+        name: string;
+        avatarUrl: string | null;
+    }>;
+}
+export {};
+//# sourceMappingURL=google.strategy.d.ts.map

+ 1 - 0
apps/api-server/src/modules/auth/strategies/google.strategy.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"google.strategy.d.ts","sourceRoot":"","sources":["google.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;;AAElD,qBACa,cAAe,SAAQ,mBAAoC;;IAUhE,QAAQ,CACZ,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACjC,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAClC;;;;;;;CAUJ"}

+ 77 - 0
apps/api-server/src/modules/auth/strategies/google.strategy.js

@@ -0,0 +1,77 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.GoogleStrategy = void 0;
+const common_1 = require("@nestjs/common");
+const passport_1 = require("@nestjs/passport");
+const passport_google_oauth20_1 = require("passport-google-oauth20");
+let GoogleStrategy = (() => {
+    let _classDecorators = [(0, common_1.Injectable)()];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    let _classSuper = (0, passport_1.PassportStrategy)(passport_google_oauth20_1.Strategy, 'google');
+    var GoogleStrategy = class extends _classSuper {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            GoogleStrategy = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+        constructor() {
+            super({
+                clientID: process.env['GOOGLE_CLIENT_ID'] || 'stub-google-client-id',
+                clientSecret: process.env['GOOGLE_CLIENT_SECRET'] || 'stub-google-client-secret',
+                callbackURL: process.env['GOOGLE_CALLBACK_URL'] || 'http://localhost:3001/v1/auth/google/callback',
+                scope: ['email', 'profile'],
+            });
+        }
+        async validate(_accessToken, _refreshToken, profile) {
+            return {
+                provider: 'google',
+                providerId: profile.id,
+                email: profile.emails?.[0]?.value ?? null,
+                name: profile.displayName ?? 'Unknown',
+                avatarUrl: profile.photos?.[0]?.value ?? null,
+            };
+        }
+    };
+    return GoogleStrategy = _classThis;
+})();
+exports.GoogleStrategy = GoogleStrategy;
+//# sourceMappingURL=google.strategy.js.map

+ 1 - 0
apps/api-server/src/modules/auth/strategies/google.strategy.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"google.strategy.js","sourceRoot":"","sources":["google.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA2C;AAC3C,+CAAmD;AACnD,qEAAkD;IAGrC,cAAc;4BAD1B,IAAA,mBAAU,GAAE;;;;sBACuB,IAAA,2BAAgB,EAAC,kCAAQ,EAAE,QAAQ,CAAC;8BAA5C,SAAQ,WAAoC;;;;YAAxE,6KA4BC;;;YA5BY,uDAAc;;QACzB;YACE,KAAK,CAAC;gBACJ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,uBAAuB;gBACpE,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,IAAI,2BAA2B;gBAChF,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,+CAA+C;gBAClG,KAAK,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;aAC5B,CAAC,CAAA;QACJ,CAAC;QAED,KAAK,CAAC,QAAQ,CACZ,YAAoB,EACpB,aAAqB,EACrB,OAKC;YAED,OAAO;gBACL,QAAQ,EAAE,QAAQ;gBAClB,UAAU,EAAE,OAAO,CAAC,EAAE;gBACtB,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI;gBACzC,IAAI,EAAE,OAAO,CAAC,WAAW,IAAI,SAAS;gBACtC,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI;aAC9C,CAAA;QACH,CAAC;;;;AA3BU,wCAAc"}

+ 34 - 0
apps/api-server/src/modules/auth/strategies/google.strategy.ts

@@ -0,0 +1,34 @@
+import { Injectable } from '@nestjs/common'
+import { PassportStrategy } from '@nestjs/passport'
+import { Strategy } from 'passport-google-oauth20'
+
+@Injectable()
+export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
+  constructor() {
+    super({
+      clientID: process.env['GOOGLE_CLIENT_ID'] || 'stub-google-client-id',
+      clientSecret: process.env['GOOGLE_CLIENT_SECRET'] || 'stub-google-client-secret',
+      callbackURL: process.env['GOOGLE_CALLBACK_URL'] || 'http://localhost:3001/v1/auth/google/callback',
+      scope: ['email', 'profile'],
+    })
+  }
+
+  async validate(
+    _accessToken: string,
+    _refreshToken: string,
+    profile: {
+      id: string
+      displayName?: string
+      emails?: Array<{ value: string }>
+      photos?: Array<{ value: string }>
+    },
+  ) {
+    return {
+      provider: 'google',
+      providerId: profile.id,
+      email: profile.emails?.[0]?.value ?? null,
+      name: profile.displayName ?? 'Unknown',
+      avatarUrl: profile.photos?.[0]?.value ?? null,
+    }
+  }
+}

+ 15 - 0
apps/api-server/src/modules/auth/strategies/jwt.strategy.d.ts

@@ -0,0 +1,15 @@
+import { Strategy } from 'passport-jwt';
+type JwtPayload = {
+    sub: string;
+    email?: string;
+};
+declare const JwtStrategy_base: new (...args: any[]) => Strategy;
+export declare class JwtStrategy extends JwtStrategy_base {
+    constructor();
+    validate(payload: JwtPayload): Promise<{
+        userId: string;
+        email: string | null;
+    }>;
+}
+export {};
+//# sourceMappingURL=jwt.strategy.d.ts.map

+ 1 - 0
apps/api-server/src/modules/auth/strategies/jwt.strategy.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"jwt.strategy.d.ts","sourceRoot":"","sources":["jwt.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEnD,KAAK,UAAU,GAAG;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;;AAED,qBACa,WAAY,SAAQ,gBAA0B;;IASnD,QAAQ,CAAC,OAAO,EAAE,UAAU;;;;CAMnC"}

+ 73 - 0
apps/api-server/src/modules/auth/strategies/jwt.strategy.js

@@ -0,0 +1,73 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.JwtStrategy = void 0;
+const common_1 = require("@nestjs/common");
+const passport_1 = require("@nestjs/passport");
+const passport_jwt_1 = require("passport-jwt");
+let JwtStrategy = (() => {
+    let _classDecorators = [(0, common_1.Injectable)()];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    let _classSuper = (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy);
+    var JwtStrategy = class extends _classSuper {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            JwtStrategy = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+        constructor() {
+            super({
+                jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(),
+                ignoreExpiration: false,
+                secretOrKey: process.env['JWT_SECRET'] || 'dev-secret-change-in-production',
+            });
+        }
+        async validate(payload) {
+            return {
+                userId: payload.sub,
+                email: payload.email ?? null,
+            };
+        }
+    };
+    return JwtStrategy = _classThis;
+})();
+exports.JwtStrategy = JwtStrategy;
+//# sourceMappingURL=jwt.strategy.js.map

+ 1 - 0
apps/api-server/src/modules/auth/strategies/jwt.strategy.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA2C;AAC3C,+CAAmD;AACnD,+CAAmD;IAQtC,WAAW;4BADvB,IAAA,mBAAU,GAAE;;;;sBACoB,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;2BAAlC,SAAQ,WAA0B;;;;YAA3D,6KAeC;;;YAfY,uDAAW;;QACtB;YACE,KAAK,CAAC;gBACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;gBACxD,gBAAgB,EAAE,KAAK;gBACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,iCAAiC;aAC5E,CAAC,CAAA;QACJ,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,OAAmB;YAChC,OAAO;gBACL,MAAM,EAAE,OAAO,CAAC,GAAG;gBACnB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI;aAC7B,CAAA;QACH,CAAC;;;;AAdU,kCAAW"}

+ 26 - 0
apps/api-server/src/modules/auth/strategies/jwt.strategy.ts

@@ -0,0 +1,26 @@
+import { Injectable } from '@nestjs/common'
+import { PassportStrategy } from '@nestjs/passport'
+import { ExtractJwt, Strategy } from 'passport-jwt'
+
+type JwtPayload = {
+  sub: string
+  email?: string
+}
+
+@Injectable()
+export class JwtStrategy extends PassportStrategy(Strategy) {
+  constructor() {
+    super({
+      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+      ignoreExpiration: false,
+      secretOrKey: process.env['JWT_SECRET'] || 'dev-secret-change-in-production',
+    })
+  }
+
+  async validate(payload: JwtPayload) {
+    return {
+      userId: payload.sub,
+      email: payload.email ?? null,
+    }
+  }
+}

+ 3 - 0
apps/api-server/src/modules/captures/captures.module.d.ts

@@ -0,0 +1,3 @@
+export declare class CapturesModule {
+}
+//# sourceMappingURL=captures.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/captures/captures.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"captures.module.d.ts","sourceRoot":"","sources":["captures.module.ts"],"names":[],"mappings":"AAEA,qBAKa,cAAc;CAAG"}

+ 61 - 0
apps/api-server/src/modules/captures/captures.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.CapturesModule = void 0;
+const common_1 = require("@nestjs/common");
+let CapturesModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var CapturesModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            CapturesModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return CapturesModule = _classThis;
+})();
+exports.CapturesModule = CapturesModule;
+//# sourceMappingURL=captures.module.js.map

+ 1 - 0
apps/api-server/src/modules/captures/captures.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"captures.module.js","sourceRoot":"","sources":["captures.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,cAAc;4BAL1B,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA8B;;;YAAjB,uDAAc;;;;;AAAd,wCAAc"}

+ 8 - 0
apps/api-server/src/modules/captures/captures.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  controllers: [],
+  providers: [],
+  exports: [],
+})
+export class CapturesModule {}

+ 49 - 0
apps/api-server/src/modules/devices/devices.controller.ts

@@ -0,0 +1,49 @@
+import {
+  Controller,
+  Get,
+  Post,
+  Body,
+  Param,
+  Query,
+  UseGuards,
+} from '@nestjs/common'
+import { DevicesService } from './devices.service'
+import { HeartbeatDto, HeartbeatResponseDto } from './dto/heartbeat.dto'
+import { ApiKeyGuard } from '../../common/guards/api-key.guard'
+import { ApiKey } from '../../common/decorators/api-key.decorator'
+
+@Controller('devices')
+export class DevicesController {
+  constructor(private readonly devicesService: DevicesService) {}
+
+  // Device heartbeat — authenticated via API key (no JWT)
+  @Post(':deviceId/heartbeat')
+  @UseGuards(ApiKeyGuard)
+  heartbeat(
+    @Param('deviceId') deviceId: string,
+    @Body() dto: HeartbeatDto,
+    @ApiKey() apiKey: string,
+  ): HeartbeatResponseDto {
+    return this.devicesService.createHeartbeat({ ...dto, deviceId, apiKey }) as any
+  }
+
+  @Get()
+  list(@Query('projectId') projectId?: string) {
+    return this.devicesService.getDevices(projectId)
+  }
+
+  @Get('stats')
+  stats() {
+    return this.devicesService.getDashboardStats()
+  }
+
+  @Get(':id')
+  getOne(@Param('id') id: string) {
+    return this.devicesService.getDeviceById(id)
+  }
+
+  @Get(':id/heartbeats')
+  heartbeats(@Param('id') id: string, @Query('limit') limit?: string) {
+    return this.devicesService.getDeviceHeartbeats(id, limit ? parseInt(limit, 10) : 10)
+  }
+}

+ 3 - 0
apps/api-server/src/modules/devices/devices.module.d.ts

@@ -0,0 +1,3 @@
+export declare class DevicesModule {
+}
+//# sourceMappingURL=devices.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/devices/devices.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"devices.module.d.ts","sourceRoot":"","sources":["devices.module.ts"],"names":[],"mappings":"AAEA,qBAKa,aAAa;CAAG"}

+ 61 - 0
apps/api-server/src/modules/devices/devices.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.DevicesModule = void 0;
+const common_1 = require("@nestjs/common");
+let DevicesModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var DevicesModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            DevicesModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return DevicesModule = _classThis;
+})();
+exports.DevicesModule = DevicesModule;
+//# sourceMappingURL=devices.module.js.map

+ 1 - 0
apps/api-server/src/modules/devices/devices.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"devices.module.js","sourceRoot":"","sources":["devices.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,aAAa;4BALzB,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA6B;;;YAAhB,uDAAa;;;;;AAAb,sCAAa"}

+ 11 - 0
apps/api-server/src/modules/devices/devices.module.ts

@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common'
+import { DevicesController } from './devices.controller'
+import { DevicesService } from './devices.service'
+import { DevicesRepository } from './devices.repository'
+
+@Module({
+  controllers: [DevicesController],
+  providers: [DevicesService, DevicesRepository],
+  exports: [DevicesService],
+})
+export class DevicesModule {}

+ 97 - 0
apps/api-server/src/modules/devices/devices.repository.ts

@@ -0,0 +1,97 @@
+import { Injectable } from '@nestjs/common'
+import { eq, and, desc, sql, gte } from 'drizzle-orm'
+import { db } from '../../db/database.module'
+import { devices, deviceHeartbeats, commands } from '../../db/schema'
+
+@Injectable()
+export class DevicesRepository {
+  async findDeviceById(id: string) {
+    const result = await db.select().from(devices).where(eq(devices.id, id)).limit(1)
+    return result[0] ?? null
+  }
+
+  async findDeviceBySerialNo(serialNo: string) {
+    const result = await db.select().from(devices).where(eq(devices.serialNo, serialNo)).limit(1)
+    return result[0] ?? null
+  }
+
+  async updateDeviceStatus(id: string, status: string, lastSeenAt: Date, firmwareVersion?: string) {
+    await db
+      .update(devices)
+      .set({
+        status: status as any,
+        lastSeenAt,
+        ...(firmwareVersion && { firmwareVersion }),
+        updatedAt: new Date(),
+      })
+      .where(eq(devices.id, id))
+  }
+
+  async insertHeartbeat(data: {
+    deviceId: string
+    tempC?: number | null
+    batteryPct?: number | null
+    storageFreeGb: number
+    capturesToday: number
+    lastCaptureAt?: string | null
+    networkStatus: string
+  }) {
+    await db.insert(deviceHeartbeats).values({
+      deviceId: data.deviceId,
+      tempC: data.tempC ?? null,
+      batteryPct: data.batteryPct ?? null,
+      storageFreeGb: data.storageFreeGb,
+      capturesToday: data.capturesToday,
+      lastCaptureAt: data.lastCaptureAt ? new Date(data.lastCaptureAt) : null,
+      networkStatus: data.networkStatus,
+    })
+  }
+
+  async findDevicesByProjectId(projectId: string) {
+    return db.select().from(devices).where(eq(devices.projectId, projectId)).orderBy(desc(devices.updatedAt))
+  }
+
+  async findAllDevices() {
+    return db.select().from(devices).orderBy(desc(devices.updatedAt))
+  }
+
+  async countByStatus(status: string) {
+    const result = await db
+      .select({ count: sql<number>`count(*)` })
+      .from(devices)
+      .where(eq(devices.status, status as any))
+    return result[0]?.count ?? 0
+  }
+
+  async getRecentHeartbeats(deviceId: string, limit = 10) {
+    return db
+      .select()
+      .from(deviceHeartbeats)
+      .where(eq(deviceHeartbeats.deviceId, deviceId))
+      .orderBy(desc(deviceHeartbeats.heartbeatAt))
+      .limit(limit)
+  }
+
+  async getTodayCapturesTotal() {
+    const startOfDay = new Date()
+    startOfDay.setHours(0, 0, 0, 0)
+    const result = await db
+      .select({ total: sql<number>`coalesce(sum(${deviceHeartbeats.capturesToday}), 0)` })
+      .from(deviceHeartbeats)
+      .where(gte(deviceHeartbeats.heartbeatAt, startOfDay))
+    return result[0]?.total ?? 0
+  }
+
+  async getPendingCommandCount(deviceId: string) {
+    const result = await db
+      .select({ count: sql<number>`count(*)` })
+      .from(commands)
+      .where(
+        and(
+          eq(commands.deviceId, deviceId),
+          eq(commands.resultStatus, 'pending' as any),
+        ),
+      )
+    return result[0]?.count ?? 0
+  }
+}

+ 82 - 0
apps/api-server/src/modules/devices/devices.service.ts

@@ -0,0 +1,82 @@
+import { Injectable, UnauthorizedException, NotFoundException } from '@nestjs/common'
+import { DevicesRepository } from './devices.repository'
+import { DeviceStatus } from '@shared/types'
+import { HeartbeatDto } from './dto/heartbeat.dto'
+
+@Injectable()
+export class DevicesService {
+  constructor(private readonly repo: DevicesRepository) {}
+
+  async createHeartbeat(dto: HeartbeatDto) {
+    const device = await this.repo.findDeviceById(dto.deviceId)
+    if (!device) {
+      throw new NotFoundException(`Device ${dto.deviceId} not found`)
+    }
+
+    // API key check (Phase 1: simple compare — replace with hash in production)
+    if (device.apiKeyHash !== dto.apiKey) {
+      throw new UnauthorizedException('Invalid API key')
+    }
+
+    const now = new Date()
+
+    await Promise.all([
+      this.repo.insertHeartbeat({
+        deviceId: dto.deviceId,
+        tempC: dto.tempC ?? null,
+        batteryPct: dto.batteryPct ?? null,
+        storageFreeGb: dto.storageFreeGb,
+        capturesToday: dto.capturesToday,
+        lastCaptureAt: dto.lastCaptureAt ?? null,
+        networkStatus: dto.networkStatus ?? 'online',
+      }),
+      this.repo.updateDeviceStatus(dto.deviceId, dto.status, now, dto.firmwareVersion),
+    ])
+
+    const pendingCommands = await this.repo.getPendingCommandCount(dto.deviceId)
+
+    return {
+      success: true,
+      serverTime: now.toISOString(),
+      pendingCommands,
+    }
+  }
+
+  async getDevices(projectId?: string) {
+    if (projectId) {
+      return this.repo.findDevicesByProjectId(projectId)
+    }
+    return this.repo.findAllDevices()
+  }
+
+  async getDeviceById(id: string) {
+    const device = await this.repo.findDeviceById(id)
+    if (!device) {
+      throw new NotFoundException(`Device ${id} not found`)
+    }
+    return device
+  }
+
+  async getDeviceHeartbeats(deviceId: string, limit = 10) {
+    const device = await this.repo.findDeviceById(deviceId)
+    if (!device) {
+      throw new NotFoundException(`Device ${deviceId} not found`)
+    }
+    return this.repo.getRecentHeartbeats(deviceId, limit)
+  }
+
+  async getDashboardStats() {
+    const [devicesOnline, devicesOffline, capturesToday] = await Promise.all([
+      this.repo.countByStatus(DeviceStatus.ONLINE),
+      this.repo.countByStatus(DeviceStatus.OFFLINE),
+      this.repo.getTodayCapturesTotal(),
+    ])
+
+    return {
+      devicesOnline,
+      devicesOffline,
+      capturesToday,
+      devicesTotal: devicesOnline + devicesOffline,
+    }
+  }
+}

+ 52 - 0
apps/api-server/src/modules/devices/dto/heartbeat.dto.ts

@@ -0,0 +1,52 @@
+import { IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'
+import { DeviceStatus } from '@shared/types'
+
+export class HeartbeatDto {
+  @IsString()
+  @IsNotEmpty()
+  deviceId!: string
+
+  @IsString()
+  @IsNotEmpty()
+  apiKey!: string
+
+  @IsEnum(DeviceStatus)
+  status!: DeviceStatus
+
+  @IsOptional()
+  @IsNumber()
+  @Min(-50)
+  @Max(100)
+  tempC?: number | null
+
+  @IsOptional()
+  @IsInt()
+  @Min(0)
+  @Max(100)
+  batteryPct?: number | null
+
+  @IsInt()
+  @Min(0)
+  storageFreeGb!: number
+
+  @IsInt()
+  @Min(0)
+  capturesToday!: number
+
+  @IsOptional()
+  @IsString()
+  lastCaptureAt?: string | null
+
+  @IsString()
+  @IsNotEmpty()
+  firmwareVersion!: string
+
+  @IsEnum(DeviceStatus)
+  networkStatus?: 'online' | 'offline' | 'degraded'
+}
+
+export class HeartbeatResponseDto {
+  success!: boolean
+  serverTime!: string
+  pendingCommands!: number
+}

+ 3 - 0
apps/api-server/src/modules/orgs/orgs.module.d.ts

@@ -0,0 +1,3 @@
+export declare class OrgsModule {
+}
+//# sourceMappingURL=orgs.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/orgs/orgs.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"orgs.module.d.ts","sourceRoot":"","sources":["orgs.module.ts"],"names":[],"mappings":"AAEA,qBAKa,UAAU;CAAG"}

+ 61 - 0
apps/api-server/src/modules/orgs/orgs.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.OrgsModule = void 0;
+const common_1 = require("@nestjs/common");
+let OrgsModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var OrgsModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            OrgsModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return OrgsModule = _classThis;
+})();
+exports.OrgsModule = OrgsModule;
+//# sourceMappingURL=orgs.module.js.map

+ 1 - 0
apps/api-server/src/modules/orgs/orgs.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"orgs.module.js","sourceRoot":"","sources":["orgs.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,UAAU;4BALtB,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA0B;;;YAAb,uDAAU;;;;;AAAV,gCAAU"}

+ 8 - 0
apps/api-server/src/modules/orgs/orgs.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  controllers: [],
+  providers: [],
+  exports: [],
+})
+export class OrgsModule {}

+ 3 - 0
apps/api-server/src/modules/projects/projects.module.d.ts

@@ -0,0 +1,3 @@
+export declare class ProjectsModule {
+}
+//# sourceMappingURL=projects.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/projects/projects.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"projects.module.d.ts","sourceRoot":"","sources":["projects.module.ts"],"names":[],"mappings":"AAEA,qBAKa,cAAc;CAAG"}

+ 61 - 0
apps/api-server/src/modules/projects/projects.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.ProjectsModule = void 0;
+const common_1 = require("@nestjs/common");
+let ProjectsModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var ProjectsModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            ProjectsModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return ProjectsModule = _classThis;
+})();
+exports.ProjectsModule = ProjectsModule;
+//# sourceMappingURL=projects.module.js.map

+ 1 - 0
apps/api-server/src/modules/projects/projects.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"projects.module.js","sourceRoot":"","sources":["projects.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,cAAc;4BAL1B,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA8B;;;YAAjB,uDAAc;;;;;AAAd,wCAAc"}

+ 8 - 0
apps/api-server/src/modules/projects/projects.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  controllers: [],
+  providers: [],
+  exports: [],
+})
+export class ProjectsModule {}

+ 3 - 0
apps/api-server/src/modules/videos/videos.module.d.ts

@@ -0,0 +1,3 @@
+export declare class VideosModule {
+}
+//# sourceMappingURL=videos.module.d.ts.map

+ 1 - 0
apps/api-server/src/modules/videos/videos.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"videos.module.d.ts","sourceRoot":"","sources":["videos.module.ts"],"names":[],"mappings":"AAEA,qBAKa,YAAY;CAAG"}

+ 61 - 0
apps/api-server/src/modules/videos/videos.module.js

@@ -0,0 +1,61 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.VideosModule = void 0;
+const common_1 = require("@nestjs/common");
+let VideosModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            controllers: [],
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var VideosModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            VideosModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return VideosModule = _classThis;
+})();
+exports.VideosModule = VideosModule;
+//# sourceMappingURL=videos.module.js.map

+ 1 - 0
apps/api-server/src/modules/videos/videos.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"videos.module.js","sourceRoot":"","sources":["videos.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAO1B,YAAY;4BALxB,IAAA,eAAM,EAAC;YACN,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA4B;;;YAAf,uDAAY;;;;;AAAZ,oCAAY"}

+ 8 - 0
apps/api-server/src/modules/videos/videos.module.ts

@@ -0,0 +1,8 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  controllers: [],
+  providers: [],
+  exports: [],
+})
+export class VideosModule {}

+ 3 - 0
apps/api-server/src/realtime/realtime.module.d.ts

@@ -0,0 +1,3 @@
+export declare class RealtimeModule {
+}
+//# sourceMappingURL=realtime.module.d.ts.map

+ 1 - 0
apps/api-server/src/realtime/realtime.module.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"realtime.module.d.ts","sourceRoot":"","sources":["realtime.module.ts"],"names":[],"mappings":"AAEA,qBAIa,cAAc;CAAG"}

+ 60 - 0
apps/api-server/src/realtime/realtime.module.js

@@ -0,0 +1,60 @@
+"use strict";
+var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
+    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
+    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
+    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
+    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
+    var _, done = false;
+    for (var i = decorators.length - 1; i >= 0; i--) {
+        var context = {};
+        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
+        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
+        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
+        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
+        if (kind === "accessor") {
+            if (result === void 0) continue;
+            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
+            if (_ = accept(result.get)) descriptor.get = _;
+            if (_ = accept(result.set)) descriptor.set = _;
+            if (_ = accept(result.init)) initializers.unshift(_);
+        }
+        else if (_ = accept(result)) {
+            if (kind === "field") initializers.unshift(_);
+            else descriptor[key] = _;
+        }
+    }
+    if (target) Object.defineProperty(target, contextIn.name, descriptor);
+    done = true;
+};
+var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
+    var useValue = arguments.length > 2;
+    for (var i = 0; i < initializers.length; i++) {
+        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
+    }
+    return useValue ? value : void 0;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.RealtimeModule = void 0;
+const common_1 = require("@nestjs/common");
+let RealtimeModule = (() => {
+    let _classDecorators = [(0, common_1.Module)({
+            providers: [],
+            exports: [],
+        })];
+    let _classDescriptor;
+    let _classExtraInitializers = [];
+    let _classThis;
+    var RealtimeModule = class {
+        static { _classThis = this; }
+        static {
+            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
+            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
+            RealtimeModule = _classThis = _classDescriptor.value;
+            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
+            __runInitializers(_classThis, _classExtraInitializers);
+        }
+    };
+    return RealtimeModule = _classThis;
+})();
+exports.RealtimeModule = RealtimeModule;
+//# sourceMappingURL=realtime.module.js.map

+ 1 - 0
apps/api-server/src/realtime/realtime.module.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"realtime.module.js","sourceRoot":"","sources":["realtime.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAuC;IAM1B,cAAc;4BAJ1B,IAAA,eAAM,EAAC;YACN,SAAS,EAAE,EAAE;YACb,OAAO,EAAE,EAAE;SACZ,CAAC;;;;;;;;YACF,6KAA8B;;;YAAjB,uDAAc;;;;;AAAd,wCAAc"}

+ 7 - 0
apps/api-server/src/realtime/realtime.module.ts

@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common'
+
+@Module({
+  providers: [],
+  exports: [],
+})
+export class RealtimeModule {}

+ 13 - 0
apps/api-server/tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../../packages/config/tsconfig.node.json",
+  "compilerOptions": {
+    "outDir": "./dist",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"],
+      "@shared/types": ["../../packages/shared-types/src"]
+    }
+  },
+  "include": ["src/**/*", "../../packages/shared-types/src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 33 - 0
apps/device-agent/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "device-agent",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "python3 -m agent.main",
+    "start": "python3 -m agent.main",
+    "setup": "bash scripts/setup.sh",
+    "lint": "ruff check src/"
+  },
+  "dependencies": {
+    "requests": "^2.31.0",
+    "pygobject": "^3.48.0",
+    "Pillow": "^10.2.0",
+    "schedule": "^1.2.0",
+    "python-dotenv": "^1.0.0",
+    "sqlite3": "^5.1.7",
+    "paho-mqtt": "^1.6.8",
+    "watchdog": "^3.0.0",
+    "raspberry-py-toutrial": "^0.0.1"
+  },
+  "systemdUnits": [
+    "systemd/capture-scheduler.service",
+    "systemd/camera-daemon.service",
+    "systemd/upload-queue.service",
+    "systemd/heartbeat-agent.service"
+  ],
+  "devDependencies": {
+    "ruff": "^0.2.0",
+    "black": "^24.1.0",
+    "mypy": "^1.8.0"
+  }
+}

+ 4 - 0
apps/web-dashboard/.dockerignore

@@ -0,0 +1,4 @@
+node_modules
+.env
+.next
+out

+ 24 - 0
apps/web-dashboard/Dockerfile

@@ -0,0 +1,24 @@
+FROM node:20-alpine AS base
+
+FROM base AS deps
+WORKDIR /app
+COPY package.json packages/*/package.json apps/*/package.json ./
+RUN npm install --workspaces --include-workspace-root
+
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run build --workspace=apps/web-dashboard
+
+FROM base AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+COPY --from=builder /app/apps/web-dashboard/dist ./out
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/apps/web-dashboard/package.json ./
+
+EXPOSE 3000
+CMD ["npm", "start"]

+ 5 - 0
apps/web-dashboard/next-env.d.ts

@@ -0,0 +1,5 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

+ 31 - 0
apps/web-dashboard/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "web-dashboard",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev -p 3000",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "next": "^14.1.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "@tanstack/react-query": "^5.17.0",
+    "zustand": "^4.4.7",
+    "@shared/types": "workspace:*"
+  },
+  "devDependencies": {
+    "@types/node": "^20.11.0",
+    "@types/react": "^18.2.48",
+    "@types/react-dom": "^18.2.18",
+    "autoprefixer": "^10.4.27",
+    "eslint": "^8.56.0",
+    "postcss": "^8.5.8",
+    "prettier": "^3.2.0",
+    "tailwindcss": "3",
+    "typescript": "^5.3.3"
+  }
+}

Някои файлове не бяха показани, защото твърде много файлове са промени