Task + goal tracker: private tasks schema, /trackers/tasks page, idempotent seed import (daisy-import)
All checks were successful
Deploy production / deploy (push) Successful in 2m38s
All checks were successful
Deploy production / deploy (push) Successful in 2m38s
This commit is contained in:
@@ -129,6 +129,28 @@ try {
|
||||
health.public_usage
|
||||
)
|
||||
throw new Error(`health schema grants are wrong: ${JSON.stringify(health)}`);
|
||||
// Private task tracker: same rules as health (app role read/write, personal_ro none).
|
||||
const [tasks] = await client`
|
||||
select
|
||||
count(*) filter (where has_table_privilege(c.oid, 'select, insert, update, delete')) as app_rw,
|
||||
count(*) filter (
|
||||
where exists (
|
||||
select 1 from aclexplode(c.relacl) a join pg_roles r on r.oid = a.grantee
|
||||
where r.rolname = 'personal_ro'
|
||||
)
|
||||
) as ro_read,
|
||||
count(*) as tables,
|
||||
has_schema_privilege('public', 'tasks', 'usage') as public_usage
|
||||
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
||||
where n.nspname = 'tasks' and c.relkind = 'r'
|
||||
`;
|
||||
if (
|
||||
Number(tasks.tables) !== 4 ||
|
||||
Number(tasks.app_rw) !== 4 ||
|
||||
Number(tasks.ro_read) !== 0 ||
|
||||
tasks.public_usage
|
||||
)
|
||||
throw new Error(`tasks schema grants are wrong: ${JSON.stringify(tasks)}`);
|
||||
// Energy tracker reads the ned-collector tables (SELECT only).
|
||||
const [energy] = await client`
|
||||
select count(*) as hours, max(valid_from) as latest from energy.ned_utilization
|
||||
@@ -138,7 +160,7 @@ try {
|
||||
if (!Number(energy.hours))
|
||||
console.warn('Warning: energy.ned_utilization has no hourly data from the last 7 days');
|
||||
console.log(
|
||||
`health: ${health.tables} tables, app read/write, personal_ro none; energy: ${energy.hours} hourly rows, latest ${energy.latest ? new Date(energy.latest).toISOString() : 'none'}`
|
||||
`health: ${health.tables} tables, tasks: ${tasks.tables} tables, app read/write, personal_ro none; energy: ${energy.hours} hourly rows, latest ${energy.latest ? new Date(energy.latest).toISOString() : 'none'}`
|
||||
);
|
||||
const [catalog] = await client`
|
||||
select
|
||||
|
||||
46
scripts/tasks-import.ts
Normal file
46
scripts/tasks-import.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Import tasks, goals and areas into the private `tasks` schema (idempotent).
|
||||
*
|
||||
* bun scripts/tasks-import.ts data/tasks/daisy-import.json # insert what is missing
|
||||
* bun scripts/tasks-import.ts --dry-run data/tasks/daisy-import.json # validate only
|
||||
* bun scripts/tasks-import.ts --update data/tasks/daisy-import.json # also overwrite existing rows
|
||||
*
|
||||
* Dedupe: areas on slug, goals on title, tasks on area + title (case-insensitive).
|
||||
* Uses DB_URL (the app role on prod, personal_dev on dev).
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import { importSeed, seedFile } from '../src/lib/server/tasks';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const update = args.includes('--update');
|
||||
const files = args.filter((a) => !a.startsWith('--'));
|
||||
if (files.length !== 1) {
|
||||
console.error('usage: bun scripts/tasks-import.ts [--dry-run] [--update] <seed.json>');
|
||||
process.exit(2);
|
||||
}
|
||||
const parsed = seedFile.safeParse(JSON.parse(await readFile(files[0], 'utf8')));
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues.slice(0, 50))
|
||||
console.error(`${issue.path.join('.')}: ${issue.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const seed = parsed.data;
|
||||
console.log(
|
||||
`Valid: ${seed.areas.length} areas, ${seed.goals.length} goals, ${seed.tasks.length} tasks`
|
||||
);
|
||||
if (dryRun) process.exit(0);
|
||||
if (!process.env.DB_URL) throw new Error('DB_URL is required');
|
||||
const client = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
const r = await importSeed(drizzle({ client }), seed, { update });
|
||||
console.log(
|
||||
`areas: ${r.areas.inserted} inserted, ${r.areas.existing} existing; ` +
|
||||
`goals: ${r.goals.inserted} inserted, ${r.goals.existing} existing, ${r.goals.updated} updated; ` +
|
||||
`tasks: ${r.tasks.inserted} inserted, ${r.tasks.existing} existing, ${r.tasks.updated} updated`
|
||||
);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
Reference in New Issue
Block a user