diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 8567c28cf..3ea1d799a 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -15,18 +15,20 @@ info: license: name: "GPLv3" basePath: "/api/v1" -tags: -- name: "openDCIM" - description: "Everything about your Data Center Inventory." - externalDocs: - description: "Find out more" - url: "http://wiki.opendcim.org" +tags: +- name: "openDCIM" + description: "Everything about your Data Center Inventory." + externalDocs: + description: "Find out more" + url: "http://wiki.opendcim.org" +- name: "HDD" + description: "HDD inventory endpoints." paths: - /audit: - get: - summary: Audit history for the requested device - tags: - - "AuditLogs" + /audit: + get: + summary: Audit history for the requested device + tags: + - "AuditLogs" description: '' produces: - "application/json" @@ -86,12 +88,166 @@ paths: type: "array" items: $ref: "#/definitions/Audit" - security: - - api_key: [] - - user_id: [] - /cabinet: - get: - summary: Information about one or more cabinets + security: + - api_key: [] + - user_id: [] + /hdd: + get: + summary: "List HDD entries" + tags: + - "HDD" + description: "Requires ManageHDD or SiteAdmin." + produces: + - "application/json" + operationId: "GetHdd" + parameters: + - name: DeviceID + in: query + required: false + type: integer + format: "DeviceID filter" + - name: HDDID + in: query + required: false + type: string + description: "Single HDDID or comma separated list." + - name: Status + in: query + required: false + type: string + description: "Status filter (single value or comma separated list)." + - name: SerialNo + in: query + required: false + type: string + description: "Partial serial number match." + responses: + "401": + description: "Access denied." + "200": + description: "successful operation" + schema: + type: "array" + items: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + put: + summary: "Create HDD entry" + tags: + - "HDD" + consumes: + - "application/json" + - "application/x-www-form-urlencoded" + produces: + - "application/json" + description: "Creates an HDD entry. DeviceID and SerialNo are required; ProofFile is read-only." + operationId: "PutHdd" + parameters: + - in: body + name: body + description: "New HDD payload." + required: true + schema: + $ref: "#/definitions/HDD" + responses: + "401": + description: "Access denied." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + /hdd/{hddid}: + get: + summary: "Get HDD entry" + tags: + - "HDD" + produces: + - "application/json" + operationId: "GetHddById" + parameters: + - name: hddid + in: path + required: true + type: integer + format: "HDDID" + responses: + "401": + description: "Access denied." + "404": + description: "HDD not found." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + post: + summary: "Update HDD entry" + tags: + - "HDD" + consumes: + - "application/json" + - "application/x-www-form-urlencoded" + produces: + - "application/json" + description: "Updates SerialNo, Size, TypeMedia, Status, or DeviceID for the specified HDD. ProofFile cannot be modified via the API." + operationId: "PostHdd" + parameters: + - name: hddid + in: path + required: true + type: integer + - in: body + name: body + required: true + schema: + $ref: "#/definitions/HDD" + responses: + "401": + description: "Access denied." + "404": + description: "HDD not found." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDD" + security: + - api_key: [] + - user_id: [] + /hdd/{hddid}/proof: + get: + summary: "Retrieve destruction proof metadata" + tags: + - "HDD" + produces: + - "application/json" + operationId: "GetHddProof" + parameters: + - name: hddid + in: path + required: true + type: integer + responses: + "401": + description: "Access denied." + "404": + description: "Proof not available." + "200": + description: "successful operation" + schema: + $ref: "#/definitions/HDDProof" + security: + - api_key: [] + - user_id: [] + /cabinet: + get: + summary: Information about one or more cabinets tags: - "Cabinet" description: 'If no parameters are specified, all record information that you are authorized to view is returned.' @@ -1412,12 +1568,18 @@ definitions: PSCount: type: "integer" format: "Number of power inputs" - NumPorts: - type: "integer" - format: "Number of data ports" - Notes: - type: "string" - format: "Freeform text" + NumPorts: + type: "integer" + format: "Number of data ports" + EnableHDDFeature: + type: "integer" + format: "1 when the HDD management feature is enabled" + HDDCount: + type: "integer" + format: "Maximum HDD slots supported by this template" + Notes: + type: "string" + format: "Freeform text" FrontPictureFile: type: "string" format: "Relative path to the image file for the front" @@ -1605,12 +1767,15 @@ definitions: RackAdmin: type: boolean format: "User has rights to complete rack requests" - BulkOperations: - type: boolean - format: "User has rights to perform Bulk Operations" - SiteAdmin: - type: boolean - format: "User is a site administrator" + BulkOperations: + type: boolean + format: "User has rights to perform Bulk Operations" + ManageHDD: + type: boolean + format: "User has rights to manage HDD inventory" + SiteAdmin: + type: boolean + format: "User is a site administrator" APIKey: type: string format: "API Token" @@ -1659,24 +1824,80 @@ definitions: Notes: type: "string" format: "Freeform text" - SensorReading: - type: "object" - properties: - DeviceID: - type: "integer" - format: "keyValue" + SensorReading: + type: "object" + properties: + DeviceID: + type: "integer" + format: "keyValue" Temperature: type: "number" format: "Temperature in localized units" Humidity: type: "integer" format: "Humidity" - LastRead: - type: "string" - format: "Timestamp of last reading" -securityDefinitions: - api_key: - type: "apiKey" + LastRead: + type: "string" + format: "Timestamp of last reading" + HDD: + type: "object" + properties: + HDDID: + type: "integer" + format: "keyValue" + DeviceID: + type: "integer" + format: "keyValue" + SerialNo: + type: "string" + format: "Serial number" + Status: + type: "string" + enum: + - "On" + - "Off" + - "Pending_destruction" + - "Destroyed" + - "Spare" + TypeMedia: + type: "string" + format: "Media type" + Size: + type: "integer" + format: "Capacity in GB" + DateAdd: + type: "string" + format: "Timestamp" + DateWithdrawn: + type: "string" + format: "Timestamp" + DateDestroyed: + type: "string" + format: "Timestamp" + ProofFile: + type: "string" + format: "Stored proof reference" + HDDProof: + type: "object" + properties: + HDDID: + type: "integer" + SerialNo: + type: "string" + ProofFile: + type: "string" + public_url: + type: "string" + format: "URL" + filesystem_path: + type: "string" + format: "Filesystem path when accessible" + file_exists: + type: "boolean" + format: "Indicates if the file is present on disk" +securityDefinitions: + api_key: + type: "apiKey" name: "APIKey" in: "header" user_id: diff --git a/api/v1/getRoutes.php b/api/v1/getRoutes.php index 66045d287..55519487f 100644 --- a/api/v1/getRoutes.php +++ b/api/v1/getRoutes.php @@ -100,6 +100,218 @@ return $response->withJson($r, $r['errorcode']); }); +// +// URL: /api/v1/hdd +// Method: GET +// Params: Optional DeviceID, Status (single value, comma separated list, or array), SerialNo (partial match), HDDID (single value or list) +// Returns: List of HDD objects matching the filter +// +$app->get('/hdd', function(Request $request, Response $response) use ($person) { + global $dbh; + + $r = array(); + + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $filters = $request->getQueryParams() ?: $request->getParsedBody(); + $sql = "SELECT * FROM fac_HDD WHERE 1=1"; + $params = array(); + + if (isset($filters['DeviceID']) && intval($filters['DeviceID']) > 0) { + $sql .= " AND DeviceID = :DeviceID"; + $params[':DeviceID'] = intval($filters['DeviceID']); + } + + if (isset($filters['HDDID'])) { + $ids = $filters['HDDID']; + if (!is_array($ids)) { + $ids = array_map('trim', explode(',', $ids)); + } + $ids = array_values(array_filter(array_map('intval', $ids), function($v){ return $v > 0; })); + if (!empty($ids)) { + $placeholders = array(); + foreach ($ids as $idx => $id) { + $ph = ":hddid{$idx}"; + $placeholders[] = $ph; + $params[$ph] = $id; + } + $sql .= " AND HDDID IN (" . implode(',', $placeholders) . ")"; + } + } + + if (isset($filters['SerialNo']) && strlen(trim($filters['SerialNo'])) > 0) { + $sql .= " AND SerialNo LIKE :SerialNo"; + $params[':SerialNo'] = '%' . trim($filters['SerialNo']) . '%'; + } + + if (isset($filters['Status'])) { + $rawStatus = $filters['Status']; + if (!is_array($rawStatus)) { + $rawStatus = array_map('trim', explode(',', $rawStatus)); + } + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $statusPlaceholders = array(); + $statusIndex = 0; + foreach ($rawStatus as $statusValue) { + if ($statusValue === '') { + continue; + } + if (!in_array($statusValue, $allowedStatus, true)) { + continue; + } + $ph = ":status{$statusIndex}"; + $statusIndex++; + $statusPlaceholders[] = $ph; + $params[$ph] = $statusValue; + } + if (!empty($statusPlaceholders)) { + $sql .= " AND Status IN (" . implode(',', $statusPlaceholders) . ")"; + } + } + + $sql .= " ORDER BY DeviceID, HDDID"; + + $stmt = $dbh->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $hddList = array(); + foreach ($rows as $row) { + $hddList[] = HDD::RowToObject($row); + } + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hddList; + $r['filters'] = $filters; + + return $response->withJson($r, $r['errorcode']); +}); + +// +// URL: /api/v1/hdd/:hddid +// Method: GET +// Params: hddid (path) +// Returns: HDD detail for the requested identifier +// +$app->get('/hdd/{hddid}', function(Request $request, Response $response, $args) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + } else { + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hdd; + } + + return $response->withJson($r, $r['errorcode']); +}); + +// +// URL: /api/v1/hdd/:hddid/proof +// Method: GET +// Params: hddid (path) +// Returns: Metadata about destruction proof for the requested HDD +// +$app->get('/hdd/{hddid}/proof', function(Request $request, Response $response, $args) use ($person, $config) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + return $response->withJson($r, $r['errorcode']); + } + + $proofValue = trim((string)$hdd->ProofFile); + + if ($proofValue === '') { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("No destruction proof available for this HDD"); + return $response->withJson($r, $r['errorcode']); + } + + $pathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $cleanBase = rtrim($pathSetting, '/\\'); + $publicUrl = $proofValue; + + $isAbsoluteUrl = (preg_match('#^(?:[a-z]+:)?//#i', $proofValue) === 1); + $isAbsolutePath = (!$isAbsoluteUrl && (strpos($proofValue, '/') === 0 || preg_match('#^[A-Za-z]:\\\\#', $proofValue) === 1)); + + if (!$isAbsoluteUrl && !$isAbsolutePath) { + if ($cleanBase !== '') { + $publicUrl = $cleanBase . '/' . ltrim($proofValue, '/\\'); + } else { + $publicUrl = ltrim($proofValue, '/\\'); + } + } + + $projectRoot = realpath(__DIR__ . "/../.."); + if ($projectRoot === false) { + $projectRoot = dirname(dirname(__DIR__)); + } + + $filesystemPath = ''; + $fileExists = false; + + if ($isAbsoluteUrl) { + $filesystemPath = ''; + } elseif ($isAbsolutePath) { + $filesystemPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $proofValue); + $fileExists = is_file($filesystemPath); + } else { + $relativeBase = trim($cleanBase, '/\\'); + $relativeStored = ltrim($proofValue, '/\\'); + + if ($relativeBase !== '' && stripos($relativeStored, $relativeBase) === 0) { + $relativePath = $relativeStored; + } else { + $relativePath = ($relativeBase !== '' ? $relativeBase . '/' : '') . $relativeStored; + } + + $filesystemPath = rtrim($projectRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath); + $fileExists = is_file($filesystemPath); + } + + $r['error'] = false; + $r['errorcode'] = 200; + $r['proof'] = array( + 'HDDID' => $hddid, + 'SerialNo' => $hdd->SerialNo, + 'ProofFile' => $proofValue, + 'public_url' => $publicUrl, + 'filesystem_path' => $filesystemPath, + 'file_exists' => $fileExists + ); + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/department // Method: GET diff --git a/api/v1/index.php b/api/v1/index.php index 2e20e07e4..26077c5f7 100644 --- a/api/v1/index.php +++ b/api/v1/index.php @@ -9,7 +9,8 @@ $loginPage = true; } - require_once( "../../facilities.inc.php" ); + require_once( "../../facilities.inc.php" ); + require_once( __DIR__ . "/../../classes/hdd.class.php" ); use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; diff --git a/api/v1/postRoutes.php b/api/v1/postRoutes.php index 333483aa1..98172882d 100644 --- a/api/v1/postRoutes.php +++ b/api/v1/postRoutes.php @@ -128,6 +128,103 @@ return $response->withJson($r, $r['errorcode']); }); +// +// URL: /api/v1/hdd/:hddid +// Method: POST +// Params: hddid (path), optional SerialNo, Status, TypeMedia, Size, DeviceID +// Returns: Updated HDD record +// +$app->post('/hdd/{hddid}', function(Request $request, Response $response, $args) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $hddid = intval($args['hddid']); + $hdd = HDD::GetHDDByID($hddid); + + if (!$hdd) { + $r['error'] = true; + $r['errorcode'] = 404; + $r['message'] = __("HDD not found"); + return $response->withJson($r, $r['errorcode']); + } + + $payload = $request->getQueryParams() ?: $request->getParsedBody(); + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $fieldsUpdated = array(); + + if (isset($payload['SerialNo'])) { + $hdd->SerialNo = $payload['SerialNo']; + $fieldsUpdated['SerialNo'] = $payload['SerialNo']; + } + + if (isset($payload['Status']) && in_array($payload['Status'], $allowedStatus, true)) { + $hdd->Status = $payload['Status']; + $fieldsUpdated['Status'] = $payload['Status']; + } + + if (isset($payload['TypeMedia'])) { + $hdd->TypeMedia = $payload['TypeMedia']; + $fieldsUpdated['TypeMedia'] = $payload['TypeMedia']; + } + + if (isset($payload['Size'])) { + $hdd->Size = intval($payload['Size']); + $fieldsUpdated['Size'] = intval($payload['Size']); + } + + $reassigned = false; + if (isset($payload['DeviceID'])) { + $targetDevice = intval($payload['DeviceID']); + if ($targetDevice > 0 && $targetDevice != intval($hdd->DeviceID)) { + if (HDD::GetRemainingSlotCount($targetDevice) <= 0) { + $r['error'] = true; + $r['errorcode'] = 409; + $r['message'] = __("slot hdd is full"); + return $response->withJson($r, $r['errorcode']); + } + if (!HDD::ReassignToDevice($hddid, $targetDevice)) { + $r['error'] = true; + $r['errorcode'] = 500; + $r['message'] = __("Unable to reassign HDD to the requested device"); + return $response->withJson($r, $r['errorcode']); + } + $hdd->DeviceID = $targetDevice; + $fieldsUpdated['DeviceID'] = $targetDevice; + $reassigned = true; + } + } + + $updated = false; + if (!empty($fieldsUpdated)) { + if (!$hdd->Update()) { + $r['error'] = true; + $r['errorcode'] = 500; + $r['message'] = __("HDD update failed"); + return $response->withJson($r, $r['errorcode']); + } + $updated = true; + } + + if (!$updated && !$reassigned) { + $r['error'] = true; + $r['errorcode'] = 400; + $r['message'] = __("No valid parameters were supplied for update"); + return $response->withJson($r, $r['errorcode']); + } + + HDD::RecordGenericLog($hdd->DeviceID, $person->UserID, 'HDD_API_UPDATE', json_encode($fieldsUpdated, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = HDD::GetHDDByID($hddid); + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/powerport/:deviceid // Method: POST @@ -395,13 +492,23 @@ $dt->$prop=$val; } } + $hddPayload = array(); + foreach (array('EnableHDDFeature','HDDCount') as $field) { + if (isset($vars[$field])) { + $hddPayload[$field] = $vars[$field]; + } + } if(!$dt->UpdateTemplate()){ $r['error']=true; $r['errorcode']=400; $r['message']=__("Device template update failed"); }else{ + if (!empty($hddPayload)) { + $dt->UpdateTemplateHDD($hddPayload); + } $r['error']=false; $r['errorcode']=200; + $r['devicetemplate']=$dt; } } } diff --git a/api/v1/putRoutes.php b/api/v1/putRoutes.php index 5a772d033..572be0882 100644 --- a/api/v1/putRoutes.php +++ b/api/v1/putRoutes.php @@ -130,6 +130,65 @@ }); +// +// URL: /api/v1/hdd +// Method: PUT +// Params: DeviceID (required), SerialNo (required), optional Status, TypeMedia, Size +// Returns: record as created +// +$app->put('/hdd', function(Request $request, Response $response) use ($person) { + if (!($person->ManageHDD || $person->SiteAdmin)) { + $r['error'] = true; + $r['errorcode'] = 401; + $r['message'] = __("Access Denied"); + return $response->withJson($r, $r['errorcode']); + } + + $vars = $request->getQueryParams() ?: $request->getParsedBody(); + $deviceId = isset($vars['DeviceID']) ? intval($vars['DeviceID']) : 0; + $serial = isset($vars['SerialNo']) ? trim($vars['SerialNo']) : ''; + + if ($deviceId <= 0 || $serial === '') { + $r['error'] = true; + $r['errorcode'] = 400; + $r['message'] = __("DeviceID and SerialNo are required"); + return $response->withJson($r, $r['errorcode']); + } + + if (HDD::GetRemainingSlotCount($deviceId) <= 0) { + $r['error'] = true; + $r['errorcode'] = 409; + $r['message'] = __("slot hdd is full"); + return $response->withJson($r, $r['errorcode']); + } + + $allowedStatus = array('On','Off','Pending_destruction','Destroyed','Spare'); + $status = (isset($vars['Status']) && in_array($vars['Status'], $allowedStatus, true)) ? $vars['Status'] : 'On'; + + $hdd = new HDD(); + $hdd->DeviceID = $deviceId; + $hdd->SerialNo = $serial; + $hdd->Status = $status; + $hdd->TypeMedia = isset($vars['TypeMedia']) ? $vars['TypeMedia'] : 'HDD'; + $hdd->Size = isset($vars['Size']) ? intval($vars['Size']) : 0; + + $hdd->Create(); + + $details = array( + 'HDDID' => $hdd->HDDID, + 'DeviceID' => $deviceId, + 'SerialNo' => $serial, + 'Status' => $status + ); + HDD::RecordGenericLog($deviceId, $person->UserID, 'HDD_API_CREATE', json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $r['error'] = false; + $r['errorcode'] = 200; + $r['hdd'] = $hdd; + + return $response->withJson($r, $r['errorcode']); +}); + // // URL: /api/v1/cabinet // Method: PUT @@ -520,6 +579,15 @@ $r['errorcode']=400; $r['message']=__("Device template creation failed"); }else{ + $hddPayload = array(); + foreach (array('EnableHDDFeature','HDDCount') as $field) { + if (isset($vars[$field])) { + $hddPayload[$field] = $vars[$field]; + } + } + if (!empty($hddPayload)) { + $dt->UpdateTemplateHDD($hddPayload); + } // refresh the model in case we extended it elsewhere $d=new DeviceTemplate($dt->TemplateID); $d->GetTemplateByID(); diff --git a/classes/DeviceTemplate.class.php b/classes/DeviceTemplate.class.php index d4b62b29d..2b8f5961c 100644 --- a/classes/DeviceTemplate.class.php +++ b/classes/DeviceTemplate.class.php @@ -40,6 +40,9 @@ class DeviceTemplate { var $SNMPVersion; var $CustomValues; var $GlobalID; + public $EnableHDDFeature = 0; + public $HDDCount = 0; + public function __construct($dtid=false){ if($dtid){ @@ -100,6 +103,7 @@ static function RowToObject($row,$extendmodel=true){ $Template->GlobalID = $row["GlobalID"]; $Template->MakeDisplay(); $Template->GetCustomValues(); + $Template->LoadHDDConfig(); if($extendmodel){ // Extend our device model @@ -241,6 +245,8 @@ function UpdateTemplate(){ (class_exists('LogActions'))?LogActions::LogThis($this,$old):''; $this->MakeDisplay(); + $this->UpdateTemplateHDD(); // manageHDD + return true; } } @@ -251,6 +257,8 @@ function DeleteTemplate(){ // If we're removing the template clean up the children $this->DeleteSlots(); $this->DeletePorts(); + $this->DeleteTemplateHDD(); + $sql="DELETE FROM fac_DeviceTemplate WHERE TemplateID=$this->TemplateID;"; (class_exists('LogActions'))?LogActions::LogThis($this):''; @@ -748,6 +756,71 @@ static function getAvailableImages(){ } return $array; } -} + //feature manager for HDD + public function UpdateTemplateHDD(array $payload = null) { + global $dbh; + + $source = ($payload === null) ? $_POST : $payload; + $EnableHDDFeature = isset($source['EnableHDDFeature']) ? intval($source['EnableHDDFeature']) : 0; + $HDDCount = isset($source['HDDCount']) ? intval($source['HDDCount']) : 0; + + $check = $dbh->prepare("SELECT COUNT(*) FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"); + $check->execute([$this->TemplateID]); + + if ($check->fetchColumn() > 0) { + $sql = "UPDATE fac_DeviceTemplateHdd SET EnableHDDFeature = ?, HDDCount = ? WHERE TemplateID = ?"; + $dbh->prepare($sql)->execute([$EnableHDDFeature, $HDDCount, $this->TemplateID]); + } else { + $sql = "INSERT INTO fac_DeviceTemplateHdd (TemplateID, EnableHDDFeature, HDDCount) VALUES (?, ?, ?)"; + $dbh->prepare($sql)->execute([$this->TemplateID, $EnableHDDFeature, $HDDCount]); + } + $this->EnableHDDFeature = $EnableHDDFeature; // update data in the object + $this->HDDCount = $HDDCount; // update HDD count in the object + } + + public function LoadHDDConfig() { + global $dbh; + error_log("LoadHDDConfig called for TemplateID: " . $this->TemplateID); + $this->EnableHDDFeature = 0; + $this->HDDCount = 0; + + if ($this->TemplateID > 0) { + $sql = "SELECT EnableHDDFeature, HDDCount FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + $stmt->execute([$this->TemplateID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $this->EnableHDDFeature = $row['EnableHDDFeature']; + $this->HDDCount = $row['HDDCount']; + } + } + } + + public function DeleteTemplateHDD() { + global $dbh; + + if ($this->TemplateID > 0) { + $sql = "DELETE FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + return $stmt->execute([$this->TemplateID]); + } + return false; + } + + public function ExportTemplateHDD($asJSON = false) { + global $dbh; + + $sql = "SELECT EnableHDDFeature, HDDCount FROM fac_DeviceTemplateHdd WHERE TemplateID = ?"; + $stmt = $dbh->prepare($sql); + $stmt->execute([$this->TemplateID]); + + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return $asJSON ? json_encode($row) : $row; + } else { + $data = ['EnableHDDFeature' => 0, 'HDDCount' => 0]; + return $asJSON ? json_encode($data) : $data; + } + } + +} ?> diff --git a/classes/People.class.php b/classes/People.class.php index e78930594..d00c1cf66 100644 --- a/classes/People.class.php +++ b/classes/People.class.php @@ -31,27 +31,28 @@ class People { things greatly. */ - var $PersonID; - var $UserID; - var $LastName; - var $FirstName; - var $Phone1; - var $Phone2; - var $countryCode; - var $Email; - var $AdminOwnDevices; - var $ReadAccess; - var $WriteAccess; - var $DeleteAccess; - var $ContactAdmin; - var $RackRequest; - var $RackAdmin; - var $BulkOperations; - var $SiteAdmin; - var $APIKey; - var $Disabled; - var $LastActivity; - var $ExpirationDate; + public $PersonID; + public $UserID; + public $LastName; + public $FirstName; + public $Phone1; + public $Phone2; + public $countryCode; + public $Email; + public $AdminOwnDevices; + public $ReadAccess; + public $WriteAccess; + public $DeleteAccess; + public $ContactAdmin; + public $RackRequest; + public $RackAdmin; + public $BulkOperations; + public $SiteAdmin; + public $APIKey; + public $Disabled; + public $LastActivity; + public $ExpirationDate; + public $ManageHDD; function MakeSafe(){ $this->PersonID=intval($this->PersonID); @@ -71,6 +72,7 @@ function MakeSafe(){ $this->RackAdmin=intval($this->RackAdmin); $this->BulkOperations=intval($this->BulkOperations); $this->SiteAdmin=intval($this->SiteAdmin); + $this->ManageHDD=intval($this->ManageHDD); $this->Disabled=intval($this->Disabled); $this->ExpirationDate=sanitize($this->ExpirationDate); } @@ -95,6 +97,7 @@ function MakeDisplay(){ $this->RackAdmin=intval($this->RackAdmin); $this->BulkOperations=intval($this->BulkOperations); $this->SiteAdmin=intval($this->SiteAdmin); + $this->ManageHDD=intval($this->ManageHDD); $this->Disabled=intval($this->Disabled); } @@ -117,6 +120,7 @@ static function RowToObject($row){ $person->RackAdmin=$row["RackAdmin"]; $person->BulkOperations=$row["BulkOperations"]; $person->SiteAdmin=$row["SiteAdmin"]; + $person->ManageHDD=$row["ManageHDD"]; $person->APIKey=$row["APIKey"]; $person->Disabled=$row["Disabled"]; $person->LastActivity=$row["LastActivity"]; @@ -165,6 +169,7 @@ function revokeAll() { $this->ContactAdmin = false; $this->BulkOperations = false; $this->SiteAdmin = false; + $this->ManageHDD = false; } function canRead( $Owner ) { @@ -209,7 +214,7 @@ function CreatePerson() { AdminOwnDevices=$this->AdminOwnDevices, ReadAccess=$this->ReadAccess, WriteAccess=$this->WriteAccess, DeleteAccess=$this->DeleteAccess, ContactAdmin=$this->ContactAdmin, RackRequest=$this->RackRequest, - RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin, + RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin,ManageHDD=$this->ManageHDD, APIKey=\"$this->APIKey\", Disabled=$this->Disabled, ExpirationDate=\"$this->ExpirationDate\";"; if(!$this->query($sql)){ @@ -234,6 +239,7 @@ static function Current(){ $cperson->ReadAccess=true; $cperson->WriteAccess=true; $cperson->SiteAdmin=true; + $cperson->ManageHDD=true; $cperson->Disabled=false; }elseif(AUTHENTICATION=="Apache"){ if(!isset($_SERVER["REMOTE_USER"])){ @@ -452,7 +458,7 @@ function UpdatePerson() { AdminOwnDevices=$this->AdminOwnDevices, ReadAccess=$this->ReadAccess, WriteAccess=$this->WriteAccess, DeleteAccess=$this->DeleteAccess, ContactAdmin=$this->ContactAdmin, RackRequest=$this->RackRequest, - RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin, + RackAdmin=$this->RackAdmin, BulkOperations=$this->BulkOperations, SiteAdmin=$this->SiteAdmin,ManageHDD=$this->ManageHDD, APIKey=\"$this->APIKey\", ExpirationDate=\"$formattedDate\", Disabled=$this->Disabled WHERE PersonID=$this->PersonID;"; diff --git a/classes/hdd.class.php b/classes/hdd.class.php new file mode 100644 index 000000000..fcec6030c --- /dev/null +++ b/classes/hdd.class.php @@ -0,0 +1,578 @@ +logger = $logger ?? new NullLogger(); + } + + // Sanitize data before database operations + public function MakeSafe(): void { + //$this->HDDID = intval($this->HDDID); + $this->DeviceID = intval($this->DeviceID); + $this->SerialNo = sanitize($this->SerialNo); + $this->Status = sanitize($this->Status); + $this->Size = intval($this->Size); + $this->TypeMedia = sanitize($this->TypeMedia); + if (isset($this->ProofFile) && $this->ProofFile !== null) { + global $config; + $pf = sanitize($this->ProofFile); + $relBase = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $relBase = rtrim($relBase, '/') . '/'; + // If path doesn't start with configured base, collapse to base + basename + if (strpos($pf, $relBase) !== 0) { + $pf = $relBase . basename($pf); + } + $this->ProofFile = $pf; + } + } + + public function MakeDisplay(): void { + // ex. $this->DateAdd = date('d/m/Y', strtotime($this->DateAdd)); + } + + // Convert a PDO row into an HDD object + public static function RowToObject(array $row): self { + $hdd = new self(); + foreach ($row as $prop => $val) { + if (property_exists($hdd, $prop)) { + $hdd->$prop = $val; + } + } + $hdd->MakeDisplay(); + return $hdd; + } + + // Get a HDD by its ID + public static function GetHDDByID(int $id): ?self { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE HDDID = ?"); + $stmt->execute([$id]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return self::RowToObject($row); + } + return null; + } + + // Create a HDD (instance method) + public function Create(): void { + global $dbh; + $this->MakeSafe(); + $sql = "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, Size, TypeMedia, DateAdd) + VALUES + (:DeviceID, :SerialNo, :Status, :Size, :TypeMedia, NOW())"; + $stmt = $dbh->prepare($sql); + $stmt->execute([ + ":DeviceID" => $this->DeviceID, + ":SerialNo" => $this->SerialNo, + ":Status" => $this->Status, + ":Size" => $this->Size, + ":TypeMedia" => $this->TypeMedia, + ]); + $this->HDDID = intval($dbh->lastInsertId()); + self::logAction("Created", $this->HDDID); + } + + // Quick creation from form data + public static function CreateFromForm(int $deviceID, string $serialNo, string $typeMedia, int $size): int { + global $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, TypeMedia, Size, DateAdd) + VALUES + (:DeviceID, :SerialNo, 'On', :TypeMedia, :Size, NOW())" + ); + $stmt->execute([ + ':DeviceID' => $deviceID, + ':SerialNo' => sanitize($serialNo), + ':TypeMedia' => sanitize($typeMedia), + ':Size' => $size + ]); + $newId = intval($dbh->lastInsertId()); + self::logAction("Created from form", $newId); + return $newId; + } + + // Update an existing HDD + public function Update(): bool { + global $dbh; + $this->MakeSafe(); + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + DeviceID = :DeviceID, + SerialNo = :SerialNo, + Status = :Status, + Size = :Size, + TypeMedia = :TypeMedia, + ProofFile = :ProofFile + WHERE HDDID = :HDDID" + ); + $res = $stmt->execute([ + ":DeviceID" => $this->DeviceID, + ":SerialNo" => $this->SerialNo, + ":Status" => $this->Status, + ":Size" => $this->Size, + ":TypeMedia" => $this->TypeMedia, + ":ProofFile" => $this->ProofFile, + ":HDDID" => $this->HDDID + ]); + if ($res) self::logAction("Updated", $this->HDDID); + return $res; + } + + // Delete this HDD + public static function DeleteByID(int $id): bool { + global $dbh; + $stmt = $dbh->prepare("DELETE FROM fac_HDD WHERE HDDID = ?"); + $res = $stmt->execute([$id]); + if ($res) { + self::logAction("Deleted", $id); + } + return $res; + } + + // Duplicate this HDD + public static function DuplicateToEmptySlots(int $sourceHDDID): array { + global $dbh; + $created = []; + // Récupère le disque source + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE HDDID = ?"); + $stmt->execute([$sourceHDDID]); + $hdd = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$hdd) { + return $created; + }// Nombre max de HDD permis par template + $deviceID = intval($hdd['DeviceID']); + $stmt = $dbh->prepare( + "SELECT dt.HDDCount + FROM fac_DeviceTemplateHdd dt + JOIN fac_Device d ON d.TemplateID = dt.TemplateID + WHERE d.DeviceID = ?" + ); + $stmt->execute([$deviceID]); + $max = intval($stmt->fetchColumn()); + + if ($max <= 0) { + return $created; + }// Combien sont déjà présents ? + $stmt = $dbh->prepare("SELECT COUNT(*) FROM fac_HDD WHERE DeviceID = ? AND (Status = 'On' OR Status = 'Off')"); + $stmt->execute([$deviceID]); + $current = intval($stmt->fetchColumn()); + + $remaining = $max - $current; + if ($remaining <= 0) { + return $created; + } // Insère les duplicata + $stmt = $dbh->prepare( + "INSERT INTO fac_HDD + (DeviceID, SerialNo, Status, TypeMedia, Size, DateAdd) + VALUES + (?, ?, ?, ?, ?, NOW())" + ); + for ($i = 0; $i < $remaining; $i++) { + $stmt->execute([ + $deviceID, + uniqid('HDD_', true), + $hdd['Status'], + $hdd['TypeMedia'], + $hdd['Size'] + ]); + $newId = intval($dbh->lastInsertId()); + $created[] = $newId; + self::logAction("Duplicated from HDDID $sourceHDDID", $newId); + } + return $created; + } + + // Send HDD for destruction + public function SendForDestruction(string $note = ''): bool { + global $dbh; + $this->MakeSafe(); + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Pending_destruction', + DateWithdrawn = NOW() + WHERE HDDID = :HDDID" + ); + return $stmt->execute([ + ":HDDID" => $this->HDDID, + ]); + } + + // Mark HDD as destroyed + public static function MarkDestroyed(int $id, ?string $destroyDate = null): bool { + global $dbh; + $dateValue = null; + if ($destroyDate !== null && trim($destroyDate) !== '') { + $destroyDate = trim($destroyDate); + $formats = ['Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d']; + foreach ($formats as $format) { + $dt = DateTime::createFromFormat($format, $destroyDate); + if ($dt instanceof DateTime) { + $dateValue = $dt->format('Y-m-d H:i:s'); + break; + } + } + } + + if ($dateValue) { + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Destroyed', + DateDestroyed = :DateDestroyed + WHERE HDDID = :HDDID" + ); + $params = [ + ':DateDestroyed' => $dateValue, + ':HDDID' => $id + ]; + } else { + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Destroyed', + DateDestroyed = NOW() + WHERE HDDID = :HDDID" + ); + $params = [':HDDID' => $id]; + } + + $res = $stmt->execute($params); + if ($res) { + self::logAction("Marked as destroyed", $id); + } + return $res; + } + + //Reassign a HDD to another device + public static function ReassignToDevice(int $id, int $deviceID): bool { + global $dbh; + $current = self::GetHDDByID($id); + $currentDevice = ($current instanceof HDD) ? intval($current->DeviceID) : 0; + if ($currentDevice !== $deviceID && self::GetRemainingSlotCount($deviceID) <= 0) { + return false; + } + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + DeviceID = ?, + Status = 'On', + DateWithdrawn = NULL, + DateDestroyed = NULL + WHERE HDDID = ?" + ); + $res = $stmt->execute([$deviceID, $id]); + if ($res) { + self::logAction("Reassigned to DeviceID $deviceID", $id); + } + return $res; + } + + //Mark a HDD as spare by its ID + public static function MarkAsSpare(int $id): bool { + global $dbh; + $stmt = $dbh->prepare( + "UPDATE fac_HDD SET + Status = 'Spare' + WHERE HDDID = ?" + ); + $res = $stmt->execute([$id]); + if ($res) { + self::logAction("Marked as Spare", $id); + } + return $res; + } + + public static function GetRemainingSlotCount(int $deviceID): int { + global $dbh; + $stmt = $dbh->prepare( + "SELECT COALESCE(cfg.HDDCount, 0) AS Capacity, + COALESCE(u.ActiveCount, 0) AS Used + FROM fac_Device d + LEFT JOIN fac_DeviceTemplateHdd cfg ON cfg.TemplateID = d.TemplateID AND cfg.EnableHDDFeature = 1 + LEFT JOIN ( + SELECT DeviceID, COUNT(*) AS ActiveCount + FROM fac_HDD + WHERE Status IN ('On','Off') + GROUP BY DeviceID + ) u ON u.DeviceID = d.DeviceID + WHERE d.DeviceID = :DeviceID" + ); + $stmt->execute([':DeviceID' => $deviceID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $capacity = intval($row['Capacity']); + $used = intval($row['Used']); + if ($capacity <= 0) { + return 0; + } + $remaining = $capacity - $used; + return ($remaining > 0) ? $remaining : 0; + } + return 0; + } + + public static function RecordAudit(int $deviceID, string $userID): bool { + global $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, ChildID, Action, Property, OldVal, NewVal) + VALUES + (:UserID, 'HDD', :ObjectID, NULL, 'HDD_Audit', 'Audit', '', '')" + ); + return $stmt->execute([ + ':UserID' => $userID, + ':ObjectID' => $deviceID + ]); + } + + public static function RecordGenericLog(?int $deviceID, string $userID, string $action, string $details = ''): bool { + global $dbh; + $objectId = $deviceID ?? 0; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, ChildID, Action, Property, OldVal, NewVal) + VALUES + (:UserID, 'HDD', :ObjectID, NULL, :Action, 'Details', '', :NewVal)" + ); + return $stmt->execute([ + ':UserID' => $userID, + ':ObjectID' => $objectId, + ':Action' => $action, + ':NewVal' => $details + ]); + } + + public static function GetLastAudit(int $deviceID): ?array { + global $dbh; + $sql = "SELECT g.Time AS AuditTime, g.UserID, + NULLIF(TRIM(CONCAT(COALESCE(p.FirstName,''), ' ', COALESCE(p.LastName,''))), '') AS PersonName + FROM fac_GenericLog g + LEFT JOIN fac_People p ON p.UserID = g.UserID + WHERE g.Class='HDD' AND g.Action='HDD_Audit' AND g.ObjectID = :DeviceID + ORDER BY g.Time DESC LIMIT 1"; + $stmt = $dbh->prepare($sql); + $stmt->execute([':DeviceID' => $deviceID]); + if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + return [ + 'AuditTime' => $row['AuditTime'], + 'DisplayName' => ($row['PersonName'] ?: $row['UserID']) + ]; + } + return null; + } + + // List active HDDs for a device + public static function GetHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE DeviceID = :DeviceID AND (Status = 'On' OR Status = 'Off') ORDER BY SerialNo ASC"); + $stmt->execute([":DeviceID" => $DeviceID]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // List HDDs pending destruction + public static function GetPendingByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Pending_destruction' + ORDER BY DateWithdrawn DESC" + ); + $stmt->execute([':DeviceID' => $DeviceID]); + + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // List destroyed HDDs (destruction completed) + public static function GetDestroyedHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Destroyed' + ORDER BY DateDestroyed DESC" + ); + $stmt->execute([":DeviceID" => $DeviceID]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + // Accessor for proof file + public function GetProofFile(): ?string { + return $this->ProofFile ?? null; + } + + // Mutator to set/update proof file for this HDD + public function SetProofFile(string $filename): bool { + global $dbh; + $this->ProofFile = $filename; + $this->MakeSafe(); + $stmt = $dbh->prepare("UPDATE fac_HDD SET ProofFile = :ProofFile WHERE HDDID = :HDDID"); + $res = $stmt->execute([ + ':ProofFile' => $this->ProofFile, + ':HDDID' => $this->HDDID + ]); + if ($res) { + self::logAction("Set proof file", $this->HDDID); + } + return $res; + } + + // Batch update proof file for multiple HDD IDs + public static function SetProofFileForIds(array $ids, string $filename): int { + global $dbh; + $ids = array_values(array_unique(array_map('intval', $ids))); + if (empty($ids)) return 0; + $pf = sanitize($filename); + if (strpos($pf, 'files/hdd/') !== 0) { + $pf = 'files/hdd/' . basename($pf); + } + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $sql = "UPDATE fac_HDD SET ProofFile = ? WHERE HDDID IN ($placeholders)"; + $stmt = $dbh->prepare($sql); + $params = array_merge([$pf], $ids); + $stmt->execute($params); + return $stmt->rowCount(); + } +// List HDDs Spare destruction + public static function GetSpareHDDByDevice(int $DeviceID): array { + global $dbh; + $stmt = $dbh->prepare( + "SELECT * FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = 'Spare' + ORDER BY DateWithdrawn DESC" + ); + $stmt->execute([':DeviceID' => $DeviceID]); + + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + // Search HDDs by serial number + public static function SearchBySerial(string $SerialNo): array { + global $dbh; + $stmt = $dbh->prepare("SELECT * FROM fac_HDD WHERE SerialNo LIKE :SerialNo"); + $stmt->execute([":SerialNo" => "%" . sanitize($SerialNo) . "%"]); + $list = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $list[] = self::RowToObject($row); + } + return $list; + } + + //Export all HDDs of a device into a 3-sheet XLS + //- sheet "hdd in prod" for Status = 'On' + //- sheet "hdd out prod" for Status = 'Off' + //- sheet "Pending_destruction" for Status = 'Pending_destruction' + //- sheet "Destroyed" for Status = 'Destroyed' + //- sheet "Spare" for Status = 'Spare' + + public static function ExportAllToXls(int $deviceID): void { + global $dbh; + // Crée le classeur + $spreadsheet = new Spreadsheet(); + + $statuses = [ + 'On' => 'hdd in prod', + 'Off' => 'hdd out prod', + 'Pending_destruction' => 'Pending_destruction', + 'Destroyed' => 'Destroyed', + 'Spare' => 'Spare' + ]; + + $first = true; + foreach ($statuses as $status => $title) { + // Feuille active ou création + $sheet = $first + ? $spreadsheet->getActiveSheet() + : $spreadsheet->createSheet(); + $first = false; + $sheet->setTitle($title); + + // En-têtes colonnes + $headers = ['HDDID','SerialNo','Status','TypeMedia','Size','DateAdd','DateWithdrawn','DateDestroyed']; + $sheet->fromArray($headers, null, 'A1'); + + // Récupère les HDDs pour ce statut + $stmt = $dbh->prepare( + "SELECT HDDID, SerialNo, Status, TypeMedia, Size, + DateAdd, DateWithdrawn, DateDestroyed + FROM fac_HDD + WHERE DeviceID = :DeviceID + AND Status = :Status + ORDER BY SerialNo ASC" + ); + $stmt->execute([ + ':DeviceID' => $deviceID, + ':Status' => $status, + ]); + + // Remplit les lignes + $row = 2; + while ($h = $stmt->fetch(PDO::FETCH_NUM)) { + // $h est un array indexé de 0 à 10, dans le même ordre que les headers + $sheet->fromArray($h, null, "A{$row}"); + $row++; + } + } + + // En-têtes HTTP & envoi du fichier + header('Content-Type: application/vnd.ms-excel'); + header('Content-Disposition: attachment; filename="HDD_List_Device_' . $deviceID . '.xls"'); + + $writer = new Xls($spreadsheet); + $writer->save('php://output'); + exit; + } + + + // Generic logging to fac_GenericLog + private static function logAction(string $action, int $HDDID): void { + global $person, $dbh; + $stmt = $dbh->prepare( + "INSERT INTO fac_GenericLog + (UserID, Class, ObjectID, Action, Time) + VALUES + (:UserID, 'HDD', :ObjectID, :Action, CURRENT_TIMESTAMP)" + ); + $stmt->execute([ + ":UserID" => $person->UserID, + ":ObjectID" => $HDDID, + ":Action" => $action + ]); + } +} +?> diff --git a/configuration.php b/configuration.php index 029605263..82decf2db 100644 --- a/configuration.php +++ b/configuration.php @@ -1793,11 +1793,15 @@ function uploadifive() {
-
-
-
-
-
+
+
+
+
+
+
+
+
+

