mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:32:12 +01:00
Add initial wrangler configuration for livedash-node project
- Created wrangler.json with project metadata and settings - Configured D1 database binding for database interaction - Enabled observability for monitoring - Added placeholders for smart placement, environment variables, static assets, and service bindings
This commit is contained in:
186
.gitignore
vendored
186
.gitignore
vendored
@ -261,3 +261,189 @@ Thumbs.db
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,macos
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Moved from ./templates for ignoring all locks in templates
|
||||||
|
templates/**/*-lock.*
|
||||||
|
templates/**/*.lock
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,macos
|
||||||
|
|
||||||
|
# Wrangler output
|
||||||
|
.wrangler/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Turbo output
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
.dev.vars*
|
||||||
|
test-transcript-format.js
|
||||||
|
|||||||
54
.prettierignore
Normal file
54
.prettierignore
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
prisma/migrations/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.generated.*
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
40
README.md
40
README.md
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
A real-time analytics dashboard for monitoring user sessions and interactions with interactive data visualizations and detailed metrics.
|
A real-time analytics dashboard for monitoring user sessions and interactions with interactive data visualizations and detailed metrics.
|
||||||
|
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=nextdotjs&label=Nextjs&color=%23000000)
|
.*%22&replace=%24%3Cversion%3E&logo=nextdotjs&label=Nextjs&color=%23000000>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=react&label=React&color=%2361DAFB)
|
.*%22&replace=%24%3Cversion%3E&logo=react&label=React&color=%2361DAFB>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=typescript&label=TypeScript&color=%233178C6)
|
.*%22&replace=%24%3Cversion%3E&logo=typescript&label=TypeScript&color=%233178C6>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=prisma&label=Prisma&color=%232D3748)
|
.*%22&replace=%24%3Cversion%3E&logo=prisma&label=Prisma&color=%232D3748>)
|
||||||
.*%22&replace=%24%3Cversion%3E&logo=tailwindcss&label=TailwindCSS&color=%2306B6D4)
|
.*%22&replace=%24%3Cversion%3E&logo=tailwindcss&label=TailwindCSS&color=%2306B6D4>)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -37,30 +37,30 @@ A real-time analytics dashboard for monitoring user sessions and interactions wi
|
|||||||
|
|
||||||
1. Clone this repository:
|
1. Clone this repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/kjanat/livedash-node.git
|
git clone https://github.com/kjanat/livedash-node.git
|
||||||
cd livedash-node
|
cd livedash-node
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Set up the database:
|
3. Set up the database:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run prisma:generate
|
npm run prisma:generate
|
||||||
npm run prisma:migrate
|
npm run prisma:migrate
|
||||||
npm run prisma:seed
|
npm run prisma:seed
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the development server:
|
4. Start the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open your browser and navigate to <http://localhost:3000>
|
5. Open your browser and navigate to <http://localhost:3000>
|
||||||
|
|
||||||
|
|||||||
12
TODO.md
12
TODO.md
@ -9,6 +9,7 @@
|
|||||||
## Component Specific
|
## Component Specific
|
||||||
|
|
||||||
- [ ] **Implement robust emailing of temporary passwords**
|
- [ ] **Implement robust emailing of temporary passwords**
|
||||||
|
|
||||||
- File: `pages/api/dashboard/users.ts`
|
- File: `pages/api/dashboard/users.ts`
|
||||||
- Set up proper email service integration
|
- Set up proper email service integration
|
||||||
|
|
||||||
@ -25,9 +26,11 @@
|
|||||||
## Database Schema Improvements
|
## Database Schema Improvements
|
||||||
|
|
||||||
- [ ] **Update EndTime field**
|
- [ ] **Update EndTime field**
|
||||||
|
|
||||||
- Make `endTime` field nullable in Prisma schema to match TypeScript interfaces
|
- Make `endTime` field nullable in Prisma schema to match TypeScript interfaces
|
||||||
|
|
||||||
- [ ] **Add database indices**
|
- [ ] **Add database indices**
|
||||||
|
|
||||||
- Add appropriate indices to improve query performance
|
- Add appropriate indices to improve query performance
|
||||||
- Focus on dashboard metrics and session listing queries
|
- Focus on dashboard metrics and session listing queries
|
||||||
|
|
||||||
@ -38,10 +41,12 @@
|
|||||||
## General Enhancements & Features
|
## General Enhancements & Features
|
||||||
|
|
||||||
- [ ] **Real-time updates**
|
- [ ] **Real-time updates**
|
||||||
|
|
||||||
- Implement for dashboard and session list
|
- Implement for dashboard and session list
|
||||||
- Consider WebSockets or Server-Sent Events
|
- Consider WebSockets or Server-Sent Events
|
||||||
|
|
||||||
- [ ] **Data export functionality**
|
- [ ] **Data export functionality**
|
||||||
|
|
||||||
- Allow users (especially admins) to export session data
|
- Allow users (especially admins) to export session data
|
||||||
- Support CSV format initially
|
- Support CSV format initially
|
||||||
|
|
||||||
@ -52,11 +57,13 @@
|
|||||||
## Testing & Quality Assurance
|
## Testing & Quality Assurance
|
||||||
|
|
||||||
- [ ] **Comprehensive testing suite**
|
- [ ] **Comprehensive testing suite**
|
||||||
|
|
||||||
- [ ] Unit tests for utility functions and API logic
|
- [ ] Unit tests for utility functions and API logic
|
||||||
- [ ] Integration tests for API endpoints with database
|
- [ ] Integration tests for API endpoints with database
|
||||||
- [ ] End-to-end tests for user flows (Playwright or Cypress)
|
- [ ] End-to-end tests for user flows (Playwright or Cypress)
|
||||||
|
|
||||||
- [ ] **Error monitoring and logging**
|
- [ ] **Error monitoring and logging**
|
||||||
|
|
||||||
- Integrate robust error monitoring service (Sentry)
|
- Integrate robust error monitoring service (Sentry)
|
||||||
- Enhance server-side logging
|
- Enhance server-side logging
|
||||||
|
|
||||||
@ -68,10 +75,12 @@
|
|||||||
## Security Enhancements
|
## Security Enhancements
|
||||||
|
|
||||||
- [x] **Password reset functionality** ✅
|
- [x] **Password reset functionality** ✅
|
||||||
|
|
||||||
- Implemented secure password reset mechanism
|
- Implemented secure password reset mechanism
|
||||||
- Files: `app/forgot-password/page.tsx`, `app/reset-password/page.tsx`, `pages/api/forgot-password.ts`, `pages/api/reset-password.ts`
|
- Files: `app/forgot-password/page.tsx`, `app/reset-password/page.tsx`, `pages/api/forgot-password.ts`, `pages/api/reset-password.ts`
|
||||||
|
|
||||||
- [ ] **Two-Factor Authentication (2FA)**
|
- [ ] **Two-Factor Authentication (2FA)**
|
||||||
|
|
||||||
- Consider adding 2FA, especially for admin accounts
|
- Consider adding 2FA, especially for admin accounts
|
||||||
|
|
||||||
- [ ] **Input validation and sanitization**
|
- [ ] **Input validation and sanitization**
|
||||||
@ -81,12 +90,15 @@
|
|||||||
## Code Quality & Development
|
## Code Quality & Development
|
||||||
|
|
||||||
- [ ] **Code review process**
|
- [ ] **Code review process**
|
||||||
|
|
||||||
- Enforce code reviews for all changes
|
- Enforce code reviews for all changes
|
||||||
|
|
||||||
- [ ] **Environment configuration**
|
- [ ] **Environment configuration**
|
||||||
|
|
||||||
- Ensure secure management of environment-specific configurations
|
- Ensure secure management of environment-specific configurations
|
||||||
|
|
||||||
- [ ] **Dependency management**
|
- [ ] **Dependency management**
|
||||||
|
|
||||||
- Periodically review dependencies for vulnerabilities
|
- Periodically review dependencies for vulnerabilities
|
||||||
- Keep dependencies updated
|
- Keep dependencies updated
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import { useState, useEffect } from "react";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { Company } from "../../../lib/types";
|
import { Company } from "../../../lib/types";
|
||||||
|
|
||||||
|
interface CompanyConfigResponse {
|
||||||
|
company: Company;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CompanySettingsPage() {
|
export default function CompanySettingsPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
// We store the full company object for future use and updates after save operations
|
// We store the full company object for future use and updates after save operations
|
||||||
@ -22,7 +26,7 @@ export default function CompanySettingsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/dashboard/config");
|
const res = await fetch("/api/dashboard/config");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as CompanyConfigResponse;
|
||||||
setCompany(data.company);
|
setCompany(data.company);
|
||||||
setCsvUrl(data.company.csvUrl || "");
|
setCsvUrl(data.company.csvUrl || "");
|
||||||
setCsvUsername(data.company.csvUsername || "");
|
setCsvUsername(data.company.csvUsername || "");
|
||||||
@ -58,10 +62,10 @@ export default function CompanySettingsPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage("Settings saved successfully!");
|
setMessage("Settings saved successfully!");
|
||||||
// Update local state if needed
|
// Update local state if needed
|
||||||
const data = await res.json();
|
const data = (await res.json()) as CompanyConfigResponse;
|
||||||
setCompany(data.company);
|
setCompany(data.company);
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const error = (await res.json()) as { message?: string; };
|
||||||
setMessage(
|
setMessage(
|
||||||
`Failed to save settings: ${error.message || "Unknown error"}`
|
`Failed to save settings: ${error.message || "Unknown error"}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,6 +17,11 @@ import GeographicMap from "../../../components/GeographicMap";
|
|||||||
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
||||||
import WelcomeBanner from "../../../components/WelcomeBanner";
|
import WelcomeBanner from "../../../components/WelcomeBanner";
|
||||||
|
|
||||||
|
interface MetricsApiResponse {
|
||||||
|
metrics: MetricsResult;
|
||||||
|
company: Company;
|
||||||
|
}
|
||||||
|
|
||||||
// Safely wrapped component with useSession
|
// Safely wrapped component with useSession
|
||||||
function DashboardContent() {
|
function DashboardContent() {
|
||||||
const { data: session, status } = useSession(); // Add status from useSession
|
const { data: session, status } = useSession(); // Add status from useSession
|
||||||
@ -40,7 +45,7 @@ function DashboardContent() {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch("/api/dashboard/metrics");
|
const res = await fetch("/api/dashboard/metrics");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as MetricsApiResponse;
|
||||||
console.log("Metrics from API:", {
|
console.log("Metrics from API:", {
|
||||||
avgSessionLength: data.metrics.avgSessionLength,
|
avgSessionLength: data.metrics.avgSessionLength,
|
||||||
avgSessionTimeTrend: data.metrics.avgSessionTimeTrend,
|
avgSessionTimeTrend: data.metrics.avgSessionTimeTrend,
|
||||||
@ -76,10 +81,10 @@ function DashboardContent() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// Refetch metrics
|
// Refetch metrics
|
||||||
const metricsRes = await fetch("/api/dashboard/metrics");
|
const metricsRes = await fetch("/api/dashboard/metrics");
|
||||||
const data = await metricsRes.json();
|
const data = (await metricsRes.json()) as MetricsApiResponse;
|
||||||
setMetrics(data.metrics);
|
setMetrics(data.metrics);
|
||||||
} else {
|
} else {
|
||||||
const errorData = await res.json();
|
const errorData = (await res.json()) as { error: string; };
|
||||||
alert(`Failed to refresh sessions: ${errorData.error}`);
|
alert(`Failed to refresh sessions: ${errorData.error}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import TranscriptViewer from "../../../../components/TranscriptViewer";
|
|||||||
import { ChatSession } from "../../../../lib/types";
|
import { ChatSession } from "../../../../lib/types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface SessionApiResponse {
|
||||||
|
session: ChatSession;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SessionViewPage() {
|
export default function SessionViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter(); // Initialize useRouter
|
const router = useRouter(); // Initialize useRouter
|
||||||
@ -30,13 +34,13 @@ export default function SessionViewPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/dashboard/session/${id}`);
|
const response = await fetch(`/api/dashboard/session/${id}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = (await response.json()) as { error: string; };
|
||||||
throw new Error(
|
throw new Error(
|
||||||
errorData.error ||
|
errorData.error ||
|
||||||
`Failed to fetch session: ${response.statusText}`
|
`Failed to fetch session: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as SessionApiResponse;
|
||||||
setSession(data.session);
|
setSession(data.session);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
@ -150,7 +154,8 @@ export default function SessionViewPage() {
|
|||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
No transcript content available for this session.
|
No transcript content available for this session.
|
||||||
</p>
|
</p>
|
||||||
{session.fullTranscriptUrl && (
|
{session.fullTranscriptUrl &&
|
||||||
|
process.env.NODE_ENV !== "production" && (
|
||||||
<a
|
<a
|
||||||
href={session.fullTranscriptUrl}
|
href={session.fullTranscriptUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -14,6 +14,11 @@ interface FilterOptions {
|
|||||||
languages: string[];
|
languages: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionsApiResponse {
|
||||||
|
sessions: ChatSession[];
|
||||||
|
totalSessions: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SessionsPage() {
|
export default function SessionsPage() {
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -58,7 +63,7 @@ export default function SessionsPage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch filter options");
|
throw new Error("Failed to fetch filter options");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as FilterOptions;
|
||||||
setFilterOptions(data);
|
setFilterOptions(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
@ -88,7 +93,7 @@ export default function SessionsPage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
|
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = (await response.json()) as SessionsApiResponse;
|
||||||
setSessions(data.sessions || []);
|
setSessions(data.sessions || []);
|
||||||
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
|
setTotalPages(Math.ceil((data.totalSessions || 0) / pageSize));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -12,6 +12,10 @@ interface UserManagementProps {
|
|||||||
session: UserSession;
|
session: UserSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsersApiResponse {
|
||||||
|
users: UserItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserManagement({ session }: UserManagementProps) {
|
export default function UserManagement({ session }: UserManagementProps) {
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
@ -21,7 +25,7 @@ export default function UserManagement({ session }: UserManagementProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/dashboard/users")
|
fetch("/api/dashboard/users")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setUsers(data.users));
|
.then((data) => setUsers((data as UsersApiResponse).users));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function inviteUser() {
|
async function inviteUser() {
|
||||||
|
|||||||
@ -9,6 +9,10 @@ interface UserItem {
|
|||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsersApiResponse {
|
||||||
|
users: UserItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserManagementPage() {
|
export default function UserManagementPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
@ -27,7 +31,7 @@ export default function UserManagementPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/dashboard/users");
|
const res = await fetch("/api/dashboard/users");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as UsersApiResponse;
|
||||||
setUsers(data.users);
|
setUsers(data.users);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch users:", error);
|
console.error("Failed to fetch users:", error);
|
||||||
@ -52,7 +56,7 @@ export default function UserManagementPage() {
|
|||||||
// Refresh the user list
|
// Refresh the user list
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} else {
|
} else {
|
||||||
const error = await res.json();
|
const error = (await res.json()) as { message?: string; };
|
||||||
setMessage(
|
setMessage(
|
||||||
`Failed to invite user: ${error.message || "Unknown error"}`
|
`Failed to invite user: ${error.message || "Unknown error"}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -146,7 +146,8 @@ export default function SessionDetails({ session }: SessionDetailsProps) {
|
|||||||
{/* Fallback to link only if we only have the URL but no content - this might also be redundant if parent handles all transcript display */}
|
{/* Fallback to link only if we only have the URL but no content - this might also be redundant if parent handles all transcript display */}
|
||||||
{(!session.transcriptContent ||
|
{(!session.transcriptContent ||
|
||||||
session.transcriptContent.length === 0) &&
|
session.transcriptContent.length === 0) &&
|
||||||
session.fullTranscriptUrl && (
|
session.fullTranscriptUrl &&
|
||||||
|
process.env.NODE_ENV !== "production" && (
|
||||||
<div className="flex justify-between pt-2">
|
<div className="flex justify-between pt-2">
|
||||||
<span className="text-gray-600">Transcript:</span>
|
<span className="text-gray-600">Transcript:</span>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw"; // Import rehype-raw
|
import rehypeRaw from "rehype-raw";
|
||||||
|
|
||||||
interface TranscriptViewerProps {
|
interface TranscriptViewerProps {
|
||||||
transcriptContent: string;
|
transcriptContent: string;
|
||||||
@ -23,6 +23,7 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
const elements: React.ReactNode[] = [];
|
const elements: React.ReactNode[] = [];
|
||||||
let currentSpeaker: string | null = null;
|
let currentSpeaker: string | null = null;
|
||||||
let currentMessages: string[] = [];
|
let currentMessages: string[] = [];
|
||||||
|
let currentTimestamp: string | null = null;
|
||||||
|
|
||||||
// Process each line
|
// Process each line
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
@ -32,8 +33,15 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a new speaker line
|
// Check if this is a new speaker line with or without datetime
|
||||||
if (line.startsWith("User:") || line.startsWith("Assistant:")) {
|
// Format 1: [29.05.2025 21:26:44] User: message
|
||||||
|
// Format 2: User: message
|
||||||
|
const datetimeMatch = line.match(
|
||||||
|
/^\[([^\]]+)\]\s*(User|Assistant):\s*(.*)$/
|
||||||
|
);
|
||||||
|
const simpleMatch = line.match(/^(User|Assistant):\s*(.*)$/);
|
||||||
|
|
||||||
|
if (datetimeMatch || simpleMatch) {
|
||||||
// If we have accumulated messages for a previous speaker, add them
|
// If we have accumulated messages for a previous speaker, add them
|
||||||
if (currentSpeaker && currentMessages.length > 0) {
|
if (currentSpeaker && currentMessages.length > 0) {
|
||||||
elements.push(
|
elements.push(
|
||||||
@ -48,6 +56,11 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{currentTimestamp && (
|
||||||
|
<div className="text-xs opacity-60 mb-1">
|
||||||
|
{currentTimestamp}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{currentMessages.map((msg, i) => (
|
{currentMessages.map((msg, i) => (
|
||||||
// Use ReactMarkdown to render each message part
|
// Use ReactMarkdown to render each message part
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -73,13 +86,23 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
currentMessages = [];
|
currentMessages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new current speaker
|
if (datetimeMatch) {
|
||||||
currentSpeaker = line.startsWith("User:") ? "User" : "Assistant";
|
// Format with datetime: [29.05.2025 21:26:44] User: message
|
||||||
// Add the content after "User:" or "Assistant:"
|
currentTimestamp = datetimeMatch[1];
|
||||||
const messageContent = line.substring(line.indexOf(":") + 1).trim();
|
currentSpeaker = datetimeMatch[2];
|
||||||
|
const messageContent = datetimeMatch[3].trim();
|
||||||
if (messageContent) {
|
if (messageContent) {
|
||||||
currentMessages.push(messageContent);
|
currentMessages.push(messageContent);
|
||||||
}
|
}
|
||||||
|
} else if (simpleMatch) {
|
||||||
|
// Format without datetime: User: message
|
||||||
|
currentTimestamp = null;
|
||||||
|
currentSpeaker = simpleMatch[1];
|
||||||
|
const messageContent = simpleMatch[2].trim();
|
||||||
|
if (messageContent) {
|
||||||
|
currentMessages.push(messageContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (currentSpeaker) {
|
} else if (currentSpeaker) {
|
||||||
// This is a continuation of the current speaker's message
|
// This is a continuation of the current speaker's message
|
||||||
currentMessages.push(line);
|
currentMessages.push(line);
|
||||||
@ -100,6 +123,9 @@ function formatTranscript(content: string): React.ReactNode[] {
|
|||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{currentTimestamp && (
|
||||||
|
<div className="text-xs opacity-60 mb-1">{currentTimestamp}</div>
|
||||||
|
)}
|
||||||
{currentMessages.map((msg, i) => (
|
{currentMessages.map((msg, i) => (
|
||||||
// Use ReactMarkdown to render each message part
|
// Use ReactMarkdown to render each message part
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -138,6 +164,9 @@ export default function TranscriptViewer({
|
|||||||
|
|
||||||
const formattedElements = formatTranscript(transcriptContent);
|
const formattedElements = formatTranscript(transcriptContent);
|
||||||
|
|
||||||
|
// Hide "View Full Raw" button in production environment
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow-lg rounded-lg p-4 md:p-6 mt-6">
|
<div className="bg-white shadow-lg rounded-lg p-4 md:p-6 mt-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@ -145,7 +174,7 @@ export default function TranscriptViewer({
|
|||||||
Session Transcript
|
Session Transcript
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{transcriptUrl && (
|
{transcriptUrl && !isProduction && (
|
||||||
<a
|
<a
|
||||||
href={transcriptUrl}
|
href={transcriptUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -10,6 +10,41 @@ interface SessionCreateData {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches transcript content from a URL with optional authentication
|
||||||
|
* @param url The URL to fetch the transcript from
|
||||||
|
* @param username Optional username for Basic Auth
|
||||||
|
* @param password Optional password for Basic Auth
|
||||||
|
* @returns The transcript content or null if fetching fails
|
||||||
|
*/
|
||||||
|
async function fetchTranscriptContent(
|
||||||
|
url: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const authHeader =
|
||||||
|
username && password
|
||||||
|
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: authHeader ? { Authorization: authHeader } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
process.stderr.write(
|
||||||
|
`Error fetching transcript: ${response.statusText}\n`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`Failed to fetch transcript: ${error}\n`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function startScheduler() {
|
export function startScheduler() {
|
||||||
cron.schedule("*/15 * * * *", async () => {
|
cron.schedule("*/15 * * * *", async () => {
|
||||||
const companies = await prisma.company.findMany();
|
const companies = await prisma.company.findMany();
|
||||||
@ -23,6 +58,16 @@ export function startScheduler() {
|
|||||||
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
// Fetch transcript content if URL is available
|
||||||
|
let transcriptContent: string | null = null;
|
||||||
|
if (session.fullTranscriptUrl) {
|
||||||
|
transcriptContent = await fetchTranscriptContent(
|
||||||
|
session.fullTranscriptUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sessionData: SessionCreateData = {
|
const sessionData: SessionCreateData = {
|
||||||
...session,
|
...session,
|
||||||
companyId: company.id,
|
companyId: company.id,
|
||||||
@ -51,6 +96,8 @@ export function startScheduler() {
|
|||||||
? session.messagesSent
|
? session.messagesSent
|
||||||
: 0,
|
: 0,
|
||||||
category: session.category || null,
|
category: session.category || null,
|
||||||
|
fullTranscriptUrl: session.fullTranscriptUrl || null,
|
||||||
|
transcriptContent: transcriptContent, // Add the transcript content
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
9506
package-lock.json
generated
9506
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@ -3,29 +3,53 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev:old": "next dev --turbopack",
|
||||||
|
"format": "pnpm dlx prettier --write .",
|
||||||
|
"format:check": "pnpm dlx prettier --check .",
|
||||||
|
"format:standard": "pnpm dlx standard . --fix",
|
||||||
|
"lint": "next lint",
|
||||||
|
"lint:fix": "pnpm dlx eslint --fix",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:seed": "node prisma/seed.mjs",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
|
"start": "next start",
|
||||||
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||||
|
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||||
|
"cf-typegen": "wrangler types",
|
||||||
|
"check": "tsc && wrangler deploy --dry-run",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"predeploy": "wrangler d1 migrations apply DB --remote",
|
||||||
|
"seedLocalD1": "wrangler d1 migrations apply DB --local"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.8.2",
|
||||||
"@rapideditor/country-coder": "^5.4.0",
|
"@rapideditor/country-coder": "^5.4.0",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/d3-cloud": "^1.2.9",
|
"@types/d3-cloud": "^1.2.9",
|
||||||
|
"@types/d3-selection": "^3.0.11",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/leaflet": "^1.9.18",
|
"@types/leaflet": "^1.9.18",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chart.js": "^4.0.0",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-annotation": "^3.1.0",
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
"csv-parse": "^5.5.0",
|
"csv-parse": "^5.6.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"d3-cloud": "^1.2.7",
|
"d3-cloud": "^1.2.7",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"iso-639-1": "^3.1.5",
|
"iso-639-1": "^3.1.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "^15.3.2",
|
"next": "^15.3.3",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"node-cron": "^4.0.7",
|
"node-cron": "^4.1.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-chartjs-2": "^5.0.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@ -33,42 +57,28 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.27.0",
|
"@eslint/js": "^9.28.0",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.29",
|
||||||
"@types/node-cron": "^3.0.8",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "^19.1.5",
|
"@types/react": "^19.1.6",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.33.0",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-next": "^15.3.2",
|
"eslint-config-next": "^15.3.3",
|
||||||
"eslint-plugin-prettier": "^5.4.0",
|
"eslint-plugin-prettier": "^5.4.1",
|
||||||
"markdownlint-cli2": "^0.18.1",
|
"markdownlint-cli2": "^0.18.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.4",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-jinja-template": "^2.1.0",
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.8.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.8",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.8.3",
|
||||||
},
|
"wrangler": "4.18.0"
|
||||||
"scripts": {
|
|
||||||
"build": "next build",
|
|
||||||
"dev": "next dev --turbopack",
|
|
||||||
"format": "npx prettier --write .",
|
|
||||||
"format:check": "npx prettier --check .",
|
|
||||||
"lint": "next lint",
|
|
||||||
"lint:fix": "npx eslint --fix",
|
|
||||||
"prisma:generate": "prisma generate",
|
|
||||||
"prisma:migrate": "prisma migrate dev",
|
|
||||||
"prisma:seed": "node prisma/seed.mjs",
|
|
||||||
"prisma:studio": "prisma studio",
|
|
||||||
"start": "next start",
|
|
||||||
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
|
||||||
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\""
|
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
@ -118,5 +128,22 @@
|
|||||||
".git",
|
".git",
|
||||||
"*.json"
|
"*.json"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"cloudflare": {
|
||||||
|
"label": "Worker + D1 Database",
|
||||||
|
"products": [
|
||||||
|
"Workers",
|
||||||
|
"D1"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"icon_urls": [
|
||||||
|
"https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/c6fc5da3-1e0a-4608-b2f1-9628577ec800/public",
|
||||||
|
"https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/5ca0ca32-e897-4699-d4c1-6b680512f000/public"
|
||||||
|
],
|
||||||
|
"docs_url": "https://developers.cloudflare.com/d1/",
|
||||||
|
"preview_image_url": "https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/cb7cb0a9-6102-4822-633c-b76b7bb25900/public",
|
||||||
|
"publish": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,13 +12,27 @@ interface SessionCreateData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches transcript content from a URL
|
* Fetches transcript content from a URL with optional authentication
|
||||||
* @param url The URL to fetch the transcript from
|
* @param url The URL to fetch the transcript from
|
||||||
|
* @param username Optional username for Basic Auth
|
||||||
|
* @param password Optional password for Basic Auth
|
||||||
* @returns The transcript content or null if fetching fails
|
* @returns The transcript content or null if fetching fails
|
||||||
*/
|
*/
|
||||||
async function fetchTranscriptContent(url: string): Promise<string | null> {
|
async function fetchTranscriptContent(
|
||||||
|
url: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string
|
||||||
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const authHeader =
|
||||||
|
username && password
|
||||||
|
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: authHeader ? { Authorization: authHeader } : {},
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`Error fetching transcript: ${response.statusText}\n`
|
`Error fetching transcript: ${response.statusText}\n`
|
||||||
@ -111,7 +125,9 @@ export default async function handler(
|
|||||||
let transcriptContent: string | null = null;
|
let transcriptContent: string | null = null;
|
||||||
if (session.fullTranscriptUrl) {
|
if (session.fullTranscriptUrl) {
|
||||||
transcriptContent = await fetchTranscriptContent(
|
transcriptContent = await fetchTranscriptContent(
|
||||||
session.fullTranscriptUrl
|
session.fullTranscriptUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6853
pnpm-lock.yaml
generated
Normal file
6853
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ async function main() {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
fullTranscriptUrl: true,
|
fullTranscriptUrl: true,
|
||||||
|
companyId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,10 +29,44 @@ async function main() {
|
|||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
|
// Group sessions by company to fetch credentials once per company
|
||||||
|
const sessionsByCompany = new Map<string, typeof sessionsToUpdate>();
|
||||||
for (const session of sessionsToUpdate) {
|
for (const session of sessionsToUpdate) {
|
||||||
|
if (!sessionsByCompany.has(session.companyId)) {
|
||||||
|
sessionsByCompany.set(session.companyId, []);
|
||||||
|
}
|
||||||
|
sessionsByCompany.get(session.companyId)!.push(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [companyId, companySessions] of Array.from(
|
||||||
|
sessionsByCompany.entries()
|
||||||
|
)) {
|
||||||
|
// Fetch company credentials once per company
|
||||||
|
const company = await prisma.company.findUnique({
|
||||||
|
where: { id: companyId },
|
||||||
|
select: {
|
||||||
|
csvUsername: true,
|
||||||
|
csvPassword: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
console.warn(`Company ${companyId} not found, skipping sessions.`);
|
||||||
|
errorCount += companySessions.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Processing ${companySessions.length} sessions for company: ${company.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const session of companySessions) {
|
||||||
if (!session.fullTranscriptUrl) {
|
if (!session.fullTranscriptUrl) {
|
||||||
// Should not happen due to query, but good for type safety
|
// Should not happen due to query, but good for type safety
|
||||||
console.warn(`Session ${session.id} has no fullTranscriptUrl, skipping.`);
|
console.warn(
|
||||||
|
`Session ${session.id} has no fullTranscriptUrl, skipping.`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +74,19 @@ async function main() {
|
|||||||
`Fetching transcript for session ${session.id} from ${session.fullTranscriptUrl}...`
|
`Fetching transcript for session ${session.id} from ${session.fullTranscriptUrl}...`
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(session.fullTranscriptUrl);
|
// Prepare authentication if credentials are available
|
||||||
|
const authHeader =
|
||||||
|
company.csvUsername && company.csvPassword
|
||||||
|
? "Basic " +
|
||||||
|
Buffer.from(
|
||||||
|
`${company.csvUsername}:${company.csvPassword}`
|
||||||
|
).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const response = await fetch(session.fullTranscriptUrl, {
|
||||||
|
headers: authHeader ? { Authorization: authHeader } : {},
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to fetch transcript for session ${session.id}: ${response.status} ${response.statusText}`
|
`Failed to fetch transcript for session ${session.id}: ${response.status} ${response.statusText}`
|
||||||
@ -71,6 +118,7 @@ async function main() {
|
|||||||
errorCount++;
|
errorCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Transcript fetching complete.");
|
console.log("Transcript fetching complete.");
|
||||||
console.log(`Successfully updated: ${successCount} sessions.`);
|
console.log(`Successfully updated: ${successCount} sessions.`);
|
||||||
|
|||||||
@ -8,9 +8,11 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node", // bundler
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noImplicitAny": false, // Allow implicit any types
|
"noImplicitAny": false, // Allow implicit any types
|
||||||
|
"preserveSymlinks": false,
|
||||||
|
"types": ["./worker-configuration.d.ts"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
@ -23,10 +25,11 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"target": "es5"
|
"target": "ESNext"
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"include": [
|
"include": [
|
||||||
|
"src",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
|
|||||||
6870
worker-configuration.d.ts
vendored
Normal file
6870
worker-configuration.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
55
wrangler.json
Normal file
55
wrangler.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* For more details on how to configure Wrangler, refer to:
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"compatibility_date": "2025-04-01",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"name": "livedash-node",
|
||||||
|
"upload_source_maps": true,
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "DB",
|
||||||
|
"database_id": "d4ee7efe-d37a-48e4-bed7-fdfaa5108131",
|
||||||
|
"database_name": "d1-notso-livedash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Smart Placement
|
||||||
|
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
|
*/
|
||||||
|
// "placement": { "mode": "smart" },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindings
|
||||||
|
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||||
|
* databases, object storage, AI inference, real-time communication and more.
|
||||||
|
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment Variables
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||||
|
*/
|
||||||
|
// "vars": { "MY_VARIABLE": "production_value" },
|
||||||
|
/**
|
||||||
|
* Note: Use secrets to store sensitive data.
|
||||||
|
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static Assets
|
||||||
|
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||||
|
*/
|
||||||
|
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service Bindings (communicate between multiple Workers)
|
||||||
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
|
*/
|
||||||
|
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user