Files
PnvPanel/backend/src/PnvPanel.Domain/Inbounds/Inbound.cs
T
mrleo1nidandClaude Opus 5.5 bb5e59904f
CI / Backend (build + test) (push) Failing after 1m36s
CI / Frontend (lint + typecheck + build) (push) Successful in 41s
Add inbound country flags and node load in location picker
- Inbound.CountryCode (ISO alpha-2), set on publish; flag shown before
  the location name (SVG via country-flag-icons on the site, emoji in bot)
- Node.Capacity (optional, not a limit); available inbounds report load
  percent of capacity or active config count, sorted least loaded first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:45:17 +03:00

97 lines
4.5 KiB
C#

using PnvPanel.Domain.Common;
namespace PnvPanel.Domain.Inbounds;
/// <summary>
/// Проекция inbound из 3x-ui. <see cref="RemoteInboundId"/> — идентификатор на стороне панели
/// (в ThreeXui.Net это string, а не число — так отдаёт API 3x-ui).
/// AllowedRoleIds хранит только Guid ролей (не навигацию на AppRole — тот живёт в Infrastructure/Identity,
/// Domain не должен на него ссылаться).
/// </summary>
public sealed class Inbound : Entity
{
public Guid NodeId { get; private set; }
public string RemoteInboundId { get; private set; } = string.Empty;
public VpnProtocol Protocol { get; private set; }
public string Remark { get; private set; } = string.Empty;
public int Port { get; private set; }
public bool IsPublished { get; private set; }
public bool IsAvailable { get; private set; } = true;
public string? DisplayName { get; private set; }
/// <summary>ISO 3166-1 alpha-2 (верхний регистр) — страна, флаг которой пользователь видит перед
/// именем инбаунда. Только для отображения, на подключение не влияет.</summary>
public string? CountryCode { get; private set; }
public IReadOnlyList<Guid> AllowedRoleIds { get; private set; } = [];
public DateTimeOffset? LastSyncAt { get; private set; }
private Inbound() { }
public static Inbound FromRemote(
Guid nodeId,
string remoteInboundId,
VpnProtocol protocol,
string remark,
int port
)
{
return new Inbound
{
Id = Guid.NewGuid(),
NodeId = nodeId,
RemoteInboundId = remoteInboundId,
Protocol = protocol,
Remark = remark,
Port = port,
IsPublished = false,
IsAvailable = true,
LastSyncAt = DateTimeOffset.UtcNow,
};
}
public void UpdateFromRemote(VpnProtocol protocol, string remark, int port)
{
Protocol = protocol;
Remark = remark;
Port = port;
LastSyncAt = DateTimeOffset.UtcNow;
// Инбаунд снова пришёл в синхронизации — значит панель его видит, снимаем MarkUnavailable()
// (иначе после ЛЮБОГО временного сбоя синка — не только реального удаления — Revoke навсегда
// пропускал бы обращение к панели для конфигов на этом инбаунде, хотя клиент в панели жив).
IsAvailable = true;
}
public void Publish(
string? displayName,
string? countryCode,
IReadOnlyCollection<Guid> allowedRoleIds
)
{
IsPublished = true;
IsAvailable = true;
DisplayName = displayName;
CountryCode = string.IsNullOrWhiteSpace(countryCode)
? null
: countryCode.Trim().ToUpperInvariant();
AllowedRoleIds = allowedRoleIds.Distinct().ToList();
}
public void Unpublish() => IsPublished = false;
/// <summary>Роль удалена админом — снимаем её из списка разрешённых, чтобы не оставлять ссылку на
/// несуществующую роль (иначе она навсегда "съедала" бы место в списке, ни на что не влияя, но и
/// не позволяя понять, что инбаунд стал доступен меньшему числу ролей, чем задумано).</summary>
public void RemoveAllowedRole(Guid roleId) => AllowedRoleIds = AllowedRoleIds.Where(id => id != roleId).ToList();
/// <summary>
/// Инбаунд пропал на панели (не пришёл в очередной синхронизации), но по нему есть конфиги —
/// удалить запись нельзя (FK), поэтому помечаем недоступной вместо тихого "не опубликован":
/// UI показывает явный статус, а Revoke пропускает обращение к панели как заведомо мёртвое.
/// </summary>
public void MarkUnavailable()
{
IsAvailable = false;
IsPublished = false;
}
}