feat(registratura): all 4 NAS drives (A/O/P/T) + hostnameIP fallback

- nas-paths.ts: A:\=Arhiva, O:\=Organizare, P:\=Proiecte, T:\=Transfer
- toUncPathByIp() / toFileUrlByIp() helpers for DNS failure fallback
- shareLabelFor() returns human-readable share name for badges
- UI: 'IP' fallback link on hover, badge shows share label
- Validation hints updated to show all 4 drive letters
- Docs updated: CLAUDE.md, ROADMAP.md, SESSION-LOG.md
This commit is contained in:
AI Assistant
2026-02-28 17:23:38 +02:00
parent 4f00cb2de8
commit f4b1d4b8dd
5 changed files with 144 additions and 42 deletions
+52 -12
View File
@@ -4,6 +4,10 @@
* The office NAS (\\newamun / 10.10.10.10) exposes several shares.
* Windows maps these to drive letters. This config lets us normalise
* user-pasted paths to UNC and back, and build clickable `file:///` links.
*
* Fallback strategy: hostname → IP.
* If DNS fails to resolve `newamun`, every helper has an `...ByIp()` variant
* that substitutes the hostname with the raw IP address.
*/
export interface NasDriveMapping {
@@ -15,17 +19,18 @@ export interface NasDriveMapping {
label: string;
}
/** All known drive-letter → UNC mappings */
export const NAS_DRIVE_MAPPINGS: NasDriveMapping[] = [
{ drive: "P", unc: "\\\\newamun\\Proiecte", label: "Proiecte" },
// Add more as needed:
// { drive: "S", unc: "\\\\newamun\\Shared", label: "Shared" },
];
/** NAS hostname / IP — used for display only */
/** NAS hostname / IP */
export const NAS_HOST = "newamun";
export const NAS_IP = "10.10.10.10";
/** All known drive-letter → UNC mappings */
export const NAS_DRIVE_MAPPINGS: NasDriveMapping[] = [
{ drive: "A", unc: `\\\\${NAS_HOST}\\Arhiva`, label: "Arhiva" },
{ drive: "O", unc: `\\\\${NAS_HOST}\\Organizare`, label: "Organizare" },
{ drive: "P", unc: `\\\\${NAS_HOST}\\Proiecte`, label: "Proiecte" },
{ drive: "T", unc: `\\\\${NAS_HOST}\\Transfer`, label: "Transfer" },
];
// ── helpers ──
/**
@@ -34,7 +39,7 @@ export const NAS_IP = "10.10.10.10";
*/
export function isNetworkPath(input: string): boolean {
const trimmed = input.trim();
// UNC path
// UNC path (hostname or IP)
if (trimmed.startsWith("\\\\")) return true;
// Mapped drive letter that we recognise
const match = trimmed.match(/^([A-Z]):\\/i);
@@ -46,7 +51,7 @@ export function isNetworkPath(input: string): boolean {
}
/**
* Normalise to UNC path (replace drive letter with \\newamun\Share).
* Normalise to UNC path using hostname (replace drive letter with \\newamun\Share).
* If already UNC or unrecognised → returns the original trimmed.
*/
export function toUncPath(input: string): string {
@@ -62,13 +67,36 @@ export function toUncPath(input: string): string {
return trimmed;
}
/**
* Swap hostname → IP in a UNC path.
* `\\newamun\Proiecte\file.pdf` → `\\10.10.10.10\Proiecte\file.pdf`
* Already-IP or unrecognised paths are returned unchanged.
*/
export function toUncPathByIp(input: string): string {
const unc = toUncPath(input);
const hostPrefix = `\\\\${NAS_HOST}\\`;
if (unc.toLowerCase().startsWith(hostPrefix.toLowerCase())) {
return `\\\\${NAS_IP}\\${unc.slice(hostPrefix.length)}`;
}
return unc;
}
/**
* Build a clickable `file:///` URL from a UNC or drive-letter path.
* Windows Explorer opens this natively.
* Uses hostname by default.
*/
export function toFileUrl(input: string): string {
const unc = toUncPath(input);
// file:///\\server\share\path → file://///server/share/path
const slashed = unc.replace(/\\/g, "/");
return `file:///${slashed}`;
}
/**
* Build a clickable `file:///` URL using the IP fallback.
* Use when DNS fails to resolve hostname.
*/
export function toFileUrlByIp(input: string): string {
const unc = toUncPathByIp(input);
const slashed = unc.replace(/\\/g, "/");
return `file:///${slashed}`;
}
@@ -96,3 +124,15 @@ export function shortDisplayPath(input: string): string {
const last2 = parts.slice(-2).join("\\");
return `${share}\\\\${last2}`;
}
/**
* Return the human-readable label of the share a path belongs to.
* e.g. `P:\095\file.pdf` → `Proiecte`
*/
export function shareLabelFor(input: string): string | undefined {
const unc = toUncPath(input).toLowerCase();
for (const m of NAS_DRIVE_MAPPINGS) {
if (unc.startsWith(m.unc.toLowerCase())) return m.label;
}
return undefined;
}
@@ -34,8 +34,10 @@ import {
isNetworkPath,
toUncPath,
toFileUrl,
toFileUrlByIp,
pathFileName,
shortDisplayPath,
shareLabelFor,
} from "@/config/nas-paths";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
@@ -1223,7 +1225,7 @@ export function RegistryEntryForm({
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/30 p-3 space-y-2">
<Label className="text-xs flex items-center gap-1">
<HardDrive className="h-3 w-3" />
Cale fișier pe NAS (\\newamun sau P:\\...)
Cale fișier pe NAS (A:\ O:\ P:\ T:\ sau \\newamun\\...)
</Label>
<div className="flex gap-2">
<Input
@@ -1254,7 +1256,8 @@ export function RegistryEntryForm({
)}
{networkPathInput.trim() && !isNetworkPath(networkPathInput) && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Calea nu pare a fi pe NAS. Introdu o cale de tip P:\\... sau \\newamun\\...
Calea nu pare a fi pe NAS. Introdu o cale de tip A:\ O:\ P:\ T:\
sau \\newamun\\...
</p>
)}
</div>
@@ -1275,7 +1278,6 @@ export function RegistryEntryForm({
className="flex-1 min-w-0 flex items-center gap-1 text-blue-700 dark:text-blue-300 hover:underline cursor-pointer"
title={`Deschide în Explorer: ${att.networkPath}`}
onClick={(e) => {
// file:/// links might be blocked by browser; copy path as fallback
e.preventDefault();
window.open(toFileUrl(att.networkPath!), "_blank");
}}
@@ -1285,11 +1287,23 @@ export function RegistryEntryForm({
{shortDisplayPath(att.networkPath)}
</span>
</a>
{/* IP fallback link — visible on hover when DNS fails */}
<a
href={toFileUrlByIp(att.networkPath)}
className="opacity-0 group-hover:opacity-100 transition-opacity text-[10px] text-blue-400 hover:text-blue-600 dark:hover:text-blue-200 shrink-0"
title={`Fallback IP: ${toFileUrlByIp(att.networkPath!)}`}
onClick={(e) => {
e.preventDefault();
window.open(toFileUrlByIp(att.networkPath!), "_blank");
}}
>
IP
</a>
<Badge
variant="outline"
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 shrink-0"
>
NAS
{shareLabelFor(att.networkPath) ?? "NAS"}
</Badge>
<button
type="button"