Rrestore point page created
This commit is contained in:
parent
fbc07a00e5
commit
aebaa00371
54
PlotLine/Controllers/ProjectRestorePointsController.cs
Normal file
54
PlotLine/Controllers/ProjectRestorePointsController.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PlotLine.Services;
|
||||||
|
|
||||||
|
namespace PlotLine.Controllers;
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePointsController(IProjectRestorePointService restorePoints, ILogger<ProjectRestorePointsController> logger) : Controller
|
||||||
|
{
|
||||||
|
public async Task<IActionResult> Index(int projectId)
|
||||||
|
{
|
||||||
|
var model = await restorePoints.GetAsync(projectId);
|
||||||
|
return model is null ? NotFound() : View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Create(int projectId, string? restorePointName, string? notes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var created = await restorePoints.CreateManualAsync(projectId, restorePointName, notes);
|
||||||
|
TempData["ProjectRestorePointMessage"] = created is null
|
||||||
|
? "Restore point could not be created because the project was not found."
|
||||||
|
: "Restore point created.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Manual restore point creation failed for project {ProjectID}.", projectId);
|
||||||
|
TempData["ProjectRestorePointMessage"] = "Restore point could not be created. Please try again, or check the application logs if the problem continues.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index), new { projectId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Download(int projectId, int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var download = await restorePoints.DownloadAsync(id, projectId);
|
||||||
|
if (download is null)
|
||||||
|
{
|
||||||
|
TempData["ProjectRestorePointMessage"] = "Restore point could not be found.";
|
||||||
|
return RedirectToAction(nameof(Index), new { projectId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(System.Text.Encoding.UTF8.GetBytes(download.Json), download.ContentType, download.FileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Restore point download failed for restore point {ProjectRestorePointID} in project {ProjectID}.", id, projectId);
|
||||||
|
TempData["ProjectRestorePointMessage"] = "Restore point could not be downloaded. Please try again, or check the application logs if the problem continues.";
|
||||||
|
return RedirectToAction(nameof(Index), new { projectId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
PlotLine/Data/ProjectRestorePointRepository.cs
Normal file
50
PlotLine/Data/ProjectRestorePointRepository.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Data;
|
||||||
|
|
||||||
|
public interface IProjectRestorePointRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<ProjectRestorePoint>> ListAsync(int projectId);
|
||||||
|
Task<ProjectRestorePoint?> GetAsync(int projectRestorePointId, int projectId);
|
||||||
|
Task<ProjectRestorePoint> CreateAsync(int projectId, string restorePointName, string restorePointType, string jsonData, string? notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePointRepository(ISqlConnectionFactory connectionFactory) : IProjectRestorePointRepository
|
||||||
|
{
|
||||||
|
public async Task<IReadOnlyList<ProjectRestorePoint>> ListAsync(int projectId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<ProjectRestorePoint>(
|
||||||
|
"dbo.ProjectRestorePoint_List",
|
||||||
|
new { ProjectID = projectId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectRestorePoint?> GetAsync(int projectRestorePointId, int projectId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<ProjectRestorePoint>(
|
||||||
|
"dbo.ProjectRestorePoint_Get",
|
||||||
|
new { ProjectRestorePointID = projectRestorePointId, ProjectID = projectId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectRestorePoint> CreateAsync(int projectId, string restorePointName, string restorePointType, string jsonData, string? notes)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<ProjectRestorePoint>(
|
||||||
|
"dbo.ProjectRestorePoint_Create",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
ProjectID = projectId,
|
||||||
|
RestorePointName = restorePointName,
|
||||||
|
RestorePointType = restorePointType,
|
||||||
|
JsonData = jsonData,
|
||||||
|
Notes = notes
|
||||||
|
},
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
PlotLine/Models/ProjectRestorePointModels.cs
Normal file
21
PlotLine/Models/ProjectRestorePointModels.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace PlotLine.Models;
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePoint
|
||||||
|
{
|
||||||
|
public int ProjectRestorePointID { get; set; }
|
||||||
|
public int ProjectID { get; set; }
|
||||||
|
public string ProjectName { get; set; } = string.Empty;
|
||||||
|
public string RestorePointName { get; set; } = string.Empty;
|
||||||
|
public string RestorePointType { get; set; } = "Manual";
|
||||||
|
public DateTime CreatedDateUtc { get; set; }
|
||||||
|
public string? JsonData { get; set; }
|
||||||
|
public long JsonSizeBytes { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePointDownload
|
||||||
|
{
|
||||||
|
public string FileName { get; init; } = string.Empty;
|
||||||
|
public string ContentType { get; init; } = "application/json";
|
||||||
|
public string Json { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@ -38,6 +38,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IWriterWorkspaceRepository, WriterWorkspaceRepository>();
|
builder.Services.AddScoped<IWriterWorkspaceRepository, WriterWorkspaceRepository>();
|
||||||
builder.Services.AddScoped<IExportRepository, ExportRepository>();
|
builder.Services.AddScoped<IExportRepository, ExportRepository>();
|
||||||
builder.Services.AddScoped<IProjectBackupRepository, ProjectBackupRepository>();
|
builder.Services.AddScoped<IProjectBackupRepository, ProjectBackupRepository>();
|
||||||
|
builder.Services.AddScoped<IProjectRestorePointRepository, ProjectRestorePointRepository>();
|
||||||
builder.Services.AddScoped<IDynamicsRepository, DynamicsRepository>();
|
builder.Services.AddScoped<IDynamicsRepository, DynamicsRepository>();
|
||||||
builder.Services.AddScoped<IScenarioRepository, ScenarioRepository>();
|
builder.Services.AddScoped<IScenarioRepository, ScenarioRepository>();
|
||||||
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
||||||
@ -60,6 +61,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
|
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
|
||||||
builder.Services.AddScoped<IExportService, ExportService>();
|
builder.Services.AddScoped<IExportService, ExportService>();
|
||||||
builder.Services.AddScoped<IProjectExportService, ProjectExportService>();
|
builder.Services.AddScoped<IProjectExportService, ProjectExportService>();
|
||||||
|
builder.Services.AddScoped<IProjectRestorePointService, ProjectRestorePointService>();
|
||||||
builder.Services.AddScoped<IDynamicsService, DynamicsService>();
|
builder.Services.AddScoped<IDynamicsService, DynamicsService>();
|
||||||
builder.Services.AddScoped<IScenarioService, ScenarioService>();
|
builder.Services.AddScoped<IScenarioService, ScenarioService>();
|
||||||
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
||||||
|
|||||||
97
PlotLine/Services/ProjectRestorePointService.cs
Normal file
97
PlotLine/Services/ProjectRestorePointService.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using PlotLine.Data;
|
||||||
|
using PlotLine.Models;
|
||||||
|
using PlotLine.ViewModels;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public interface IProjectRestorePointService
|
||||||
|
{
|
||||||
|
Task<ProjectRestorePointListViewModel?> GetAsync(int projectId);
|
||||||
|
Task<ProjectRestorePoint?> CreateManualAsync(int projectId, string? restorePointName, string? notes);
|
||||||
|
Task<ProjectRestorePointDownload?> DownloadAsync(int projectRestorePointId, int projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePointService(
|
||||||
|
IProjectRepository projects,
|
||||||
|
IProjectExportService projectExports,
|
||||||
|
IProjectRestorePointRepository restorePoints,
|
||||||
|
ILogger<ProjectRestorePointService> logger) : IProjectRestorePointService
|
||||||
|
{
|
||||||
|
private const string ManualRestorePointType = "Manual";
|
||||||
|
|
||||||
|
public async Task<ProjectRestorePointListViewModel?> GetAsync(int projectId)
|
||||||
|
{
|
||||||
|
var project = await projects.GetAsync(projectId);
|
||||||
|
if (project is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProjectRestorePointListViewModel
|
||||||
|
{
|
||||||
|
Project = project,
|
||||||
|
RestorePoints = await restorePoints.ListAsync(projectId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectRestorePoint?> CreateManualAsync(int projectId, string? restorePointName, string? notes)
|
||||||
|
{
|
||||||
|
var export = await projectExports.ExportAsync(projectId);
|
||||||
|
if (export is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Manual restore point requested for missing project {ProjectID}.", projectId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = string.IsNullOrWhiteSpace(restorePointName)
|
||||||
|
? $"Manual restore point - {DateTime.Now:yyyy-MM-dd HH:mm}"
|
||||||
|
: restorePointName.Trim();
|
||||||
|
|
||||||
|
var created = await restorePoints.CreateAsync(projectId, name, ManualRestorePointType, export.Json, Clean(notes));
|
||||||
|
logger.LogInformation("Manual restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.",
|
||||||
|
created.ProjectRestorePointID,
|
||||||
|
projectId,
|
||||||
|
created.JsonSizeBytes);
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectRestorePointDownload?> DownloadAsync(int projectRestorePointId, int projectId)
|
||||||
|
{
|
||||||
|
var restorePoint = await restorePoints.GetAsync(projectRestorePointId, projectId);
|
||||||
|
if (restorePoint?.JsonData is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Restore point download requested for missing restore point {ProjectRestorePointID} in project {ProjectID}.", projectRestorePointId, projectId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProjectRestorePointDownload
|
||||||
|
{
|
||||||
|
FileName = BuildFileName(restorePoint.ProjectName, restorePoint.CreatedDateUtc),
|
||||||
|
ContentType = "application/json",
|
||||||
|
Json = restorePoint.JsonData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Clean(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildFileName(string projectName, DateTime createdDateUtc)
|
||||||
|
{
|
||||||
|
var safeTitle = new string(projectName
|
||||||
|
.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
|
||||||
|
.ToArray())
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
safeTitle = string.Join("_", safeTitle.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(safeTitle))
|
||||||
|
{
|
||||||
|
safeTitle = "PlotWeaver_Project";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{safeTitle}_RestorePoint_{createdDateUtc:yyyy-MM-dd_HHmm}.plotweaver";
|
||||||
|
}
|
||||||
|
}
|
||||||
97
PlotLine/Sql/035_ProjectRestorePoints.sql
Normal file
97
PlotLine/Sql/035_ProjectRestorePoints.sql
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.ProjectRestorePoints', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.ProjectRestorePoints
|
||||||
|
(
|
||||||
|
ProjectRestorePointID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ProjectRestorePoints PRIMARY KEY,
|
||||||
|
ProjectID int NOT NULL,
|
||||||
|
RestorePointName nvarchar(255) NOT NULL,
|
||||||
|
RestorePointType nvarchar(50) NOT NULL,
|
||||||
|
CreatedDateUtc datetime2 NOT NULL CONSTRAINT DF_ProjectRestorePoints_CreatedDateUtc DEFAULT SYSUTCDATETIME(),
|
||||||
|
JsonData nvarchar(max) NOT NULL,
|
||||||
|
JsonSizeBytes bigint NOT NULL,
|
||||||
|
Notes nvarchar(max) NULL,
|
||||||
|
CONSTRAINT FK_ProjectRestorePoints_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ProjectRestorePoints_ProjectCreated' AND object_id = OBJECT_ID(N'dbo.ProjectRestorePoints'))
|
||||||
|
CREATE INDEX IX_ProjectRestorePoints_ProjectCreated ON dbo.ProjectRestorePoints(ProjectID, CreatedDateUtc DESC);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectRestorePoint_List
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT rp.ProjectRestorePointID, rp.ProjectID, p.ProjectName, rp.RestorePointName, rp.RestorePointType,
|
||||||
|
rp.CreatedDateUtc, CAST(NULL AS nvarchar(max)) AS JsonData, rp.JsonSizeBytes, rp.Notes
|
||||||
|
FROM dbo.ProjectRestorePoints rp
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = rp.ProjectID
|
||||||
|
WHERE rp.ProjectID = @ProjectID
|
||||||
|
ORDER BY rp.CreatedDateUtc DESC, rp.ProjectRestorePointID DESC;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectRestorePoint_Get
|
||||||
|
@ProjectRestorePointID int,
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT rp.ProjectRestorePointID, rp.ProjectID, p.ProjectName, rp.RestorePointName, rp.RestorePointType,
|
||||||
|
rp.CreatedDateUtc, rp.JsonData, rp.JsonSizeBytes, rp.Notes
|
||||||
|
FROM dbo.ProjectRestorePoints rp
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = rp.ProjectID
|
||||||
|
WHERE rp.ProjectRestorePointID = @ProjectRestorePointID
|
||||||
|
AND rp.ProjectID = @ProjectID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectRestorePoint_Create
|
||||||
|
@ProjectID int,
|
||||||
|
@RestorePointName nvarchar(255),
|
||||||
|
@RestorePointType nvarchar(50),
|
||||||
|
@JsonData nvarchar(max),
|
||||||
|
@Notes nvarchar(max) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
INSERT INTO dbo.ProjectRestorePoints
|
||||||
|
(
|
||||||
|
ProjectID,
|
||||||
|
RestorePointName,
|
||||||
|
RestorePointType,
|
||||||
|
CreatedDateUtc,
|
||||||
|
JsonData,
|
||||||
|
JsonSizeBytes,
|
||||||
|
Notes
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@ProjectID,
|
||||||
|
@RestorePointName,
|
||||||
|
@RestorePointType,
|
||||||
|
SYSUTCDATETIME(),
|
||||||
|
@JsonData,
|
||||||
|
DATALENGTH(@JsonData),
|
||||||
|
@Notes
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @ProjectRestorePointID int = SCOPE_IDENTITY();
|
||||||
|
|
||||||
|
SELECT rp.ProjectRestorePointID, rp.ProjectID, p.ProjectName, rp.RestorePointName, rp.RestorePointType,
|
||||||
|
rp.CreatedDateUtc, CAST(NULL AS nvarchar(max)) AS JsonData, rp.JsonSizeBytes, rp.Notes
|
||||||
|
FROM dbo.ProjectRestorePoints rp
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = rp.ProjectID
|
||||||
|
WHERE rp.ProjectRestorePointID = @ProjectRestorePointID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -455,6 +455,12 @@ public sealed class ProjectSceneMetricsViewModel
|
|||||||
public bool HasEnabledMetrics => Metrics.Any(x => x.IsEnabledForProject && x.IsActive);
|
public bool HasEnabledMetrics => Metrics.Any(x => x.IsEnabledForProject && x.IsActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectRestorePointListViewModel
|
||||||
|
{
|
||||||
|
public Project Project { get; init; } = new();
|
||||||
|
public IReadOnlyList<ProjectRestorePoint> RestorePoints { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class SceneMetricTypeEditViewModel
|
public sealed class SceneMetricTypeEditViewModel
|
||||||
{
|
{
|
||||||
public int MetricTypeID { get; set; }
|
public int MetricTypeID { get; set; }
|
||||||
|
|||||||
107
PlotLine/Views/ProjectRestorePoints/Index.cshtml
Normal file
107
PlotLine/Views/ProjectRestorePoints/Index.cshtml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
@model ProjectRestorePointListViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Restore Points";
|
||||||
|
var defaultRestorePointName = $"Manual restore point - {DateTime.Now:yyyy-MM-dd HH:mm}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private static string FormatSize(long bytes)
|
||||||
|
{
|
||||||
|
if (bytes >= 1024 * 1024)
|
||||||
|
{
|
||||||
|
return $"{bytes / 1024d / 1024d:0.0} MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes >= 1024)
|
||||||
|
{
|
||||||
|
return $"{bytes / 1024d:0.0} KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{bytes} bytes";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<nav class="breadcrumb-trail">
|
||||||
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
||||||
|
<span>Restore Points</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="page-heading compact">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Project backup</p>
|
||||||
|
<h1>Restore Points</h1>
|
||||||
|
<p class="lead-text">Create manual database-saved snapshots of the project export JSON.</p>
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Back to project</a>
|
||||||
|
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@Model.Project.ProjectID">Download Project Backup</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (TempData["ProjectRestorePointMessage"] is string message)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">@message</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<div class="story-bible-section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Manual snapshot</p>
|
||||||
|
<h2>Create restore point</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form asp-action="Create" method="post" class="row g-3">
|
||||||
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label" for="restorePointName">Name</label>
|
||||||
|
<input class="form-control" id="restorePointName" name="restorePointName" maxlength="255" placeholder="@defaultRestorePointName" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label" for="notes">Notes</label>
|
||||||
|
<input class="form-control" id="notes" name="notes" maxlength="1000" placeholder="Optional reason or reminder" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
|
<button class="btn btn-primary w-100" type="submit">Create Restore Point</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="list-section">
|
||||||
|
<h2>Saved restore points</h2>
|
||||||
|
@if (!Model.RestorePoints.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No restore points yet.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var restorePoint in Model.RestorePoints)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@restorePoint.RestorePointName</td>
|
||||||
|
<td><span class="status-pill">@restorePoint.RestorePointType</span></td>
|
||||||
|
<td>@restorePoint.CreatedDateUtc.ToLocalTime().ToString("dd MMM yyyy HH:mm")</td>
|
||||||
|
<td>@FormatSize(restorePoint.JsonSizeBytes)</td>
|
||||||
|
<td>@restorePoint.Notes</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" asp-action="Download" asp-route-projectId="@Model.Project.ProjectID" asp-route-id="@restorePoint.ProjectRestorePointID">Download</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
@ -62,6 +62,7 @@
|
|||||||
<a class="btn btn-outline-secondary" asp-controller="Acceptance" asp-action="Phase1" asp-route-projectId="@Model.Project.ProjectID">Phase 1 Checklist</a>
|
<a class="btn btn-outline-secondary" asp-controller="Acceptance" asp-action="Phase1" asp-route-projectId="@Model.Project.ProjectID">Phase 1 Checklist</a>
|
||||||
<a class="btn btn-outline-secondary" asp-controller="Archives" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Archived Items</a>
|
<a class="btn btn-outline-secondary" asp-controller="Archives" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Archived Items</a>
|
||||||
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@Model.Project.ProjectID">Download Project Backup</a>
|
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="ProjectBackup" asp-route-projectId="@Model.Project.ProjectID">Download Project Backup</a>
|
||||||
|
<a class="btn btn-outline-secondary" asp-controller="ProjectRestorePoints" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Restore Points</a>
|
||||||
<a class="btn btn-primary" asp-controller="Books" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add book</a>
|
<a class="btn btn-primary" asp-controller="Books" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add book</a>
|
||||||
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this project? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this project? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
||||||
<input type="hidden" name="id" value="@Model.Project.ProjectID" />
|
<input type="hidden" name="id" value="@Model.Project.ProjectID" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user