feat: require location-site filter and add departmental sync summaries

Initial department sync now requires InitialSyncLocationSiteId and can
run across all active departments when department IDs are empty. Employee
selection filters by location site, reports inactive/missing departments,
and writes per-department outcomes to a dedicated DepartmentalSync biz log.
main
SYED MUSTUFA AHMED NAQVI 2026-08-25 10:30:59 +05:00
parent e202923a69
commit ec6aaac0fa
4 changed files with 436 additions and 56 deletions

View File

@ -54,6 +54,7 @@ internal enum BizChannel
Attendance,
UserSync,
Template,
DepartmentalSync,
Unreachable
}

View File

@ -7,7 +7,7 @@
"EnableTemplateDeviceToDbSync": false,
"EnableTemplateDbToDeviceSync": false,
"SourceMachineIp": "",
"SourceMachineIp": "192.168.91.80",
"TargetMachineIps": [
"192.168.91.80"
],
@ -15,11 +15,10 @@
"SyncEmployeeIds": [],
"SyncDepartmentIds": [],
"EnableInitialDepartmentSync": true,
"InitialSyncDepartmentIds": [
"357"
],
"EnableInitialDepartmentSync": false,
"InitialSyncDepartmentIds": [],
"InitialSyncEmployeeIds": [],
"InitialSyncLocationSiteId": 2,
"EnableEmployeePhotoSource": true,
"EmployeePhotoBaseUrl": "https://portal.utopiaindustries.pk/uind/employee-photo/"
}

View File

