New route to perform a new analysis of the email
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
0ac51ac06d
commit
dfc0eeb323
6 changed files with 183 additions and 4 deletions
|
|
@ -134,6 +134,41 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/report/{id}/reanalyze:
|
||||
post:
|
||||
tags:
|
||||
- reports
|
||||
summary: Reanalyze email and regenerate report
|
||||
description: Re-run the analysis on the stored raw email to regenerate the report with the latest analyzer version. This is useful after analyzer improvements or bug fixes.
|
||||
operationId: reanalyzeReport
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: Base32-encoded test ID (with hyphens)
|
||||
responses:
|
||||
'200':
|
||||
description: Report regenerated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Report'
|
||||
'404':
|
||||
description: Email not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error during reanalysis
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/status:
|
||||
get:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -35,18 +35,26 @@ import (
|
|||
"git.happydns.org/happyDeliver/internal/utils"
|
||||
)
|
||||
|
||||
// EmailAnalyzer defines the interface for email analysis
|
||||
// This interface breaks the circular dependency with pkg/analyzer
|
||||
type EmailAnalyzer interface {
|
||||
AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error)
|
||||
}
|
||||
|
||||
// APIHandler implements the ServerInterface for handling API requests
|
||||
type APIHandler struct {
|
||||
storage storage.Storage
|
||||
config *config.Config
|
||||
analyzer EmailAnalyzer
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewAPIHandler creates a new API handler
|
||||
func NewAPIHandler(store storage.Storage, cfg *config.Config) *APIHandler {
|
||||
func NewAPIHandler(store storage.Storage, cfg *config.Config, analyzer EmailAnalyzer) *APIHandler {
|
||||
return &APIHandler{
|
||||
storage: store,
|
||||
config: cfg,
|
||||
analyzer: analyzer,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +200,63 @@ func (h *APIHandler) GetRawEmail(c *gin.Context, id string) {
|
|||
c.Data(http.StatusOK, "text/plain", rawEmail)
|
||||
}
|
||||
|
||||
// ReanalyzeReport re-analyzes an existing email and regenerates the report
|
||||
// (POST /report/{id}/reanalyze)
|
||||
func (h *APIHandler) ReanalyzeReport(c *gin.Context, id string) {
|
||||
// Convert base32 ID to UUID
|
||||
testUUID, err := utils.Base32ToUUID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error{
|
||||
Error: "invalid_id",
|
||||
Message: "Invalid test ID format",
|
||||
Details: stringPtr(err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the existing report (mainly to get the raw email)
|
||||
_, rawEmail, err := h.storage.GetReport(testUUID)
|
||||
if err != nil {
|
||||
if err == storage.ErrNotFound {
|
||||
c.JSON(http.StatusNotFound, Error{
|
||||
Error: "not_found",
|
||||
Message: "Email not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, Error{
|
||||
Error: "internal_error",
|
||||
Message: "Failed to retrieve email",
|
||||
Details: stringPtr(err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Re-analyze the email using the current analyzer
|
||||
reportJSON, err := h.analyzer.AnalyzeEmailBytes(rawEmail, testUUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, Error{
|
||||
Error: "analysis_error",
|
||||
Message: "Failed to re-analyze email",
|
||||
Details: stringPtr(err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update the report in storage
|
||||
if err := h.storage.UpdateReport(testUUID, reportJSON); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, Error{
|
||||
Error: "internal_error",
|
||||
Message: "Failed to update report",
|
||||
Details: stringPtr(err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return the updated report JSON directly
|
||||
c.Data(http.StatusOK, "application/json", reportJSON)
|
||||
}
|
||||
|
||||
// GetStatus retrieves service health status
|
||||
// (GET /status)
|
||||
func (h *APIHandler) GetStatus(c *gin.Context) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
"git.happydns.org/happyDeliver/internal/config"
|
||||
"git.happydns.org/happyDeliver/internal/lmtp"
|
||||
"git.happydns.org/happyDeliver/internal/storage"
|
||||
"git.happydns.org/happyDeliver/pkg/analyzer"
|
||||
"git.happydns.org/happyDeliver/web"
|
||||
)
|
||||
|
||||
|
|
@ -63,8 +64,11 @@ func RunServer(cfg *config.Config) error {
|
|||
}
|
||||
}()
|
||||
|
||||
// Create analyzer adapter for API
|
||||
analyzerAdapter := analyzer.NewAPIAdapter(cfg)
|
||||
|
||||
// Create API handler
|
||||
handler := api.NewAPIHandler(store, cfg)
|
||||
handler := api.NewAPIHandler(store, cfg, analyzerAdapter)
|
||||
|
||||
// Set up Gin router
|
||||
if os.Getenv("GIN_MODE") == "" {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type Storage interface {
|
|||
CreateReport(testID uuid.UUID, rawEmail []byte, reportJSON []byte) (*Report, error)
|
||||
GetReport(testID uuid.UUID) (reportJSON []byte, rawEmail []byte, err error)
|
||||
ReportExists(testID uuid.UUID) (bool, error)
|
||||
UpdateReport(testID uuid.UUID, reportJSON []byte) error
|
||||
DeleteOldReports(olderThan time.Time) (int64, error)
|
||||
|
||||
// Close closes the database connection
|
||||
|
|
@ -117,6 +118,18 @@ func (s *DBStorage) GetReport(testID uuid.UUID) ([]byte, []byte, error) {
|
|||
return dbReport.ReportJSON, dbReport.RawEmail, nil
|
||||
}
|
||||
|
||||
// UpdateReport updates the report JSON for an existing test ID
|
||||
func (s *DBStorage) UpdateReport(testID uuid.UUID, reportJSON []byte) error {
|
||||
result := s.db.Model(&Report{}).Where("test_id = ?", testID).Update("report_json", reportJSON)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to update report: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteOldReports deletes reports older than the specified time
|
||||
func (s *DBStorage) DeleteOldReports(olderThan time.Time) (int64, error) {
|
||||
result := s.db.Where("created_at < ?", olderThan).Delete(&Report{})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ package analyzer
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -85,3 +86,32 @@ func (a *EmailAnalyzer) GetScoreSummaryText(result *AnalysisResult) string {
|
|||
}
|
||||
return a.generator.GetScoreSummaryText(result.Results)
|
||||
}
|
||||
|
||||
// APIAdapter adapts the EmailAnalyzer to work with the API package
|
||||
// This adapter implements the interface expected by the API handler
|
||||
type APIAdapter struct {
|
||||
analyzer *EmailAnalyzer
|
||||
}
|
||||
|
||||
// NewAPIAdapter creates a new API adapter for the email analyzer
|
||||
func NewAPIAdapter(cfg *config.Config) *APIAdapter {
|
||||
return &APIAdapter{
|
||||
analyzer: NewEmailAnalyzer(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// AnalyzeEmailBytes performs analysis and returns JSON bytes directly
|
||||
func (a *APIAdapter) AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) ([]byte, error) {
|
||||
result, err := a.analyzer.AnalyzeEmailBytes(rawEmail, testID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Marshal report to JSON
|
||||
reportJSON, err := json.Marshal(result.Report)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal report: %w", err)
|
||||
}
|
||||
|
||||
return reportJSON, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import { getTest, getReport } from "$lib/api";
|
||||
import { getTest, getReport, reanalyzeReport } from "$lib/api";
|
||||
import type { Test, Report } from "$lib/api/types.gen";
|
||||
import { ScoreCard, CheckCard, SpamAssassinCard, PendingState } from "$lib/components";
|
||||
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
let report = $state<Report | null>(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let reanalyzing = $state(false);
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let nextfetch = $state(23);
|
||||
let nbfetch = $state(0);
|
||||
|
|
@ -134,6 +135,24 @@
|
|||
if (percentage >= 50) return "text-warning";
|
||||
return "text-danger";
|
||||
}
|
||||
|
||||
async function handleReanalyze() {
|
||||
if (!testId || reanalyzing) return;
|
||||
|
||||
reanalyzing = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await reanalyzeReport({ path: { id: testId } });
|
||||
if (response.data) {
|
||||
report = response.data;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : "Failed to reanalyze report";
|
||||
} finally {
|
||||
reanalyzing = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -208,9 +227,22 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Test Again Button -->
|
||||
<!-- Action Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-lg me-3"
|
||||
onclick={handleReanalyze}
|
||||
disabled={reanalyzing}
|
||||
>
|
||||
{#if reanalyzing}
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Reanalyzing...
|
||||
{:else}
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
Reanalyze with Latest Version
|
||||
{/if}
|
||||
</button>
|
||||
<a href="/test/" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>
|
||||
Test Another Email
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue