diff --git a/docs/superpowers/plans/2026-07-21-underlying-only-chart.md b/docs/superpowers/plans/2026-07-21-underlying-only-chart.md
new file mode 100644
index 0000000..4f65bbf
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-21-underlying-only-chart.md
@@ -0,0 +1,577 @@
+# Grafico Standalone Sottostante (senza certificato) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Aggiungere l'endpoint `GET /api/chart/underlying/{idUnderlyings}` che genera un grafico con solo la linea prezzo storico di un sottostante, senza alcun contesto certificato (no strike/barriere/legenda).
+
+**Architecture:** Stessa pipeline di `/api/chart/v2/{isin}` ma senza certificato: nuova SP `cedlab_Chart_UnderlyingOnly`, nuovo servizio dati `UnderlyingChartDataService`, nuovo metodo statico `RenderSingleSeriesToPng` in `SkiaChartRendererV2` (riusa i suoi helper privati per griglia/assi), nuova route in `ChartController` esistente che riusa `WrapPngInPdf` già presente.
+
+**Tech Stack:** ASP.NET Core 8, `Microsoft.Data.SqlClient`, SkiaSharp.
+
+Spec di riferimento: `docs/superpowers/specs/2026-07-21-underlying-only-chart-design.md`.
+
+---
+
+## Task 1: Stored Procedure `cedlab_Chart_UnderlyingOnly`
+
+**Files:**
+- Create: `docs/sql/cedlab_Chart_UnderlyingOnly.sql`
+
+- [ ] **Step 1: Scrivere lo script SQL**
+
+```sql
+USE [FirstSolutionDB]
+GO
+
+-- ============================================================
+-- SP: cedlab_Chart_UnderlyingOnly
+-- Restituisce lo storico prezzi di UN SOLO sottostante, senza
+-- alcun contesto certificato (nessuno strike/barriera/trigger).
+-- Usata da UnderlyingChartDataService per il grafico standalone
+-- sottostante.
+--
+-- Output (Px_date ASC, TOP 350 più recenti):
+-- Nome VARCHAR -- "{Name} ({Ticker_bbg})", uguale su ogni riga
+-- Px_date DATE
+-- Px DECIMAL -- Px_close o Px_closeadj in base ad AdjustedPrices
+-- ============================================================
+
+CREATE OR ALTER PROCEDURE [dbo].[cedlab_Chart_UnderlyingOnly]
+ @IDUnderlyings INT
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @AdjustedPrices BIT;
+ SELECT @AdjustedPrices = ISNULL(AdjustedPrices, 0)
+ FROM dbo.Underlyings
+ WHERE IDUnderlyings = @IDUnderlyings
+ AND deleted = 0
+ AND sospeso = 0;
+
+ IF @AdjustedPrices IS NULL RETURN; -- non trovato / cancellato / sospeso
+
+ ;WITH Ranked AS
+ (
+ SELECT
+ p.Px_date,
+ CASE WHEN @AdjustedPrices = 0 THEN p.Px_close ELSE p.Px_closeadj END AS Px,
+ ROW_NUMBER() OVER (ORDER BY p.Px_date DESC) AS rn
+ FROM dbo.Prices p
+ WHERE p.UnderlyingsID = @IDUnderlyings
+ AND (p.Px_low IS NOT NULL OR p.Px_high IS NOT NULL OR
+ p.Px_open IS NOT NULL OR p.Px_close IS NOT NULL)
+ )
+ SELECT
+ u.Name + ' (' + u.Ticker_bbg + ')' AS Nome,
+ r.Px_date,
+ r.Px
+ FROM Ranked r
+ CROSS JOIN (SELECT TOP 1 * FROM dbo.Underlyings WHERE IDUnderlyings = @IDUnderlyings) u
+ WHERE r.rn <= 350
+ ORDER BY r.Px_date ASC;
+END
+GO
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add docs/sql/cedlab_Chart_UnderlyingOnly.sql
+git commit -m "docs: add cedlab_Chart_UnderlyingOnly SP script"
+```
+
+> **Nota per l'utente**: questo script va eseguito manualmente sul DB `FirstSolutionDB` prima di testare l'endpoint (Task 5). Nessun task successivo lo esegue automaticamente.
+
+---
+
+## Task 2: Modelli dati
+
+**Files:**
+- Create: `CertReports.Syncfusion/Models/UnderlyingChartModels.cs`
+
+- [ ] **Step 1: Scrivere il file dei modelli**
+
+```csharp
+namespace CertReports.Syncfusion.Models;
+
+///
+/// Singolo punto prezzo per il grafico standalone sottostante
+/// (da SP cedlab_Chart_UnderlyingOnly).
+///
+public class UnderlyingChartPoint
+{
+ public DateTime Date { get; set; }
+ public decimal Px { get; set; }
+}
+
+///
+/// Dati completi per il grafico standalone di un sottostante,
+/// senza alcun contesto certificato.
+///
+public class UnderlyingChartData
+{
+ public int IDUnderlyings { get; set; }
+
+ /// Titolo del grafico: "{Name} ({Ticker_bbg})".
+ public string Nome { get; set; } = string.Empty;
+
+ public List Points { get; set; } = new();
+}
+```
+
+- [ ] **Step 2: Build per verificare che compili**
+
+Run: `dotnet build CertReports.Syncfusion`
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add CertReports.Syncfusion/Models/UnderlyingChartModels.cs
+git commit -m "feat: add UnderlyingChartData model"
+```
+
+---
+
+## Task 3: Servizio dati `UnderlyingChartDataService`
+
+**Files:**
+- Create: `CertReports.Syncfusion/Services/Implementations/UnderlyingChartDataService.cs`
+- Modify: `CertReports.Syncfusion/Program.cs`
+
+- [ ] **Step 1: Scrivere il servizio**
+
+```csharp
+using CertReports.Syncfusion.Models;
+using Microsoft.Data.SqlClient;
+using System.Data;
+
+namespace CertReports.Syncfusion.Services.Implementations;
+
+///
+/// Recupera i dati per il grafico standalone di un singolo sottostante
+/// (nessun contesto certificato). SP utilizzata: cedlab_Chart_UnderlyingOnly.
+///
+public interface IUnderlyingChartDataService
+{
+ Task GetChartDataAsync(int idUnderlyings);
+}
+
+public class UnderlyingChartDataService : IUnderlyingChartDataService
+{
+ private readonly string _connectionString;
+ private readonly ILogger _logger;
+
+ public UnderlyingChartDataService(IConfiguration config, ILogger logger)
+ {
+ _connectionString = config.GetConnectionString("CertDb")
+ ?? throw new InvalidOperationException("ConnectionString 'CertDb' non configurata.");
+ _logger = logger;
+ }
+
+ public async Task GetChartDataAsync(int idUnderlyings)
+ {
+ await using var conn = new SqlConnection(_connectionString);
+ await conn.OpenAsync();
+
+ await using var cmd = new SqlCommand("cedlab_Chart_UnderlyingOnly", conn)
+ { CommandType = CommandType.StoredProcedure };
+ cmd.Parameters.AddWithValue("@IDUnderlyings", idUnderlyings);
+
+ var result = new UnderlyingChartData { IDUnderlyings = idUnderlyings };
+
+ await using var r = await cmd.ExecuteReaderAsync();
+ while (await r.ReadAsync())
+ {
+ if (result.Points.Count == 0)
+ result.Nome = ToStr(r, "Nome");
+
+ result.Points.Add(new UnderlyingChartPoint
+ {
+ Date = r.GetDateTime(r.GetOrdinal("Px_date")),
+ Px = Convert.ToDecimal(r.GetValue(r.GetOrdinal("Px"))),
+ });
+ }
+
+ if (result.Points.Count == 0)
+ {
+ _logger.LogWarning(
+ "Nessun prezzo trovato per il sottostante {IDUnderlyings} (non trovato, cancellato, sospeso, o senza storico prezzi)",
+ idUnderlyings);
+ return null;
+ }
+
+ _logger.LogInformation(
+ "Dati grafico sottostante caricati per {IDUnderlyings}: {Points} punti",
+ idUnderlyings, result.Points.Count);
+
+ return result;
+ }
+
+ private static string ToStr(SqlDataReader r, string column)
+ {
+ int ord = r.GetOrdinal(column);
+ return r.IsDBNull(ord) ? string.Empty : r.GetString(ord);
+ }
+}
+```
+
+- [ ] **Step 2: Registrare il servizio in Program.cs**
+
+In `CertReports.Syncfusion/Program.cs`, subito dopo la riga 37 (`builder.Services.AddScoped();`):
+
+```csharp
+builder.Services.AddScoped();
+```
+
+- [ ] **Step 3: Build per verificare che compili**
+
+Run: `dotnet build CertReports.Syncfusion`
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add CertReports.Syncfusion/Services/Implementations/UnderlyingChartDataService.cs CertReports.Syncfusion/Program.cs
+git commit -m "feat: add UnderlyingChartDataService and DI registration"
+```
+
+---
+
+## Task 4: Renderer — `RenderSingleSeriesToPng` in `SkiaChartRendererV2`
+
+**Files:**
+- Modify: `CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs`
+
+Il file espone già come `private static` tutti gli helper necessari (`CreateFont`, `DrawGrid`, `DateToX`, `ValueToY`, `XAxisIntervalMonths`, `XAxisStart`). Il nuovo metodo pubblico va aggiunto nella stessa classe per poterli riusare senza cambiarne la visibilità.
+
+- [ ] **Step 1: Aggiungere il metodo `RenderSingleSeriesToPng`**
+
+Inserire subito dopo la chiusura del metodo `RenderToPng` esistente (dopo la riga `return imgData.ToArray();` e la relativa `}` di chiusura, prima del blocco `// Titolo`):
+
+```csharp
+ // ═══════════════════════════════════════════════════════════════════
+ // Entry point — grafico standalone singolo sottostante (no certificato)
+ // ═══════════════════════════════════════════════════════════════════
+
+ public static byte[] RenderSingleSeriesToPng(UnderlyingChartData data, int width = 1100, int height = 700, bool jpeg = false)
+ {
+ using var surface = SKSurface.Create(new SKImageInfo(width, height));
+ var canvas = surface.Canvas;
+ canvas.Clear(SKColors.White);
+
+ float titleBottom = DrawUnderlyingTitle(canvas, width, data.Nome);
+
+ float marginLeft = 70;
+ float marginRight = 40;
+ float marginTop = titleBottom + 10;
+ float marginBottom = 45;
+
+ var plotArea = new SKRect(marginLeft, marginTop, width - marginRight, height - marginBottom);
+
+ var points = data.Points.OrderBy(p => p.Date).ToList();
+ if (points.Count == 0)
+ return Array.Empty();
+
+ DateTime minDate = points[0].Date;
+ DateTime maxDate = points[^1].Date;
+ double minY = (double)points.Min(p => p.Px);
+ double maxY = (double)points.Max(p => p.Px);
+ double range = maxY - minY;
+ if (range == 0) range = Math.Max(minY * 0.1, 1);
+ double margin = range * 0.1;
+ minY -= margin;
+ maxY += margin;
+
+ DrawGrid(canvas, plotArea, minDate, maxDate, minY, maxY);
+ DrawUnderlyingAxisLabels(canvas, plotArea, minDate, maxDate, minY, maxY);
+ DrawUnderlyingSeries(canvas, plotArea, points, minDate, maxDate, minY, maxY);
+
+ using var borderPaint = new SKPaint
+ {
+ Color = SKColors.Gray, StrokeWidth = 1,
+ Style = SKPaintStyle.Stroke, IsAntialias = true,
+ };
+ canvas.DrawRect(plotArea, borderPaint);
+
+ using var image = surface.Snapshot();
+ using var imgData = jpeg
+ ? image.Encode(SKEncodedImageFormat.Jpeg, 90)
+ : image.Encode(SKEncodedImageFormat.Png, 95);
+ return imgData.ToArray();
+ }
+
+ private static float DrawUnderlyingTitle(SKCanvas canvas, int width, string nome)
+ {
+ float y = 15f;
+ var boldFont = CreateFont(13f, bold: true);
+ using var titlePaint = new SKPaint { Color = TitleColor, IsAntialias = true };
+ canvas.DrawText(nome, width / 2f, y + 13, SKTextAlign.Center, boldFont, titlePaint);
+ return y + 22 + 5;
+ }
+
+ private static void DrawUnderlyingAxisLabels(SKCanvas canvas, SKRect area,
+ DateTime minDate, DateTime maxDate, double minY, double maxY)
+ {
+ var font = CreateFont(11);
+ using var paint = new SKPaint { Color = SKColors.DimGray, IsAntialias = true };
+
+ int ySteps = 8;
+ for (int i = 0; i <= ySteps; i++)
+ {
+ double val = maxY - ((maxY - minY) / ySteps) * i;
+ float y = area.Top + (area.Height / ySteps) * i;
+ canvas.DrawText($"{val:F2}", area.Left - 55, y + 4, SKTextAlign.Left, font, paint);
+ }
+
+ double totalDays = (maxDate - minDate).TotalDays;
+ int intervalMonths = XAxisIntervalMonths(minDate, maxDate);
+ string fmt = totalDays > 365 ? "MMM yy" : "dd/MM/yy";
+
+ if (intervalMonths > 0)
+ {
+ var d = XAxisStart(minDate, intervalMonths);
+ while (d <= maxDate)
+ {
+ float x = DateToX(d, area, minDate, maxDate);
+ if (x >= area.Left && x <= area.Right)
+ canvas.DrawText(d.ToString(fmt), x, area.Bottom + 20, SKTextAlign.Center, font, paint);
+ d = d.AddMonths(intervalMonths);
+ }
+ }
+ else
+ {
+ canvas.DrawText(minDate.ToString("dd/MM/yy"), area.Left, area.Bottom + 20, SKTextAlign.Left, font, paint);
+ canvas.DrawText(maxDate.ToString("dd/MM/yy"), area.Right, area.Bottom + 20, SKTextAlign.Right, font, paint);
+ }
+ }
+
+ private static void DrawUnderlyingSeries(SKCanvas canvas, SKRect area,
+ List points,
+ DateTime minDate, DateTime maxDate, double minY, double maxY)
+ {
+ using var paint = new SKPaint
+ {
+ Color = CertColor, StrokeWidth = 2f,
+ Style = SKPaintStyle.Stroke, IsAntialias = true,
+ StrokeCap = SKStrokeCap.Round, StrokeJoin = SKStrokeJoin.Round,
+ };
+
+ using var path = new SKPath();
+ bool first = true;
+ foreach (var pt in points)
+ {
+ float x = DateToX(pt.Date, area, minDate, maxDate);
+ float y = ValueToY((double)pt.Px, area, minY, maxY);
+ y = Math.Max(area.Top, Math.Min(area.Bottom, y));
+ if (first) { path.MoveTo(x, y); first = false; }
+ else path.LineTo(x, y);
+ }
+
+ canvas.Save();
+ canvas.ClipRect(area);
+ canvas.DrawPath(path, paint);
+ canvas.Restore();
+ }
+```
+
+Aggiungere anche `using CertReports.Syncfusion.Models;` in cima al file se non già presente (è già presente, riga 1 — nessuna modifica necessaria).
+
+- [ ] **Step 2: Build per verificare che compili**
+
+Run: `dotnet build CertReports.Syncfusion`
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add CertReports.Syncfusion/Services/Implementations/SkiaChartRendererV2.cs
+git commit -m "feat: add RenderSingleSeriesToPng for standalone underlying chart"
+```
+
+---
+
+## Task 5: Endpoint `GET /api/chart/underlying/{idUnderlyings}`
+
+**Files:**
+- Modify: `CertReports.Syncfusion/Controllers/ChartController.cs`
+
+- [ ] **Step 1: Aggiungere il campo del nuovo servizio e iniettarlo nel costruttore**
+
+Modificare l'inizio della classe (righe 20-37):
+
+```csharp
+public class ChartController : ControllerBase
+{
+ private readonly IChartDataService _chartDataService;
+ private readonly IChartDataServiceV2 _chartDataServiceV2;
+ private readonly IUnderlyingChartDataService _underlyingChartDataService;
+ private readonly ILogger _logger;
+ private readonly IConfiguration _configuration;
+
+ public ChartController(
+ IChartDataService chartDataService,
+ IChartDataServiceV2 chartDataServiceV2,
+ IUnderlyingChartDataService underlyingChartDataService,
+ ILogger logger,
+ IConfiguration configuration)
+ {
+ _chartDataService = chartDataService;
+ _chartDataServiceV2 = chartDataServiceV2;
+ _underlyingChartDataService = underlyingChartDataService;
+ _logger = logger;
+ _configuration = configuration;
+ }
+```
+
+- [ ] **Step 2: Aggiungere la nuova route**
+
+Inserire subito dopo la chiusura del metodo `GenerateChartV2` (dopo la riga `}` che chiude quel metodo, prima del metodo `SaveChartToDiskAsync`):
+
+```csharp
+ ///
+ /// Endpoint standalone: grafico con solo la linea prezzo storico di UN sottostante,
+ /// identificato da IDUnderlyings — nessun contesto certificato (no strike/barriere/legenda).
+ /// Richiede SP cedlab_Chart_UnderlyingOnly nel DB.
+ /// Formati supportati: png (default), jpg/jpeg, pdf. Non supporta jpgEnc né ?save=true
+ /// (nessun alias-certificato applicabile a un sottostante puro).
+ ///
+ [HttpGet("underlying/{idUnderlyings:int}")]
+ public async Task GenerateChartUnderlying(
+ int idUnderlyings,
+ [FromQuery] int width = 1100,
+ [FromQuery] int height = 700,
+ [FromQuery] string format = "png")
+ {
+ if (idUnderlyings <= 0)
+ return BadRequest("IDUnderlyings non valido.");
+
+ width = Math.Clamp(width, 400, 2000);
+ height = Math.Clamp(height, 300, 1500);
+
+ try
+ {
+ var chartData = await _underlyingChartDataService.GetChartDataAsync(idUnderlyings);
+
+ if (chartData == null || chartData.Points.Count == 0)
+ {
+ return NotFound(new
+ {
+ status = "KO",
+ message = $"Nessun dato per il grafico del sottostante {idUnderlyings}.",
+ });
+ }
+
+ bool isJpeg = format.Equals("jpg", StringComparison.OrdinalIgnoreCase)
+ || format.Equals("jpeg", StringComparison.OrdinalIgnoreCase);
+
+ byte[] imgBytes = SkiaChartRendererV2.RenderSingleSeriesToPng(chartData, width, height, jpeg: isJpeg);
+
+ if (format.Equals("pdf", StringComparison.OrdinalIgnoreCase))
+ {
+ byte[] pdfBytes = WrapPngInPdf(imgBytes);
+ Response.Headers.Append("Content-Disposition", $"inline; filename=chart_underlying_{idUnderlyings}.pdf");
+ return File(pdfBytes, "application/pdf");
+ }
+
+ if (isJpeg)
+ {
+ Response.Headers.Append("Content-Disposition", $"inline; filename=chart_underlying_{idUnderlyings}.jpg");
+ return File(imgBytes, "image/jpeg");
+ }
+
+ Response.Headers.Append("Content-Disposition", $"inline; filename=chart_underlying_{idUnderlyings}.png");
+ return File(imgBytes, "image/png");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Errore generazione chart sottostante per IDUnderlyings {IDUnderlyings}", idUnderlyings);
+ return StatusCode(500, new { status = "KO", message = "Errore nella generazione del grafico sottostante." });
+ }
+ }
+
+```
+
+- [ ] **Step 3: Build per verificare che compili**
+
+Run: `dotnet build CertReports.Syncfusion`
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add CertReports.Syncfusion/Controllers/ChartController.cs
+git commit -m "feat: add GET /api/chart/underlying/{idUnderlyings} endpoint"
+```
+
+---
+
+## Task 6: Verifica manuale end-to-end
+
+**Prerequisito**: lo script `docs/sql/cedlab_Chart_UnderlyingOnly.sql` (Task 1) deve essere già stato eseguito manualmente sul DB `FirstSolutionDB`. Serve inoltre un `IDUnderlyings` reale con storico prezzi in `dbo.Prices` — recuperabile con una query come `SELECT TOP 5 IDUnderlyings, Name, Ticker_bbg FROM dbo.Underlyings WHERE deleted = 0 AND sospeso = 0`.
+
+Non esistono test automatici nel progetto (vedi CLAUDE.md) — questa verifica è manuale, come per gli altri endpoint chart.
+
+- [ ] **Step 1: Avviare il progetto in locale**
+
+Run: `dotnet run --project CertReports.Syncfusion`
+Expected: log di avvio Kestrel con l'URL locale (es. `https://localhost:XXXX`), nessuna eccezione.
+
+- [ ] **Step 2: Testare il caso positivo (PNG)**
+
+Aprire nel browser (sostituire `{porta}` e `{idUnderlyings}` con valori reali):
+`https://localhost:{porta}/api/chart/underlying/{idUnderlyings}`
+
+Expected: immagine PNG con una sola linea nera, titolo `{Name} ({Ticker_bbg})` in alto, assi con date e valori prezzo (non percentuale), nessuna legenda.
+
+- [ ] **Step 3: Testare formato PDF**
+
+`https://localhost:{porta}/api/chart/underlying/{idUnderlyings}?format=pdf`
+
+Expected: PDF landscape con l'immagine centrata, apribile senza errori.
+
+- [ ] **Step 4: Testare caso 404 (sottostante inesistente)**
+
+`https://localhost:{porta}/api/chart/underlying/999999999`
+
+Expected: risposta HTTP 404 con body JSON `{"status":"KO","message":"Nessun dato per il grafico del sottostante 999999999."}`.
+
+- [ ] **Step 5: Testare caso 400 (id non valido)**
+
+`https://localhost:{porta}/api/chart/underlying/0`
+
+Expected: risposta HTTP 400.
+
+- [ ] **Step 6: Arrestare il server**
+
+Interrompere il processo `dotnet run` (Ctrl+C nel terminale in cui è in esecuzione).
+
+- [ ] **Step 7: Aggiornare CLAUDE.md**
+
+In `CertReports.Syncfusion/CLAUDE.md` (root repo), sezione `## API Endpoints`, aggiungere la riga:
+
+```
+- `GET /api/chart/underlying/{idUnderlyings}[?format=png|jpg|jpeg|pdf&width=&height=]` — grafico standalone di un solo sottostante (nessun contesto certificato, no strike/barriere/legenda)
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add CertReports.Syncfusion/CLAUDE.md
+git commit -m "docs: document GET /api/chart/underlying/{idUnderlyings} endpoint"
+```
+
+---
+
+## Riepilogo copertura spec
+
+| Requisito spec | Task |
+|---|---|
+| SP `cedlab_Chart_UnderlyingOnly` con nome `{Name} ({Ticker_bbg})` | Task 1 |
+| Modello `UnderlyingChartData` | Task 2 |
+| Servizio dati + DI | Task 3 |
+| Renderer solo linea prezzo, titolo, no legenda/barriere | Task 4 |
+| Endpoint `GET /api/chart/underlying/{idUnderlyings}`, formati png/jpg/jpeg/pdf, no jpgEnc/save | Task 5 |
+| Error handling 400/404/500 | Task 5 |
+| Verifica end-to-end e doc | Task 6 |