AI tracker: /trackers/ai with public news, owner-only bots and usage; ai schema (news_items, bot_status, usage_entries); idempotent import scripts; preflight ai grants check
Some checks failed
Deploy production / deploy (push) Has been cancelled

This commit is contained in:
2026-10-25 23:58:39 +00:00
parent 9b21220280
commit cf0fec973c
24 changed files with 38866 additions and 3 deletions

View File

@@ -0,0 +1,84 @@
import { sql } from 'drizzle-orm';
import { check, date, doublePrecision, index, text, uniqueIndex } from 'drizzle-orm/pg-core';
import { aiSchema } from './schemas';
import { id, timestamps, timestamptz } from './helpers';
// The AI tracker (/trackers/ai). No live tracking: rows arrive through the idempotent
// import scripts (scripts/ai-news-import.ts, scripts/ai-bots-import.ts) that Daisy runs on
// the server, plus the owner's manual usage form. News is public; bot_status and
// usage_entries are owner-only (checked in the server loads and actions). Like tasks and
// health, the schema is granted to the app role only (no personal_ro).
export const AI_NEWS_SECTIONS = ['ai', 'software'] as const;
export type AiNewsSection = (typeof AI_NEWS_SECTIONS)[number];
export const AI_PROVIDERS = ['claude', 'chatgpt', 'gemini', 'api'] as const;
export type AiProvider = (typeof AI_PROVIDERS)[number];
/** scheduled = a scheduled task in the provider app; ccusage = ccusage on the server. */
export const AI_USAGE_SOURCES = ['scheduled', 'ccusage', 'export', 'manual'] as const;
export type AiUsageSource = (typeof AI_USAGE_SOURCES)[number];
/** Digest items from Ilya's weekday AI/software digest; `url` is the import key. */
export const aiNewsItems = aiSchema.table(
'news_items',
{
id: id(),
date: date('date').notNull(),
title: text('title').notNull(),
url: text('url').notNull(),
note: text('note'),
section: text('section').notNull().default('ai'),
...timestamps
},
(t) => [
uniqueIndex('news_items_url_key').on(t.url),
index('news_items_date_idx').on(t.date),
check('news_items_section_check', sql`${t.section} IN ('ai','software')`),
check('news_items_title_check', sql`length(btrim(${t.title})) > 0`)
]
);
/** One row per Grok Bot agent: short neutral summaries only, never raw transcripts. */
export const aiBotStatus = aiSchema.table(
'bot_status',
{
id: id(),
name: text('name').notNull(),
focus: text('focus'),
recent: text('recent'),
lastActive: timestamptz('last_active'),
...timestamps
},
(t) => [
uniqueIndex('bot_status_name_key').on(t.name),
check('bot_status_name_check', sql`length(btrim(${t.name})) > 0`)
]
);
/** Dated usage numbers per provider and metric (one value per day, metric and source). */
export const aiUsageEntries = aiSchema.table(
'usage_entries',
{
id: id(),
provider: text('provider').notNull(),
date: date('date').notNull(),
metric: text('metric').notNull(),
value: doublePrecision('value').notNull(),
unit: text('unit'),
source: text('source').notNull().default('manual'),
note: text('note'),
...timestamps
},
(t) => [
uniqueIndex('usage_entries_day_key').on(t.provider, t.metric, t.date, t.source),
index('usage_entries_provider_idx').on(t.provider, t.metric, t.date),
check(
'usage_entries_provider_check',
sql`${t.provider} IN ('claude','chatgpt','gemini','api')`
),
check(
'usage_entries_source_check',
sql`${t.source} IN ('scheduled','ccusage','export','manual')`
),
check('usage_entries_metric_check', sql`length(btrim(${t.metric})) > 0`)
]
);

View File

@@ -28,3 +28,4 @@ export * from './hardware';
export * from './dataholders';
export * from './stats';
export * from './pubg-map';
export * from './ai';

View File

@@ -25,3 +25,4 @@ export const wildlifeSchema = pgSchema('wildlife');
export const trainsSchema = pgSchema('trains');
export const dataholdersSchema = pgSchema('dataholders');
export const statsSchema = pgSchema('stats');
export const aiSchema = pgSchema('ai');