diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-31 10:58:12 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-31 10:58:12 +0300 |
| commit | 75f9d84d6c80376e209acf2a32a50d6f4e531a96 (patch) | |
| tree | e8ef8e26150853a477a296c24ddf17bf7045799b | |
| parent | 2ce4dec574ec501f1c950e94924270b05edaade3 (diff) | |
Reports: make 30‑day the index; remove 7‑day + yearly; reorder sections (feeds → Top URLs → hosts → summary); drop "Back to Index"; add "Top 3 URLs Per Day (last 30 days)" to monthly report
| -rw-r--r-- | foostats.pl | 296 |
1 files changed, 154 insertions, 142 deletions
diff --git a/foostats.pl b/foostats.pl index 42d203e..be32602 100644 --- a/foostats.pl +++ b/foostats.pl @@ -86,6 +86,22 @@ package DateHelper { return @dates; } + + sub last_n_months_day_dates ($months) { + my $today = localtime; + my $start_year = $today->year; + my $start_month = $today->mon - $months; + while ($start_month <= 0) { $start_month += 12; $start_year--; } + + my $start = Time::Piece->strptime(sprintf('%04d-%02d-01', $start_year, $start_month), '%Y-%m-%d'); + my @dates; + my $t = $start; + while ($t <= $today) { + push @dates, $t->strftime('%Y%m%d'); + $t += 24 * 60 * 60; # one day + } + return @dates; + } } package Foostats::Logreader { @@ -1041,23 +1057,7 @@ $content $report_content .= "## Stats for $year-$month-$day\n\n"; - # Summary - $report_content .= "### Summary\n\n"; - my $total_requests = - ( $stats->{count}{gemini} // 0 ) + ( $stats->{count}{web} // 0 ); - $report_content .= "* Total requests: $total_requests\n"; - $report_content .= - "* Filtered requests: " . ( $stats->{count}{filtered} // 0 ) . "\n"; - $report_content .= - "* Gemini requests: " . ( $stats->{count}{gemini} // 0 ) . "\n"; - $report_content .= - "* Web requests: " . ( $stats->{count}{web} // 0 ) . "\n"; - $report_content .= - "* IPv4 requests: " . ( $stats->{count}{IPv4} // 0 ) . "\n"; - $report_content .= - "* IPv6 requests: " . ( $stats->{count}{IPv6} // 0 ) . "\n\n"; - - # Feed IPs + # Feed counts first $report_content .= "### Feed Statistics\n\n"; my @feed_rows; push @feed_rows, [ 'Total', $stats->{feed_ips}{'Total'} // 0 ]; @@ -1073,8 +1073,32 @@ $content $report_content .= format_table( [ 'Feed Type', 'Count' ], \@feed_rows ); $report_content .= "\n```\n\n"; + # Top 50 URLs next + $report_content .= "### Top 50 URLs\n\n"; + my @url_rows; + my $urls = $stats->{page_ips}{urls}; + my @sorted_urls = + sort { ( $urls->{$b} // 0 ) <=> ( $urls->{$a} // 0 ) } + keys %$urls; + my $truncated = @sorted_urls > 50; + @sorted_urls = @sorted_urls[ 0 .. 49 ] if $truncated; + + for my $url (@sorted_urls) { + push @url_rows, [ $url, $urls->{$url} // 0 ]; + } + + # Truncate URLs to fit within 100-character rows + truncate_urls_for_table( \@url_rows, 'Unique Visitors' ); + $report_content .= "```\n"; + $report_content .= + format_table( [ 'URL', 'Unique Visitors' ], \@url_rows ); + $report_content .= "\n```\n"; + if ($truncated) { + $report_content .= "\n... and more (truncated to 50 entries).\n"; + } + $report_content .= "\n"; - # Page IPs (Hosts) + # Other tables afterwards: Hosts, then Summary $report_content .= "### Page Statistics (by Host)\n\n"; my @host_rows; my $hosts = $stats->{page_ips}{hosts}; @@ -1082,7 +1106,7 @@ $content sort { ( $hosts->{$b} // 0 ) <=> ( $hosts->{$a} // 0 ) } keys %$hosts; - my $truncated = @sorted_hosts > 50; + $truncated = @sorted_hosts > 50; @sorted_hosts = @sorted_hosts[ 0 .. 49 ] if $truncated; for my $host (@sorted_hosts) { @@ -1097,39 +1121,27 @@ $content } $report_content .= "\n"; - # Page IPs (URLs) - $report_content .= "### Page Statistics (by URL)\n\n"; - my @url_rows; - my $urls = $stats->{page_ips}{urls}; - my @sorted_urls = - sort { ( $urls->{$b} // 0 ) <=> ( $urls->{$a} // 0 ) } - keys %$urls; - $truncated = @sorted_urls > 50; - @sorted_urls = @sorted_urls[ 0 .. 49 ] if $truncated; - - for my $url (@sorted_urls) { - push @url_rows, [ $url, $urls->{$url} // 0 ]; - } - - # Truncate URLs to fit within 100-character rows - truncate_urls_for_table( \@url_rows, 'Unique Visitors' ); - $report_content .= "```\n"; + # Summary last + $report_content .= "### Summary\n\n"; + my $total_requests = + ( $stats->{count}{gemini} // 0 ) + ( $stats->{count}{web} // 0 ); + $report_content .= "* Total requests: $total_requests\n"; $report_content .= - format_table( [ 'URL', 'Unique Visitors' ], \@url_rows ); - $report_content .= "\n```\n"; - if ($truncated) { - $report_content .= "\n... and more (truncated to 50 entries).\n"; - } - $report_content .= "\n"; + "* Filtered requests: " . ( $stats->{count}{filtered} // 0 ) . "\n"; + $report_content .= + "* Gemini requests: " . ( $stats->{count}{gemini} // 0 ) . "\n"; + $report_content .= + "* Web requests: " . ( $stats->{count}{web} // 0 ) . "\n"; + $report_content .= + "* IPv4 requests: " . ( $stats->{count}{IPv4} // 0 ) . "\n"; + $report_content .= + "* IPv6 requests: " . ( $stats->{count}{IPv6} // 0 ) . "\n\n"; - # Add links to summary reports + # Add links to summary reports (only monthly) $report_content .= "## Related Reports\n\n"; my $now = localtime; my $current_date = $now->strftime('%Y%m%d'); - $report_content .= "=> ./7day_summary_$current_date.gmi 7-Day Summary Report\n"; - $report_content .= "=> ./30day_summary_$current_date.gmi 30-Day Summary Report\n"; - $report_content .= "=> ./365day_summary_$current_date.gmi 365-Day Summary Report\n"; - $report_content .= "=> ./index.gmi Back to Index\n\n"; + $report_content .= "=> ./30day_summary_$current_date.gmi 30-Day Summary Report\n\n"; # Ensure output directory exists mkdir $output_dir unless -d $output_dir; @@ -1148,9 +1160,7 @@ $content } # Generate summary reports - generate_summary_report( 7, $stats_dir, $output_dir, $html_output_dir, %merged ); generate_summary_report( 30, $stats_dir, $output_dir, $html_output_dir, %merged ); - generate_summary_report( 365, $stats_dir, $output_dir, $html_output_dir, %merged ); # Generate index.gmi and index.html generate_index( $output_dir, $html_output_dir ); @@ -1169,14 +1179,16 @@ $content # Build report content my $report_content = build_report_header($today, $days); - $report_content .= build_daily_summary_section( \@dates, \%merged ); + # Order: feed counts -> Top URLs -> daily top 3 for last 30 days -> other tables $report_content .= build_feed_statistics_section( \@dates, \%merged ); # Aggregate and add top lists my ( $all_hosts, $all_urls ) = aggregate_hosts_and_urls( \@dates, \%merged ); - $report_content .= build_top_hosts_section($all_hosts); - $report_content .= build_top_urls_section($all_urls); + $report_content .= build_top_urls_section($all_urls, $days); + $report_content .= build_top3_urls_last_n_days_per_day($stats_dir, 30, \%merged); + $report_content .= build_top_hosts_section($all_hosts, $days); + $report_content .= build_daily_summary_section( \@dates, \%merged ); # Add links to other summary reports $report_content .= build_summary_links($days, $report_date); @@ -1319,9 +1331,10 @@ $content } sub build_top_hosts_section { - my ($all_hosts) = @_; + my ($all_hosts, $days) = @_; + $days //= 30; - my $content = "## Top 50 Hosts (30-Day Total)\n\n```\n"; + my $content = "## Top 50 Hosts (${days}-Day Total)\n\n```\n"; my @host_rows; my @sorted_hosts = @@ -1339,9 +1352,10 @@ $content } sub build_top_urls_section { - my ($all_urls) = @_; + my ($all_urls, $days) = @_; + $days //= 30; - my $content = "## Top 50 URLs (30-Day Total)\n\n```\n"; + my $content = "## Top 50 URLs (${days}-Day Total)\n\n```\n"; my @url_rows; my @sorted_urls = @@ -1364,107 +1378,105 @@ $content sub build_summary_links { my ( $current_days, $report_date ) = @_; - my $content = "## Other Summary Reports\n\n"; - - # Add links to other summary periods - my @periods = (7, 30, 365); - - for my $days (@periods) { - next if $days == $current_days; # Skip current report type - $content .= "=> ./${days}day_summary_$report_date.gmi ${days}-Day Summary Report\n"; + my $content = ''; + # Only add link to 30-day summary when not on the 30-day report itself + if ($current_days != 30) { + $content .= "## Other Summary Reports\n\n"; + $content .= "=> ./30day_summary_$report_date.gmi 30-Day Summary Report\n\n"; } - - # Add link to index - $content .= "\n=> ./index.gmi Back to Index\n"; return $content; } + +sub build_top3_urls_last_n_days_per_day { + my ($stats_dir, $days, $merged) = @_; + $days //= 30; + my $content = "## Top 3 URLs Per Day (Last ${days} Days)\n\n"; + + my @all = DateHelper::last_month_dates(); + my @dates = @all; + @dates = @all[0 .. $days-1] if @all > $days; + return $content . "(no data)\n\n" unless @dates; + + for my $date (@dates) { + # Prefer in-memory merged stats if available; otherwise merge from disk + my $stats = $merged->{$date}; + if (!$stats || !($stats->{page_ips} && $stats->{page_ips}{urls})) { + $stats = Foostats::Merger::merge_for_date($stats_dir, $date); + } + next unless $stats && $stats->{page_ips} && $stats->{page_ips}{urls}; + + my ($y,$m,$d) = $date =~ /(\d{4})(\d{2})(\d{2})/; + $content .= "### $y-$m-$d\n\n"; + + my $urls = $stats->{page_ips}{urls}; + my @sorted = sort { ($urls->{$b}//0) <=> ($urls->{$a}//0) } keys %$urls; + next unless @sorted; + my $limit = @sorted < 3 ? @sorted : 3; + @sorted = @sorted[0..$limit-1]; + + my @rows; + for my $u (@sorted) { push @rows, [ $u, $urls->{$u} // 0 ]; } + truncate_urls_for_table( \@rows, 'Visitors' ); + $content .= "```\n" . format_table([ 'URL', 'Visitors' ], \@rows) . "\n```\n\n"; + } + + return $content; +} sub generate_index { my ($output_dir, $html_output_dir) = @_; - - # Get all .gmi files in the output directory + + # Find latest 30-day summary opendir(my $dh, $output_dir) or die "Cannot open directory $output_dir: $!"; my @gmi_files = grep { /\.gmi$/ && $_ ne 'index.gmi' } readdir($dh); closedir($dh); - - # Sort files by type and date (newest first) - my @summaries_7day = sort { $b cmp $a } grep { /^7day_summary_/ } @gmi_files; + my @summaries_30day = sort { $b cmp $a } grep { /^30day_summary_/ } @gmi_files; - my @summaries_365day = sort { $b cmp $a } grep { /^365day_summary_/ } @gmi_files; - my @daily = sort { $b cmp $a } grep { /^\d{8}\.gmi$/ } @gmi_files; - - # Build index content - my $content = "# Foostats Reports Index\n\n"; - $content .= "Generated on " . localtime->strftime('%Y-%m-%d %H:%M:%S') . "\n\n"; - - # Add 7-day summaries - if (@summaries_7day) { - $content .= "## 7-Day Summary Reports\n\n"; - for my $summary (@summaries_7day) { - my ($date) = $summary =~ /7day_summary_(\d{8})\.gmi/; - if ($date) { - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - $content .= "=> ./$summary 7-Day Summary ($year-$month-$day)\n"; - } - } - $content .= "\n"; - } - - # Add 30-day summaries - if (@summaries_30day) { - $content .= "## 30-Day Summary Reports\n\n"; - for my $summary (@summaries_30day) { - my ($date) = $summary =~ /30day_summary_(\d{8})\.gmi/; - if ($date) { - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - $content .= "=> ./$summary 30-Day Summary ($year-$month-$day)\n"; - } - } - $content .= "\n"; - } - - # Add 365-day summaries - if (@summaries_365day) { - $content .= "## 365-Day Summary Reports\n\n"; - for my $summary (@summaries_365day) { - my ($date) = $summary =~ /365day_summary_(\d{8})\.gmi/; - if ($date) { - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - $content .= "=> ./$summary 365-Day Summary ($year-$month-$day)\n"; - } - } - $content .= "\n"; - } - - if (@daily) { - $content .= "## Daily Reports\n\n"; - my $count = 0; - for my $daily_file (@daily) { - last if ++$count > 90; # Show only last 90 days - my ($date) = $daily_file =~ /(\d{8})\.gmi/; - if ($date) { - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - $content .= "=> ./$daily_file $year-$month-$day\n"; - } - } - if (@daily > 90) { - $content .= "\n(Showing most recent 90 daily reports)\n"; - } - $content .= "\n"; - } - - # Write index file + my $latest_30 = $summaries_30day[0]; + my $index_path = "$output_dir/index.gmi"; - say "Writing index to $index_path"; - FileHelper::write($index_path, $content); - - # Also write HTML version mkdir $html_output_dir unless -d $html_output_dir; my $html_path = "$html_output_dir/index.html"; - my $html_content = gemtext_to_html($content); + + if ($latest_30) { + # Read 30-day summary content and use it as index + my $summary_path = "$output_dir/$latest_30"; + open my $sfh, '<', $summary_path or die "$summary_path: $!"; + local $/ = undef; + my $content = <$sfh>; + close $sfh; + + say "Writing index to $index_path (using $latest_30)"; + FileHelper::write($index_path, $content); + + # HTML: use existing 30-day summary HTML if present, else convert + (my $latest_html = $latest_30) =~ s/\.gmi$/.html/; + my $summary_html_path = "$html_output_dir/$latest_html"; + if (-e $summary_html_path) { + open my $hh, '<', $summary_html_path or die "$summary_html_path: $!"; + local $/ = undef; + my $html_page = <$hh>; + close $hh; + say "Writing HTML index to $html_path (copy of $latest_html)"; + FileHelper::write($html_path, $html_page); + } else { + my $html_content = gemtext_to_html($content); + my $html_page = generate_html_page("30-Day Summary Report", $html_content); + say "Writing HTML index to $html_path (from gemtext)"; + FileHelper::write($html_path, $html_page); + } + return; + } + + # Fallback: minimal index if no 30-day summary found + my $fallback = "# Foostats Reports Index\n\n30-day summary not found.\n"; + say "Writing fallback index to $index_path"; + FileHelper::write($index_path, $fallback); + + my $html_content = gemtext_to_html($fallback); my $html_page = generate_html_page("Foostats Reports Index", $html_content); - say "Writing HTML index to $html_path"; + say "Writing fallback HTML index to $html_path"; FileHelper::write($html_path, $html_page); } } |