',__("Time and Measurements"),'

@@ -1928,16 +1932,33 @@ function uploadifive() {
-
-
-
-
-
-
-
+

',__("Features Options"),'

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

',__("Site Level Security Options"),'

diff --git a/create.sql b/create.sql index 3ac925d65..a805fd3da 100644 --- a/create.sql +++ b/create.sql @@ -863,7 +863,10 @@ INSERT INTO fac_Config VALUES ('HumidityRedLow', '35', 'percentage', 'float', '35'), ('HumidityYellowHigh', '55', 'percentage', 'float', '55'), ('HumidityYellowLow', '45', 'percentage', 'float', '45'), - ('WorkOrderBuilder', 'disabled', 'Enabled/Disabled', 'string', 'Disabled'), + ('feature_hdd', 'disabled', 'Enabled/Disabled', 'string', 'disabled'), + ('Log_for_user_hdd', 'disabled', 'Enabled/Disabled', 'string', 'disabled'), + ('hdd_proof_path', 'assets/files/hdd/', 'path', 'string', 'assets/files/hdd/'), + ('WorkOrderBuilder', 'disabled', 'Enabled/Disabled', 'string', 'disabled'), ('RackRequests', 'enabled', 'Enabled/Disabled', 'string', 'Enabled'), ('dot', '/usr/bin/dot', 'path', 'string', '/usr/bin/dot'), ('AppendCabDC', 'disabled', 'Enabled/Disabled', 'string', 'Disabled'), @@ -1078,3 +1081,32 @@ CREATE TABLE fac_Country ( -- INSERT INTO `fac_Country` VALUES ('AD','Andorra'),('AE','United Arab Emirates'),('AF','Afghanistan'),('AG','Antigua and Barbuda'),('AI','Anguilla'),('AL','Albania'),('AM','Armenia'),('AO','Angola'),('AQ','Antarctica'),('AR','Argentina'),('AS','American Samoa'),('AT','Austria'),('AU','Australia'),('AW','Aruba'),('AX','Åland'),('AZ','Azerbaijan'),('BA','Bosnia and Herzegovina'),('BB','Barbados'),('BD','Bangladesh'),('BE','Belgium'),('BF','Burkina Faso'),('BG','Bulgaria'),('BH','Bahrain'),('BI','Burundi'),('BJ','Benin'),('BL','Saint Barthélemy'),('BM','Bermuda'),('BN','Brunei'),('BO','Bolivia'),('BQ','Bonaire, Sint Eustatius, and Saba'),('BR','Brazil'),('BS','Bahamas'),('BT','Bhutan'),('BV','Bouvet Island'),('BW','Botswana'),('BY','Belarus'),('BZ','Belize'),('CA','Canada'),('CC','Cocos (Keeling) Islands'),('CD','DR Congo'),('CF','Central African Republic'),('CG','Congo Republic'),('CH','Switzerland'),('CI','Ivory Coast'),('CK','Cook Islands'),('CL','Chile'),('CM','Cameroon'),('CN','China'),('CO','Colombia'),('CR','Costa Rica'),('CU','Cuba'),('CV','Cabo Verde'),('CW','Curaçao'),('CX','Christmas Island'),('CY','Cyprus'),('CZ','Czechia'),('DE','Germany'),('DJ','Djibouti'),('DK','Denmark'),('DM','Dominica'),('DO','Dominican Republic'),('DZ','Algeria'),('EC','Ecuador'),('EE','Estonia'),('EG','Egypt'),('EH','Western Sahara'),('ER','Eritrea'),('ES','Spain'),('ET','Ethiopia'),('FI','Finland'),('FJ','Fiji'),('FK','Falkland Islands'),('FM','Micronesia'),('FO','Faroe Islands'),('FR','France'),('GA','Gabon'),('GB','United Kingdom'),('GD','Grenada'),('GE','Georgia'),('GF','French Guiana'),('GG','Guernsey'),('GH','Ghana'),('GI','Gibraltar'),('GL','Greenland'),('GM','The Gambia'),('GN','Guinea'),('GP','Guadeloupe'),('GQ','Equatorial Guinea'),('GR','Greece'),('GS','South Georgia and South Sandwich Islands'),('GT','Guatemala'),('GU','Guam'),('GW','Guinea-Bissau'),('GY','Guyana'),('HK','Hong Kong'),('HM','Heard and McDonald Islands'),('HN','Honduras'),('HR','Croatia'),('HT','Haiti'),('HU','Hungary'),('ID','Indonesia'),('IE','Ireland'),('IL','Israel'),('IM','Isle of Man'),('IN','India'),('IO','British Indian Ocean Territory'),('IQ','Iraq'),('IR','Iran'),('IS','Iceland'),('IT','Italy'),('JE','Jersey'),('JM','Jamaica'),('JO','Jordan'),('JP','Japan'),('KE','Kenya'),('KG','Kyrgyzstan'),('KH','Cambodia'),('KI','Kiribati'),('KM','Comoros'),('KN','St Kitts and Nevis'),('KP','North Korea'),('KR','South Korea'),('KW','Kuwait'),('KY','Cayman Islands'),('KZ','Kazakhstan'),('LA','Laos'),('LB','Lebanon'),('LC','Saint Lucia'),('LI','Liechtenstein'),('LK','Sri Lanka'),('LR','Liberia'),('LS','Lesotho'),('LT','Lithuania'),('LU','Luxembourg'),('LV','Latvia'),('LY','Libya'),('MA','Morocco'),('MC','Monaco'),('MD','Moldova'),('ME','Montenegro'),('MF','Saint Martin'),('MG','Madagascar'),('MH','Marshall Islands'),('MK','North Macedonia'),('ML','Mali'),('MM','Myanmar'),('MN','Mongolia'),('MO','Macao'),('MP','Northern Mariana Islands'),('MQ','Martinique'),('MR','Mauritania'),('MS','Montserrat'),('MT','Malta'),('MU','Mauritius'),('MV','Maldives'),('MW','Malawi'),('MX','Mexico'),('MY','Malaysia'),('MZ','Mozambique'),('NA','Namibia'),('NC','New Caledonia'),('NE','Niger'),('NF','Norfolk Island'),('NG','Nigeria'),('NI','Nicaragua'),('NL','Netherlands'),('NO','Norway'),('NP','Nepal'),('NR','Nauru'),('NU','Niue'),('NZ','New Zealand'),('OM','Oman'),('PA','Panama'),('PE','Peru'),('PF','French Polynesia'),('PG','Papua New Guinea'),('PH','Philippines'),('PK','Pakistan'),('PL','Poland'),('PM','Saint Pierre and Miquelon'),('PN','Pitcairn Islands'),('PR','Puerto Rico'),('PS','Palestine'),('PT','Portugal'),('PW','Palau'),('PY','Paraguay'),('QA','Qatar'),('RE','Réunion'),('RO','Romania'),('RS','Serbia'),('RU','Russia'),('RW','Rwanda'),('SA','Saudi Arabia'),('SB','Solomon Islands'),('SC','Seychelles'),('SD','Sudan'),('SE','Sweden'),('SG','Singapore'),('SH','Saint Helena'),('SI','Slovenia'),('SJ','Svalbard and Jan Mayen'),('SK','Slovakia'),('SL','Sierra Leone'),('SM','San Marino'),('SN','Senegal'),('SO','Somalia'),('SR','Suriname'),('SS','South Sudan'),('ST','São Tomé and Príncipe'),('SV','El Salvador'),('SX','Sint Maarten'),('SY','Syria'),('SZ','Eswatini'),('TC','Turks and Caicos Islands'),('TD','Chad'),('TF','French Southern Territories'),('TG','Togo'),('TH','Thailand'),('TJ','Tajikistan'),('TK','Tokelau'),('TL','Timor-Leste'),('TM','Turkmenistan'),('TN','Tunisia'),('TO','Tonga'),('TR','Turkey'),('TT','Trinidad and Tobago'),('TV','Tuvalu'),('TW','Taiwan'),('TZ','Tanzania'),('UA','Ukraine'),('UG','Uganda'),('UM','U.S. Outlying Islands'),('US','United States'),('UY','Uruguay'),('UZ','Uzbekistan'),('VA','Vatican City'),('VC','St Vincent and Grenadines'),('VE','Venezuela'),('VG','British Virgin Islands'),('VI','U.S. Virgin Islands'),('VN','Vietnam'),('VU','Vanuatu'),('WF','Wallis and Futuna'),('WS','Samoa'),('XK','Kosovo'),('YE','Yemen'),('YT','Mayotte'),('ZA','South Africa'),('ZM','Zambia'),('ZW','Zimbabwe'); + +-- +-- Table for fac_HDD feature managementHDD +-- +CREATE TABLE fac_HDD ( + HDDID INT(11) NOT NULL AUTO_INCREMENT, + DeviceID INT(11) NOT NULL, + SerialNo VARCHAR(100), + Status ENUM('On','Off','Pending_destruction','Destroyed','Spare') + DEFAULT 'On', + Size INT(11) DEFAULT NULL, -- en Go + TypeMedia ENUM('HDD','SSD','MVME'), + ProofFile VARCHAR(255) DEFAULT NULL, + DateAdd DATETIME DEFAULT CURRENT_TIMESTAMP, + DateWithdrawn DATETIME DEFAULT NULL, + DateDestroyed DATETIME DEFAULT NULL, + PRIMARY KEY (HDDID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Table for fac_DeviceTemplateHdd feature managementHDD +-- + + CREATE TABLE fac_DeviceTemplateHdd ( + TemplateID int(11) NOT NULL, + EnableHDDFeature tinyint(1) DEFAULT 0, + HDDCount int(11) DEFAULT 0, + PRIMARY KEY (TemplateID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/css/inventory.php b/css/inventory.php index 0faa03b06..2f9465197 100644 --- a/css/inventory.php +++ b/css/inventory.php @@ -394,6 +394,34 @@ .whiteborder, .whiteborder div {border: 1px solid white;} .border, .border div {border: 1px solid gray;} +.assign-device-table { + text-align: center; +} +#assignDeviceList { + margin: 0 auto; + border-collapse: separate !important; + border-spacing: 10px 6px; +} +#assignDeviceList th, +#assignDeviceList td { + padding: 8px 18px; +} +#assignDeviceList .assign-device-row:hover { + background-color: #eef6ff; + cursor: pointer; +} + +.hdd-status-select { + transition: background-color .2s ease-in-out; + color: #000; +} +.hdd-status-select.status-on { + background-color: #dff5d8; +} +.hdd-status-select.status-off { + background-color: #ffd6d6; +} + /* Search Results */ .search .center {text-align: left;} .search .main ol, .search .main ul{list-style-type: none;margin-left: 1em;} diff --git a/db-23.04-to-24.01.sql b/db-23.04-to-24.01.sql index 22c9ac1e1..fa60738bf 100755 --- a/db-23.04-to-24.01.sql +++ b/db-23.04-to-24.01.sql @@ -3,3 +3,34 @@ --- UPDATE fac_Config set Value="24.01" WHERE Parameter="Version"; +--- feature hdd +--- -fac_config +INSERT INTO fac_Config (Parameter, Value, UnitOfMeasure, ValType, DefaultVal) VALUES ('feature_hdd', 'disabled','Enabled/Disabled','string','Disabled') +ON DUPLICATE KEY UPDATE Value = Value; +INSERT INTO fac_Config (Parameter, Value, UnitOfMeasure, ValType, DefaultVal) VALUES ('Log_for_user_hdd', 'disabled','Enabled/Disabled','string','Disabled') +ON DUPLICATE KEY UPDATE Value = Value; +--- -fac_people +ALTER TABLE fac_People ADD COLUMN ManageHDD TINYINT(1) DEFAULT 0 AFTER SiteAdmin; +--- -fac_devicetemplatehdd +CREATE TABLE fac_DeviceTemplateHdd ( + TemplateID INT NOT NULL, + EnableHDDFeature TINYINT(1) DEFAULT 0, + HDDCount INT DEFAULT 0, + PRIMARY KEY (TemplateID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +--- -fac_hdd +CREATE TABLE fac_HDD ( + HDDID INT NOT NULL AUTO_INCREMENT, + DeviceID INT NOT NULL, + SerialNo VARCHAR(100), + Status ENUM('On','Off','Pending_destruction','Destroyed','Spare') + DEFAULT 'On', + Size INT(11) DEFAULT NULL, -- en Go + TypeMedia ENUM('HDD','SSD','MVME') DEFAULT NULL, + DateAdd DATETIME DEFAULT CURRENT_TIMESTAMP, + DateWithdrawn DATETIME DEFAULT NULL, + DateDestroyed DATETIME DEFAULT NULL, + PRIMARY KEY (HDDID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + diff --git a/db-24.01-to-24.02.sql b/db-24.01-to-24.02.sql new file mode 100644 index 000000000..eb919370b --- /dev/null +++ b/db-24.01-to-24.02.sql @@ -0,0 +1,7 @@ +-- HDD proof of destruction support +ALTER TABLE fac_HDD ADD COLUMN ProofFile VARCHAR(255) DEFAULT NULL AFTER TypeMedia; + +-- Config parameter for HDD proofs base path (with trailing slash) +INSERT INTO fac_Config (Parameter, Value, UnitOfMeasure, ValType, DefaultVal) +SELECT 'hdd_proof_path','assets/files/hdd/','path','string','assets/files/hdd/' +WHERE NOT EXISTS (SELECT 1 FROM fac_Config WHERE Parameter='hdd_proof_path'); diff --git a/device_templates.php b/device_templates.php index a09fcc480..9c96e580f 100644 --- a/device_templates.php +++ b/device_templates.php @@ -1,4 +1,8 @@ DeleteTemplate(); + $tempplate->DeleteTemplateHDD(); // feature management hdd: delete } echo '1'; exit; @@ -82,6 +87,7 @@ $template->TemplateID=$_REQUEST['TemplateID']; $template->GetTemplateByID(); $deviceList = Device::GetDevicesByTemplate( $template->TemplateID ); + $template->LoadHDDConfig(); // feature management hdd: Load } if(isset($_POST['action'])){ @@ -240,6 +246,8 @@ function updatecdu($template,$status){ if($template->CreateTemplate()){ $oldstatus=$status; $status=UpdateSlotsPorts($template,$status); + $template->UpdateTemplateHDD(); // feature management hdd + //$template->LoadHDDConfig(); // load data from hdd if($oldstatus==$status){ $status=UpdateCustomValues($template,$status); } @@ -255,6 +263,7 @@ function updatecdu($template,$status){ $status=($template->UpdateTemplate())?__("Updated"):__("Error updating template"); if($status==__("Updated")){ $status=UpdateSlotsPorts($template,$status); + $template->UpdateTemplateHDD();// feature management hdd } if($status==__("Updated")){ $status=UpdateCustomValues($template,$status); @@ -271,6 +280,7 @@ function updatecdu($template,$status){ $status=($template->UpdateTemplate())?__("Updated"):__("Error updating template"); if ($status==__("Updated")){ $status=UpdateSlotsPorts($template,$status); + $template->UpdateTemplateHDD();// feature management hdd } if ($status==__("Updated")){ $status=UpdateCustomValues($template,$status); @@ -288,6 +298,7 @@ function updatecdu($template,$status){ $status=($template->UpdateTemplate())?__("Updated"):__("Error"); if ($status==__("Updated")){ $status=UpdateSlotsPorts($template,$status); + $template->UpdateTemplateHDD();// feature management hdd } if ($status==__("Updated")){ $status=UpdateCustomValues($template,$status); @@ -826,6 +837,22 @@ function applynames(inputs,portnames,e){
'; +// feature management hdd +if($config->ParameterArray['feature_hdd'] == 'enabled'){ + echo ' +
+
+
+
+
+
+
+
'; +} + foreach($dcaList as $dca) { $templatedcaChecked = ""; $templatedcaDisabled = ""; diff --git a/devices.php b/devices.php index 26d2e822d..19a41055f 100644 --- a/devices.php +++ b/devices.php @@ -1,6 +1,9 @@ ParameterArray['feature_hdd'] == 'enabled'){ + require_once( 'classes/hdd.class.php' ); + } $subheader=__("Data Center Device Detail"); @@ -2052,10 +2055,36 @@ function setPreferredLayout() {DeviceType){$selected=" selected";}else{$selected="";} print "\t\t\t\n"; } -echo ' + echo '
-
- + '; + //feature management hdd + if( + $config->ParameterArray['feature_hdd'] == 'enabled' && + $dev->DeviceID > 0 && + $person->ManageHDD == 1 + ){ + $template = new DeviceTemplate($dev->TemplateID); + $template->GetTemplateByID(); + $template->LoadHDDConfig(); + if( + $template->EnableHDDFeature == 1 + ){ + $deviceHddAudit = HDD::GetLastAudit($dev->DeviceID); + echo ' +
+
+
',__("Manage HDDs"),'
'; + if($deviceHddAudit){ + echo '
+
',__("Last HDD audit"),'
+
',date('Y-m-d H:i', strtotime($deviceHddAudit['AuditTime'])),'
+
'; + } + } + } + + echo' '; if ($dev->DeviceType=='Sensor'){ @@ -2065,7 +2094,8 @@ function setPreferredLayout() { '; } - + + //device images echo '
'.__("Device Images").'
'; diff --git a/hdd_log_view.php b/hdd_log_view.php new file mode 100644 index 000000000..e4c868bf6 --- /dev/null +++ b/hdd_log_view.php @@ -0,0 +1,92 @@ +ManageHDD) { + header("Location: index.php"); + exit; +} + +$deviceID = isset($_GET['DeviceID']) ? intval($_GET['DeviceID']) : 0; + +if (!$deviceID) { + echo __("DeviceID is required"); + exit; +} + +$sql = " + SELECT Time, UserID, Action, NewVal + FROM fac_GenericLog + WHERE Class = 'HDD' AND ObjectID = :DeviceID + ORDER BY Time DESC"; +$stmt = $dbh->prepare($sql); +$stmt->execute([':DeviceID' => $deviceID]); +$logEntries = $stmt->fetchAll(PDO::FETCH_ASSOC); + +function formatHddLogDetails($action, $payload) { + $data = json_decode($payload, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { + return $payload; + } + switch ($action) { + case 'HDD_BULK_DESTROY': + $count = isset($data['count']) ? intval($data['count']) : 0; + $list = isset($data['ids']) && is_array($data['ids']) ? implode(', ', $data['ids']) : ''; + return sprintf(__("Bulk destroy of %d HDD(s). IDs: %s"), $count, $list); + case 'HDD_CSV_BATCH': + $notes = isset($data['notes']) && $data['notes'] !== '' ? $data['notes'] : __('None'); + $processed = isset($data['processed']) && is_array($data['processed']) ? implode(', ', $data['processed']) : __('None'); + $already = isset($data['already_processed']) && is_array($data['already_processed']) ? implode(', ', $data['already_processed']) : __('None'); + $missing = isset($data['missing']) && is_array($data['missing']) ? implode(', ', $data['missing']) : __('None'); + return sprintf(__("CSV batch - Notes: %s | Processed: %s | Already processed: %s | Unknown: %s"), $notes, $processed, $already, $missing); + case 'HDD_Audit': + return __("Audit certified for this device."); + default: + return $payload; + } +} +?> + + + + + + <?php echo $subheader; ?> + + + + + + + +
+ +
+

+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/import_hdd_csv.php b/import_hdd_csv.php new file mode 100644 index 000000000..f8ac78260 --- /dev/null +++ b/import_hdd_csv.php @@ -0,0 +1,256 @@ +ManageHDD) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => __('Permission denied')]); + exit; +} + +$sendJson = function(array $payload, int $status = 200) { + http_response_code($status); + header('Content-Type: application/json'); + echo json_encode($payload); + exit; +}; + +$detectDelimiter = function(string $line): string { + $candidates = [',', ';', "\t", '|']; + $best = ','; + $max = 0; + foreach ($candidates as $delim) { + $count = substr_count($line, $delim); + if ($count > $max) { + $max = $count; + $best = $delim; + } + } + return $best; +}; + +try { + $force = !empty($_POST['force']); + $columnName = trim($_POST['csv_column'] ?? ''); + if ($columnName === '') { + throw new Exception(__('Please select the CSV column containing serial numbers')); + } + + if (!isset($_FILES['batch_csv'])) { + throw new Exception(__('CSV file is required')); + } + + if ($_FILES['batch_csv']['error'] !== UPLOAD_ERR_OK) { + throw new Exception(__('Error while uploading the CSV file')); + } + + if ($_FILES['batch_csv']['size'] > 2 * 1024 * 1024) { + throw new Exception(__('CSV file is too large (max 2 MB)')); + } + + $csvTmp = $_FILES['batch_csv']['tmp_name']; + $lines = file($csvTmp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines === false || count($lines) === 0) { + throw new Exception(__('Unable to read CSV file or file is empty')); + } + + $headerLine = $lines[0]; + $delimiter = $detectDelimiter($headerLine); + $headers = str_getcsv($headerLine, $delimiter); + $headersNormalized = array_map(function($h) { + return strtolower(trim($h)); + }, $headers); + + $targetIndex = array_search(strtolower($columnName), $headersNormalized, true); + if ($targetIndex === false) { + throw new Exception(__('Selected column was not found in the CSV header')); + } + + $serials = []; + for ($i = 1; $i < count($lines); $i++) { + $row = str_getcsv($lines[$i], $delimiter); + if (!isset($row[$targetIndex])) { + continue; + } + $sn = trim($row[$targetIndex]); + if ($sn !== '') { + $serials[] = $sn; + } + } + $serials = array_values(array_unique($serials)); + if (empty($serials)) { + throw new Exception(__('No serial numbers were found in the selected column')); + } + + $placeholders = implode(',', array_fill(0, count($serials), '?')); + $stmt = $dbh->prepare("SELECT HDDID, SerialNo, Status, DateDestroyed FROM fac_HDD WHERE SerialNo IN ($placeholders)"); + $stmt->execute($serials); + $foundActive = []; + $alreadyProcessed = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $serial = $row['SerialNo']; + $status = strtolower($row['Status']); + $isDestroyed = ($status === 'destroyed') || (!empty($row['DateDestroyed'])); + if ($isDestroyed) { + $alreadyProcessed[$serial] = (int)$row['HDDID']; + } else { + $foundActive[$serial] = (int)$row['HDDID']; + } + } + + $missing = array_values(array_diff($serials, array_keys($foundActive), array_keys($alreadyProcessed))); + $recognizedIds = array_values($foundActive); + $recognizedSerials = array_keys($foundActive); + $alreadyProcessedSerials = array_keys($alreadyProcessed); + + if (!empty($missing) && !$force) { + $sendJson([ + 'require_confirm' => true, + 'missing' => $missing, + 'message' => sprintf(__('Found %d serial numbers. %d were not recognized. Continue processing the recognized entries?'), count($recognizedIds), count($missing)) + ]); + } + + if (empty($recognizedIds)) { + throw new Exception(__('No matching HDD serial numbers were available for processing')); + } + + $applyDestroyStatus = !empty($_POST['apply_destroy_status']); + $destroyDateInput = trim($_POST['destroy_date'] ?? ''); + $notes = trim($_POST['notes'] ?? ''); + if ($applyDestroyStatus) { + if ($destroyDateInput === '') { + throw new Exception(__('Please provide a destruction date when applying the destroyed status')); + } + $dt = DateTime::createFromFormat('Y-m-d', $destroyDateInput); + if (!$dt) { + throw new Exception(__('Invalid destruction date format (expected YYYY-MM-DD)')); + } + $destroyDateValue = $dt->format('Y-m-d 00:00:00'); + } else { + $destroyDateValue = null; + } + + $proofFileName = null; + if (isset($_FILES['batch_proof']) && $_FILES['batch_proof']['error'] !== UPLOAD_ERR_NO_FILE) { + if ($_FILES['batch_proof']['error'] !== UPLOAD_ERR_OK) { + throw new Exception(__('Error while uploading the proof file')); + } + if ($_FILES['batch_proof']['size'] > 5 * 1024 * 1024) { + throw new Exception(__('Proof file is too large (max 5 MB)')); + } + $allowedProofExtensions = [ + 'pdf' => ['application/pdf'], + 'xls' => ['application/vnd.ms-excel', 'application/octet-stream'], + 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/octet-stream'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet', 'application/zip', 'application/octet-stream'], + ]; + $proofExtension = strtolower(pathinfo($_FILES['batch_proof']['name'], PATHINFO_EXTENSION)); + if (!array_key_exists($proofExtension, $allowedProofExtensions)) { + throw new Exception(__('Proof file type not allowed (PDF, XLS, XLSX or ODS only)')); + } + if (!class_exists('finfo')) { + throw new Exception(__('The fileinfo PHP extension is required to validate the proof file')); + } + $finfo = new finfo(FILEINFO_MIME_TYPE); + $proofMime = $finfo->file($_FILES['batch_proof']['tmp_name']); + if (!in_array($proofMime, $allowedProofExtensions[$proofExtension], true)) { + throw new Exception(__('Proof file type not allowed (PDF, XLS, XLSX or ODS only)')); + } + $datePart = date('Ymd-His'); + $randPart = substr(bin2hex(random_bytes(4)), 0, 8); + $proofFileName = "proof_batch_{$datePart}_{$randPart}.{$proofExtension}"; + + $pathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $storageRoot = $pathSetting; + if (preg_match('#^(?:[A-Za-z]:\\\\|/)#', $storageRoot) === 1) { + $baseDir = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $storageRoot), DIRECTORY_SEPARATOR); + } else { + $baseDir = rtrim(__DIR__ . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, trim($storageRoot, '/\\')), DIRECTORY_SEPARATOR); + } + if (!is_dir($baseDir)) { + if (!@mkdir($baseDir, 0750, true)) { + throw new Exception(__('Unable to create the storage directory') . ' : ' . $baseDir); + } + } + if (!is_writable($baseDir)) { + throw new Exception(__('Storage directory is not writable: ') . $baseDir); + } + $destPath = $baseDir . DIRECTORY_SEPARATOR . $proofFileName; + if (!@move_uploaded_file($_FILES['batch_proof']['tmp_name'], $destPath)) { + throw new Exception(__('Error while saving the proof file')); + } + @chmod($destPath, 0644); + } + + $idPlaceholders = implode(',', array_fill(0, count($recognizedIds), '?')); + if ($proofFileName !== null) { + $proofStmt = $dbh->prepare("UPDATE fac_HDD SET ProofFile = ? WHERE HDDID IN ($idPlaceholders)"); + $proofParams = array_merge([$proofFileName], $recognizedIds); + $proofStmt->execute($proofParams); + } + + if ($applyDestroyStatus) { + $statusStmt = $dbh->prepare("UPDATE fac_HDD SET Status='Destroyed', DateDestroyed = ? WHERE HDDID IN ($idPlaceholders)"); + $statusParams = array_merge([$destroyDateValue], $recognizedIds); + $statusStmt->execute($statusParams); + } + + $timestamp = date('c'); + $logLines = []; + $logLines[] = 'Timestamp: '.$timestamp; + $logLines[] = 'User: '.$person->UserID; + $logLines[] = 'Notes: '.($notes !== '' ? $notes : __('None')); + $logLines[] = sprintf('Processed serials (%d): %s', count($recognizedSerials), $recognizedSerials ? implode(', ', $recognizedSerials) : __('None')); + $logLines[] = sprintf('Already processed serials (%d): %s', count($alreadyProcessedSerials), $alreadyProcessedSerials ? implode(', ', $alreadyProcessedSerials) : __('None')); + $logLines[] = sprintf('Unknown serials (%d): %s', count($missing), $missing ? implode(', ', $missing) : __('None')); + $logLines[] = 'Proof file: '.($proofFileName !== null ? $proofFileName : __('None')); + if ($applyDestroyStatus) { + $logLines[] = 'Destruction date: '.$destroyDateValue; + } + $logText = implode("\n", $logLines); + + $logSummary = json_encode([ + 'timestamp' => $timestamp, + 'user' => $person->UserID, + 'notes' => $notes, + 'processed' => $recognizedSerials, + 'already_processed' => $alreadyProcessedSerials, + 'missing' => $missing + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + HDD::RecordGenericLog(null, $person->UserID, 'HDD_CSV_BATCH', $logSummary); + + $messageParts = []; + $messageParts[] = sprintf(__('Processed %d serial numbers.'), count($recognizedIds)); + if (!empty($alreadyProcessedSerials)) { + $messageParts[] = __('Serial numbers already processed! Only valid serials were handled.'); + } + if (!empty($missing)) { + $messageParts[] = sprintf(__('Skipped %d unknown serial numbers.'), count($missing)); + } + if ($proofFileName !== null) { + $messageParts[] = __('Proof file assigned to matching HDDs.'); + } + + $response = [ + 'success' => true, + 'message' => implode(' ', $messageParts), + 'missing' => $missing, + 'log_text' => $logText, + 'reload' => true + ]; + if (!empty($alreadyProcessedSerials)) { + $response['already_message'] = __('Serial numbers already processed! Only valid serials were handled.'); + } + $sendJson($response); +} catch (Exception $ex) { + $sendJson(['success' => false, 'error' => $ex->getMessage()], 400); +} diff --git a/inventory.php b/inventory.php new file mode 100644 index 000000000..614ab0fc6 --- /dev/null +++ b/inventory.php @@ -0,0 +1,1517 @@ + + + +/* Reset All Broswers to Nothing */ +@import url('reset.css'); + +html { + font-family: helvetica,arial; + font-size: .833em; + padding-left: .9em; + padding-right: .9em; +} +select {padding: .05em;} +fieldset table, table {border: 1px solid grey;} +textarea {white-space: pre;word-wrap: break-word;} +.hide {display: none !important;} +.show {display: block;} +.greybg {background-color: lightGrey;} +.warning {text-align: center; color: red; text-transform: uppercase;} +.right {text-align: right;} +.left {text-align: left;} +.custom-combobox {position: relative;display: inline-block;} +.monospace {font-family: monospace !important;} +.noborder {border: 0px !important;} + +.floatleft { float: left; margin-right: 5px; } +.floatright { float: right; margin-left: 5px; } + +[readonly],[disabled] { + background-color: #dcdcdc; + border: 1px dotted grey; + padding: 1px; + color: #000000; + cursor: default; +} + +.arrow_left { position: relative; background: #ffffff; border: 1px solid #000000; } +.arrow_left:after, .arrow_left:before { right: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } +.arrow_left:after { border-color: rgba(255, 255, 255, 0); border-right-color: #ffffff; border-width: 15px; top: 15px; margin-top: -15px; } +.arrow_left:before { border-color: rgba(0, 0, 0, 0); border-right-color: #000000; border-width: 16px; top: 15px; margin-top: -16px; } + +.no-close .ui-dialog-titlebar-close {display: none;} + +@keyframes loading{ + from { + -webkit-transform:rotate(0deg); + -moz-transform:rotate(0deg); + -o-transform:rotate(0deg); + } + to { + -webkit-transform:rotate(360deg); + -moz-transform:rotate(360deg); + -o-transform:rotate(360deg); + } +} + +@-webkit-keyframes loading{ + from { + -webkit-transform:rotate(0deg); + -moz-transform:rotate(0deg); + -o-transform:rotate(0deg); + } + to { + -webkit-transform:rotate(360deg); + -moz-transform:rotate(360deg); + -o-transform:rotate(360deg); + } +} + + +.rotate{ + animation: loading 0.8s; + -webkit-animation: loading 0.8s; + + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; /*Safari and Chrome*/ + + overflow:hidden; +} + + +/* css for timepicker */ +.ui-timepicker-div .ui-widget-header {margin-bottom: 8px;} +.ui-timepicker-div dl {text-align: left;} +.ui-timepicker-div dl dt {height: 25px; margin-bottom: -25px;} +.ui-timepicker-div dl dd {margin: 0 10px 10px 65px;} +.ui-timepicker-div td {font-size: 90%;} +.ui-tpicker-grid-label {background: none; border: none; margin: 0; padding: 0;} + +/* Header/logo */ +#header{ + padding:5px 0; + background:ParameterArray['HeaderColor']; ?> url("../ParameterArray['PDFLogoFile']; ?>") no-repeat left center; + height:66px; + position: relative; +} +#header > span {color: white;display: block;margin-top: 5px;text-align: center; + text-shadow: 1px 1px 0 #063, 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000; +} +#header1 {font-size: xx-large;} +#header2 {font-size: x-large;} +#header > #version {bottom: 2px;position: absolute;right: 4px;font-size:small; + text-shadow: 1px 1px 0 #063, 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000; +} + +/* Configuration Page */ +div.cp { position: relative;} +.miniColors-trigger { position: absolute; top: 0; right: 0;} +.config .center input { width: 95%; } +#configtabs { min-width: 670px; } +#configtabs button { margin-left: 0.5em; margin-right: 0.5em;} +#configtabs span { font-style: italic; font-size: -1;} +#configtabs label:after {content:":"; margin-right: 0.5em;} +#configtabs #general div > input {width: 20em; } +#configtabs #general #rackusage input {width: 2em; } +#configtabs #general #rackusage > div > div:nth-child(5) { width: 6em; } +#configtabs #style .cp > input {width: 7em; } +#configtabs #email div > input {width: 20em; } + +#configtabs #reporting div:first-child + div > input {width: 20em; } + +div#directoryselection { display: none;} +#directoryselection #filelist { position: absolute; top: 30px; left: 1em; height: 380px; width: 245px; overflow-y: scroll; overflow-x: hidden; white-space: nowrap;} +#directoryselection #filelist a { line-height: 1.5em; } +#directoryselection #filelist a::before, +#imageselection #filelist a.dir::before { + display: inline-block; + background-image: url(../images/folder.gif); + content: ''; + background-size: 1.5em; + height: 1.75em; + width: 1.5em; + padding-right: 5px; + margin-bottom: -5px; + background-repeat: no-repeat; +} + +div#imageselection { display: none;} +#imageselection span { display: block; padding: 0.25em 0 0.5em 0.5em; cursor: pointer; text-decoration: underline; border: 1px solid white;} +#imageselection a.dir span { display: inherit; } +#imageselection #preview { position: absolute; top: 30px; right: 0; height: 340px; width: 340px; margin: 0.1em 0 0 0; padding: 0; border: 0px solid black;} +#imageselection #filelist { position: absolute; top: 30px; left: 1em; height: 380px; width: 245px; overflow-y: scroll; overflow-x: hidden; white-space: nowrap;} + +#configtabs .ui-menu-item ul { max-height: 200px; /* overflow: auto; */ } +#tzmenu {display: none;} +#tzmenu li > a {display: block; width: 100%; line-height: 1.25; color: #aaaaaa !important;} +#tzmenu li > a.ui-state-active {color: black !important; line-height: 1.5 !important; background: url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x !important;} +#tzmenu li > ul { overflow-x: hidden; } +#tzmenu li > ul { -ms-overflow-style: none; scrollbar-width: none; } +#tzmenu li > ul::-webkit-scrollbar { display: none; } + +#tooltip, #cdutooltip { min-height: 300px; min-width: 550px; } + +#tt .available.connected-list { + -ms-overflow-style: none; + scrollbar-width: none; +} +#tt .available.connected-list::-webkit-scrollbar { + display: none; +} + +.customattrsheader { padding-right: 10px; } +#customattrs input, #customattrs select { background-color: transparent; border-style: ridge; } + +/* index */ +.index .table, .index .table .title {background-color: white;} +.index .table .title {font-weight: bold; font-size: 1.25em;} +.index .table div {padding: 3px;} +.rackrequest div:first-child div {background-color: gray;text-align: center;color: white;font-weight: bold;} +.overdue {background-color:#FFE6F4;} +.soon {background-color:#FFFFAA;} +.clear {background-color:white;} + +/* Rack Request Page */ +.request fieldset { + background-color: white; + border: 1px solid grey; + padding: 10px; + margin-bottom: 8px; +} +.request legend {border: 1px ParameterArray['HeaderColor']; ?> solid;background-color: white; padding: .15em;} +.errmsg {display:block;font-style:italic;margin-left:2em;} +.hlight {color: red;} + + + +/* Data Center Stats */ +.dcstats .heading > div{width: 89%;display: inline-block;vertical-align: middle;} +.dcstats .heading > div + div {width: 10%;} +.dcstats .heading > div + div button {display: block;width: 100%;} +.dcstats .table, .dcstats .table .title { background-color: white; } +.dcstats .table .title { font-weight: bold; font-size: 1.25em; } +.dcstats .table .title span { font-size: 0.6em; vertical-align: top;} +.dcstats .table .title span:before { content:" [ "; } +.dcstats .table .title span:after { content:" ]";} +.dcstats .table div {padding: 3px;} +div#dcstats { display: table;} +div#dcstats > div{ width: 100%;} +div#dcstats .table + .table > div > div + div{white-space: pre; text-align: right;} +.canvas {position: relative; background-repeat: no-repeat;} +.canvas img {position: absolute; top: 0; left: 0; z-index: 10;} +.dcstats ~ #tt span {font-size: 1.5em; text-align: center; font-weight: bold;} +.dcstats ~ #tt ul {list-style-type: none;} +.dcstats ~ #tt ul li.red {background: url('../images/rs.png') left center no-repeat; line-height: 20px; padding-left: 20px;} +.dcstats ~ #tt ul li.green {background: url('../images/gs.png') left center no-repeat; line-height: 20px; padding-left: 20px;} +.dcstats ~ #tt ul li.yellow {background: url('../images/ys.png') left center no-repeat; line-height: 20px; padding-left: 20px;} +.dcstats ~ #tt ul li.wtf {background: url('../images/us.png') left center no-repeat; line-height: 20px; padding-left: 20px;} +#maptitle {padding: 8px; font-size: 120%; font-weight: bold;} +#maptitle .nav {float: right; height: 21px;} +#mapCanvas { margin-bottom: 50px; position: relative;} +canvas#background { position: absolute; } + +/* Storage Room */ +.storage .table, .storage .table #title { background: white; } +.storage .table #title {filter: none;} +.storage .table .title { font-weight: bold; font-size: 1.25em; } +.storage .table div {padding: 3px;} +.storage .table {min-width: 400px; max-width: 600px; margin-top: 2em;} + +/* Sidebar Menu */ +#sidebar { + position: relative; + min-width: 200px; + display: inline-block; + vertical-align: top; +} +#sidebar input, #sidebar textarea { + height: 27px; + width: 170px; + margin: 0; + vertical-align: text-bottom; + clear: left; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + padding: 5px; +} +#sidebar textarea { + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + border-style: solid none solid solid; + border-width: 1px 0 1px 1px; + border-color: black; + resize: none; +} +#sidebar input + button, #sidebar .text-core + button, button.iebug, #sidebar textarea + button { + height: 27px; + padding: 0px; + margin: 0px; + display: inline-block; + vertical-align: top; + clear: right; + border-left: 0px solid; + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + -moz-border-radius: 0px; + -moz-box-shadow: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +} +#sidebar form { margin-bottom: 4px; } +#sidebar input.search { height: 15px; padding: 5px; width: 141px; border: 1px solid black; border-right: 0; vertical-align: top;} +#sidebar input + button img, #sidebar .text-arrow + button img {height: 27px;} +#sidebar div.text-core {width: 150px; height: 27px;} +#sidebar div.text-core textarea{ width: 151px; height: 27px;} +#sidebar .advsearch { background: white; display: block; height: 4.5em; position: absolute; top: 0px; width: 350px; z-index: 99; } +#searchadv ~ select { padding: 5px; border: 1px solid black; } +#sidebar .advsearch.hide { display: none; } +#advsrch { color: ParameterArray['LinkColor']; ?>; cursor: pointer; } +#advsrch:before {content:"[ ";} +#advsrch:after {content:" ]";} +#searchadv ~ .ui-icon.ui-icon-close { position: absolute; top: 0; right: 0; cursor: pointer;} + +.text-arrow { + -moz-box-sizing: border-box; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAOAQMAAADHWqTrAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///8yXJnt8Ns4AAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1MzmNZGAwAAABpJREFUCJljYEAF/xsY6hkY7BgYZBgYOFBkADkdAmFDagYFAAAAAElFTkSuQmCC") no-repeat scroll 50% 50% transparent; + cursor: pointer; + height: 22px; + position: absolute; + right: 0; + top: 0; + width: 22px; + z-index: 2; +} +.text-core { display: inline-block; } + +.ui-menu {z-index: 100;} +.ui-autocomplete { max-height: 10em; overflow-y: auto; overflow-x: hidden; padding-right: 20px;} +* html .ui-autocomplete {height: 100px;} +.ui-autocomplete li.ui-menu-item {display: block;} +.ui-menu .ui-menu-item a { line-height: 1 !important; white-space: nowrap !important; overflow: hidden;} +#mapadjust .main .ui-menu .ui-menu-item a { line-height: 1.5 !important;} + +#gandalf { + height: 100%; + width: 100%; + z-index: 99; + background-color: white; + position: absolute; + top: 0; +} +#gandalf div { + font-family: monospace; + white-space: pre; + width: 400px; + margin-left: auto; + margin-right: auto; +} + +/* Mapmaker */ +.mapmaker > div{width: 77%;display: inline-block;vertical-align: middle;} +.mapmaker > div + div {width: 19%;} +.mapmaker .table .table {margin-left: auto;} +.mapmaker + .center div{position: relative;width: 100%;} +.mapmaker + .center > div > div.container {position: absolute;top: 0px;left: 0px;} + +/* Zonemaker */ +.zonemaker > div{width: 100%;display: inline-block;vertical-align: middle;} +.zonemaker .table .table {margin-left: auto;} +.zonemaker + .center div{position: relative;width: 100%;} +.zonemaker + .center > div > div.container {position: absolute;top: 0px;left: 0px;} + +/* templatemaker */ +#regulartemplateattributes, #hiddencdudata, #hiddensensordata {display:inline-block;vertical-align:top;} +.templatemaker > div{width: 100%;display: inline-block;vertical-align: middle;} +.templatemaker .table .table {margin-left: auto;} +.templatemaker + .center div{position: relative;width: 100%;} +.templatemaker + .center > div > div.container {position: absolute; top: 0px; left: 0px;} +.templatemaker input + button, #btn_override, #btn_snmptest { line-height: 1em; vertical-align: middle; height: 1.5em; margin-top: -1px; } +.templatemaker #hiddencoords { position: absolute; left: -10000px; top: -10000px;} +.templatemaker #previewimage { width: 400px;} +.templatemaker fieldset label {padding-right: 1em;padding-left:0.25em;} +.templatemaker #atsbox {border: 1px solid black;padding:0.25em;margin-top: 1em;} +.templatemaker .ui-button { margin: 0; } +.table.front #previewimage, .table.rear #previewimage { position: relative; } +.table.front #coordstable, .table.rear #coordstable { width: 320px; padding-left: 10px;} +#coordstable input { width: 40px; } +#coordstable > .table > div:first-child { text-align: center; } +table.coordinates th {background-color: #CCC; text-align: center; vertical-align: middle; padding-left: .5em; padding-right: .5em; padding-top: .2em;padding-bottom: .2em;} +table.coordinates td {text-align: center; padding-left: .5em; padding-right: .5em; padding-top: 0.1em;padding-bottom: 0.1em;} +table.coordinates input {text-align: center; border: 0px;} +table.coordinates select {text-align: center; border: 0px;} +span.cdudisclaimer {color:red;font-weight:bold;} + +#hiddenports,#hiddenpowerports { position: absolute; left: -10000px; top: -10000px;} +.hiddenports .table { border: 1px solid black; } +.hiddenports .table > div:first-child { text-align: center; background-color: lightgray; border: 1px solid black;} +.hiddenports .table > div { background-color: white; } +.hiddenports .table > div > div { padding: 3px; } + +#rightside { vertical-align: top; } +#img_FrontPictureFile, #img_RearPictureFile { max-width: 125px; max-height: 200px; padding-right: 5px;} + +/* Basic Page Layout */ +.page {position: relative;width: 100%;} +.clear {clear: both;} +p, h2, h3, h1 {margin-top: 1em;margin-bottom: 1em;} +h2 {font-size: 1.5em;text-align: center;} +h3 {font-size: 1.16em;text-align: center;} +h3 + h3 {color: red;font-weight: bold;} +h4 {font-size: 1.1em;text-align: center;} +h3 + h5 {margin-bottom: 0.5em;} +a:link, a:hover, a:visited:hover {color:ParameterArray['LinkColor']; ?>;} +a:visited {color: ParameterArray['VisitedLinkColor']; ?>;} + +div.main { + display: inline-block; + vertical-align: top; + min-height: 500px; + padding: 5px; + background-color: ParameterArray['BodyColor']; ?>; + border: 1px dotted #333; + margin-bottom: 2em; +} +.main > div { + margin-bottom: 2em; +} +div.center > div {display: inline-block;text-align: left;} +.center {text-align: center;min-height: 400px;} +.centermargin {margin-left: auto;margin-right: auto;} + +.table {display: table;text-align: left;border-collapse: collapse;} +.caption {caption-side: bottom; text-align: center; display: table-caption !important; white-space: nowrap;} +.title {caption-side: top; text-align: center; display: table-caption !important;} +div.table > div {display: table-row;} +div.table > div > div {display: table-cell;vertical-align: middle; /* padding-bottom: .75em; */} +/* div.table > div > div span {display: block;font-size: 0.75em;} */ +.table label{width:130px;} +.whiteborder, .whiteborder div {border: 1px solid white;} +.border, .border div {border: 1px solid gray;} + +/* Search Results */ +.search .center {text-align: left;} +.search .main ol, .search .main ul{list-style-type: none;margin-left: 1em;} +.search ol {margin-top: .35em;} +.search ol li {margin-bottom: .35em;} +.search ol ul li {margin-left: 1em;margin-bottom: 0em;} +.search ol ul li div, .search ol li.datacenter div {display: inline;} +.search ol ul li div, .search ol li.cabinet div {display: inline;} +.search ol ul li div img, .search ol li.cabinet div img {vertical-align: middle;height: 1em;margin-right: .25em;} +.search .main .bullet { background: url("minus.gif") no-repeat scroll left center transparent; cursor: pointer; padding-left: 15px;} +.search .hidecontents li.cabinet > ol { display: none; } + +/* User Rights */ +.rights > div:nth-last-child(2) div {text-align: center;padding-top: .75em;padding-bottom: .75em;} +div.table > div + div + div + div > div + div label {float: none;} +#primarycontact {cursor:pointer;} +#deptgroup .ui-multiselect ul.available li { overflow-x: hidden; } + +/* Project Catalog */ +#projectgroup .ui-multiselect ul.available li { padding: 0.5em 0.5em 0.5em 20px; height: auto; line-height: inherit;} + +/* Contact Editor */ +#deletedialog {display: none;} +#deletedialog p {font-weight: bold;} +#deletedialog li {margin-left: 1em; list-style: disc outside none;} +#deletedialog div {width: 45%; display: inline-block; vertical-align: top;} +#deletedialog .middle {width: 9%;} + +/* Inventory Reports */ +.reports fieldset {margin-right: 20px;} +#reports > div {display: inline-block;vertical-align: top;} +#reports > div a {display: block;} + +/* PDU Info */ +.pdu .center > div + div > .table > div > div{padding: 3px;} +.pdu #btn_override { height: 1.2em; line-height: 1em; margin: 3px 0 3px 10px; vertical-align: middle; } + +/* Power Panels */ +div.center > div + div {vertical-align: top;padding-left: 1em;} +div.center > div + div div.table {background-color: white;} +div.center div table { + background-color: white; + border-collapse: collapse; + margin-left: auto; + margin-right: auto; +/* max-width: 400px; */ +} +div.center div table table{min-width: 150px;} +div.center div table, div.center div tr, div.center div td {border: 1px solid gray;} +.cabinet tr > td:first-child, .panelmgr .polenumber {padding: 0.25em 0.5em;text-align: center;} +.panelmgr .polelabel { + min-width: 150px; + max-width: 400px; + padding: 0.25em 0.5em 0.25em 1em; + vertical-align: middle; +} +.panelmgr .main form input, .panelmgr .main form select { + padding-right: 0px; + width: 100%; +} +.polelabel a {display: block;margin-bottom: 0.35em;} +.polelabel a span {display: block;margin-left: 1.5em;} +td#oddeven {padding: 0px;text-align: left;width: 150px;} +.caption h3 {margin-bottom: 0px;font-size: 1.25em;} +#powerinfo {margin-top: 0em;} +#powerinfo .table {background-color: white;} +#powerinfo .caption {border: 0px !important;} +div.error {margin-top: 2em;margin-bottom: 2em;border: 1px dotted gray;} +.error legend {color: red;font-weight: bold;} +.error > div > div {width: 200px;vertical-align: top !important;} +.error > div > div + div {font-style: italic;} +.error span {display: block;margin-left: 1.5em;} +#pdutest {display: none;} +.panelmgr .main form, .panelmgr .main form ~ div { display: inline-block; vertical-align: top;} +.panelmgr .main .center > div { margin-right: 200px; } +.pwr_gauge { position: absolute; right: 50px; top: 0px; } +.pwr_gauge + .pwr_gauge { top: 200px; } +.pwr_gauge + .pwr_gauge + .pwr_gauge { top: 400px; } + +/* Department Administration */ +#groupadmin { + overflow: hidden; + min-width: 580px; + min-height: 150px; + display: none; + margin-top: 20px; + border: 1px solid gray; +} +#deptgroup {background-color: ParameterArray['BodyColor']; ?>;} +#deptgroup > div {padding:5px 10px;width:580px;min-height:300px;} +#deptgroup > div h3 {margin-top: 0;margin-bottom: 5px;} +#deptgroup > div h3 button {margin-left:10px;vertical-align:middle;} +#deptgroup h3 + div {margin-left: 42.5px;} +#deptgroup select {width: 440px;} +#displaynone {display: none !important;} +#cnt_cabinets, #cnt_devices, #cnt_users { cursor: pointer; text-decoration: underline; } + +#projectadmin { + overflow: hidden; + min-width: 700px; + min-height: 150px; + display: none; + margin-top: 20px; + border: 1px solid gray; +} +#projectgroup {background-color: ParameterArray['BodyColor']; ?>;} +#projectgroup > div {padding:5px 10px;width:580px;min-height:300px;} +#projectgroup > div h3 {margin-top: 0;margin-bottom: 5px;} +#projectgroup > div h3 button {margin-left:10px;vertical-align:middle;} +#projectgroup h3 + div {margin-left: 42.5px;} +#projectgroup select {width: 600px;} + +/* Rack Content */ +.legenditem {padding: 0.2em;height: 1.1em;line-height: 1.2em;overflow: hidden;padding: 0.2em;white-space: nowrap;width: 210px;} +.colorbox {width: 1.1em; display: inline-block; vertical-align: text-bottom;height: 1.1em; margin: 0px; padding: 0px;} +#infopanel { + position: relative; + display: inline-block; + max-width: 240px; +} +#infopanel fieldset, .reports fieldset { + background-color: white; + border: 1px solid grey; + padding: 10px; + margin-bottom: 8px; +} +#infopanel fieldset button, #infopanel fieldset input[type=submit], #infopanel fieldset input[type=button],.reports fieldset button, .reports fieldset input[type=submit], .reports fieldset input[type=button] {width: 100%;} +#infopanel legend, .device legend, .reports legend {border: 1px ParameterArray['HeaderColor']; ?> solid;background-color: white;} +div.cabinet { + display: inline-block; + vertical-align: top; + min-width: 200px; + max-width: 250px; + margin-right: 20px; +} + +#servercontainer .dept0, #servercontainer-rear .dept0, #servercontainer-side .dept0 {background-color: #fff;} + +.cabinet .pos { text-align: center; } +/* stupid safari layout glitch */ +.cabinet table.cabinet { border-collapse: collapse; } +.cabinet table.cabinet tr:nth-child(n+3) {height: 21px;} +.cabinet #servercontainer, .cabinet #servercontainer-rear, .cabinet #servercontainer-side { background-image: url("../images/racku-background.png"); position: relative; padding: 0px; margin: 0px;} +.genericdevice {display: flex;justify-content: center; align-items: center; height: 100%; border: 2px black solid; background-color: inherit; overflow: hidden; white-space: nowrap;} + +.cabinet td + td {vertical-align: middle;width: 220px; } +.cabinet td.cabpos {text-align: center; vertical-align: middle;padding: 0.25em 0.5em;width: 10%;} +.cabinet th{font-size: 1.5em;padding: 0.25em;text-align: center;} +#zerou a{display: block;} + +.cabnavigator .nav { text-align: center; } +.cabnavigator .nav li { margin-top: 0.1em; border: 1px solid darkGray;} +.cabnavigator .nav a:hover li { border-color: black; } + +.cabnavigator th a { color: black; text-decoration: none; pointer-events: none; } + +.cabnavigator.tooltip { + min-height: 30px; + min-width: 30px; + z-index: 99; + position: absolute; + white-space: nowrap; +} +.cabnavigator.tooltip div { + border: 0 none; + line-height: 1.25em; + margin: 5px; + padding: 3px; +} +.blackout { background-color: black; } +.rowview .noprint span:last-child {display: none;} +.rowview div.cabinet { vertical-align: bottom; } +.cabinet .error { background-color: ParameterArray['CriticalColor']; ?> !important; } + +/* flippingpostits - START */ +.loader { + width: 100px; + height: 100px; + -webkit-perspective: 100px; + perspective: 100px; + position: absolute; + top: 25%; + left: 50%; + margin-top: -50px; + margin-left: -50px; +} + +.loader__tile { + display: block; + float: left; + width: 33.33%; + height: 33.33%; + -webkit-animation-name: flip; + animation-name: flip; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + -webkit-animation-duration: 2s; + animation-duration: 2s; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + -webkit-transform: rotateY(0deg); + transform: rotateY(0deg); + z-index: 0; +} + +.loader__tile__1 {background-color: #943048;-webkit-animation-delay: 0.1s;animation-delay: 0.1s;} +.loader__tile__2 {background-color: #d7532d;-webkit-animation-delay: 0.2s;animation-delay: 0.2s;} +.loader__tile__3 {background-color: #d2cabb;-webkit-animation-delay: 0.3s;animation-delay: 0.3s;} +.loader__tile__4 {background-color: #9faad0;-webkit-animation-delay: 0.4s;animation-delay: 0.4s;} +.loader__tile__5 {background-color: #b39a3b;-webkit-animation-delay: 0.5s;animation-delay: 0.5s;} +.loader__tile__6 {background-color: #dc2c34;-webkit-animation-delay: 0.6s;animation-delay: 0.6s;} +.loader__tile__7 {background-color: #ece5be;-webkit-animation-delay: 0.7s;animation-delay: 0.7s;} +.loader__tile__8 {background-color: #d07500;-webkit-animation-delay: 0.8s;animation-delay: 0.8s;} +.loader__tile__9 {background-color: #7983a9;-webkit-animation-delay: 0.9s;animation-delay: 0.9s;} + +@-webkit-keyframes flip { + 0% {-webkit-transform: rotateY(0deg); transform: rotateY(0deg); } + 11% {-webkit-transform: rotateY(180deg);transform: rotateY(180deg);} +} + +@keyframes flip { + 0% {-webkit-transform: rotateY(0deg); transform: rotateY(0deg); } + 11% {-webkit-transform: rotateY(180deg);transform: rotateY(180deg);} +} + +/* flippingpostits - END */ +/* spinningsquares - START */ + +.dizzy-gillespie { + -webkit-filter: saturate(3); + filter: saturate(3); + width: 0.1px; + height: 0.1px; + border: 40px solid transparent; + border-radius: 5px; + -webkit-animation: loader 3s ease-in infinite, spin 1s linear infinite; + animation: loader 3s ease-in infinite, spin 1s linear infinite; + position: absolute; + top: 25%; + left: 50%; + margin-top: -50px; + margin-left: -50px; +} + +.dizzy-gillespie::before { + -webkit-filter: saturate(0.3); + filter: saturate(0.3); + display: block; + position: absolute; + z-index: -1; + margin-left: -40px; + margin-top: -40px; + content: ''; + height: 0.1; + width: 0.1; + border: 40px solid transparent; + border-radius: 5px; + animation: loader 2s ease-in infinite reverse, spin 0.8s linear infinite reverse; +} + +.dizzy-gillespie::after { + display: block; + position: absolute; + z-index: 2; + margin-left: -10px; + margin-top: -10px; + content: ''; + height: 20px; + width: 20px; + border-radius: 20px; + background-color: white; +} + +@-webkit-keyframes loader { + 0% {border-bottom-color: transparent;border-top-color: #114357;} + 25% {border-left-color: transparent;border-right-color: #826C75;} + 50% {border-top-color: transparent;border-bottom-color: #F29492;} + 75% {border-right-color: transparent;border-left-color: #826C75;} + 100% {border-bottom-color: transparent;border-top-color: #114357;} +} + +@keyframes loader { + 0% {border-bottom-color: transparent;border-top-color: #114357;} + 25% {border-left-color: transparent;border-right-color: #826C75;} + 50% {border-top-color: transparent;border-bottom-color: #F29492;} + 75% {border-right-color: transparent;border-left-color: #826C75;} + 100% {border-bottom-color: transparent;border-top-color: #114357;} +} +@-webkit-keyframes spin { + 0% {-webkit-transform: rotate(0deg); transform: rotate(0deg);} + 100% {-webkit-transform: rotate(-360deg);transform: rotate(-360deg);} +} +@keyframes spin { + 0% {-webkit-transform: rotate(0deg); transform: rotate(0deg);} + 100% {-webkit-transform: rotate(-360deg);transform: rotate(-360deg);} +} + +/* spinningsquares - END */ +/* multiaxistrainer - START */ + +.preloader { + position: absolute; + margin: -48px 0 0 -48px; + display: block; + position: relative; + width: 90px; + height: 90px; + border: 3px solid #eb1777; + border-radius: 50%; + top: 25%; + left: 50%; + -webkit-animation-delay:0.2s; + animation-delay:0.2s +} + +.preloader:before { + content: ""; + display: block; + position: absolute; + width: 58px; + height: 58px; + border: 3px solid #3bb4e5; + top: 50%; + left: 50%; + margin: -32px 0 0 -32px; + border-radius: 50%; + -webkit-animation-delay:0.4s; + animation-delay:0.4s +} + +.preloader:after { + content: ""; + display: block; + position: absolute; + border: 3px solid #ccdc25; + width: 26px; + height: 26px; + top: 50%; + left: 50%; + margin: -16px 0 0 -16px; + border-radius: 50%; + -webkit-animation-delay:0.6s; + animation-delay:0.6s +} + +.preloader, +.preloader:before, +.preloader:after { + animation-name: Scale; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + animation-direction: alternate; + -webkit-animation-name: Scale; + -webkit-animation-duration: 3s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: ease-in-out; + -webkit-animation-direction: alternate; +} + +@keyframes Scale { + 25% {-webkit-transform: scale(-1.2, 1.2);transform: scale(-1.2, 1.2)} + 50% {-webkit-transform: scale(-1, -1); transform: scale(-1, -1)} + 75% {-webkit-transform: scale(1.2, -1.2);transform: scale(1.2, -1.2)} + 100% {-webkit-transform: scale(1, 1); transform: scale(1, 1)} +} + +@-webkit-keyframes Scale { + 25% {-webkit-transform: scale(-1.2, 1.2)} + 50% {-webkit-transform: scale(-1, -1)} + 75% {-webkit-transform: scale(1.2, -1.2)} +} + +/* multiaxistrainer - END */ +/* rotatingloader - START */ + +@-webkit-keyframes rotate { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} +@-moz-keyframes rotate { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} +@-o-keyframes rotate { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} +@keyframes rotate { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} +} +@-webkit-keyframes rotateCounter { + from {transform: rotate(0deg);} + to {transform: rotate(-360deg);} +} +@-moz-keyframes rotateCounter { + from {transform: rotate(0deg);} + to {transform: rotate(-360deg);} +} +@-o-keyframes rotateCounter { + from {transform: rotate(0deg);} + to {transform: rotate(-360deg);} +} +@keyframes rotateCounter { + from {transform: rotate(0deg);} + to {transform: rotate(-360deg);} +} + +.rotateloader { + display: flex; + flex-flow: column nowrap; + justify-content: center; + max-width: 200px; + margin: 5em; + animation-duration: 2s; + animation-iteration-count: infinite; + animation-timing-function: linear; + position: absolute; + top: 15%; +} +.rotateloader.one {animation-duration: 3s;animation-name: rotate;} +.rotateloader.one .row .box {animation-duration: 1.5s;animation-name: rotateCounter;} +.rotateloader.two {animation-duration: 3s;animation-name: rotate;} +.rotateloader.two .row {animation-duration: 1.5s;animation-name: rotateCounter;} +.rotateloader.two .row .box {animation-duration: 3s;} +.rotateloader.three .row {animation-duration: 2s;animation-name: rotateCounter;} +.rotateloader.four {animation-name: rotate;} +.rotateloader.four .row {animation-duration: 10s;animation-name: rotate;} +.rotateloader.four .row .box {animation-duration: 4s;animation-name: rotateCounter;transform-origin: 50% 75%;} + +.rotateloader .row {animation-duration: 1s;animation-iteration-count: infinite;animation-timing-function: linear;display: flex;justify-content: center;flex-direction: row;} +.rotateloader .row .box {animation-duration: 2s;animation-iteration-count: infinite;animation-timing-function: linear;animation-name: rotate;} + +.box { + width: 20px; + height: 20px; + line-height: 20px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.8); + border-radius: 5px; + margin: 0.2em; + text-align: center; +} +.box.white {background-color: white;} +.box.red {background-color: ParameterArray['BodyColor']; ?>;} +.box.blue {background-color: ParameterArray['HeaderColor']; ?>;} + +/* rotatingloader - END */ +/* rollingbox - START */ +.boxLoading { + width: 50px; + height: 50px; + margin: auto; + position: relative; + left: 0; + right: 0; + top: 25%; + bottom: 0; +} +.boxLoading:before { + content: ''; + width: 50px; + height: 5px; + background: #000; + opacity: 0.1; + position: absolute; + top: 59px; + left: 0; + border-radius: 50%; + animation: shadow .5s linear infinite; +} +.boxLoading:after { + content: ''; + width: 50px; + height: 50px; + background: ParameterArray['HeaderColor']; ?>; + animation: animate .5s linear infinite; + position: absolute; + top: 0; + left: 0; + border-radius: 3px; +} +@keyframes animate { + 17% {border-bottom-right-radius: 3px;} + 25% {transform: translateY(9px) rotate(22.5deg);} + 50% {transform: translateY(18px) scale(1, 0.9) rotate(45deg);border-bottom-right-radius: 40px;} + 75% {transform: translateY(9px) rotate(67.5deg);} + 100% {transform: translateY(0) rotate(90deg);} +} +@keyframes shadow { + 0%, + 100% {transform: scale(1, 1);} + 50% {transform: scale(1.2, 1);} +} +/* rollingbox - END */ + + +/* PICTURES */ +.disabled {pointer-events: none;cursor: default;} +.cabnavigator div.picture {position:relative; left:0px; top:0px; z-index: 5;} +.picture div {position:absolute; z-index: 10; padding: 0 !important;} +.picture .label { + z-index: 11; + text-align: center; + vertical-align: middle; + color: white; + font-family: arial; + text-shadow: 1px 1px 0 #063, 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000; + filter: glow(color=#063,strength=2), alpha(opacity=90); +} +.picture .label { height: 16px; } +.picture .label > div {text-align: center;width: 100%;} +.picture .label > div, +.picture div > a > div > div { top: 10%; height: 80%; padding-left: 0.3em;} +.picture div > a > div > div {overflow: hidden;} +.picture div .label {overflow: hidden;} +.label.noimage { margin: -2px; border: 2px solid black; } +.cabnavigator .picture div img:hover, .cabnavigator .picture a > div:hover { border: 2px solid red; margin: -2px;} +.cabnavigator .picture div img.rlt:hover { margin: -2px 0 0 2px;} + +.picture { + left: 0; + position: relative; + top: 0; + z-index: 5; + display: inline-block; + padding: 0 !important; +} +.picture div { + position: absolute; +} +.picture img { + height: 100%; + width: 100%; + position: absolute; +} +.picture div > a ~ .label { + pointer-events: none; +} +.picture > .label { + text-align: center; + pointer-events: none; +} +.picture > div > .label { + pointer-events: none; +} +.picture > div .label { + top: 0; +} +.label { + display: block; + z-index: 5; + top: 25%; + width: 90%; + height: 13px; + left: 5%; + overflow: hidden; + word-break: break-all; +} +.rotar_d{ + transform:rotate(90deg); + -webkit-transform:rotate(90deg); + -moz-transform:rotate(90deg); + -ms-transform:rotate(90deg); + -o-transform:rotate(90deg);} +.rlt { + transform-origin: left top; + -webkit-transform-origin: left top; + -moz-transform-origin: left top; + -ms-transform-origin: left top; + -o-transform-origin: left top; +} + +/* Cabinet Properties */ + +#infopanel #cabprop { + margin: 0px 0px 2px 0px; + border-collapse: separate; + border: 0px none; + border-spacing: 3px; + width: 100%; + min-width: 200px; max-width: 350px; +} +#infopanel #cabprop td:first-child{ + padding: 3px 2px 3px 2px; + font-weight: bold; + border: 0px none; + border-bottom: 1px solid gray; + margin: 2px 2px 2px 0px; +} +#infopanel #cabprop td:nth-child(2){ + text-align: left; + padding: 3px 2px 3px 4px; + border-style: none solid solid none; + border-width: 0 2px 1px 0; + margin: 2px 0px 2px 2px; +} +#infopanel #cabprop td:nth-child(2) > span { + -webkit-border-radius: 2px; + border-radius: 2px; + box-sizing: border-box; + border: 1px solid #9daccc; + background: #e2e6f0; + padding: 0px 3px 0px 3px; + margin: 0 2px 2px 0; + font: 11px "lucida grande",tahoma,verdana,arial,sans-serif; + display: inline-block; +} + +/* image_management */ +.imagem div.center > div { width: 350px; } +.imagem div.center > div + div { width: 550px; } + +.imagem div.preview { + background-color: #FFFFFF; + border: 1px solid #808080; + height: 300px; + padding: 5px; + width: 500px; + overflow: scroll; +} + +.imagem .preview > div { + border: 1px solid #000000; + display: inline-block; + margin: 3px; + padding: 5px; + position: relative; +} +.imagem .preview > div > .del { + position: absolute; + top: 0; + right: 0; + height: 20px; + width: 20px; + background-image: url('../images/x.gif'); + opacity: .4; + z-index: 5; +} +.imagem .preview > div > div:first-child { + background-size: contain; + height: 100px; + width: 100px; + background-repeat: no-repeat; + background-position: center center; + margin: -1px auto 5px; + padding: 2px; +} +.preview .filename { max-width: 100px; word-break: break-all; } +.imagem .heading { border-bottom: 1px solid; font-size: 2em; margin-bottom: 5px; text-align: right; } + +.uploadifive-queue-item .close { cursor: pointer; } +.uploadifive-button { padding: 0 8px; } + +/* devices.php Device Detail */ +.device fieldset { + display: block; + vertical-align: top; + margin-bottom: 1.5em; + margin-top: 1em; + background-color: white; + border: 1px dotted gray; + padding: 0.25em; +} +//.device fieldset .custom-combobox{margin: 0;padding: 0 0 0 2px;} +.device fieldset .custom-combobox{margin: 0;padding: 0;} +.device fieldset .custom-combobox input{margin: 0;} +.device fieldset .custom-combobox a {padding: 1px 0;position: absolute;} +.device div.right { max-width: 495px; } +.device div.left, .device div.right { + margin-bottom: 1.5em; + display: inline-block; + vertical-align: top; + text-align: left; +} + +.on { color: green; } +.off { color: red; } + +.device #deviceimages > div { width: 355px; margin-left: auto; margin-right: auto; } +.device #deviceimages > div > img { width: 175px; } + +.device #auditdate { line-height: 2em; } + +.device .table {width: 100%;} +.device .table.style > div:nth-child(2n+1) > div {border-top: 1px solid grey;vertical-align: top;} +.device .table.style > div:nth-child(2n+1) > div:first-child {background-color: lightGray;border-left: 1px solid grey;} +.device .table > div > div {min-width: 100px;} +.device .caption {margin-top: 2em;} +.device .table .table .table, .right .table + .table {background-color: white;width: 100%; height: 100%;} +.device .table .table .table > div > div {padding: 3px;} +.right .table + .table {margin-top: 1em;} + +.table.patchpanel > div:first-child > div > div, +.table.switch > div:first-child > div > div, +.table.power > div:first-child > div > div { position: relative; border: 0px none; margin: -3px; padding-right: 20px; } +.table.patchpanel > div:first-child > div select, +.table.switch > div:first-child > div select, +.table.power > div:first-child > div select { position: absolute; top: -3px; right: 0px; } + +.table.patchpanel > div:first-child, .table.switch > div:first-child { white-space: nowrap; } + +.device div[id^="controls"] { border: 0 none; white-space: nowrap; } + +.device .table.patchpanel div[id^="pp"] { border-left: 2px solid black; min-width: 10px;} +.device .table.patchpanel > div:first-child div[id^="pp"], +.device .table.patchpanel > div:first-child div[id^="mt"] {border-top: 1px solid black; } +.device .table.patchpanel > div:last-child div[id^="pp"], +.device .table.patchpanel > div:last-child div[id^="mt"] {border-bottom: 1px solid black; } +.device .table.patchpanel div[id^="pp"]:NOT([id="pp"]) { cursor: pointer; text-decoration: underline; } +.device .table.patchpanel div[id^="mt"] { border-right: 2px solid black; } +.device .table.patchpanel div[id^="pp"], +.device .table.patchpanel div[id^="mt"] { background-color: rgba(211, 211, 211, 0.5);} +.device .table.patchpanel > div > div {min-width: auto;} +.device .table.patchpanel > div:first-child select, +.device .table.switch > div:first-child select, +.device .table.power > div:first-child select { position: absolute; background-color: transparent; border: 0px none; width: auto;} +.device .table.patchpanel > div:first-child select::-moz-focus-inner, +.device .table.patchpanel > div:first-child select:focus::-moz-focus-inner, +.device .table.switch > div:first-child select::-moz-focus-inner, +.device .table.switch > div:first-child select:focus::-moz-focus-inner {border: none;} + +.device .path div { border: 0px none; } +.device .path > div > div { position: relative; height: 1em; } +.device .path > div > div > div { position: absolute; top: 0.15em; min-width: 550px; padding-left: 25px; white-space: nowrap; font-weight: 100; font-size: 0.85em;} +.device .path span:after{ content: "\2192";} +.device .path span:last-child:after{ content: "";} + +#pandn.table span.custom-combobox { width: 100%;} +#pandn.table .custom-combobox input, #pandn.table .custom-combobox a {border-top: 2px; border-bottom: 2px; border-style: inset; width: auto; height: 18px;} +#pandn.table .custom-combobox input {width: calc(100% - 18px);} +#pandn.table .custom-combobox input {background-image: none; border-left: 2px; border-right: 0px; padding-left: 4px; font-size: inherit;} +#pandn.table .custom-combobox a {margin: 0; vertical-align: top; border-left: 0px; border-right: 2px; position: absolute;} + +#olog > div:first-child { border-bottom: 2px solid black; } +#olog > div > div:first-child { width: 100px; padding-right: 5px; white-space: nowrap; } +#olog > div:first-child > div:first-child { border-right: 0 none; } +#olog > div:first-child > div:first-child ~ div { border-left: 0 none; } + +#olog > div:nth-child(2) > div { padding: 0px; } +#olog > div:nth-child(2) > div > div { max-height: 9em; overflow-y: scroll; overflow-x: hidden; border: 0;} + +#olog > div:last-child > div > button { float: right; line-height: 1em; height: 1.75em;} +#olog > div:last-child > div > button ~ div { overflow: hidden; padding-right: 1em; border: 0 none; } +#olog > div:last-child > div > button ~ div > input { width: 100%; } + +#olog .table > div > div ~ div {white-space: pre-wrap; max-width: 800px; word-wrap: break-word;} + +#devicetype-limiter, #connection-limiter { display: inline-block; margin-top: 10px; margin-bottom: 2px; vertical-align: super; } +#devicetype-limiter .ui-button-text-only .ui-button-text, +#connection-limiter .ui-button-text-only .ui-button-text { padding: 0.2em; } +#devicetype-limiter label, #connection-limiter label { width: auto; } + +.device #tags { width: 95%; min-width: 250px;} + +#firstport.hide { display: none; } + +.device fieldset .table label { white-space: nowrap;} + +.device .delete .ui-icon.status.down {cursor: pointer;} +.switch .delete, .patchpanel .delete { border: 0 none; } +.switch.table > div > div, +.power.table > div > div, +.patchpanel.table > div > div { min-width: 0px; } +.switch.table > div > div:first-child, +.patchpanel.table > div > div:first-child { min-width: 15px; } +/* can't explain where the 2px is coming from */ +.switch.table input, .patchpanel.table input { height: 18px; } +.switch.table input, .switch.table select, +.patchpanel.table input, .patchpanel.table select { padding: 0; background-color: transparent;} +.switch.table div[id^=n] input { width:98%; } + +.switch .status, .power .status, .patchpanel .down { background-image: url("../images/portstatus.png");} +.switch .down, .patchpanel .down { background-position: left; } +.switch .up { background-position: right; } +.power .up { background-position: right; } + +.chassis .table input{text-align:center;} +.chassis .table > div > div{text-align:center;} +.chassis .table + .table > div > div{text-align:left;} +.chassis .table > div:first-child > div, .chassis label{font-weight:bold;padding-bottom:0.5em;} +.chassis .table + .table > div > div{min-width:0px;padding-right:0.75em;padding-bottom:0.25em;} +.chassis .table + .table > div > div:first-child, .chassis .table + .table > div > div:nth-child(2){text-align: center;} + +.positionselector {font-size: .7em; background-color: white;} +.positionselector > div > div > div {width: 1em; height: 1em; padding-left: .5em; padding-right: .5em; text-align: right;} +.positionselector > div > div + div > div {width: 3em; padding-right: 1em; padding-right: 1em;} +.notavail {background-color: black; border-color: black !important;} +/* borders were too thick looking */ +.positionselector > div > div > div{ border-top: 0px; border-left: 0px;} +.positionselector > div > div + div > div{ border-top: 0px; border-right: 0px;} +.positionselector > div { border-width: 1px;} +.positionselector, .positionselector > div > div {border-width: 0px;} +#Positionselector .positionselector > div > div {min-width: 0;} +#Positionselector {padding: 10px; position: absolute; left: -1000px; background-color: white; border: 1px solid black; z-index: 99;} + +#editbtn { display: block; margin-bottom: 5px;} +#preview { width: 340px; min-height: 130px; background-color: white; border: 1px solid grey; padding: 5px;} +#preview img { display: block; border: 0px; max-width: 330px;} + +/* hey I do something function */ +.wade{ + position: relative; + width: 250px; + height: 120px; + padding: 0px; + background: #FFFFFF; + -webkit-border-radius: 17px; + -moz-border-radius: 17px; + border-radius: 17px; + border: #000000 solid 1px; +} + +.wade:after{ + content: ''; + position: absolute; + border-style: solid; + border-width: 15px 16px 0; + border-color: #FFFFFF transparent; + display: block; + width: 0; + z-index: 1; + bottom: -15px; + left: 19px; +} + +.wade:before{ + content: ''; + position: absolute; + border-style: solid; + border-width: 15px 16px 0; + border-color: #000000 transparent; + display: block; + width: 0; + z-index: 0; + bottom: -16px; + left: 19px; +} + + + +/* Logging style */ +#logtable { width: 100%; width: calc(100% - 36px); border: 1px solid black; } +#logtable > div:first-child { border-bottom: 1px solid black; font-size: large;} +#logtable > div:nth-child(2n) { background-color: lightgray; border-bottom: 1px dotted black; } +#logtable > div ~ div > div:first-child{ padding: 3px; white-space: nowrap;} +#logtable > div ~ div > div:nth-child(4){ border-left: 2px dotted black; padding-left: 3px; white-space: nowrap;} +#logtable > div ~ div > div:nth-child(5){ text-align: right; } +#logtable > div ~ div > div:nth-child(5):before{ content:"'"; } +#logtable > div ~ div > div:nth-child(5):after{ content:"' => "; } +.logtable > div.ui-dialog-content { overflow-y: auto; overflow-x: hidden; } + +/* Button code primarily from http://somadesign.ca */ +/* Button */ +.button, input[type=button], input[type=submit], button { + text-decoration: none; + border-color:#888; + border-color:rgba(0, 0, 0, 0.56); + cursor: pointer; + outline: none; + color:#111; + display:inline-block; + vertical-align:top; + position:relative; + font-size:12px; + text-align:center; + background-color:#aaa; + background-image:url(gradient.png); + background-image: -moz-linear-gradient(top, rgba(255,255,255,.75), rgba(255,255,255,0)); + background-image: -o-linear-gradient(top, rgba(255,255,255,.75), rgba(255,255,255,0)); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(255,255,255,.75)), to(rgba(255,255,255,0))); + background-image: linear-gradient(top, rgba(255,255,255,.75), rgba(255,255,255,0)); + background-repeat:repeat-x; + text-shadow:1px 1px 0 rgba(255,255,255,.67); + line-height:2; + height:2em; + -moz-box-shadow:1px 1px 0 rgba(255,255,255,.5) inset, -1px -1px 0 rgba(255,255,255,.5) inset; + -webkit-box-shadow:1px 1px 0 rgba(255,255,255,.5) inset, -1px -1px 0 rgba(255,255,255,.5) inset; + box-shadow:1px 1px 0 rgba(255,255,255,.5) inset, -1px -1px 0 rgba(255,255,255,.5) inset; + -webkit-transition: background .185s linear; + -moz-transition: all .185s linear; + -o-transition: all .185s linear; + transition: all .185s linear; + /** Make the text unselectable **/ + -moz-user-select: none; + -webkit-user-select: none; +} +.button, .button:after, button, button:after, input[type=submit], input[type=button], ul.nav li { + -moz-border-radius:4px; + -webkit-border-radius:4px; + border-radius:4px; + border-width:1px; + border-style:solid; +} +.button:after, button:after { + display:block; + position:absolute; + width:100%; + height:100%; + border-color: transparent transparent #ccc; + border-color: transparent transparent rgba(255, 255, 255, 0.67); + bottom:-2px; + left:-1px; +} +.button:hover, .button:focus, button:hover, button:focus, input[type=button]:hover, input[type=button]:focus, input[type=submit]:hover, input[type=submit]:focus { + background-color:#a8c0cb; +} +.button:active, button:active, input[type=submit]:active, input[type=button]:active { + line-height:2.2; + -moz-box-shadow:0 .33em 1em rgba(0,0,0,.67) inset,1px 1px 0 rgba(255,255,255,.25) inset,-1px -1px 0 rgba(255,255,255,.25) inset; + -webkit-box-shadow:0 .33em 2em rgba(0,0,0,.67) inset,1px 1px 0 rgba(255,255,255,.25) inset,-1px -1px 0 rgba(255,255,255,.25) inset; + box-shadow:0 .33em 2em rgba(0,0,0,.67) inset,1px 1px 0 rgba(255,255,255,.25) inset,-1px -1px 0 rgba(255,255,255,.25) inset; + -webkit-transition: line-height .1s linear; + -moz-transition: all .1s linear; + -o-transition: all .1s linear; + transition: all .1s linear; +} +.button.bg, .button.bg:hover, .button.bg:focus, ul.nav li { + background-image:url(gradient.png); + background-image: -moz-linear-gradient(top, rgba(255,255,255,.75), rgba(255,255,255,0)); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(255,255,255,.75)), to(rgba(255,255,255,0))); +} + +/* Put this inside a @media qualifier so Netscape 4 ignores it */ +@media screen, print { + /* Set printouts to landscape */ + @page {size: landscape} + + /* Turn off list bullets */ + ul.mktree li { list-style: none; } + /* Control how "spaced out" the tree is */ + ul.mktree, ul.mktree ul , ul.mktree li { margin-left:5px; padding:0px; } + /* Provide space for our own "bullet" inside the LI */ + ul.mktree li .bullet { padding-left: 15px; } + /* Show "bullets" in the links, depending on the class of the LI that the link's in */ + ul.mktree li.liOpen .bullet { cursor: pointer; background: url(minus.gif) center left no-repeat; } + ul.mktree li.liClosed .bullet { cursor: pointer; background: url(plus.gif) center left no-repeat; } + ul.mktree li.liBullet .bullet { cursor: default; background: url(bullet.gif) center left no-repeat; } + /* Sublists are visible or not based on class of parent LI */ + ul.mktree li.liOpen ul { display: block; } + ul.mktree li.liClosed ul { display: none; } + /* Format menu items differently depending on what level of the tree they are in */ + ul.mktree li { font-family: arial, helvetica; font-size: 11pt; font-weight: bold; } + ul.mktree a.DC { color: #000088; font-weight: bold; } + ul.mktree a.CONTAINER { color: #005500; } + ul.mktree a.ZONE { color: #330066; } + ul.mktree a.CABROW { color: #AA3300; } + ul.mktree a.RACK { color: #660000; } + ul.mktree a { text-decoration: none; white-space: pre;} + ul.mktree a:hover { color: red; } + ul.mktree li ul li { font-family: arial, helvetica; font-size: 11pt; font-weight: normal;} +} +@media print { + .noprint { display: none; } + .page { + page-break-after: always; + } +} +.meter-wrap{position: relative;background-color: lightgrey;overflow:hidden;} +.meter-wrap, .meter-value, .meter-text {width: 210px; height: 1.1em;} +.meter-text { + position: absolute; + top:0; left:0; + padding-top: 0px; + color: #000; + text-align: center; + width: 100%; +} +fieldset[name=pdu] > div > img { vertical-align: text-bottom; } + +/* Supplies */ +.supply .table > div:first-child > div {padding-bottom:0.5em;font-weight: bold;} +.supply .table > div > div {padding-right: 0.25em;} +.supply .table > div > div:first-child {width: 22px;} +.supply .table .quantity {text-align: center;} +.supply .table { margin-bottom: 2em; width: 100%;} +.supply .table select { width: 100%; } +.supply .table:first-child { margin-left: 25px; width: auto;} +.supply .table:first-child > div > div:first-child {width: auto;} +.supply #location {width: 97%;} + +.supply .table ~ .table { background-color: white; } +.supply .table ~ .table > div > div:first-child { width: auto; } +.supply .table ~ .table > div > div { padding: 3px; } + + +/* Installer */ +.installer ul li, ul.nav li{ + display: block; + padding: 1.5em; + background-color: lightGray; + border: 0px solid lightGray; +} +.installer ul li{border: 1px dashed darkGray;} +.installer #sidebar a, .nav a {text-decoration: none;} +.installer #sidebar a:hover li.active, .nav a:hover li.active {background-color: white;border-color: lightGray;} +.installer .active, .nav .active {background-color: white;border: 1px solid darkGray;} +.installer a.active span:first-child, .nav a.active span:first-child {background-position: -144px 0;} +.installer div.table > div > div + div {width: 300px;} +.installer .rights > div:nth-last-child(2) div {padding-top: 0;padding-bottom: 2em;text-align: left;} +.installer #configtabs div.table > div > div + div {width: auto;} +.installer .center #configtabs ~ div input {width: auto;} +div.page.installer {min-width: 1100px;} +div.page.installer .main{max-width: 850px;} + +.installer .ui-multiselect ul li { padding: 0.5em 0.5em 0.5em 20px; height: auto; line-height: inherit;} + + +/* Menu */ +ul.nav li {padding: .5em;} +.nav a:visited {color: #000000;} +#sidebar .nav li a { display: block;} +#sidebar .nav .ui-state-focus { + background: white; + border-color: black; + border-width: 1px; + border-style: solid; + margin: 0; +} + +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #aaaaaa !important; + background: url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x !important; + font-weight: normal !important; + color: rgb(51, 51, 51) !important; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121 !important; +} +.ui-state-active .ui-icon, .ui-button:active .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png") !important; +} +.active.ui-state-focus, .active.ui-state-active { + margin: 0px !important; +} + +/* Search Export */ +div.center div table#export { margin: auto; max-width: none; } +#export_wrapper a.dt-button { margin-right: 0px; } + + +/* Paths */ +/* Paths form */ +fieldset.crit_busc {border: 1px solid grey; padding:0.5em; background-color: #EEEEEE;} +fieldset.crit_busc legend {background-color: white; padding:0.5em; border: 1px solid grey;} +table#crit_busc {border: 0px; background: transparent; padding:0.5em;} +table#crit_busc tr {border: 0px; background: transparent; padding:0.5em;} +table#crit_busc td {border: 0px; background: transparent; padding:0.5em;} + +table#parcheos {border: 3px outset; text-align: center; text-valign: center; max-width: 800px; margin-left: auto; margin-right: auto;} +table#parcheos tr {border: 0px;} +table#parcheos td {padding: 0px; border: 0px; vertical-align: top;} + +#parcheos .f-right {background: url("../images/a2f.png") no-repeat #FFF; width:25px;} +#parcheos .f-left {background: url("../images/a1f.png") no-repeat #FFF; width:25px;} +#parcheos .r-right {background: url("../images/a2r.png") no-repeat #FFF; width:25px;} +#parcheos .r-left {background: url("../images/a1r.png") no-repeat #FFF; width:25px;} + +#parcheos .base-f, #parcheos .base-r {background: url("../images/b0f.png") no-repeat top left #FFF; height: 5px; padding: 0px; border: 0px;} + +#parcheos .connection-f-1 {background: url("../images/b1f.png") no-repeat #FFF;} +#parcheos .connection-f-2 {background: url("../images/b2f.png") no-repeat #FFF; width:25px;} +#parcheos .connection-f-3 {background: url("../images/b3f.png") no-repeat #FFF; height:30px;} +#parcheos .connection-f-4 {background: url("../images/b4f.png") no-repeat top right #FFF; height:30px;} +#parcheos .connection-r-1 {background: url("../images/b1r.png") no-repeat #FFF;} +#parcheos .connection-r-2 {background: url("../images/b2r.png") no-repeat #FFF; width:25px;} +#parcheos .connection-r-3 {background: url("../images/b3r.png") no-repeat #FFF; height:30px;} +#parcheos .connection-r-4 {background: url("../images/b4r.png") no-repeat top right #FFF; height:30px;} + +table#parcheos table tr + tr > td + td{background-color:yellow;} +table#parcheos table {margin: 0px; border: 0px; border-collapse: collapse; text-align: left; vertical-align: middle; min-width: 50px; white-space: nowrap;} +table#parcheos table tr th {background-color: #DDDDDD; padding: 2px; border: 1px solid grey; text-align: left; border-collapse: collapse;} +table#parcheos table tr td {padding: 2px; border: 1px solid grey; text-align: left; border-collapse: collapse;} +table#parcheos tr td:first-child + td table {margin-left: auto;} + +p.errormsg {padding: 20px; background-color: #DDDDDD; font-size: 120%; font-weight: bold; color: red;} + +/* feature managementHDD */ +#addHDDModal { + display: none; + position: fixed; + z-index: 1000; + left: 0; top: 0; width: 100%; height: 100%; + background-color: rgba(0,0,0,0.5); + } + #addHDDModal .modal-content { + background-color: #fff; + margin: 10% auto; + padding: 20px; + border: 1px solid #888; + width: 400px; + border-radius: 8px; + } + + .responsive-table { +overflow-x: auto; +align: center; +} +.table2{ + margin: 0 auto; + width: 750px; + align: center; + table { + border-collapse: collapse; + border: 1px; + } + th, td { + padding: 1px; + text-align: center; + vertical-align: middle; + } + th { + background-color:rgba(242, 242, 242, 0.78); + } +} diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 9263f60e8..e9742366b 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6183,6 +6183,97 @@ msgstr "" #~ msgid "Outlet Status" #~ msgstr "Statut de la prise électrique" +# New strings for HDD CSV automation and logging +msgid "Process CSV Batch" +msgstr "Traiter un lot CSV" + +msgid "Import CSV for Automated Destruction" +msgstr "Importer un CSV pour l'automatisation de la destruction" + +msgid "CSV file (UTF-8, max 2 MB)" +msgstr "Fichier CSV (UTF-8, max 2 Mo)" + +msgid "Destruction proof file (PDF / Excel / ODS, max 5 MB)" +msgstr "Fichier de preuve de destruction (PDF / Excel / ODS, max 5 Mo)" + +msgid "Apply destroyed status and date to matching HDDs" +msgstr "Appliquer le statut détruit et la date aux HDD correspondants" + +msgid "Destruction date (applied to all matching HDDs)" +msgstr "Date de destruction (appliquée à tous les HDD correspondants)" + +msgid "Optional note / reference" +msgstr "Note / référence optionnelle" + +msgid "Reference or ticket number" +msgstr "Référence ou numéro de ticket" + +msgid "Download log" +msgstr "Télécharger le journal" + +msgid "Please select the CSV column containing serial numbers" +msgstr "Veuillez sélectionner la colonne CSV contenant les numéros de série" + +msgid "Some serial numbers were not recognized. Continue processing the others?" +msgstr "Certains numéros de série n'ont pas été reconnus. Continuer avec les autres ?" + +msgid "An error occurred while processing the CSV." +msgstr "Une erreur est survenue lors du traitement du CSV." + +msgid "CSV file is required" +msgstr "Le fichier CSV est requis" + +msgid "Error while uploading the CSV file" +msgstr "Erreur lors du transfert du fichier CSV" + +msgid "CSV file is too large (max 2 MB)" +msgstr "Le fichier CSV est trop volumineux (max 2 Mo)" + +msgid "Unable to read CSV file or file is empty" +msgstr "Impossible de lire le fichier CSV ou le fichier est vide" + +msgid "Selected column was not found in the CSV header" +msgstr "La colonne sélectionnée est introuvable dans l'en-tête CSV" + +msgid "No serial numbers were found in the selected column" +msgstr "Aucun numéro de série n'a été trouvé dans la colonne sélectionnée" + +msgid "Please select a destruction date when applying the destroyed status" +msgstr "Veuillez sélectionner une date de destruction lors de l'application du statut détruit" + +msgid "Invalid destruction date format (expected YYYY-MM-DD)" +msgstr "Format de date de destruction invalide (format attendu AAAA-MM-JJ)" + +msgid "Please provide a destruction date when applying the destroyed status" +msgstr "Veuillez fournir une date de destruction lors de l'application du statut détruit" + +msgid "Serial numbers already processed! Only valid serials were handled." +msgstr "Numéros de série déjà traités ! Seuls les numéros valides seront pris en compte." + +msgid "No matching HDD serial numbers were available for processing" +msgstr "Aucun numéro de série HDD correspondant n'est disponible pour le traitement" + +msgid "Processed %d serial numbers." +msgstr "Traitement de %d numéros de série." + +msgid "Skipped %d unknown serial numbers." +msgstr "Ignoré %d numéros de série inconnus." + +msgid "Proof file assigned to matching HDDs." +msgstr "Fichier de preuve appliqué aux HDD correspondants." + +msgid "CSV batch - Notes: %s | Processed: %s | Already processed: %s | Unknown: %s" +msgstr "Lot CSV - Notes : %s | Traités : %s | Déjà traités : %s | Inconnus : %s" + +msgid "Bulk destroy of %d HDD(s). IDs: %s" +msgstr "Destruction en lot de %d HDD. ID : %s" + +msgid "Audit certified for this device." +msgstr "Audit certifié pour cet équipement." + +msgid "Details" +msgstr "Détails" + #~ msgid "Outlet Status On State" #~ msgstr "Statut de la prise électrique en l'état" diff --git a/managementhdd.php b/managementhdd.php new file mode 100644 index 000000000..947cec27f --- /dev/null +++ b/managementhdd.php @@ -0,0 +1,743 @@ +ManageHDD) { + header("Location: index.php"); exit; +} + +// 4. Récupère DeviceID +$DeviceID = filter_input(INPUT_GET, 'DeviceID', FILTER_VALIDATE_INT); +if (!$DeviceID) { + echo __('DeviceID is required'); exit; +} +$dev = new Device(); $dev->DeviceID = $DeviceID; +if (!$dev->GetDevice()) { + echo __('Invalid DeviceID'); exit; +} +// Load Template +$template = new DeviceTemplate(); +$template->TemplateID = $dev->TemplateID; +$template->GetTemplateByID(); +$template->LoadHDDConfig(); + +if (!$template->EnableHDDFeature) { + echo '
'.__("This equipment does not support HDD management.").'
'; + exit; +} +// Get lists +$hddList = HDD::GetHDDByDevice($dev->DeviceID); +$hddWaitList = HDD::GetPendingByDevice($dev->DeviceID); +$hdddestroyedList = HDD::GetDestroyedHDDByDevice($dev->DeviceID); +$hddSpareList = HDD::GetSpareHDDByDevice($dev->DeviceID); +$lastAudit = HDD::GetLastAudit($dev->DeviceID); +$proofPathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; +$proofWebBase = rtrim($proofPathSetting, '/') . '/'; + +$assignableDevices = []; +$assignSql = " + SELECT d.DeviceID, d.Label, d.SerialNo, cfg.HDDCount, + COALESCE(active.ActiveCount, 0) AS ActiveCount + FROM fac_Device d + INNER JOIN fac_DeviceTemplateHdd cfg ON cfg.TemplateID = d.TemplateID AND cfg.EnableHDDFeature = 1 + LEFT JOIN ( + SELECT DeviceID, COUNT(*) AS ActiveCount + FROM fac_HDD + WHERE Status IN ('On','Off') + GROUP BY DeviceID + ) active ON active.DeviceID = d.DeviceID + WHERE d.DeviceID <> :DeviceID + ORDER BY d.Label ASC"; +$assignStmt = $dbh->prepare($assignSql); +$assignStmt->execute([':DeviceID' => $dev->DeviceID]); +while ($row = $assignStmt->fetch(PDO::FETCH_ASSOC)) { + $capacity = intval($row['HDDCount']); + $used = intval($row['ActiveCount']); + $freeSlots = $capacity - $used; + if ($capacity > 0 && $freeSlots > 0) { + $assignableDevices[] = [ + 'DeviceID' => intval($row['DeviceID']), + 'Label' => $row['Label'], + 'SerialNo' => $row['SerialNo'] ?? '', + 'FreeSlots' => $freeSlots + ]; + } +} + +if (!function_exists('build_hdd_proof_url')) { + /** + * Build a public URL for a stored proof file value. + */ + function build_hdd_proof_url($storedValue, $webBase) { + $value = trim((string)$storedValue); + if ($value === '') { + return ''; + } + if (preg_match('#^(?:[a-z]+:)?//#i', $value) === 1 || strpos($value, '/') === 0) { + return $value; + } + if (preg_match('#^[A-Za-z]:\\\\#', $value) === 1 || strpos($value, '/') !== false || strpos($value, '\\') !== false) { + return $value; + } + return $webBase . $value; + } +} + +?> + + + + + + openDCIM Data Center Inventory + + + + + + + + +
+ +
+
+

Label, ENT_QUOTES, 'UTF-8'); ?>

+ '.htmlspecialchars($_SESSION['LastError'], ENT_QUOTES, 'UTF-8').'
'; unset($_SESSION['LastError']); } ?> + '.htmlspecialchars($_SESSION['Message'], ENT_QUOTES, 'UTF-8').'
'; unset($_SESSION['Message']); } ?> +
+ + +

+ + + + + + + + + + + + + + + + +HDDID; + echo ' + + + + + + + + + '; + $i++; +} +?> + +
#
'.$i.' + + + + +
+ +

