Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,174 @@
@{
ViewData["Title"] = "Storage Migration";
ViewData["PageIcon"] = "bi-cloud-upload";
bool mediaExists = ViewBag.MediaExists;
int localFileCount = ViewBag.LocalFileCount;
}
<div class="container-fluid">
<div class="row g-4">
<!-- Status Card -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-hdd me-2"></i>Local Filesystem Status</h6>
</div>
<div class="card-body">
@if (mediaExists)
{
<div class="d-flex align-items-center gap-3 mb-3">
<div class="rounded-circle bg-warning bg-opacity-10 p-3">
<i class="bi bi-folder2-open fs-4 text-warning"></i>
</div>
<div>
<div class="fw-semibold">@localFileCount file@(localFileCount != 1 ? "s" : "") found locally</div>
<small class="text-muted">@ViewBag.MediaPath</small>
</div>
</div>
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-1"></i>
These files will be uploaded to Azure Blob Storage. Files already present in Azure will be skipped automatically.
</div>
}
else
{
<div class="d-flex align-items-center gap-3">
<div class="rounded-circle bg-success bg-opacity-10 p-3">
<i class="bi bi-check-circle fs-4 text-success"></i>
</div>
<div>
<div class="fw-semibold">No local media files found</div>
<small class="text-muted">@ViewBag.MediaPath</small>
</div>
</div>
}
</div>
</div>
</div>
<!-- Azure Targets Card -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-cloud me-2"></i>Azure Container Mapping</h6>
</div>
<div class="card-body p-0">
<!-- Desktop table -->
<table class="table table-sm mb-0 d-none d-lg-table">
<thead>
<tr>
<th class="ps-3">File Type</th>
<th>Container</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-3"><i class="bi bi-person-circle me-1 text-primary"></i>Profile photos</td>
<td><code>profileimages</code></td>
</tr>
<tr>
<td class="ps-3"><i class="bi bi-camera me-1 text-success"></i>Job photos</td>
<td><code>jobimages</code></td>
</tr>
<tr>
<td class="ps-3"><i class="bi bi-building me-1 text-info"></i>Company logos</td>
<td><code>companylogos</code></td>
</tr>
<tr>
<td class="ps-3"><i class="bi bi-file-earmark-pdf me-1 text-danger"></i>Equipment manuals</td>
<td><code>manuals</code></td>
</tr>
</tbody>
</table>
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view d-lg-none p-3">
<div class="mobile-card-list">
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-primary"><i class="bi bi-person-circle"></i></div>
<div class="mobile-card-title">
<h6>Profile photos</h6>
<small><code>profileimages</code></small>
</div>
</div>
</div>
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-success"><i class="bi bi-camera"></i></div>
<div class="mobile-card-title">
<h6>Job photos</h6>
<small><code>jobimages</code></small>
</div>
</div>
</div>
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-info"><i class="bi bi-building"></i></div>
<div class="mobile-card-title">
<h6>Company logos</h6>
<small><code>companylogos</code></small>
</div>
</div>
</div>
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-danger"><i class="bi bi-file-earmark-pdf"></i></div>
<div class="mobile-card-title">
<h6>Equipment manuals</h6>
<small><code>manuals</code></small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if (mediaExists && localFileCount > 0)
{
<!-- Migration Form -->
<div class="card mt-4">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-play-circle me-2"></i>Run Migration</h6>
</div>
<div class="card-body">
<form asp-action="Migrate" method="post" id="migrationForm">
@Html.AntiForgeryToken()
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="deleteAfterMigration" value="true" id="deleteAfterMigration">
<label class="form-check-label" for="deleteAfterMigration">
Delete local files after successful migration
</label>
</div>
<div class="form-text text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>
Leave unchecked on the first run. Files already in Azure are always skipped — re-running is safe.
</div>
</div>
<button type="submit" class="btn btn-primary" id="migrateBtn">
<i class="bi bi-cloud-upload me-2"></i>Start Migration
</button>
<span class="ms-3 text-muted d-none" id="migrationSpinner">
<span class="spinner-border spinner-border-sm me-1"></span>Migrating @localFileCount files, please wait…
</span>
</form>
</div>
</div>
}
</div>
@section Scripts {
<script>
document.getElementById('migrationForm')?.addEventListener('submit', function () {
document.getElementById('migrateBtn').disabled = true;
document.getElementById('migrationSpinner').classList.remove('d-none');
});
</script>
}
@@ -0,0 +1,136 @@
@using PowderCoating.Application.Interfaces
@model StorageMigrationResult
@{
ViewData["Title"] = "Migration Results";
ViewData["PageIcon"] = "bi-cloud-check";
}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-muted mb-0">Completed in @Model.Duration.TotalSeconds.ToString("F1")s</p>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
</div>
<!-- Summary Cards -->
<div class="row g-3 mb-4">
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-success bg-opacity-10">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-success">@Model.Migrated</div>
<div class="text-muted small">Migrated</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-secondary bg-opacity-10">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-secondary">@Model.Skipped</div>
<div class="text-muted small">Already in Azure</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 @(Model.Failed > 0 ? "bg-danger bg-opacity-10" : "bg-secondary bg-opacity-10")">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold @(Model.Failed > 0 ? "text-danger" : "text-secondary")">@Model.Failed</div>
<div class="text-muted small">Failed</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card border-0 bg-info bg-opacity-10">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-info">@FormatBytes(Model.BytesMigrated)</div>
<div class="text-muted small">Data Uploaded</div>
</div>
</div>
</div>
</div>
@if (Model.HasErrors)
{
<div class="alert alert-danger mb-4">
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-1"></i>Errors (@Model.Errors.Count)</h6>
<ul class="mb-0 small">
@foreach (var error in Model.Errors)
{
<li>@error</li>
}
</ul>
</div>
}
@if (Model.Total == 0)
{
<div class="alert alert-info">
<i class="bi bi-info-circle me-1"></i>No files were found in the local media directory.
</div>
}
else
{
<!-- File Detail Table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-list-ul me-2"></i>File Details (@Model.Total total)</h6>
<div class="d-flex gap-2">
<span class="badge bg-success">@Model.Migrated migrated</span>
<span class="badge bg-secondary">@Model.Skipped skipped</span>
@if (Model.Failed > 0)
{
<span class="badge bg-danger">@Model.Failed failed</span>
}
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th class="ps-3">File</th>
<th>Container</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach (var file in Model.Files.OrderBy(f => f.Status).ThenBy(f => f.RelativePath))
{
<tr>
<td class="ps-3">
<code class="small">@file.RelativePath</code>
</td>
<td><span class="badge bg-light text-dark">@file.Container</span></td>
<td class="text-muted small">@FormatBytes(file.FileSize)</td>
<td>
@switch (file.Status)
{
case MigrationFileStatus.Migrated:
<span class="badge bg-success"><i class="bi bi-check me-1"></i>Migrated</span>
break;
case MigrationFileStatus.Skipped:
<span class="badge bg-secondary"><i class="bi bi-skip-forward me-1"></i>Already in Azure</span>
break;
case MigrationFileStatus.Failed:
<span class="badge bg-danger"><i class="bi bi-x me-1"></i>Failed</span>
break;
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@functions {
string FormatBytes(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024):F1} MB";
}
}