@meyicloud/meyi-cost-server 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -27
- package/README.md +29 -55
- package/index.js +0 -1
- package/package.json +4 -4
- package/src/controllers/cost-analysis.controller.js +11 -1
- package/src/plugin.js +7 -13
- package/src/repositories/aws-onboarding.repository.js +6 -4
- package/src/repositories/cost-analysis-report.repository.js +22 -3
- package/src/routes/index.js +1 -0
- package/src/schema/cost-analysis-report.schema.js +14 -0
- package/src/services/cost-analyser.service.js +256 -0
- package/src/services/cost-analysis-report.service.js +23 -6
- package/src/services/cost-report-artifact.service.js +22 -0
- package/src/workers/cost-analysis-report.worker.js +10 -9
- package/src/lib/cost-analysis.js +0 -116
- package/src/lib/llm-provider.js +0 -239
- package/src/repositories/cost-analysis.repository.js +0 -50
- package/src/schema/cost-analysis.schema.js +0 -7
- package/src/services/cost-analysis-data.service.js +0 -39
- package/src/services/cost-analysis-pdf.service.js +0 -42
- package/src/services/cost-analysis.service.js +0 -190
package/AGENTS.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Scope
|
|
4
4
|
|
|
5
5
|
This package is a tenant-aware Express plugin for AWS cost overview, reports,
|
|
6
|
-
budgets, alert dismissals,
|
|
6
|
+
budgets, alert dismissals, and mandatory CUR/Athena data. It is
|
|
7
7
|
designed to be mounted inside a host backend; it does not own login, tenant
|
|
8
8
|
onboarding, or the host plugin registry.
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ insight-cost-server/
|
|
|
20
20
|
| |-- routes/ # Express paths and common error handling
|
|
21
21
|
| |-- models/ # Immutable customer, SaaS CUR, and status models
|
|
22
22
|
| |-- repositories/ # Tenant-scoped onboarding persistence reads
|
|
23
|
-
| |-- services/ #
|
|
23
|
+
| |-- services/ # CUR/Athena, external analyser, context, budgets
|
|
24
24
|
| |-- schema/ # Plugin-owned database tables and installation
|
|
25
25
|
| |-- lib/ # Stateless date, cost, and filter helpers
|
|
26
26
|
| `-- plugin.js # Dependency composition and lifecycle
|
|
@@ -35,23 +35,13 @@ Keep HTTP concerns in routes/controllers, AWS and database behavior in services,
|
|
|
35
35
|
schema creation in `schema`, and pure calculations in `lib`. `index.js` and
|
|
36
36
|
`cur.js` must remain thin compatibility entry points.
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## External cost analyser
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"AWS Bedrock" resolve correctly.
|
|
46
|
-
|
|
47
|
-
`CostAnalysisService.resolveProvider(tenantId)` picks between a host-supplied
|
|
48
|
-
`providerResolver` and the `COST_AI_*` environment defaults. Two rules matter:
|
|
49
|
-
the analysis fingerprint is computed from the **effective** model so switching
|
|
50
|
-
provider cannot serve a cached answer from the previous one, and a resolver
|
|
51
|
-
failure degrades to the environment rather than failing the request.
|
|
52
|
-
|
|
53
|
-
When adding a provider, add it here and to `test/llm-provider.test.js`; no
|
|
54
|
-
other file should need to change.
|
|
40
|
+
The backend package never calls an LLM directly. `CostAnalysisReportWorker`
|
|
41
|
+
claims due tenant schedules and delegates execution to `CostAnalyserService`,
|
|
42
|
+
which starts one tenant-scoped `meyi-cost-ai-analyser` Fargate task. The task
|
|
43
|
+
writes canonical Markdown and derived PDF artifacts to private S3. Keep ECS
|
|
44
|
+
task overrides tenant-scoped and never log the customer role external ID.
|
|
55
45
|
|
|
56
46
|
## Request and data flow
|
|
57
47
|
|
|
@@ -62,21 +52,17 @@ flowchart LR
|
|
|
62
52
|
Enabled --> Router[/api/v1/cost router]
|
|
63
53
|
Router --> Controller[Cost controller]
|
|
64
54
|
Controller --> Customer[Customer AWS context service]
|
|
65
|
-
|
|
66
|
-
Controller --> Source{Configured source}
|
|
67
|
-
Source -->|CUR| SaaS[SaaS Athena context service]
|
|
55
|
+
Controller --> SaaS[SaaS Athena context service]
|
|
68
56
|
SaaS --> Athena[Central tenant CUR partition]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Athena --> Controller
|
|
73
|
-
CE --> Controller
|
|
57
|
+
Athena --> Scheduler[Scheduled report worker]
|
|
58
|
+
Scheduler --> ECS[Cost analyser Fargate task]
|
|
59
|
+
ECS --> Artifacts[Private Markdown and PDF S3 objects]
|
|
74
60
|
```
|
|
75
61
|
|
|
76
62
|
The plugin currently owns these route groups under `${apiBaseUri}/cost`:
|
|
77
63
|
|
|
78
64
|
- `GET /accounts`, `/data-status`, `/overview`, `/filter-options`, `/reports`, `/tags`
|
|
79
|
-
- `GET`/`PUT /analysis/schedule`, `GET /analysis/reports`, and tenant-scoped PDF downloads
|
|
65
|
+
- `GET`/`PUT /analysis/schedule`, `GET /analysis/reports`, and tenant-scoped Markdown/PDF downloads
|
|
80
66
|
- `GET /budgets` and `POST /budgets`
|
|
81
67
|
- `DELETE /budgets/:id`
|
|
82
68
|
- `GET /budget-alert-dismissals` and `POST /budget-alert-dismissals`
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ and AWS onboarding. The plugin owns cost routes and its budget-related tables.
|
|
|
32
32
|
- User-, tenant-, month-, and status-scoped budget-alert dismissals
|
|
33
33
|
- Tenant-configurable daily, weekly, and monthly AI report schedules
|
|
34
34
|
- PostgreSQL-backed background report jobs with failure history
|
|
35
|
-
-
|
|
35
|
+
- Analyser-generated Markdown and PDF reports available through tenant-scoped downloads
|
|
36
36
|
|
|
37
37
|
Budgets are application rules stored in PostgreSQL; they are not AWS Budgets
|
|
38
38
|
resources. The consuming application evaluates them against current cost and
|
|
@@ -97,7 +97,7 @@ For a local package installation test:
|
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
npm pack
|
|
100
|
-
npm install /path/to/meyicloud-meyi-cost-server-1.
|
|
100
|
+
npm install /path/to/meyicloud-meyi-cost-server-1.6.0.tgz
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
## Publish to npm
|
|
@@ -258,12 +258,12 @@ SaaS Athena resources.
|
|
|
258
258
|
|
|
259
259
|
### AI analysis variables
|
|
260
260
|
|
|
261
|
-
AI
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
complete
|
|
266
|
-
|
|
261
|
+
AI reports are generated by a separate `meyi-cost-ai-analyser` Fargate task.
|
|
262
|
+
The backend only schedules jobs, launches and monitors the task, and serves
|
|
263
|
+
tenant-scoped artifacts. It does not invoke Bedrock or generate PDFs itself.
|
|
264
|
+
Daily reports cover the previous complete day, weekly reports cover the
|
|
265
|
+
previous seven complete days, and monthly reports cover the previous calendar
|
|
266
|
+
month in the configured timezone.
|
|
267
267
|
|
|
268
268
|
The authenticated report API is:
|
|
269
269
|
|
|
@@ -274,6 +274,7 @@ The authenticated report API is:
|
|
|
274
274
|
| `GET` | `/analysis/reports` | List tenant report history without PDF bytes. |
|
|
275
275
|
| `GET` | `/analysis/reports/:id` | Read one tenant report. |
|
|
276
276
|
| `GET` | `/analysis/reports/:id/pdf` | Download a completed report PDF. |
|
|
277
|
+
| `GET` | `/analysis/reports/:id/markdown` | Download the canonical Markdown report. |
|
|
277
278
|
|
|
278
279
|
Schedule and report rows live in `cost_ai_report_schedules` and
|
|
279
280
|
`cost_ai_reports`. The worker atomically claims due schedules with a lease, so
|
|
@@ -281,56 +282,29 @@ a restarted task can resume future work without relying on in-memory timers.
|
|
|
281
282
|
|
|
282
283
|
| Variable | Default | Required | Purpose |
|
|
283
284
|
| --- | --- | --- | --- |
|
|
284
|
-
| `COST_AI_ENABLED` | `false` | No | Enables the
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
285
|
+
| `COST_AI_ENABLED` | `false` | No | Enables the schedule worker and external analyser integration. |
|
|
286
|
+
| `COST_AI_ANALYSER_REGION` | `AWS_REGION` | When enabled | ECS and report-bucket region. |
|
|
287
|
+
| `COST_AI_ANALYSER_CLUSTER` | Empty | When enabled | ECS cluster ARN or name. |
|
|
288
|
+
| `COST_AI_ANALYSER_TASK_DEFINITION` | Empty | When enabled | Fargate task definition ARN. |
|
|
289
|
+
| `COST_AI_ANALYSER_CONTAINER_NAME` | `cost-ai-analyser` | No | Container override target. |
|
|
290
|
+
| `COST_AI_ANALYSER_SUBNETS` | Empty | When enabled | Comma-separated private subnet IDs. |
|
|
291
|
+
| `COST_AI_ANALYSER_SECURITY_GROUPS` | Empty | When enabled | Comma-separated task security groups. |
|
|
292
|
+
| `COST_AI_ANALYSER_REPORT_BUCKET` | Empty | When enabled | Private S3 Markdown/PDF artifact bucket. |
|
|
293
|
+
| `COST_AI_ANALYSER_PROVIDER_SECRET_PREFIX` | Empty | When enabled | Prefix for temporary Secrets Manager entries containing each tenant's active Meyi Connect AI provider configuration. |
|
|
294
|
+
| `COST_AI_ANALYSER_TARGET_REGIONS` | `AWS_REGION` | No | Customer regions inspected for resource detail. |
|
|
295
|
+
| `COST_AI_ANALYSER_TOP_N_SERVICES` | `10` | No | Maximum high-cost service agents run per report. |
|
|
296
|
+
| `COST_AI_ANALYSER_POLL_INTERVAL_MS` | `15000` | No | ECS task status polling interval. |
|
|
297
|
+
| `COST_AI_ANALYSER_TIMEOUT_MS` | `2700000` | No | Maximum external task duration. |
|
|
291
298
|
| `COST_AI_REPORT_POLL_INTERVAL_MS` | `60000` | No | How often the background worker claims due schedules; minimum 10 seconds. |
|
|
292
|
-
| `COST_AI_REPORT_LEASE_MS` | `
|
|
299
|
+
| `COST_AI_REPORT_LEASE_MS` | `3600000` | No | Claim lease used to recover from an interrupted worker. |
|
|
293
300
|
| `COST_AI_REPORT_BATCH_SIZE` | `5` | No | Maximum schedules claimed by one worker tick; clamped from 1 to 25. |
|
|
294
301
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```js
|
|
302
|
-
createInsightCost({
|
|
303
|
-
app, db,
|
|
304
|
-
providerResolver: async (tenantId) => ({
|
|
305
|
-
provider: "anthropic", // bedrock | anthropic | openai
|
|
306
|
-
modelId: "claude-sonnet-4-5",
|
|
307
|
-
apiKey: "...", // required for anthropic and openai
|
|
308
|
-
region: "ap-south-2", // bedrock only
|
|
309
|
-
accessKeyId: "...", // bedrock only; omit to use ambient credentials
|
|
310
|
-
secretAccessKey: "...",
|
|
311
|
-
}),
|
|
312
|
-
})
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Returning `null` falls back to the environment configuration, so a host that
|
|
316
|
-
does not pass a resolver behaves exactly as before. A resolver that throws is
|
|
317
|
-
logged and also falls back, so a credential-store outage degrades the feature
|
|
318
|
-
rather than failing the request.
|
|
319
|
-
|
|
320
|
-
Why this exists: Bedrock authenticates with SigV4 and is subject to
|
|
321
|
-
account-level Anthropic model access. The key-based providers are not, so a
|
|
322
|
-
deployment blocked on Bedrock model access can still run analysis. It also
|
|
323
|
-
lets a customer bring their own model account.
|
|
324
|
-
|
|
325
|
-
`GET /analysis/status` reports the provider that would serve the caller's
|
|
326
|
-
tenant, including a `source` of `tenant-configuration` or `environment`.
|
|
327
|
-
**Host applications are responsible for encrypting stored credentials.**
|
|
328
|
-
|
|
329
|
-
The runtime role needs `bedrock:InvokeModel` for the selected model or
|
|
330
|
-
inference profile. Analysis results are stored in the tenant-scoped
|
|
331
|
-
`cost_ai_analyses` table and scheduled PDF jobs are stored in
|
|
332
|
-
`cost_ai_reports`. Model or permission failures mark only that scheduled report
|
|
333
|
-
as failed and do not change CUR, standard reports, onboarding, or budgets.
|
|
302
|
+
`GET /analysis/status` reports `meyi-cost-ai-analyser` with an `ecs-task`
|
|
303
|
+
source. The analyser uses the shared ECS task role for Athena, S3, Bedrock, and
|
|
304
|
+
cross-account role assumption. New reports store task and private artifact
|
|
305
|
+
references in `cost_ai_reports`; existing database-backed PDF rows remain
|
|
306
|
+
downloadable for compatibility. Model or task failures mark only that report as
|
|
307
|
+
failed and do not change CUR, standard reports, or onboarding.
|
|
334
308
|
|
|
335
309
|
### CUR/Athena variables
|
|
336
310
|
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meyicloud/meyi-cost-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Tenant-aware AWS CUR and Athena cost plugin server for MeyiConnect",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@aws-sdk/client-athena": "^3.850.0",
|
|
22
|
-
"@aws-sdk/client-
|
|
22
|
+
"@aws-sdk/client-ecs": "^3.850.0",
|
|
23
23
|
"@aws-sdk/client-glue": "^3.850.0",
|
|
24
24
|
"@aws-sdk/client-s3": "^3.850.0",
|
|
25
|
+
"@aws-sdk/client-secrets-manager": "^3.850.0",
|
|
25
26
|
"@aws-sdk/client-sts": "^3.850.0",
|
|
26
27
|
"@aws-sdk/credential-providers": "^3.850.0",
|
|
27
28
|
"drizzle-orm": "^0.44.7",
|
|
28
29
|
"express": "^4.21.1",
|
|
29
|
-
"luxon": "^3.7.2"
|
|
30
|
-
"pdfkit": "^0.17.2"
|
|
30
|
+
"luxon": "^3.7.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"pg": ">=8"
|
|
@@ -12,7 +12,7 @@ export class CostAnalysisController {
|
|
|
12
12
|
|
|
13
13
|
async latest(req, res, next) {
|
|
14
14
|
try {
|
|
15
|
-
return res.json({ analysis: await this.
|
|
15
|
+
return res.json({ analysis: await this.reportService.latest(req) });
|
|
16
16
|
} catch (error) { return next(error); }
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -45,4 +45,14 @@ export class CostAnalysisController {
|
|
|
45
45
|
return res.send(report.data);
|
|
46
46
|
} catch (error) { return next(error); }
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
async downloadMarkdown(req, res, next) {
|
|
50
|
+
try {
|
|
51
|
+
const report = await this.reportService.markdown(req, req.params.id);
|
|
52
|
+
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
53
|
+
res.setHeader("Content-Disposition", `attachment; filename="${report.filename}"`);
|
|
54
|
+
res.setHeader("Content-Length", report.data.length);
|
|
55
|
+
return res.send(report.data);
|
|
56
|
+
} catch (error) { return next(error); }
|
|
57
|
+
}
|
|
48
58
|
}
|
package/src/plugin.js
CHANGED
|
@@ -11,13 +11,11 @@ import { BudgetController } from "./controllers/budget.controller.js";
|
|
|
11
11
|
import { CostAnalysisController } from "./controllers/cost-analysis.controller.js";
|
|
12
12
|
import { createCostRouter } from "./routes/index.js";
|
|
13
13
|
import { installCostBudgetSchema } from "./schema/cost-budget.schema.js";
|
|
14
|
-
import { installCostAnalysisSchema } from "./schema/cost-analysis.schema.js";
|
|
15
14
|
import { installCostAnalysisReportSchema } from "./schema/cost-analysis-report.schema.js";
|
|
16
|
-
import { CostAnalysisRepository } from "./repositories/cost-analysis.repository.js";
|
|
17
15
|
import { CostAnalysisReportRepository } from "./repositories/cost-analysis-report.repository.js";
|
|
18
|
-
import {
|
|
19
|
-
import { CostAnalysisService } from "./services/cost-analysis.service.js";
|
|
16
|
+
import { CostAnalyserService } from "./services/cost-analyser.service.js";
|
|
20
17
|
import { CostAnalysisReportService } from "./services/cost-analysis-report.service.js";
|
|
18
|
+
import { CostReportArtifactService } from "./services/cost-report-artifact.service.js";
|
|
21
19
|
import { CostAnalysisReportWorker } from "./workers/cost-analysis-report.worker.js";
|
|
22
20
|
import { installCurDiscoverySchema } from "./cur-discovery/schema.js";
|
|
23
21
|
import { CurDiscoveryRepository } from "./cur-discovery/cur-discovery.repository.js";
|
|
@@ -35,7 +33,6 @@ export function createInsightCost({ app, db, apiBaseUri = "/api/v1", logger = co
|
|
|
35
33
|
const contextService = new CustomerAwsContextService({ repository: onboardingRepository, defaultTenant });
|
|
36
34
|
const budgetRepository = new BudgetRepository({ db, qSchema });
|
|
37
35
|
const budgetAlertRepository = new BudgetAlertRepository({ db, qSchema });
|
|
38
|
-
const costAnalysisRepository = new CostAnalysisRepository({ db, qSchema });
|
|
39
36
|
const costAnalysisReportRepository = new CostAnalysisReportRepository({ db, qSchema });
|
|
40
37
|
const budgetService = new BudgetService({ repository: budgetRepository });
|
|
41
38
|
const budgetAlertService = new BudgetAlertService({ repository: budgetAlertRepository });
|
|
@@ -54,20 +51,17 @@ export function createInsightCost({ app, db, apiBaseUri = "/api/v1", logger = co
|
|
|
54
51
|
const discoveryWorker = new CurDiscoveryWorker({ repository: discoveryRepository, service: discoveryService, logger });
|
|
55
52
|
const costController = new CostController({ contextService, curProvider, discoveryRepository, logger });
|
|
56
53
|
const budgetController = new BudgetController({ contextService, budgetService, budgetAlertService });
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const costAnalysisReportWorker = new CostAnalysisReportWorker({ repository: costAnalysisReportRepository, analysisService: costAnalysisService, logger });
|
|
63
|
-
const costAnalysisController = new CostAnalysisController({ service: costAnalysisService, reportService: costAnalysisReportService });
|
|
54
|
+
const costAnalyserService = new CostAnalyserService({ contextService, athenaContextService, providerResolver, logger });
|
|
55
|
+
const artifactService = new CostReportArtifactService();
|
|
56
|
+
const costAnalysisReportService = new CostAnalysisReportService({ contextService, repository: costAnalysisReportRepository, artifactService });
|
|
57
|
+
const costAnalysisReportWorker = new CostAnalysisReportWorker({ repository: costAnalysisReportRepository, analyserService: costAnalyserService, logger });
|
|
58
|
+
const costAnalysisController = new CostAnalysisController({ service: costAnalyserService, reportService: costAnalysisReportService });
|
|
64
59
|
const router = createCostRouter({ costController, budgetController, costAnalysisController }, logger);
|
|
65
60
|
let mounted = false;
|
|
66
61
|
|
|
67
62
|
return {
|
|
68
63
|
async install() {
|
|
69
64
|
await installCostBudgetSchema(db, qSchema);
|
|
70
|
-
await installCostAnalysisSchema(db, qSchema);
|
|
71
65
|
await installCostAnalysisReportSchema(db, qSchema);
|
|
72
66
|
await installCurDiscoverySchema(db, qSchema);
|
|
73
67
|
logger.log?.("[Cost] Database migration verified");
|
|
@@ -24,9 +24,10 @@ export class AwsOnboardingRepository {
|
|
|
24
24
|
|
|
25
25
|
const connection = rows(await this.db.execute(sql`
|
|
26
26
|
SELECT
|
|
27
|
-
connection_id,
|
|
28
|
-
role_arn,
|
|
29
|
-
external_id
|
|
27
|
+
connection_id,
|
|
28
|
+
role_arn,
|
|
29
|
+
external_id,
|
|
30
|
+
management_account_id
|
|
30
31
|
FROM ${sql.raw(this.connectionsTable)}
|
|
31
32
|
WHERE tenant_id::text = ${tenant}
|
|
32
33
|
AND status = 'CONNECTED'
|
|
@@ -72,7 +73,8 @@ export class AwsOnboardingRepository {
|
|
|
72
73
|
status: item.status,
|
|
73
74
|
})),
|
|
74
75
|
meta: {
|
|
75
|
-
payerRoleArn: connection.role_arn,
|
|
76
|
+
payerRoleArn: connection.role_arn,
|
|
77
|
+
managementAccountId: connection.management_account_id,
|
|
76
78
|
externalId: connection.external_id,
|
|
77
79
|
connectionId: connection.connection_id,
|
|
78
80
|
curExportArn: curConfig.export_arn,
|
|
@@ -71,6 +71,23 @@ export class CostAnalysisReportRepository {
|
|
|
71
71
|
return rows(await this.db.execute(sql`UPDATE ${sql.raw(this.reports)} SET status = 'RUNNING', attempt_count = attempt_count + 1, started_at = now(), error = NULL, updated_at = now() WHERE id = ${id} AND status = 'PENDING' RETURNING *`))[0] || null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
async setTaskArn(id, taskArn) {
|
|
75
|
+
await this.db.execute(sql`UPDATE ${sql.raw(this.reports)} SET ecs_task_arn = ${taskArn}, updated_at = now() WHERE id = ${id}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async completeExternal(id, result) {
|
|
79
|
+
await this.db.execute(sql`
|
|
80
|
+
UPDATE ${sql.raw(this.reports)} SET status = 'COMPLETED', ecs_task_arn = ${result.taskArn},
|
|
81
|
+
model_id = ${result.modelId}, provider_name = ${result.provider}, provider_source = ${result.source},
|
|
82
|
+
data_source = ${result.dataSource}, facts = ${JSON.stringify(result.facts || {})}::jsonb,
|
|
83
|
+
result = ${JSON.stringify(result.summary || {})}::jsonb, artifact_bucket = ${result.artifactBucket},
|
|
84
|
+
markdown_key = ${result.markdownKey}, markdown_size = ${Number(result.markdownSize || 0)},
|
|
85
|
+
pdf_key = ${result.pdfKey}, pdf_size = ${Number(result.pdfSize || 0)}, pdf_data = NULL,
|
|
86
|
+
completed_at = now(), updated_at = now()
|
|
87
|
+
WHERE id = ${id}
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
async complete(id, analysis, pdf) {
|
|
75
92
|
await this.db.execute(sql`
|
|
76
93
|
UPDATE ${sql.raw(this.reports)} SET status = 'COMPLETED', model_id = ${analysis.modelId}, data_source = ${analysis.dataSource || null},
|
|
@@ -82,13 +99,14 @@ export class CostAnalysisReportRepository {
|
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
async fail(id, error) {
|
|
85
|
-
await this.db.execute(sql`UPDATE ${sql.raw(this.reports)} SET status = 'FAILED', error = ${String(error?.message || error).slice(0, 2000)}, completed_at = now(), updated_at = now() WHERE id = ${id}`);
|
|
102
|
+
await this.db.execute(sql`UPDATE ${sql.raw(this.reports)} SET status = 'FAILED', ecs_task_arn = COALESCE(${error?.taskArn || null}, ecs_task_arn), error = ${String(error?.message || error).slice(0, 2000)}, completed_at = now(), updated_at = now() WHERE id = ${id}`);
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
async list(tenantId, limit = 100) {
|
|
89
106
|
return rows(await this.db.execute(sql`
|
|
90
107
|
SELECT id, schedule_frequency, scheduled_for, period_start, period_end, status,
|
|
91
|
-
attempt_count, model_id, data_source, facts, result,
|
|
108
|
+
attempt_count, model_id, provider_name, provider_source, data_source, facts, result, artifact_bucket,
|
|
109
|
+
markdown_key, markdown_size, pdf_key, pdf_size, error,
|
|
92
110
|
started_at, completed_at, created_at
|
|
93
111
|
FROM ${sql.raw(this.reports)} WHERE tenant_id = ${tenantId}
|
|
94
112
|
ORDER BY scheduled_for DESC LIMIT ${Math.min(Math.max(Number(limit) || 100, 1), 250)}
|
|
@@ -101,7 +119,8 @@ export class CostAnalysisReportRepository {
|
|
|
101
119
|
}
|
|
102
120
|
return rows(await this.db.execute(sql`
|
|
103
121
|
SELECT id, schedule_frequency, scheduled_for, period_start, period_end, status,
|
|
104
|
-
attempt_count, model_id, data_source, facts, result,
|
|
122
|
+
attempt_count, model_id, provider_name, provider_source, data_source, facts, result, artifact_bucket,
|
|
123
|
+
markdown_key, markdown_size, pdf_key, pdf_size, error,
|
|
105
124
|
started_at, completed_at, created_at
|
|
106
125
|
FROM ${sql.raw(this.reports)} WHERE tenant_id = ${tenantId} AND id = ${id} LIMIT 1
|
|
107
126
|
`))[0] || null;
|
package/src/routes/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export function createCostRouter({ costController, budgetController, costAnalysi
|
|
|
16
16
|
router.get("/analysis/reports", costAnalysisController.reports.bind(costAnalysisController));
|
|
17
17
|
router.get("/analysis/reports/:id", costAnalysisController.report.bind(costAnalysisController));
|
|
18
18
|
router.get("/analysis/reports/:id/pdf", costAnalysisController.downloadPdf.bind(costAnalysisController));
|
|
19
|
+
router.get("/analysis/reports/:id/markdown", costAnalysisController.downloadMarkdown.bind(costAnalysisController));
|
|
19
20
|
router.get("/budgets", budgetController.list.bind(budgetController));
|
|
20
21
|
router.post("/budgets", budgetController.create.bind(budgetController));
|
|
21
22
|
router.delete("/budgets/:id", budgetController.delete.bind(budgetController));
|
|
@@ -26,11 +26,18 @@ export async function installCostAnalysisReportSchema(db, qSchema) {
|
|
|
26
26
|
status text NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED')),
|
|
27
27
|
attempt_count integer NOT NULL DEFAULT 0,
|
|
28
28
|
model_id text,
|
|
29
|
+
provider_name text,
|
|
30
|
+
provider_source text,
|
|
29
31
|
data_source text,
|
|
30
32
|
facts jsonb,
|
|
31
33
|
result jsonb,
|
|
32
34
|
pdf_data bytea,
|
|
33
35
|
pdf_size integer,
|
|
36
|
+
ecs_task_arn text,
|
|
37
|
+
artifact_bucket text,
|
|
38
|
+
markdown_key text,
|
|
39
|
+
markdown_size integer,
|
|
40
|
+
pdf_key text,
|
|
34
41
|
error text,
|
|
35
42
|
started_at timestamptz,
|
|
36
43
|
completed_at timestamptz,
|
|
@@ -40,4 +47,11 @@ export async function installCostAnalysisReportSchema(db, qSchema) {
|
|
|
40
47
|
)`));
|
|
41
48
|
await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS cost_ai_reports_tenant_created_idx ON ${qSchema}.cost_ai_reports (tenant_id, created_at DESC)`));
|
|
42
49
|
await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS cost_ai_reports_status_idx ON ${qSchema}.cost_ai_reports (status, created_at)`));
|
|
50
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS ecs_task_arn text`));
|
|
51
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS artifact_bucket text`));
|
|
52
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS markdown_key text`));
|
|
53
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS markdown_size integer`));
|
|
54
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS pdf_key text`));
|
|
55
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS provider_name text`));
|
|
56
|
+
await db.execute(sql.raw(`ALTER TABLE ${qSchema}.cost_ai_reports ADD COLUMN IF NOT EXISTS provider_source text`));
|
|
43
57
|
}
|