+ + + +

+ +

+ + + + + + + + + + + + + +HDDID; + echo ' + + + + + + + '; + $i++; +} +?> + +
#
'.$i.' '.htmlspecialchars($hdd->SerialNo, ENT_QUOTES, "UTF-8").' '.htmlspecialchars($hdd->Status, ENT_QUOTES, "UTF-8").' '.$hdd->DateWithdrawn.' + + + +
+ +

+ + +

+ +

+ + + + + + + + + + + + HDDID; + $proofUrl = build_hdd_proof_url($hdd->ProofFile ?? '', $proofWebBase); + $proofLink = $proofUrl !== '' ? ''.__('View proof').'' : ''; + echo ' + + + + + + '; + $i++; + } + ?> + +
#
'.$i.' '.htmlspecialchars($hdd->SerialNo, ENT_QUOTES, "UTF-8").' '.htmlspecialchars($hdd->Status, ENT_QUOTES, "UTF-8").' '.$hdd->DateDestroyed.''.$proofLink.'
+ +

+ + + + + + + + + + + + +HDDID; + echo ' + + + + + + + '; + $i++; +} +?> + +
#
'.$i.' '.htmlspecialchars($hdd->SerialNo, ENT_QUOTES, "UTF-8").' '.htmlspecialchars($hdd->Status, ENT_QUOTES, "UTF-8").' '.$hdd->DateWithdrawn.' + + + + +
+ +
+ + + 0){ + echo '
+
+
+
'; + } + if($lastAudit){ + echo '

'.sprintf(__('Last HDD audit: %s (%s)'), date('Y-m-d H:i', strtotime($lastAudit['AuditTime'])), htmlspecialchars($lastAudit['DisplayName'], ENT_QUOTES, 'UTF-8')).'
'; + } + ?> + +
+
+ 0){ + print " [ ".__("Return to Parent Device")." ]
\n"; + print " GetDeviceCabinetID()."\">[ ".__("Return to Navigator")." ]
\n"; + print " GetDeviceDCID()."\">[ ".__("Return to DC")." ]"; + }else{ + print "
[ ".__("Return index")." ]
"; + } + ?> +
+ +
+ + + + + + + + + + + + + + + diff --git a/readme_feat_Hdd.txt b/readme_feat_Hdd.txt new file mode 100644 index 000000000..02d35d802 --- /dev/null +++ b/readme_feat_Hdd.txt @@ -0,0 +1,72 @@ +# HDD Workflow Automation Guide + +## 1. Activation & Acc�s +- Assurez-vous que la fonctionnalit� HDD est activ�e ( eature_hdd = enabled). +- Le rapport est disponible depuis **Reports > Asset Reports** si l�option est active. +- Les pages managementhdd.php et report_hdd.php n�cessitent l�autorisation ManageHDD dans droit utilisateur. +- Activé la fonctionnalité par modél d'équipement dans devicetemplate. + +## 2. Vue Gestion HDD (managementhdd.php) +- Bouton **Certify Audit HDD** : enregistre une entr�e d�audit (visible dans View Log, Devices, Reports). +- Bouton **View HDD Activity Log** : affiche toutes les actions HDD_BULK_DESTROY, HDD_CSV_BATCH, HDD_Audit. +- Boutons **Destroy Selected** / **Export all to Excel** fonctionnent via le formulaire principal manageHddForm. + +## 3. Rapport �HDD Management Report� (report_hdd.php) +### 3.1 Destruction classique +1. S�lectionner des HDD. +2. Cliquer sur **Add destruction proof** -> uploader un PDF/Excel/ODS et, si besoin, cocher Apply destroyed status... + date. +3. Soumettre : chaque HDD re�oit le fichier de preuve; si l�option est coch�e, statut/dates sont mis � jour. +4. Les actions sont journalis�es (HDD_BULK_DESTROY) avec heure/utilisateur. + +### 3.2 Traitement CSV automatis� +1. Cliquer sur **Process CSV Batch**. +2. Choisir un fichier CSV (UTF-8, max 2 Mo). +3. Une fois le fichier charg�, s�lectionner la **colonne contenant les serial numbers**. +4. (Optionnel) Fournir un fichier de preuve commun + cocher l�application du statut/dates. +5. (Optionnel) Remplir le champ **Note/Reference** (appara�t dans le log). +6. Soumettre : + - Les SN d�j� d�truits sont ignor�s et signal�s (message + log). + - Les SN inconnus provoquent une demande de confirmation avant de continuer. + - Seuls les SN valides re�oivent la preuve / statut. + - Un fichier .txt est t�l�charg� automatiquement, r�capitulant la note, les SN trait�s/ignor�s et l�horodatage. + - Une entr�e HDD_CSV_BATCH est ajout�e dans ac_GenericLog (trace utilisateur + JSON r�capitulatif). + +## 4. Consultation des journaux (hdd_log_view.php) +- Accessible via le bouton **View HDD Activity Log** (ou directement hdd_log_view.php?DeviceID=...). +- Colonne **Action** : HDD_BULK_DESTROY, HDD_CSV_BATCH, HDD_Audit (ou futurs types). +- Colonne **Details** : + - Bulk destroy : nombre de HDD et IDs. + - CSV batch : note, liste des SN trait�s / d�j� trait�s / inconnus. + - Audit : simple confirmation. + +## 5. Messages & Traductions +- Tous les textes des modales/boutons/messages ont une traduction fran�aise dans locale/fr_FR/LC_MESSAGES/openDCIM.po (section �New strings for HDD CSV automation and logging�). + +## 6. Points de vigilance / �volutions pr�vues +- Les CSV doivent contenir au moins une colonne SN ; chaque colonne est analys�e apr�s upload (d�limiteurs auto : , ; tab |). +- Les futures �volutions pr�vues incluent l�import natif d�OCS Inventory pour afficher l��tat des disques (On/Off), faciliter maintenance/destruction et int�grer un flux 100% automatis�. +## 7. API REST HDD +Pour prparer lautomatisation (OCS ou autres), quatre routes REST ont t ajoutes. Toutes ncessitent le droit `ManageHDD` (ou `SiteAdmin`) et les en-ttes dauthentification habituels. + +### 7.1 GET /api/v1/hdd +- Paramtres optionnels : `DeviceID`, `HDDID` (valeur ou liste spare par virgules), `Status` (On, Off, Pending_destruction, Destroyed, Spare), `SerialNo` (recherche partielle). +- Retour : tableau de modles `HDD`. + +### 7.2 GET /api/v1/hdd/{HDDID} +- Retour : dtail du disque cibl. + +### 7.3 GET /api/v1/hdd/{HDDID}/proof +- Retour : JSON avec `ProofFile`, URL publique et chemin disque si le fichier existe. Aucun upload via API (GET uniquement). + +### 7.4 PUT /api/v1/hdd +- Cre un disque sur un quipement. Champs requis : `DeviceID`, `SerialNo`. Champs optionnels : `Status`, `TypeMedia`, `Size`. +- Contrle automatique du nombre de slots (message slot hdd is full si le device est plein). + +### 7.5 POST /api/v1/hdd/{HDDID} +- Met jour SerialNo/Status/TypeMedia/Size ou raffecte le disque un autre DeviceID (slots vrifis). + +### 7.6 DeviceTemplate & People +- `DeviceTemplate` expose dsormais `EnableHDDFeature` et `HDDCount` via GET/POST/PUT. +- `People` expose le boolen `ManageHDD` pour activer laccs API. + +La description complte (modles, exemples) est disponible dans `api/docs/swagger.yaml`, section `HDD`. diff --git a/report_hdd.php b/report_hdd.php new file mode 100644 index 000000000..8c72384eb --- /dev/null +++ b/report_hdd.php @@ -0,0 +1,508 @@ +ManageHDD || $person->SiteAdmin || $person->ReadAccess)) { + echo __('This report requires global read access.'); + header('Refresh: 3; url=' . redirect()); + exit; +} + +// Subheader for template +$subheader = __('HDD Management Report'); + +// 1) Build SQL: HDD en attente de destruction (avec contexte site) +$sql = " + SELECT + h.HDDID, + h.DeviceID, + d.Label AS DeviceLabel, + dc.Name AS SiteName, + h.SerialNo, + h.Status, + h.Size, + h.TypeMedia, + h.DateAdd, + h.DateWithdrawn, + h.DateDestroyed, + h.ProofFile + FROM fac_HDD h + LEFT JOIN fac_Device d ON d.DeviceID = h.DeviceID + LEFT JOIN fac_Cabinet c ON c.CabinetID = d.Cabinet + LEFT JOIN fac_DataCenter dc ON dc.DataCenterID = c.DataCenterID +"; + +// Execute query +$stmt = $dbh->prepare($sql); +$stmt->execute(); +$hddList = $stmt->fetchAll(PDO::FETCH_OBJ); +$proofPathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; +$proofWebBase = rtrim($proofPathSetting, '/') . '/'; + +if (!function_exists('build_hdd_proof_url')) { + /** + * Build a public URL for a stored proof file value. + */ + function build_hdd_proof_url($storedValue, $webBase) { + $value = trim((string)$storedValue); + if ($value === '') { + return ''; + } + if (preg_match('#^(?:[a-z]+:)?//#i', $value) === 1 || strpos($value, '/') === 0) { + return $value; + } + if (preg_match('#^[A-Za-z]:\\\\#', $value) === 1 || strpos($value, '/') !== false || strpos($value, '\\') !== false) { + return $value; + } + return $webBase . $value; + } +} +?> + + + + + <?php echo htmlspecialchars($subheader, ENT_QUOTES); ?> + + + + + + + + + + + + + + + +
+ +
+
+

+ '.htmlspecialchars($_SESSION['LastError'], ENT_QUOTES, 'UTF-8').'
'; unset($_SESSION['LastError']); } ?> + '.htmlspecialchars($_SESSION['Message'], ENT_QUOTES, 'UTF-8').'
'; unset($_SESSION['Message']); } ?> + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SiteName ?? '', ENT_QUOTES) ?>DeviceID, ENT_QUOTES) ?>DeviceLabel ?? '',ENT_QUOTES) ?>HDDID, ENT_QUOTES) ?>SerialNo, ENT_QUOTES) ?>Status, ENT_QUOTES) ?>Size, ENT_QUOTES) ?>TypeMedia, ENT_QUOTES) ?>DateAdd ?? '', ENT_QUOTES) ?>DateWithdrawn ?? '',ENT_QUOTES) ?>DateDestroyed ?? '',ENT_QUOTES) ?> + ProofFile ?? '', $proofWebBase); + if ($proofUrl !== ''): + ?> + + +
+ + + + + + + +
+ + + + + + diff --git a/reports.php b/reports.php index 40f99f381..09c367229 100644 --- a/reports.php +++ b/reports.php @@ -52,7 +52,11 @@ ',__("Warranty Expiration Report"),' ',__("Virtual Machines by Department"),' ',__("Network Map"),' - ', __("Vendor/Model Report"),' + ', __("Vendor/Model Report"),''; + if($config->ParameterArray['feature_hdd'] == 'enabled'){ + echo ' ', __("Report HDD"),''; + } +echo '
diff --git a/savehdd.php b/savehdd.php new file mode 100644 index 000000000..ba6932554 --- /dev/null +++ b/savehdd.php @@ -0,0 +1,294 @@ +ManageHDD) { + header("Location: index.php"); + exit; +} + +$deviceID = isset($_POST['DeviceID']) ? intval($_POST['DeviceID']) : 0; + +if (!$deviceID) { + header("Location: index.php"); + exit; +} + +$action = $_POST['action'] ?? ''; +$customDestroyDate = trim($_POST['custom_destroy_date'] ?? ''); +$customDestroyDate = ($customDestroyDate === '') ? null : $customDestroyDate; +$targetDeviceID = isset($_POST['target_device_id']) ? intval($_POST['target_device_id']) : 0; + +if (!function_exists('logHddManagementAction')) { + function logHddManagementAction(string $actionName, array $details = []): void { + global $deviceID, $person; + $payload = ''; + if (!empty($details)) { + $payload = json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($payload === false) { + $payload = ''; + } + } + HDD::RecordGenericLog($deviceID, $person->UserID, $actionName, $payload); + } +} + +try { + switch (true) + { // Création d’un nouveau HDD depuis le modal + case $action === 'create_hdd_form': + // Récupération et sanitation des champs + $serialNo = $_POST['SerialNo'] ?? ''; + $typeMedia = $_POST['TypeMedia']?? ''; + $size = intval($_POST['Size'] ?? 0); + + // Création via instance pour inclure le champ Note + $hdd = new HDD(); + $hdd->DeviceID = $deviceID; + $hdd->SerialNo = $serialNo; + $hdd->Status = 'On'; + $hdd->TypeMedia = $typeMedia; + $hdd->Size = $size; + $hdd->Create(); + logHddManagementAction('HDD_CREATE', [ + 'hdd_id' => $hdd->HDDID, + 'serial' => $hdd->SerialNo, + 'type' => $hdd->TypeMedia, + 'size' => $hdd->Size + ]); + break; + + case preg_match('/^update_(\d+)$/', $action, $m) === 1: + $id = intval((int)$m[1]); + // Récupère l’objet complet (avec tous les champs) + $hdd = HDD::GetHDDByID($id); + if (!$hdd) { + throw new Exception("HDDID {$id} introuvable."); + } + // Ne mettez à jour QUE ce qui vient du formulaire + $hdd->SerialNo = $_POST['SerialNo'][$id] ?? $hdd->SerialNo; + $hdd->Status = $_POST['Status'][$id] ?? $hdd->Status; + $hdd->TypeMedia = $_POST['TypeMedia'][$id]?? $hdd->TypeMedia; + $hdd->Size = intval($_POST['Size'][$id] ?? $hdd->Size); + // Maintenant vous avez déjà StatusDestruction, Note, DateAdd, etc. + $hdd->MakeSafe(); + if ($hdd->Update()) { + logHddManagementAction('HDD_UPDATE', [ + 'hdd_id' => $hdd->HDDID, + 'serial' => $hdd->SerialNo, + 'status' => $hdd->Status, + 'type' => $hdd->TypeMedia, + 'size' => $hdd->Size + ]); + } + break; + + case preg_match('/^remove_(\d+)$/', $action, $m) === 1: + $removeId = intval((int)$m[1]); + $hdd = HDD::GetHDDByID($removeId); + if ($hdd && $hdd->SendForDestruction()) { + logHddManagementAction('HDD_SEND_FOR_DESTRUCTION', [ + 'ids' => [$removeId], + 'count' => 1, + 'source' => 'single' + ]); + } + break; + + case preg_match('/^delete_(\d+)$/', $action, $m) === 1: + $deleteId = intval((int)$m[1]); + if (HDD::DeleteByID($deleteId)) { + logHddManagementAction('HDD_DELETE', [ + 'ids' => [$deleteId], + 'count' => 1 + ]); + } + break; + + case preg_match('/^duplicate_(\d+)$/', $action, $m) === 1: + $sourceId = intval((int)$m[1]); + $newIds = HDD::DuplicateToEmptySlots($sourceId); + if (!empty($newIds)) { + logHddManagementAction('HDD_DUPLICATE', [ + 'source_id' => $sourceId, + 'count' => count($newIds), + 'new_ids' => $newIds + ]); + } + break; + + case preg_match('/^destroy_(\d+)$/', $action, $m) === 1: + $destroyId = intval((int)$m[1]); + if (HDD::MarkDestroyed($destroyId, $customDestroyDate)) { + $details = [ + 'ids' => [$destroyId], + 'count' => 1, + 'source' => 'single' + ]; + if ($customDestroyDate) { + $details['destroy_date'] = $customDestroyDate; + } + logHddManagementAction('HDD_DESTROY', $details); + } + break; + + case preg_match('/^reassign_(\d+)$/', $action, $m) === 1: + $reassignId = intval((int)$m[1]); + $targetId = ($targetDeviceID > 0) ? $targetDeviceID : $deviceID; + if ($targetId <= 0) { + throw new Exception(__('Invalid target device for reassignment.')); + } + if ($targetDeviceID > 0 && HDD::GetRemainingSlotCount($targetId) <= 0) { + $_SESSION['LastError'] = __('slot hdd is full'); + break; + } + if (HDD::ReassignToDevice($reassignId, $targetId)) { + logHddManagementAction('HDD_REASSIGN', [ + 'hdd_id' => $reassignId, + 'target_device' => $targetId + ]); + $targetLabel = ''; + $targetDeviceObj = new Device(); + $targetDeviceObj->DeviceID = $targetId; + if ($targetDeviceObj->GetDevice()) { + $targetLabel = $targetDeviceObj->Label; + } + if ($targetDeviceID > 0 && $targetLabel !== '') { + $_SESSION['Message'] = sprintf(__('HDD transfered in (%s)'), $targetLabel); + } elseif ($targetDeviceID > 0) { + $_SESSION['Message'] = __('HDD reassigned successfully'); + } + } else { + if ($targetDeviceID > 0 && HDD::GetRemainingSlotCount($targetId) <= 0) { + $_SESSION['LastError'] = __('slot hdd is full'); + } else { + $_SESSION['LastError'] = __('Unable to reassign HDD'); + } + } + break; + + case preg_match('/^spare_(\d+)$/', $action, $m) === 1: + $spareId = intval((int)$m[1]); + if (HDD::MarkAsSpare($spareId)) { + logHddManagementAction('HDD_MARK_SPARE', [ + 'hdd_id' => $spareId + ]); + } + break; + + case $action === "bulk_remove": + $removedIds = []; + // $_POST['select_active'] contient un tableau d'IDs cochés + foreach ($_POST['select_active'] ?? [] as $id) { + $intId = intval($id); + // Récupère l'objet complet pour préserver ses autres propriétés + if ($intId > 0 && ($hdd = HDD::GetHDDByID($intId)) && $hdd->SendForDestruction()) { + $removedIds[] = $intId; + } + } + if (!empty($removedIds)) { + logHddManagementAction('HDD_BULK_REMOVE', [ + 'ids' => $removedIds, + 'count' => count($removedIds) + ]); + } + break; + + case $action === "bulk_delete": + $deletedIds = []; + foreach ($_POST['select_active'] ?? [] as $id) { + $intId = intval($id); + if ($intId > 0 && HDD::DeleteByID($intId)) { + $deletedIds[] = $intId; + } + } + if (!empty($deletedIds)) { + logHddManagementAction('HDD_BULK_DELETE', [ + 'ids' => $deletedIds, + 'count' => count($deletedIds) + ]); + } + break; + + case $action === "bulk_destroy": + $pendingSelected = $_POST['select_pending_destroyed'] ?? ($_POST['select_pending'] ?? []); + $destroyedIds = []; + foreach ($pendingSelected as $id) { + $intId = intval($id); + if ($intId > 0 && HDD::MarkDestroyed($intId, $customDestroyDate)) { + $destroyedIds[] = $intId; + } + } + if (!empty($destroyedIds)) { + $details = [ + 'ids' => $destroyedIds, + 'count' => count($destroyedIds), + 'source' => 'bulk_destroy' + ]; + if ($customDestroyDate) { + $details['destroy_date'] = $customDestroyDate; + } + logHddManagementAction('HDD_BULK_DESTROY', $details); + } + break; + + case $action === "bulk_destroyFromActive": + $activeSelected = $_POST['select_active'] ?? []; + $destroyedActive = []; + foreach ($activeSelected as $id) { + $intId = intval($id); + if ($intId > 0 && HDD::MarkDestroyed($intId, $customDestroyDate)) { + $destroyedActive[] = $intId; + } + } + if (!empty($destroyedActive)) { + $details = [ + 'ids' => $destroyedActive, + 'count' => count($destroyedActive), + 'source' => 'bulk_destroy_active' + ]; + if ($customDestroyDate) { + $details['destroy_date'] = $customDestroyDate; + } + logHddManagementAction('HDD_BULK_DESTROY', $details); + } + break; + + case $action === "export_list": + // Export XLS complet en 3 feuilles + logHddManagementAction('HDD_EXPORT', [ + 'mode' => 'full_device', + 'format' => 'xls' + ]); + HDD::ExportAllToXls($deviceID); + // (la méthode se termine par exit()) + break; + + case $action === "certify_audit": + if (HDD::RecordAudit($deviceID, $person->UserID)) { + $_SESSION['Message'] = __('HDD audit recorded successfully'); + } else { + $_SESSION['LastError'] = __('Unable to record HDD audit'); + } + break; + + default: + throw new Exception("Action inconnue : “{$action}”."); + } + +} catch (\PDOException $e) { + echo "

Erreur base de données :

" . htmlentities($e->getMessage()) . "
"; + exit; +} catch (\Exception $e) { + echo "

Erreur :

" . htmlentities($e->getMessage()) . "
"; + exit; +} + +// Redirect to the HDD management page +header('Location: managementhdd.php?DeviceID=' . urlencode($deviceID)); +exit; diff --git a/upload_hdd_proof.php b/upload_hdd_proof.php new file mode 100644 index 000000000..ab7911d17 --- /dev/null +++ b/upload_hdd_proof.php @@ -0,0 +1,164 @@ +ManageHDD) { + header('Location: index.php'); + exit; +} + +$return = $_POST['return'] ?? ($_SERVER['HTTP_REFERER'] ?? 'index.php'); +$isAjax = !empty($_POST['ajax']); + +try { + // IDs selection + $ids = $_POST['hdd_ids'] ?? []; + if (!is_array($ids) || count($ids) === 0) { + throw new Exception(__('No HDD selected')); + } + $ids = array_values(array_unique(array_map('intval', $ids))); + + // File presence and PHP upload error + if (!isset($_FILES['proof_pdf'])) { + throw new Exception(__('File upload error')); + } + if ($_FILES['proof_pdf']['error'] !== UPLOAD_ERR_OK) { + $e = $_FILES['proof_pdf']['error']; + $map = [ + UPLOAD_ERR_INI_SIZE => __('File too large (max 5 MB)'), + UPLOAD_ERR_FORM_SIZE => __('File too large (max 5 MB)'), + UPLOAD_ERR_PARTIAL => __('File upload error'), + UPLOAD_ERR_NO_FILE => __('No file provided'), + UPLOAD_ERR_NO_TMP_DIR => __('Temporary directory is missing'), + UPLOAD_ERR_CANT_WRITE => __('Unable to write the file to disk'), + UPLOAD_ERR_EXTENSION => __('Upload blocked by a PHP extension'), + ]; + throw new Exception($map[$e] ?? __('File upload error')); + } + + $file = $_FILES['proof_pdf']; + + // Size <= 5 MiB + if ($file['size'] > 5 * 1024 * 1024) { + throw new Exception(__('File too large (max 5 MB)')); + } + + $allowedExtensions = [ + 'pdf' => ['application/pdf'], + 'xls' => ['application/vnd.ms-excel', 'application/octet-stream'], + 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/octet-stream'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet', 'application/zip', 'application/octet-stream'], + ]; + + $originalExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!array_key_exists($originalExtension, $allowedExtensions)) { + throw new Exception(__('File type not allowed (PDF, XLS, XLSX or ODS only)')); + } + + // MIME check + if (!class_exists('finfo')) { + throw new Exception(__('The fileinfo PHP extension is not available on this server')); + } + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($file['tmp_name']); + if (!in_array($mime, $allowedExtensions[$originalExtension], true)) { + throw new Exception(__('File type not allowed (PDF, XLS, XLSX or ODS only)')); + } + + $applyDestroyStatus = !empty($_POST['apply_destroy_status']); + $destroyDateInput = trim($_POST['destroy_date'] ?? ''); + $destroyDateValue = null; + if ($applyDestroyStatus) { + if ($destroyDateInput === '') { + throw new Exception(__('Please select a destruction date when applying the destroyed status')); + } + $dateObject = DateTime::createFromFormat('Y-m-d', $destroyDateInput); + if (!$dateObject) { + throw new Exception(__('Invalid destruction date format (expected YYYY-MM-DD)')); + } + $destroyDateValue = $dateObject->format('Y-m-d'); + } + + // Normalized file name + $datePart = date('Ymd-His'); + $randPart = substr(bin2hex(random_bytes(4)), 0, 8); + $targetName = "proof_{$datePart}_{$randPart}.{$originalExtension}"; + + // Storage: use configured path for filesystem + public URL + $pathSetting = $config->ParameterArray['hdd_proof_path'] ?? 'assets/files/hdd/'; + $publicBase = rtrim($pathSetting, '/') . '/'; + + // Resolve filesystem path from configuration setting + $storageRoot = $pathSetting; + if (preg_match('#^(?:[A-Za-z]:\\\\|/)#', $storageRoot) === 1) { + $baseDir = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $storageRoot), DIRECTORY_SEPARATOR); + } else { + $baseDir = rtrim(__DIR__ . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, trim($storageRoot, '/\\')), DIRECTORY_SEPARATOR); + } + + if (!is_dir($baseDir)) { + if (!@mkdir($baseDir, 0750, true)) { + throw new Exception(__('Unable to create the storage directory') . ' : ' . $baseDir); + } + } + if (!is_writable($baseDir)) { + throw new Exception(__('Storage directory is not writable: ') . $baseDir); + } + + $destPath = $baseDir . DIRECTORY_SEPARATOR . $targetName; + if (!@move_uploaded_file($file['tmp_name'], $destPath)) { + throw new Exception(__('File upload error (move_uploaded_file)')); + } + @chmod($destPath, 0644); + + // Update DB for all selected IDs with the stored filename only + global $dbh; + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $sql = "UPDATE fac_HDD SET ProofFile = ? WHERE HDDID IN ($placeholders)"; + $params = array_merge([$targetName], $ids); + $stmt = $dbh->prepare($sql); + $stmt->execute($params); + $updated = $stmt->rowCount(); + if ($updated <= 0) { + throw new Exception(__('No database rows were updated (check ProofFile column and IDs)')); + } + + if ($applyDestroyStatus) { + $statusPlaceholders = implode(',', array_fill(0, count($ids), '?')); + $statusSql = "UPDATE fac_HDD SET Status = 'Destroyed', DateDestroyed = ? WHERE HDDID IN ($statusPlaceholders)"; + $statusParams = array_merge([$destroyDateValue], $ids); + $statusStmt = $dbh->prepare($statusSql); + $statusStmt->execute($statusParams); + } + + $successMessage = __('Destruction proof uploaded successfully'); + $publicPath = $publicBase . $targetName; + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode(['success' => true, 'message' => $successMessage, 'path' => $publicPath]); + exit; + } + + $_SESSION['Message'] = $successMessage; +} catch (Exception $ex) { + error_log('[upload_hdd_proof] ' . $ex->getMessage()); + if ($isAjax) { + header('Content-Type: application/json', true, 400); + echo json_encode(['success' => false, 'error' => $ex->getMessage()]); + exit; + } + $_SESSION['LastError'] = $ex->getMessage(); +} + +header('Location: ' . $return); +exit; +?> diff --git a/usermgr.php b/usermgr.php index b9c23e541..7b513abe4 100644 --- a/usermgr.php +++ b/usermgr.php @@ -47,6 +47,7 @@ $userRights->RackAdmin=(isset($_POST['RackAdmin']))?1:0; $userRights->BulkOperations=(isset($_POST['BulkOperations']))?1:0; $userRights->SiteAdmin=(isset($_POST['SiteAdmin']))?1:0; + $userRights->ManageHDD=(isset($_POST['ManageHDD']))?1:0; $userRights->Disabled=(isset($_POST['Disabled']))?1:0; if($_POST['action']=='Create'){ @@ -87,6 +88,7 @@ $RackAdmin=($userRights->RackAdmin)?"checked":""; $BulkOperations=($userRights->BulkOperations)?"checked":""; $admin=($userRights->SiteAdmin)?"checked":""; + $ManageHDD=($userRights->ManageHDD)?"checked":""; $Disabled=($userRights->Disabled)?"checked":""; ?> @@ -328,6 +330,7 @@ function showdept(){


+