Compare commits

..

8 Commits

Author SHA1 Message Date
8a2f97d2cd Merge pull request 'CUB-40: Add cost summary API endpoint' (#15) from agent/dex/CUB-40-cost-summary-api into dev
Some checks failed
Dev Build / build-test (push) Failing after 52s
Dev Build / deploy-dev (push) Has been skipped
Dev Build / notify-success (push) Has been skipped
Dev Build / notify-failure (push) Successful in 3s
Reviewed-on: #15
2026-04-27 14:17:30 -04:00
b43edad5f0 Merge branch 'dev' into agent/dex/CUB-40-cost-summary-api
Some checks failed
Dev Build / build-test (pull_request) Failing after 52s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 3s
2026-04-27 14:14:13 -04:00
12888c4f3f Merge pull request 'CUB-66: Frontend Dockerfile (Angular Static Build)' (#12) from agent/rex/CUB-64-frontend-dockerfile into dev
Some checks failed
Dev Build / build-test (push) Failing after 51s
Dev Build / deploy-dev (push) Has been skipped
Dev Build / notify-success (push) Has been skipped
Dev Build / notify-failure (push) Successful in 3s
Reviewed-on: #12
Reviewed-by: Otto the Minion <otto@code.cubecraftcreations.com>
2026-04-27 14:11:55 -04:00
1411b68a95 Merge branch 'dev' into agent/rex/CUB-64-frontend-dockerfile
Some checks failed
Dev Build / build-test (pull_request) Failing after 50s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 3s
2026-04-27 14:11:50 -04:00
7daa7d637c Merge pull request 'CUB-31: Add filament usage tracking model' (#10) from agent/hex/CUB-31-filament-usage-tracking-model into dev
Some checks failed
Dev Build / build-test (push) Failing after 50s
Dev Build / deploy-dev (push) Has been skipped
Dev Build / notify-success (push) Has been skipped
Dev Build / notify-failure (push) Successful in 3s
Reviewed-on: #10
Reviewed-by: Otto the Minion <otto@code.cubecraftcreations.com>
2026-04-27 14:09:22 -04:00
c1a115c938 feat(CUB-40): [Extrudex] Add cost summary API endpoint
Some checks failed
Dev Build / build-test (pull_request) Failing after 47s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 3s
2026-04-27 17:09:08 +00:00
920042acac fix(CUB-66): Resolve merge conflicts - keep only Docker setup, remove duplicate Angular app files
Some checks failed
Dev Build / build-test (pull_request) Failing after 1m4s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 5s
2026-04-27 02:23:27 +00:00
cubecraft-agents[bot]
1ee7562e81 CUB-66: scaffold Angular frontend and add Dockerfile with nginx
Some checks failed
Dev Build / build-test (pull_request) Failing after 58s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 4s
- Scaffolded Angular 21 app in frontend/ (standalone, routing, scss)
- Multi-stage Dockerfile: node:22-alpine build → nginx:alpine serve
- nginx.conf with SPA routing fallback, API proxy, gzip, asset caching
- .dockerignore excludes node_modules, dist, .angular, spec files
- docker build → PASS, container serves UI on port 80 (HTTP 200)
- Final image: 92.9MB (nginx:alpine)
2026-04-26 20:10:01 +00:00
5 changed files with 222 additions and 0 deletions

View File

@@ -413,6 +413,92 @@ public class PrintJobsController : ControllerBase
return NoContent(); return NoContent();
} }
// ── GET /api/printjobs/{id}/cost-summary ──────────────────────────
/// <summary>
/// Gets the material cost summary for a specific print job.
/// Calculates total material cost from filament usage (grams derived)
/// and the spool's purchase price. Returns warnings instead of errors
/// when cost data is unavailable.
/// </summary>
/// <param name="id">The unique identifier of the print job.</param>
/// <returns>A cost summary with breakdown and any warnings about missing data.</returns>
/// <response code="200">Returns the cost summary. Warnings field lists any missing data.</response>
/// <response code="404">If the print job with the given ID is not found.</response>
[HttpGet("{id:guid}/cost-summary")]
[ProducesResponseType(typeof(CostSummaryResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CostSummaryResponse>> GetCostSummary(Guid id)
{
_logger.LogDebug("Getting cost summary for print job {Id}", id);
var job = await _dbContext.PrintJobs
.Include(j => j.Spool)
.ThenInclude(s => s!.MaterialBase)
.FirstOrDefaultAsync(j => j.Id == id);
if (job is null)
{
_logger.LogWarning("Print job {Id} not found for cost summary", id);
return NotFound(new { error = $"Print job with ID '{id}' not found." });
}
var warnings = new List<string>();
var spool = job.Spool;
// Build response with what we have
var response = new CostSummaryResponse
{
PrintJobId = job.Id,
PrintName = job.PrintName,
SpoolId = job.SpoolId,
SpoolSerial = spool?.SpoolSerial ?? string.Empty,
SpoolBrand = spool?.Brand ?? string.Empty,
SpoolColorName = spool?.ColorName ?? string.Empty,
MmExtruded = job.MmExtruded,
GramsDerived = job.GramsDerived,
SpoolPurchasePrice = spool?.PurchasePrice,
SpoolWeightTotalGrams = spool?.WeightTotalGrams,
StoredCostPerPrint = job.CostPerPrint
};
// Validate spool data availability
if (spool is null)
{
warnings.Add("Spool data is not available for this print job. Cost cannot be calculated.");
response.Warnings = warnings;
return Ok(response);
}
// Check if we can calculate cost
if (!spool.PurchasePrice.HasValue)
{
warnings.Add("Spool purchase price is not set. Cost per gram and total material cost cannot be calculated.");
}
if (spool.WeightTotalGrams <= 0)
{
warnings.Add("Spool total weight is zero or invalid. Cost per gram and total material cost cannot be calculated.");
}
// If we have enough data, calculate the cost
if (spool.PurchasePrice.HasValue && spool.WeightTotalGrams > 0)
{
var pricePerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams;
response.PricePerGram = Math.Round(pricePerGram, 4);
response.TotalMaterialCost = Math.Round(job.GramsDerived * pricePerGram, 4);
}
// Warn if grams derived is zero but mm extruded is non-zero
if (job.GramsDerived == 0 && job.MmExtruded > 0)
{
warnings.Add("GramsDerived is zero despite MmExtruded being non-zero. Cost may be inaccurate. Consider re-deriving grams from filament parameters.");
}
response.Warnings = warnings;
return Ok(response);
}
// ── Gram Derivation Formula ──────────────────────────────────── // ── Gram Derivation Formula ────────────────────────────────────
/// <summary> /// <summary>

View File

@@ -0,0 +1,55 @@
namespace Extrudex.API.DTOs.PrintJobs;
/// <summary>
/// Response DTO for the cost summary of a print job.
/// Provides a breakdown of material cost based on filament usage
/// and spool pricing data. If cost data is incomplete, warnings
/// are returned instead of throwing an error.
/// </summary>
public class CostSummaryResponse
{
/// <summary>Unique identifier of the print job.</summary>
public Guid PrintJobId { get; set; }
/// <summary>Human-readable name of the print job.</summary>
public string PrintName { get; set; } = string.Empty;
/// <summary>Foreign key to the spool used for this print job.</summary>
public Guid SpoolId { get; set; }
/// <summary>Serial number of the spool.</summary>
public string SpoolSerial { get; set; } = string.Empty;
/// <summary>Brand of the spool.</summary>
public string SpoolBrand { get; set; } = string.Empty;
/// <summary>Color name of the spool.</summary>
public string SpoolColorName { get; set; } = string.Empty;
/// <summary>Total millimeters of filament extruded during this print.</summary>
public decimal MmExtruded { get; set; }
/// <summary>Derived grams consumed for this print job.</summary>
public decimal GramsDerived { get; set; }
/// <summary>Purchase price of the full spool, if available.</summary>
public decimal? SpoolPurchasePrice { get; set; }
/// <summary>Total weight of the spool in grams when full.</summary>
public decimal? SpoolWeightTotalGrams { get; set; }
/// <summary>Calculated price per gram (purchase price / total weight), if available.</summary>
public decimal? PricePerGram { get; set; }
/// <summary>Calculated total material cost for this print job, if available.</summary>
public decimal? TotalMaterialCost { get; set; }
/// <summary>The CostPerPrint stored on the print job entity, if set.</summary>
public decimal? StoredCostPerPrint { get; set; }
/// <summary>
/// Warnings about missing data that prevent cost calculation.
/// Empty if all data is available and cost was calculated successfully.
/// </summary>
public List<string> Warnings { get; set; } = new();
}

11
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
node_modules
dist
.git
.gitignore
.angular
.vscode
*.md
.editorconfig
.prettierrc
src/test.ts
**/*.spec.ts

28
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Stage 1: Build the Angular application
FROM node:22-alpine AS build
WORKDIR /app
# Copy package files first for better layer caching
COPY package.json package-lock.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npx ng build --configuration production
# Stage 2: Serve static files with nginx
FROM nginx:alpine
# Remove default nginx config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built Angular artifacts from build stage
COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

42
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,42 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
gzip_min_length 256;
# Angular SPA — fallback to index.html for client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy API requests to backend
# Uses resolver so nginx doesn't crash if backend isn't available at startup
resolver 127.0.0.11 valid=30s ipv6=off;
set $backend "extrudex-api:8080";
location /api/ {
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check endpoint
location /health {
access_log off;
return 200 "ok";
add_header Content-Type text/plain;
}
}