Enhance documentation with new features: added dark/light/system theme support, instructions page, and application catalog. Updated API and domain model for app management and automatic migrations on startup. Improved frontend structure with new routes and features for user instructions and app management.

This commit is contained in:
Leonid Pershin
2026-07-01 22:38:01 +03:00
parent d8930409fe
commit 1a8d33efa3
229 changed files with 9226 additions and 20 deletions
@@ -0,0 +1,23 @@
namespace PnvPanel.Application.Common.Models;
public enum ErrorType
{
Failure,
Validation,
NotFound,
Conflict,
Unauthorized,
Forbidden,
}
public sealed record Error(string Code, string Message, ErrorType Type = ErrorType.Failure)
{
public static readonly Error None = new(string.Empty, string.Empty);
public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation);
public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);
public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);
public static Error Unauthorized(string code, string message) => new(code, message, ErrorType.Unauthorized);
public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);
public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure);
}
@@ -0,0 +1,3 @@
namespace PnvPanel.Application.Common.Models;
public sealed record PagedList<T>(IReadOnlyList<T> Items, int Total, int Page, int PageSize);
@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace PnvPanel.Application.Common.Models;
public static class PagedListExtensions
{
public static async Task<PagedList<T>> ToPagedListAsync<T>(
this IQueryable<T> query, int page, int pageSize, CancellationToken cancellationToken)
{
var total = await query.CountAsync(cancellationToken);
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return new PagedList<T>(items, total, page, pageSize);
}
}
@@ -0,0 +1,37 @@
namespace PnvPanel.Application.Common.Models;
public class Result
{
public bool IsSuccess { get; }
public Error Error { get; }
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None)
throw new InvalidOperationException("Успешный результат не может содержать ошибку.");
if (!isSuccess && error == Error.None)
throw new InvalidOperationException("Неуспешный результат обязан содержать ошибку.");
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<T> Success<T>(T value) => new(value, true, Error.None);
public static Result<T> Failure<T>(Error error) => new(default, false, error);
}
public class Result<T> : Result
{
private readonly T? _value;
internal Result(T? value, bool isSuccess, Error error) : base(isSuccess, error) => _value = value;
public T Value => IsSuccess
? _value!
: throw new InvalidOperationException("Нельзя получить значение неуспешного результата.");
public static implicit operator Result<T>(T value) => Success(value);
}
@@ -0,0 +1,6 @@
namespace PnvPanel.Application.Common.Models;
public static class RoleQuota
{
public const int Unlimited = -1;
}