@ -27,21 +27,39 @@ internal sealed partial class HikvisionAttendanceManager
ct.ThrowIfCancellationRequested();
if (!_config.InitialSyncLocationSiteId.HasValue ||
_config.InitialSyncLocationSiteId.Value <= 0)
{
const string reason =
"InitialSyncLocationSiteId is not configured; Initial Department Sync requires an explicit location-site filter.";
_logger.OpsWarn("INITIAL_SYNC", "skipped — " + reason);
_logger.Biz(BizChannel.DepartmentalSync,
"DEPARTMENTAL SYNC FAILED",
"",
"Reason : " + reason,
"");
_logger.BizSeparator(BizChannel.DepartmentalSync);
return;
}
var locationSiteId = _config.InitialSyncLocationSiteId.Value;
var deptIds = (_config.InitialSyncDepartmentIds ?? new List<string>())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (deptIds.Count == 0)
{
_logger.OpsWarn("INITIAL_SYNC", "skipped — InitialSyncDepartmentIds is empty");
return;
}
var allActiveDepartmentsMode = deptIds.Count == 0;
var targets = ResolveInitialSyncTargets();
if (targets.Count == 0)
{
_logger.OpsWarn("INITIAL_SYNC", "skipped — no valid TargetMachineIps");
_logger.Biz(BizChannel.DepartmentalSync,
"DEPARTMENTAL SYNC FAILED",
"",
"Reason : No valid target machines are configured.",
"");
_logger.BizSeparator(BizChannel.DepartmentalSync);
return;
}
@ -51,22 +69,33 @@ internal sealed partial class HikvisionAttendanceManager
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var employees = LoadEmployeesForInitialDepartmentSync(deptIds, employeeIdFilter, out var loadErr);
if (allActiveDepartmentsMode && employeeIdFilter.Count > 0)
{
_logger.OpsWarn("INITIAL_SYNC",
"InitialSyncEmployeeIds ignored because InitialSyncDepartmentIds is empty; " +
"mode=ALL_ACTIVE_DEPARTMENTS_AND_EMPLOYEES");
employeeIdFilter.Clear();
}
var selection = LoadEmployeesForInitialDepartmentSync(
deptIds,
employeeIdFilter,
locationSiteId,
out var loadErr);
if (!string.IsNullOrWhiteSpace(loadErr))
{
_logger.Warn("INITIAL_SYNC: employee lookup failed err=" + loadErr);
if (employeeIdFilter.Count > 0)
_logger.Ops("INITIAL_SYNC", "Employee filter: departmentIds=" + string.Join(",", deptIds) +
" allowListCount=" + employeeIdFilter.Count +
" allowList=" + string.Join(",", employeeIdFilter) +
" matched=" + employees.Count);
_logger.Ops("INITIAL_SYNC", "departmentIds=" + string.Join(",", deptIds) +
" employeeFilter=" + (employeeIdFilter.Count > 0 ? "selective(" + employeeIdFilter.Count + ")" : "whole-department") +
" employeesFound=" + employees.Count);
if (employees.Count == 0)
_logger.Biz(BizChannel.DepartmentalSync,
"DEPARTMENTAL SYNC FAILED",
"",
"Reason : " + loadErr,
"");
_logger.BizSeparator(BizChannel.DepartmentalSync);
return;
}
LogInitialDepartmentSelection(deptIds, employeeIdFilter, selection);
var employees = selection.Employees;
var maxRetries = Math.Max(1, SyncPol.HttpMaxRetries);
var photoBase = (_config.EmployeePhotoBaseUrl ?? "").Trim();
@ -90,6 +119,12 @@ internal sealed partial class HikvisionAttendanceManager
"Machine is not connected.",
"");
_logger.BizSeparator(BizChannel.UserSync);
WriteDepartmentalSyncSummary(
target,
selection,
employeeIdFilter,
false,
Array.Empty<InitialSyncEmployeeOutcome>());
continue;
}
@ -97,9 +132,22 @@ internal sealed partial class HikvisionAttendanceManager
"MACHINE " + (target.DeviceId ?? "") + " -> Total Users : " + employees.Count,
"");
if (employees.Count == 0)
{
WriteDepartmentalSyncSummary(
target,
selection,
employeeIdFilter,
true,
Array.Empty<InitialSyncEmployeeOutcome>());
_logger.BizSeparator(BizChannel.UserSync);
continue;
}
var existing = FetchAllUsersIsapiForSync(target, maxRetries, ct);
var existingNos = new HashSet<string>(existing.Select(u => u.EmployeeNo), StringComparer.OrdinalIgnoreCase);
var faceRejectedEmployees = new List<(string EmployeeNo, string Reason)>();
var employeeOutcomes = new List<InitialSyncEmployeeOutcome>();
foreach (var emp in employees)
{
@ -186,6 +234,15 @@ internal sealed partial class HikvisionAttendanceManager
}
finally
{
employeeOutcomes.Add(new InitialSyncEmployeeOutcome
{
DepartmentId = emp.DepartmentId,
EmployeeNo = employeeNo,
Synced = faceUploaded,
Reason = faceUploaded
? ""
: (string.IsNullOrWhiteSpace(reason) ? "face_upload_failed" : reason)
});
WriteInitialSyncEmployeeBizLog(target, employeeNo, userCreated, faceUploaded, reason);
if (photoDownloaded && faceUploaded)
@ -222,6 +279,7 @@ internal sealed partial class HikvisionAttendanceManager
}
WriteFaceRejectedEmployeeSummary("INITIAL_SYNC", target, faceRejectedEmployees);
WriteDepartmentalSyncSummary(target, selection, employeeIdFilter, true, employeeOutcomes);
_logger.BizSeparator(BizChannel.UserSync);
}
}
@ -356,15 +414,14 @@ internal sealed partial class HikvisionAttendanceManager
return targets;
}
private List<InitialSyncEmployee> LoadEmployeesForInitialDepartmentSync(
private InitialSyncSelection LoadEmployeesForInitialDepartmentSync(
IReadOnlyList<string> departmentIds,
IReadOnlyList<string> employeeIdAllowList,
int locationSiteId,
out string error)
{
error = "";
var result = new List<InitialSyncEmployee>();
if (departmentIds == null || departmentIds.Count == 0)
return result;
var result = new InitialSyncSelection { LocationSiteId = locationSiteId };
if (!_config.EnableDbIntegration || _dbConnectionFactory == null)
{
error = "DB integration not available for initial department sync.";
@ -374,55 +431,132 @@ internal sealed partial class HikvisionAttendanceManager
if (!_dbConnectionFactory.TryBuildConnectionString(out var cs, out error))
return result;
var hasAllowList = employeeIdAllowList != null && employeeIdAllowList.Count > 0;
var hasDepartmentFilter = departmentIds != null && departmentIds.Count > 0;
var hasAllowList = hasDepartmentFilter &&
employeeIdAllowList != null &&
employeeIdAllowList.Count > 0;
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
var sql = new StringBuilder("SELECT id, serial_number FROM employee WHERE department_id IN (");
for (int i = 0; i < departmentIds.Count; i++)
{
if (i > 0) sql.Append(',');
sql.Append("@d").Append(i);
}
sql.Append(')');
if (hasDepartmentFilter)
{
var departmentSql = new StringBuilder(
"SELECT id, is_active FROM department WHERE id IN (");
AppendSqlParameters(departmentSql, "@d", departmentIds.Count);
departmentSql.Append(')');
using var departmentCmd = new MySqlCommand(departmentSql.ToString(), conn);
AddSqlParameters(departmentCmd, "@d", departmentIds);
var foundDepartmentIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var departmentReader = departmentCmd.ExecuteReader())
{
while (departmentReader.Read())
{
var departmentId = departmentReader["id"]?.ToString()?.Trim() ?? "";
if (departmentId.Length == 0)
continue;
foundDepartmentIds.Add(departmentId);
if (IsDatabaseActive(departmentReader["is_active"]))
result.ActiveDepartmentIds.Add(departmentId);
else
result.InactiveDepartmentIds.Add(departmentId);
}
}
foreach (var requestedId in departmentIds)
{
if (!foundDepartmentIds.Contains(requestedId))
result.MissingDepartmentIds.Add(requestedId);
}
}
else
{
using var departmentCmd = new MySqlCommand(
"SELECT id FROM department WHERE is_active = 1", conn);
using (var departmentReader = departmentCmd.ExecuteReader())
{
while (departmentReader.Read())
{
var departmentId = departmentReader["id"]?.ToString()?.Trim() ?? "";
if (departmentId.Length > 0)
result.ActiveDepartmentIds.Add(departmentId);
}
}
using var inactiveDepartmentCmd = new MySqlCommand(
"SELECT COUNT(*) FROM department WHERE is_active IS NULL OR is_active <> 1", conn);
result.InactiveDepartmentCount = Convert.ToInt32(inactiveDepartmentCmd.ExecuteScalar() ?? 0);
}
if (result.ActiveDepartmentIds.Count == 0)
return result;
var employeeSql = new StringBuilder(
"SELECT e.id, e.serial_number, e.department_id " +
"FROM employee e " +
"INNER JOIN department d ON d.id = e.department_id " +
"WHERE d.is_active = 1 AND e.is_active = 1 " +
"AND e.location_site_id = @locationSiteId AND e.department_id IN (");
AppendSqlParameters(employeeSql, "@activeDept", result.ActiveDepartmentIds.Count);
employeeSql.Append(')');
if (hasAllowList)
{
sql.Append(" AND serial_number IN (");
for (int i = 0; i < employeeIdAllowList.Count; i++)
{
if (i > 0) sql.Append(',');
sql.Append("@e").Append(i);
}
sql.Append(')');
employeeSql.Append(" AND e.serial_number IN (");
AppendSqlParameters(employeeSql, "@e", employeeIdAllowList.Count);
employeeSql.Append(')');
}
using var cmd = new MySqlCommand(sql.ToString(), conn);
for (int i = 0; i < departmentIds.Count; i++)
cmd.Parameters.AddWithValue("@d" + i, departmentIds[i]);
using var cmd = new MySqlCommand(employeeSql.ToString(), conn);
cmd.Parameters.AddWithValue("@locationSiteId", locationSiteId);
AddSqlParameters(cmd, "@activeDept", result.ActiveDepartmentIds);
if (hasAllowList)
{
for (int i = 0; i < employeeIdAllowList.Count; i++)
cmd.Parameters.AddWithValue("@e" + i, employeeIdAllowList[i]);
}
AddSqlParameters(cmd, "@e", employeeIdAllowList);
using var rd = cmd.ExecuteReader();
var seenSerial = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var rd = cmd.ExecuteReader())
{
while (rd.Read())
{
var serial = rd["serial_number"]?.ToString()?.Trim() ?? "";
if (serial.Length == 0 || !seenSerial.Add(serial))
continue;
result.Add(new InitialSyncEmployee
result.Employees.Add(new InitialSyncEmployee
{
Id = rd["id"]?.ToString()?.Trim() ?? "",
SerialNumber = serial,
DepartmentId = rd["department_id"]?.ToString()?.Trim() ?? "",
Name = ""
});
}
}
var inactiveEmployeeSql = new StringBuilder(
"SELECT COUNT(*) " +
"FROM employee e " +
"INNER JOIN department d ON d.id = e.department_id " +
"WHERE d.is_active = 1 AND (e.is_active IS NULL OR e.is_active <> 1) " +
"AND e.location_site_id = @inactiveLocationSiteId AND e.department_id IN (");
AppendSqlParameters(inactiveEmployeeSql, "@inactiveDept", result.ActiveDepartmentIds.Count);
inactiveEmployeeSql.Append(')');
if (hasAllowList)
{
inactiveEmployeeSql.Append(" AND e.serial_number IN (");
AppendSqlParameters(inactiveEmployeeSql, "@inactiveEmp", employeeIdAllowList.Count);
inactiveEmployeeSql.Append(')');
}
using var inactiveEmployeeCmd = new MySqlCommand(inactiveEmployeeSql.ToString(), conn);
inactiveEmployeeCmd.Parameters.AddWithValue("@inactiveLocationSiteId", locationSiteId);
AddSqlParameters(inactiveEmployeeCmd, "@inactiveDept", result.ActiveDepartmentIds);
if (hasAllowList)
AddSqlParameters(inactiveEmployeeCmd, "@inactiveEmp", employeeIdAllowList);
result.SkippedInactiveEmployeeCount =
Convert.ToInt32(inactiveEmployeeCmd.ExecuteScalar() ?? 0);
}
catch (Exception ex)
{
error = ex.Message;
@ -431,6 +565,212 @@ internal sealed partial class HikvisionAttendanceManager
return result;
}
private void LogInitialDepartmentSelection(
IReadOnlyList<string> requestedDepartmentIds,
IReadOnlyList<string> employeeIdAllowList,
InitialSyncSelection selection)
{
var allDepartments = requestedDepartmentIds == null || requestedDepartmentIds.Count == 0;
var selectiveEmployees = !allDepartments &&
employeeIdAllowList != null &&
employeeIdAllowList.Count > 0;
var inactiveCount = allDepartments
? selection.InactiveDepartmentCount
: selection.InactiveDepartmentIds.Count;
_logger.Ops("INITIAL_SYNC",
"InitialSyncLocationSiteId=" + selection.LocationSiteId +
" departmentMode=" + (allDepartments ? "ALL_ACTIVE" : "SELECTED") +
" selectedDepartmentCount=" + (allDepartments
? selection.ActiveDepartmentIds.Count
: requestedDepartmentIds.Count) +
" activeDepartmentCount=" + selection.ActiveDepartmentIds.Count +
" inactiveDepartmentCount=" + inactiveCount +
" activeDepartments=" + FormatLogValues(selection.ActiveDepartmentIds) +
" inactiveDepartments=" + (allDepartments
? "(all inactive departments excluded)"
: FormatLogValues(selection.InactiveDepartmentIds)) +
" missingDepartments=" + FormatLogValues(selection.MissingDepartmentIds));
_logger.Ops("INITIAL_SYNC",
"employeeFilterMode=" + (selectiveEmployees ? "SELECTIVE" : "ALL") +
(selectiveEmployees
? " allowListCount=" + employeeIdAllowList.Count +
" allowList=" + FormatLogValues(employeeIdAllowList)
: "") +
" finalEmployeeCount=" + selection.Employees.Count +
" skippedInactiveEmployees=" + selection.SkippedInactiveEmployeeCount);
}
private void WriteDepartmentalSyncSummary(
HikvisionAttendanceWindowsService.DeviceConfig target,
InitialSyncSelection selection,
IReadOnlyList<string> employeeIdAllowList,
bool targetOnline,
IReadOnlyList<InitialSyncEmployeeOutcome> outcomes)
{
var lines = new List<string>
{
"DEPARTMENTAL SYNC SUMMARY",
"",
"Machine ID : " + (target.DeviceId ?? ""),
"Machine IP : " + (target.Ip ?? ""),
"InitialSyncLocationSiteId : " + selection.LocationSiteId,
"Employee Filter : " + (employeeIdAllowList.Count > 0 ? "SELECTIVE" : "ALL"),
"Active Departments : " + selection.ActiveDepartmentIds.Count,
"Inactive Departments Skipped : " +
(selection.InactiveDepartmentIds.Count + selection.InactiveDepartmentCount),
"Eligible Active Employees : " + selection.Employees.Count,
"Skipped Inactive Employees : " + selection.SkippedInactiveEmployeeCount,
""
};
foreach (var departmentId in selection.ActiveDepartmentIds
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
var eligibleCount = selection.Employees.Count(x =>
string.Equals(x.DepartmentId, departmentId, StringComparison.OrdinalIgnoreCase));
var departmentOutcomes = outcomes.Where(x =>
string.Equals(x.DepartmentId, departmentId, StringComparison.OrdinalIgnoreCase)).ToList();
var syncedCount = departmentOutcomes.Count(x => x.Synced);
var failedCount = Math.Max(0, eligibleCount - syncedCount);
string status;
string reason = "";
if (!targetOnline)
{
status = "NOT SYNCED";
reason = "Target machine is offline.";
}
else if (eligibleCount == 0)
{
status = "NOT SYNCED";
reason = employeeIdAllowList.Count > 0
? "No selected active employees belong to this active department."
: "Department has no active employees.";
}
else if (syncedCount == eligibleCount)
{
status = "SYNCED";
}
else if (syncedCount > 0)
{
status = "PARTIALLY SYNCED";
}
else
{
status = "NOT SYNCED";
}
lines.Add("Department " + departmentId + " : " + status);
lines.Add("Eligible Users : " + eligibleCount);
lines.Add("Synced Users : " + syncedCount);
lines.Add("Failed Users : " + failedCount);
if (failedCount > 0)
lines.Add("Failed Employee Numbers : " +
string.Join(",", targetOnline
? departmentOutcomes.Where(x => !x.Synced).Select(x => x.EmployeeNo)
: selection.Employees
.Where(x => string.Equals(
x.DepartmentId,
departmentId,
StringComparison.OrdinalIgnoreCase))
.Select(x => x.SerialNumber)));
if (!string.IsNullOrWhiteSpace(reason))
lines.Add("Reason : " + reason);
foreach (var failureGroup in departmentOutcomes
.Where(x => !x.Synced)
.GroupBy(x => DescribeInitialSyncFailure(x.Reason))
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
{
lines.Add("Failure Reason : " + failureGroup.Key +
" (" + failureGroup.Count() + " user(s))");
}
lines.Add("");
}
foreach (var departmentId in selection.InactiveDepartmentIds
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
lines.Add("Department " + departmentId + " : NOT SYNCED");
lines.Add("Eligible Users : 0");
lines.Add("Synced Users : 0");
lines.Add("Failed Users : 0");
lines.Add("Reason : Department is inactive.");
lines.Add("");
}
foreach (var departmentId in selection.MissingDepartmentIds
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
lines.Add("Department " + departmentId + " : NOT SYNCED");
lines.Add("Eligible Users : 0");
lines.Add("Synced Users : 0");
lines.Add("Failed Users : 0");
lines.Add("Reason : Department was not found.");
lines.Add("");
}
if (selection.ActiveDepartmentIds.Count == 0 &&
selection.InactiveDepartmentIds.Count == 0 &&
selection.MissingDepartmentIds.Count == 0)
{
lines.Add("No active departments were available for synchronization.");
lines.Add("");
}
var totalSynced = outcomes.Count(x => x.Synced);
lines.Add("TOTAL SYNCED USERS : " + totalSynced);
lines.Add("TOTAL FAILED USERS : " + Math.Max(0, selection.Employees.Count - totalSynced));
lines.Add("");
_logger.Biz(BizChannel.DepartmentalSync, lines.ToArray());
_logger.BizSeparator(BizChannel.DepartmentalSync);
}
private static void AppendSqlParameters(StringBuilder sql, string prefix, int count)
{
for (int i = 0; i < count; i++)
{
if (i > 0)
sql.Append(',');
sql.Append(prefix).Append(i);
}
}
private static void AddSqlParameters(
MySqlCommand command,
string prefix,
IReadOnlyList<string> values)
{
for (int i = 0; i < values.Count; i++)
command.Parameters.AddWithValue(prefix + i, values[i]);
}
private static bool IsDatabaseActive(object value)
{
if (value == null || value == DBNull.Value)
return false;
if (value is bool boolValue)
return boolValue;
return string.Equals(
Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)?.Trim(),
"1",
StringComparison.Ordinal);
}
private static string FormatLogValues(IEnumerable<string> values)
{
var items = (values ?? Enumerable.Empty<string>())
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
return items.Count == 0 ? "(none)" : string.Join(",", items);
}
private static bool TryDownloadEmployeePortalPhoto(string photoUrl, out byte[] bytes, out string error)
{
bytes = Array.Empty<byte>();
@ -489,6 +829,26 @@ internal sealed partial class HikvisionAttendanceManager
{
public string Id { get; set; } = "";
public string SerialNumber { get; set; } = "";
public string DepartmentId { get; set; } = "";
public string Name { get; set; } = "";
}
private sealed class InitialSyncEmployeeOutcome
{
public string DepartmentId { get; set; } = "";
public string EmployeeNo { get; set; } = "";
public bool Synced { get; set; }
public string Reason { get; set; } = "";
}
private sealed class InitialSyncSelection
{
public int LocationSiteId { get; set; }
public List<InitialSyncEmployee> Employees { get; } = new List<InitialSyncEmployee>();
public List<string> ActiveDepartmentIds { get; } = new List<string>();
public List<string> InactiveDepartmentIds { get; } = new List<string>();
public List<string> MissingDepartmentIds { get; } = new List<string>();
public int InactiveDepartmentCount { get; set; }
public int SkippedInactiveEmployeeCount { get; set; }
}
}

View File

@ -120,6 +120,13 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember]
public List<string> InitialSyncEmployeeIds { get; set; } = new List<string>();
/// <summary>
/// Required HRMS employee.location_site_id filter for Initial Department Sync.
/// Null or a non-positive value prevents the initial sync from running.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int? InitialSyncLocationSiteId { get; set; }
/// <summary>When true, initial sync downloads JPEG photos from <see cref="EmployeePhotoBaseUrl"/>.</summary>
[DataMember]
public bool EnableEmployeePhotoSource { get; set; }
@ -582,6 +589,8 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
cfg.InitialSyncDepartmentIds = new List<string>(overlay.InitialSyncDepartmentIds);
if (overlay.InitialSyncEmployeeIds != null)
cfg.InitialSyncEmployeeIds = new List<string>(overlay.InitialSyncEmployeeIds);
if (overlay.InitialSyncLocationSiteId.HasValue)
cfg.InitialSyncLocationSiteId = overlay.InitialSyncLocationSiteId;
cfg.EnableEmployeePhotoSource = overlay.EnableEmployeePhotoSource;
if (overlay.EmployeePhotoBaseUrl != null)
cfg.EmployeePhotoBaseUrl = overlay.EmployeePhotoBaseUrl;
@ -684,6 +693,9 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember]
public List<string> InitialSyncEmployeeIds { get; set; } = new List<string>();
[DataMember(EmitDefaultValue = false)]
public int? InitialSyncLocationSiteId { get; set; }
[DataMember]
public bool EnableEmployeePhotoSource { get; set; }
@ -773,6 +785,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
private readonly string _attendanceBizDirectory;
private readonly string _templateBizDirectory;
private readonly string _userSyncBizDirectory;
private readonly string _departmentalSyncBizDirectory;
private readonly string _unreachableBizDirectory;
private readonly object _sync = new();
public readonly BizSessionTotals Totals = new BizSessionTotals();
@ -786,11 +799,13 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
_attendanceBizDirectory = Path.Combine(logsRoot, "attendance_logs");
_templateBizDirectory = Path.Combine(logsRoot, "template_fetching_logs");
_userSyncBizDirectory = Path.Combine(logsRoot, "user_sync_logs");
_departmentalSyncBizDirectory = Path.Combine(logsRoot, "departmental_sync_logs");
_unreachableBizDirectory = Path.Combine(_internalLogsDirectory, "unreachable_devices");
Directory.CreateDirectory(_internalLogsDirectory);
Directory.CreateDirectory(_attendanceBizDirectory);
Directory.CreateDirectory(_templateBizDirectory);
Directory.CreateDirectory(_userSyncBizDirectory);
Directory.CreateDirectory(_departmentalSyncBizDirectory);
Directory.CreateDirectory(_unreachableBizDirectory);
}
@ -858,6 +873,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
AppendBiz(BizChannel.Attendance, block);
AppendBiz(BizChannel.UserSync, block);
AppendBiz(BizChannel.Template, block);
AppendBiz(BizChannel.DepartmentalSync, block);
AppendBiz(BizChannel.Unreachable, block);
}
@ -873,6 +889,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
AppendBiz(BizChannel.Attendance, stopped);
AppendBiz(BizChannel.UserSync, stopped);
AppendBiz(BizChannel.Template, stopped);
AppendBiz(BizChannel.DepartmentalSync, stopped);
AppendBiz(BizChannel.Unreachable, stopped);
}
@ -905,6 +922,9 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
case BizChannel.Template:
path = Path.Combine(_templateBizDirectory, "template_fetching_logs_" + DateStamp() + ".txt");
break;
case BizChannel.DepartmentalSync:
path = Path.Combine(_departmentalSyncBizDirectory, "departmental_sync_logs_" + DateStamp() + ".txt");
break;
case BizChannel.Unreachable:
path = Path.Combine(_unreachableBizDirectory, "unreachable_devices_" + DateStamp() + ".txt");
break;