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
Some checks failed
Deploy production / deploy (push) Has been cancelled
This commit is contained in:
84
src/lib/server/db/schema/ai.ts
Normal file
84
src/lib/server/db/schema/ai.ts
Normal 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`)
|
||||
]
|
||||
);
|
||||
Reference in New Issue
Block a user