Clean up project root - remove 51 obsolete files

Deleted:
- 26 old markdown summary/documentation files
- 16 debug/test Python scripts (debug_*, test_*, diagnose_*)
- 10 untracked JSON files from api_response_samples
- terms-of-usage.md, pane_not_found.png

Also includes pending web app changes:
- Jobs management UI (JobsView, Sidebar components)
- API routes for job streaming and comparison
- Enhanced ReviewAnalytics and ScraperTest components

Final clean structure:
├── api_server_production.py  (main entry)
├── modules/                  (core Python)
├── web/                      (Next.js frontend)
├── tests/                    (test suite)
├── docs/                     (documentation)
└── examples/                 (usage examples)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-01-23 17:31:53 +00:00
parent 8ccf72a489
commit 47bb032011
69 changed files with 3417 additions and 11347 deletions

View File

@@ -1,5 +1,10 @@
// Analytics utility functions
export interface OwnerResponse {
text: string;
timestamp?: string;
}
export interface Review {
author: string;
rating: number;
@@ -8,6 +13,8 @@ export interface Review {
avatar_url: string | null;
profile_url: string | null;
review_id: string;
owner_response?: OwnerResponse | null;
photo_urls?: string[] | null;
// Derived fields (computed on load)
parsedDate?: Date;
dateCategory?: 'recent' | 'month' | 'year' | 'older'; // Time range category
@@ -22,6 +29,7 @@ export interface TimelineDataPoint {
date: string;
rating: number;
rollingAvg: number;
count: number; // Number of reviews in this period
}
export interface ReviewStats {
@@ -37,6 +45,21 @@ export interface ReviewStats {
negativeReviews: number;
responseRate: number;
averageResponseTime: string;
// Response breakdown
responseBreakdown: { answered: number; notAnswered: number };
// New trend metrics
ratingTrend: {
recentAvg: number;
olderAvg: number;
change: number; // positive = improvement, negative = decline
periodLabel: string;
};
reviewVelocity: {
recentCount: number;
olderCount: number;
changePercent: number; // positive = more reviews, negative = fewer
periodLabel: string;
};
}
export function calculateReviewStats(reviews: Review[]): ReviewStats {
@@ -55,19 +78,21 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
const totalReviews = reviews.length;
// Average rating
const averageRating = reviews.reduce((sum, r) => sum + r.rating, 0) / totalReviews;
const averageRating = totalReviews > 0
? reviews.reduce((sum, r) => sum + r.rating, 0) / totalReviews
: 0;
// Sentiment score (% of 4-5 star reviews)
const positiveReviews = reviews.filter(r => r.rating >= 4).length;
const sentimentScore = (positiveReviews / totalReviews) * 100;
const sentimentScore = totalReviews > 0 ? (positiveReviews / totalReviews) * 100 : 0;
// Photo count (reviews with avatars as proxy)
const photoCount = reviews.filter(r => r.avatar_url).length;
// Photo count (reviews with actual photos attached)
const photoCount = reviews.filter(r => r.photo_urls && r.photo_urls.length > 0).length;
// Average review length
const avgReviewLength = Math.round(
reviews.reduce((sum, r) => sum + (r.text?.split(' ').length || 0), 0) / totalReviews
);
const avgReviewLength = totalReviews > 0
? Math.round(reviews.reduce((sum, r) => sum + (r.text?.split(' ').length || 0), 0) / totalReviews)
: 0;
// Recent reviews (last 30 days - simplified check)
const recentReviews = reviews.filter(r => {
@@ -122,11 +147,50 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
// Negative reviews count
const negativeReviews = reviews.filter(r => r.rating <= 2).length;
// Response rate (placeholder - would need owner_response field)
const responseRate = 0; // TODO: Calculate when owner responses are available
// Response breakdown - count answered vs not answered reviews
const answeredReviews = reviews.filter(r => r.owner_response?.text).length;
const responseBreakdown = {
answered: answeredReviews,
notAnswered: totalReviews - answeredReviews,
};
// Average response time (placeholder)
const averageResponseTime = 'N/A'; // TODO: Calculate when response data is available
// Response rate calculated from actual data
const responseRate = totalReviews > 0 ? (answeredReviews / totalReviews) * 100 : 0;
// Average response time (placeholder - would need response timestamps)
const averageResponseTime = 'N/A'; // TODO: Calculate when response timestamps are available
// Rating Trend - compare recent 3 months vs previous 3 months
const now = new Date();
const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000);
const recentReviewsForTrend = reviews.filter(r => r.centerDate && r.centerDate >= threeMonthsAgo);
const olderReviewsForTrend = reviews.filter(r => r.centerDate && r.centerDate < threeMonthsAgo && r.centerDate >= sixMonthsAgo);
const recentAvg = recentReviewsForTrend.length > 0
? recentReviewsForTrend.reduce((sum, r) => sum + r.rating, 0) / recentReviewsForTrend.length
: 0;
const olderAvg = olderReviewsForTrend.length > 0
? olderReviewsForTrend.reduce((sum, r) => sum + r.rating, 0) / olderReviewsForTrend.length
: 0;
const ratingTrend = {
recentAvg: Math.round(recentAvg * 10) / 10,
olderAvg: Math.round(olderAvg * 10) / 10,
change: Math.round((recentAvg - olderAvg) * 10) / 10,
periodLabel: 'last 3 months vs previous 3 months',
};
// Review Velocity - compare recent 3 months vs previous 3 months
const reviewVelocity = {
recentCount: recentReviewsForTrend.length,
olderCount: olderReviewsForTrend.length,
changePercent: olderReviewsForTrend.length > 0
? Math.round(((recentReviewsForTrend.length - olderReviewsForTrend.length) / olderReviewsForTrend.length) * 100)
: (recentReviewsForTrend.length > 0 ? 100 : 0),
periodLabel: 'last 3 months vs previous 3 months',
};
return {
totalReviews,
@@ -141,6 +205,9 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
negativeReviews,
responseRate,
averageResponseTime,
responseBreakdown,
ratingTrend,
reviewVelocity,
};
}
@@ -367,9 +434,10 @@ export function calculateTimelineData(reviews: Review[]): TimelineDataPoint[] {
// Group by month
const monthlyData: Record<string, { ratings: number[]; date: Date }> = {};
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
sortedReviews.forEach(review => {
const monthKey = `${review.parsedDate.getFullYear()}-${String(review.parsedDate.getMonth() + 1).padStart(2, '0')}`;
const monthKey = `${monthNames[review.parsedDate.getMonth()]} ${review.parsedDate.getFullYear()}`;
if (!monthlyData[monthKey]) {
monthlyData[monthKey] = { ratings: [], date: review.parsedDate };
@@ -383,8 +451,17 @@ export function calculateTimelineData(reviews: Review[]): TimelineDataPoint[] {
date: monthKey,
rating: data.ratings.reduce((a, b) => a + b, 0) / data.ratings.length,
rollingAvg: 0, // Will calculate below
count: data.ratings.length, // Number of reviews this month
}))
.sort((a, b) => a.date.localeCompare(b.date));
.sort((a, b) => {
// Parse "Mon YYYY" format for sorting
const parseMonthYear = (d: string) => {
const [month, year] = d.split(' ');
const monthIndex = monthNames.indexOf(month);
return new Date(parseInt(year), monthIndex, 1).getTime();
};
return parseMonthYear(a.date) - parseMonthYear(b.date);
});
// Calculate 3-month rolling average
dataPoints.forEach((point, idx) => {