mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-13 18:58:52 +01:00
Compare commits
7 Commits
update-kic
...
copilot/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebcda8b75a | ||
|
|
17e83a9da5 | ||
|
|
815b2b6c7d | ||
|
|
3aa7480fe4 | ||
|
|
b508d57094 | ||
|
|
2769772877 | ||
|
|
aed1c4dc09 |
5
.env
5
.env
@@ -71,11 +71,6 @@ DISABLE_WEB_UPDATES=1
|
||||
# Restoring backups is a destructive operation that could overwrite your database.
|
||||
DISABLE_BACKUP_RESTORE=1
|
||||
|
||||
# Disable backup download from the Update Manager UI (0=enabled, 1=disabled).
|
||||
# Backups contain sensitive data including password hashes and secrets.
|
||||
# When enabled, users must confirm their password before downloading.
|
||||
DISABLE_BACKUP_DOWNLOAD=1
|
||||
|
||||
###################################################################################
|
||||
# SAML Single sign on-settings
|
||||
###################################################################################
|
||||
|
||||
@@ -45,7 +45,6 @@ export default class extends Controller {
|
||||
maxItems: 1,
|
||||
createOnBlur: true,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
@@ -23,8 +23,6 @@ export default class extends Controller {
|
||||
valueField: "id",
|
||||
labelField: "name",
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
preload: "focus",
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
|
||||
@@ -49,7 +49,6 @@ export default class extends Controller {
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
|
||||
@@ -35,8 +35,6 @@ export default class extends Controller {
|
||||
maxItems: 1000,
|
||||
allowEmptyOption: true,
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
plugins: ['remove_button'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export default class extends Controller {
|
||||
searchField: 'text',
|
||||
orderField: 'text',
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
|
||||
@@ -58,7 +58,6 @@ export default class extends Controller {
|
||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||
splitOn: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
searchField: [
|
||||
{field: "text", weight : 2},
|
||||
|
||||
@@ -49,7 +49,6 @@ export default class extends Controller {
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
};
|
||||
|
||||
if(this.element.dataset.autocomplete) {
|
||||
|
||||
@@ -75,7 +75,6 @@ export default class extends Controller
|
||||
searchField: "name",
|
||||
//labelField: "name",
|
||||
valueField: "name",
|
||||
clearAfterSelect: true,
|
||||
onItemAdd: this.onItemAdd.bind(this),
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
@@ -137,4 +136,4 @@ export default class extends Controller
|
||||
//Destroy the TomSelect instance
|
||||
this._tomSelect.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,11 +177,6 @@
|
||||
"allow-contrib": false,
|
||||
"require": "7.4.*",
|
||||
"docker": true
|
||||
},
|
||||
"phpstan/extension-installer": {
|
||||
"ignore" : [
|
||||
"ekino/phpstan-banned-code"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
382
composer.lock
generated
382
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -144,18 +144,6 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
||||
* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email
|
||||
notification. You have to configure the mail provider first before via the MAILER_DSN setting.
|
||||
|
||||
### Update manager settings
|
||||
* `DISABLE_WEB_UPDATES` (default `1`): Set this to 0 to enable web-based updates. When enabled, you can perform updates
|
||||
via the web interface in the update manager. This is disabled by default for security reasons, as it can be a risk if
|
||||
not used carefully. You can still use the CLI commands to perform updates, even when web updates are disabled.
|
||||
* `DISABLE_BACKUP_RESTORE` (default `1`): Set this to 0 to enable backup restore via the web interface. When enabled, you can
|
||||
restore backups via the web interface in the update manager. This is disabled by default for security reasons, as it can
|
||||
be a risk if not used carefully. You can still use the CLI commands to perform backup restores, even when web-based
|
||||
backup restore is disabled.
|
||||
* `DISABLE_BACKUP_DOWNLOAD` (default `1`): Set this to 0 to enable backup download via the web interface. When enabled, you can download backups via the web interface
|
||||
in the update manager. This is disabled by default for security reasons, as it can be a risk if not used carefully, as
|
||||
the downloads contain sensitive data like password hashes or secrets.
|
||||
|
||||
### Table related settings
|
||||
|
||||
* `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260307204859 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Increase the length of the vendor_barcode field in part_lots to 1000 characters and update the index accordingly';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode(100))');
|
||||
$this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode LONGTEXT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode)');
|
||||
$this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM part_lots');
|
||||
$this->addSql('DROP TABLE part_lots');
|
||||
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, vendor_barcode CLOB DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM __temp__part_lots');
|
||||
$this->addSql('DROP TABLE __temp__part_lots');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots');
|
||||
$this->addSql('DROP TABLE part_lots');
|
||||
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots');
|
||||
$this->addSql('DROP TABLE __temp__part_lots');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
|
||||
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX part_lots_idx_barcode');
|
||||
$this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE TEXT');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX part_lots_idx_barcode');
|
||||
$this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE VARCHAR(255)');
|
||||
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
# Manually configure ekino/phpstan-banned-code to detect usage of echo, eval, die/exit, print, shell execution and a set of functions that should not be used in production code.
|
||||
|
||||
parametersSchema:
|
||||
banned_code: structure([
|
||||
nodes: listOf(structure([
|
||||
type: string()
|
||||
functions: schema(listOf(string()), nullable())
|
||||
]))
|
||||
use_from_tests: bool()
|
||||
non_ignorable: bool()
|
||||
])
|
||||
|
||||
parameters:
|
||||
banned_code:
|
||||
nodes:
|
||||
# enable detection of echo
|
||||
-
|
||||
type: Stmt_Echo
|
||||
functions: null
|
||||
|
||||
# enable detection of eval
|
||||
-
|
||||
type: Expr_Eval
|
||||
functions: null
|
||||
|
||||
# enable detection of die/exit
|
||||
-
|
||||
type: Expr_Exit
|
||||
functions: null
|
||||
|
||||
# enable detection of a set of functions
|
||||
-
|
||||
type: Expr_FuncCall
|
||||
functions:
|
||||
- dd
|
||||
- debug_backtrace
|
||||
- dump
|
||||
- exec
|
||||
- passthru
|
||||
- phpinfo
|
||||
- print_r
|
||||
- proc_open
|
||||
- shell_exec
|
||||
- system
|
||||
- var_dump
|
||||
|
||||
# enable detection of print statements
|
||||
-
|
||||
type: Expr_Print
|
||||
functions: null
|
||||
|
||||
# enable detection of shell execution by backticks
|
||||
-
|
||||
type: Expr_ShellExec
|
||||
functions: null
|
||||
|
||||
# enable detection of empty()
|
||||
#-
|
||||
# type: Expr_Empty
|
||||
# functions: null
|
||||
|
||||
# enable detection of `use Tests\Foo\Bar` in a non-test file
|
||||
use_from_tests: true
|
||||
|
||||
# when true, errors cannot be excluded
|
||||
non_ignorable: false
|
||||
|
||||
services:
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedNodesRule
|
||||
tags:
|
||||
- phpstan.rules.rule
|
||||
arguments:
|
||||
- '%banned_code.nodes%'
|
||||
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedUseTestRule
|
||||
tags:
|
||||
- phpstan.rules.rule
|
||||
arguments:
|
||||
- '%banned_code.use_from_tests%'
|
||||
|
||||
-
|
||||
class: Ekino\PHPStanBannedCode\Rules\BannedNodesErrorBuilder
|
||||
arguments:
|
||||
- '%banned_code.non_ignorable%'
|
||||
@@ -1,6 +1,3 @@
|
||||
includes:
|
||||
- phpstan.banned_code.neon
|
||||
|
||||
parameters:
|
||||
|
||||
level: 5
|
||||
@@ -9,6 +6,9 @@ parameters:
|
||||
- src
|
||||
# - tests
|
||||
|
||||
banned_code:
|
||||
non_ignorable: false # Allow to ignore some banned code
|
||||
|
||||
excludePaths:
|
||||
- src/DataTables/Adapter/*
|
||||
- src/Configuration/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated on Mon Mar 9 04:23:25 UTC 2026
|
||||
# Generated on Tue Mar 3 14:26:21 UTC 2026
|
||||
# This file contains all footprints available in the offical KiCAD library
|
||||
Audio_Module:Reverb_BTDR-1H
|
||||
Audio_Module:Reverb_BTDR-1V
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated on Mon Mar 9 04:24:12 UTC 2026
|
||||
# Generated on Tue Mar 3 14:27:05 UTC 2026
|
||||
# This file contains all symbols available in the offical KiCAD library
|
||||
4xxx:14528
|
||||
4xxx:14529
|
||||
@@ -22324,7 +22324,6 @@ Transistor_FET:PSMN5R2-60YL
|
||||
Transistor_FET:QM6006D
|
||||
Transistor_FET:QM6015D
|
||||
Transistor_FET:Q_Dual_NMOS_G1S2G2D2S1D1
|
||||
Transistor_FET:Q_Dual_NMOS_PMOS_G1S2G2D2S1D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1D2S2G2D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D1
|
||||
Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D2D1D1
|
||||
|
||||
@@ -290,23 +290,6 @@ final class PartController extends AbstractController
|
||||
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
|
||||
}
|
||||
|
||||
$lotAmount = $request->query->get('lotAmount');
|
||||
$lotName = $request->query->get('lotName');
|
||||
$lotUserBarcode = $request->query->get('lotUserBarcode');
|
||||
|
||||
if ($lotAmount !== null || $lotName !== null || $lotUserBarcode !== null) {
|
||||
$partLot = new PartLot();
|
||||
$partLot->setAmount($lotAmount !== null ? (float)$lotAmount : 0);
|
||||
$partLot->setDescription($lotName !== null ? (string)$lotName : '');
|
||||
$partLot->setUserBarcode($lotUserBarcode !== null ? (string)$lotUserBarcode : '');
|
||||
|
||||
$new_part->addPartLot($partLot);
|
||||
|
||||
$this->addFlash('notice', t('part.create_from_info_provider.lot_filled_from_barcode'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
return $this->renderPartForm('new', $request, $new_part, [
|
||||
'info_provider_dto' => $dto,
|
||||
]);
|
||||
|
||||
@@ -23,21 +23,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\System\BackupManager;
|
||||
use App\Services\System\InstallationTypeDetector;
|
||||
use App\Services\System\UpdateChecker;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use Shivas\VersioningBundle\Service\VersionManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
@@ -54,14 +49,10 @@ class UpdateManagerController extends AbstractController
|
||||
private readonly UpdateExecutor $updateExecutor,
|
||||
private readonly VersionManagerInterface $versionManager,
|
||||
private readonly BackupManager $backupManager,
|
||||
private readonly InstallationTypeDetector $installationTypeDetector,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
#[Autowire(env: 'bool:DISABLE_WEB_UPDATES')]
|
||||
private readonly bool $webUpdatesDisabled = false,
|
||||
#[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')]
|
||||
private readonly bool $backupRestoreDisabled = false,
|
||||
#[Autowire(env: 'bool:DISABLE_BACKUP_DOWNLOAD')]
|
||||
private readonly bool $backupDownloadDisabled = false,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -85,16 +76,6 @@ class UpdateManagerController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backup download is disabled and throw exception if so.
|
||||
*/
|
||||
private function denyIfBackupDownloadDisabled(): void
|
||||
{
|
||||
if ($this->backupDownloadDisabled) {
|
||||
throw new AccessDeniedHttpException('Backup download is disabled by server configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main update manager page.
|
||||
*/
|
||||
@@ -120,8 +101,6 @@ class UpdateManagerController extends AbstractController
|
||||
'backups' => $this->backupManager->getBackups(),
|
||||
'web_updates_disabled' => $this->webUpdatesDisabled,
|
||||
'backup_restore_disabled' => $this->backupRestoreDisabled,
|
||||
'backup_download_disabled' => $this->backupDownloadDisabled,
|
||||
'is_docker' => $this->installationTypeDetector->isDocker(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -227,7 +206,6 @@ class UpdateManagerController extends AbstractController
|
||||
#[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])]
|
||||
public function startUpdate(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfWebUpdatesDisabled();
|
||||
|
||||
@@ -336,126 +314,12 @@ class UpdateManagerController extends AbstractController
|
||||
return $this->json($details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a manual backup.
|
||||
*/
|
||||
#[Route('/backup', name: 'admin_update_manager_backup', methods: ['POST'])]
|
||||
public function createBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_backup', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
if ($this->updateExecutor->isLocked()) {
|
||||
$this->addFlash('error', 'Cannot create backup while an update is in progress.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->backupManager->createBackup(null, 'manual');
|
||||
$this->addFlash('success', 'update_manager.backup.created');
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Backup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup file.
|
||||
*/
|
||||
#[Route('/backup/delete', name: 'admin_update_manager_backup_delete', methods: ['POST'])]
|
||||
public function deleteBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename');
|
||||
if ($filename && $this->backupManager->deleteBackup($filename)) {
|
||||
$this->addFlash('success', 'update_manager.backup.deleted');
|
||||
} else {
|
||||
$this->addFlash('error', 'update_manager.backup.delete_error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an update log file.
|
||||
*/
|
||||
#[Route('/log/delete', name: 'admin_update_manager_log_delete', methods: ['POST'])]
|
||||
public function deleteLog(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename');
|
||||
if ($filename && $this->updateExecutor->deleteLog($filename)) {
|
||||
$this->addFlash('success', 'update_manager.log.deleted');
|
||||
} else {
|
||||
$this->addFlash('error', 'update_manager.log.delete_error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a backup file.
|
||||
* Requires password confirmation as backups contain sensitive data (password hashes, secrets, etc.).
|
||||
*/
|
||||
#[Route('/backup/download', name: 'admin_update_manager_backup_download', methods: ['POST'])]
|
||||
public function downloadBackup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfBackupDownloadDisabled();
|
||||
|
||||
if (!$this->isCsrfTokenValid('update_manager_download', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'Invalid CSRF token.');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
// Verify password
|
||||
$password = $request->request->get('password', '');
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User || !$this->passwordHasher->isPasswordValid($user, $password)) {
|
||||
$this->addFlash('error', 'update_manager.backup.download.invalid_password');
|
||||
return $this->redirectToRoute('admin_update_manager');
|
||||
}
|
||||
|
||||
$filename = $request->request->get('filename', '');
|
||||
$details = $this->backupManager->getBackupDetails($filename);
|
||||
if (!$details) {
|
||||
throw $this->createNotFoundException('Backup not found');
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($details['path']);
|
||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $details['file']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup.
|
||||
*/
|
||||
#[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])]
|
||||
public function restore(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
$this->denyAccessUnlessGranted('@system.manage_updates');
|
||||
$this->denyIfBackupRestoreDisabled();
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ declare(strict_types=1);
|
||||
namespace App\DataTables;
|
||||
|
||||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\EnumColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Helpers\PartDataTableHelper;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\ManufacturingStatus;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\EntityURLGenerator;
|
||||
@@ -147,19 +145,6 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||
'orderField' => 'NATSORT(manufacturer.name)',
|
||||
])
|
||||
|
||||
->add('manufacturing_status', EnumColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.manufacturingStatus'),
|
||||
'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(),
|
||||
'class' => ManufacturingStatus::class,
|
||||
'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string {
|
||||
if ($status === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translator->trans($status->toTranslationKey());
|
||||
},
|
||||
])
|
||||
|
||||
->add('mountnames', TextColumn::class, [
|
||||
'label' => 'project.bom.mountnames',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
|
||||
@@ -66,7 +66,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Table(name: 'part_lots')]
|
||||
#[ORM\Index(columns: ['instock_unknown', 'expiration_date', 'id_part'], name: 'part_lots_idx_instock_un_expiration_id_part')]
|
||||
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
|
||||
#[ORM\Index(name: 'part_lots_idx_barcode', columns: ['vendor_barcode'], options: ['lengths' => [100]])]
|
||||
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
|
||||
#[ValidPartLot]
|
||||
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[ApiResource(
|
||||
@@ -81,7 +81,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
denormalizationContext: ['groups' => ['part_lot:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["description", "comment", "user_barcode"])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["description", "comment"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ['instock_unknown', 'needs_refill'])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ['amount'])]
|
||||
@@ -166,8 +166,9 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
||||
/**
|
||||
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
|
||||
*/
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::TEXT, nullable: true)]
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
|
||||
#[Groups(['part_lot:read', 'part_lot:write'])]
|
||||
#[Length(max: 255)]
|
||||
protected ?string $user_barcode = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,8 +42,6 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
|
||||
private const ALREADY_CALLED = 'STRUCTURAL_DENORMALIZER_ALREADY_CALLED';
|
||||
|
||||
private const PARENT_ELEMENT = 'STRUCTURAL_DENORMALIZER_PARENT_ELEMENT';
|
||||
|
||||
private array $object_cache = [];
|
||||
|
||||
public function __construct(
|
||||
@@ -91,59 +89,37 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
|
||||
$context[self::ALREADY_CALLED][] = $data;
|
||||
|
||||
//In the first step, denormalize without children
|
||||
$context_without_children = $context;
|
||||
$context_without_children['groups'] = array_filter(
|
||||
$context_without_children['groups'] ?? [],
|
||||
static fn($group) => $group !== 'include_children',
|
||||
);
|
||||
//Also unset any parent element, to avoid infinite loops. We will set the parent element in the next step, when we denormalize the children
|
||||
unset($context_without_children[self::PARENT_ELEMENT]);
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
$entity = $this->denormalizer->denormalize($data, $type, $format, $context_without_children);
|
||||
|
||||
//Assign the parent element to the denormalized entity, so it can be used in the denormalization of the children (e.g. for path generation)
|
||||
if (isset($context[self::PARENT_ELEMENT]) && $context[self::PARENT_ELEMENT] instanceof $entity && $entity->getID() === null) {
|
||||
$entity->setParent($context[self::PARENT_ELEMENT]);
|
||||
}
|
||||
/** @var AbstractStructuralDBElement $deserialized_entity */
|
||||
$deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context);
|
||||
|
||||
//Check if we already have the entity in the database (via path)
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
$repo = $this->entityManager->getRepository($type);
|
||||
|
||||
$path = $entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
$path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
$db_elements = $repo->getEntityByPath($path, AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
if ($db_elements !== []) {
|
||||
//We already have the entity in the database, so we can return it
|
||||
$entity = end($db_elements);
|
||||
return end($db_elements);
|
||||
}
|
||||
|
||||
|
||||
//Check if we have created the entity in this request before (so we don't create multiple entities for the same path)
|
||||
//Entities get saved in the cache by type and path
|
||||
//We use a different cache for this then the objects created by a string value (saved in repo). However, that should not be a problem
|
||||
//unless the user data has mixed structure between JSON data and a string path
|
||||
//unless the user data has mixed structure between json data and a string path
|
||||
if (isset($this->object_cache[$type][$path])) {
|
||||
$entity = $this->object_cache[$type][$path];
|
||||
} else {
|
||||
//Save the entity in the cache
|
||||
$this->object_cache[$type][$path] = $entity;
|
||||
return $this->object_cache[$type][$path];
|
||||
}
|
||||
|
||||
//In the next step we can denormalize the children, and add our children to the entity.
|
||||
if (in_array('include_children', $context['groups'], true) && isset($data['children']) && is_array($data['children'])) {
|
||||
foreach ($data['children'] as $child_data) {
|
||||
$child_entity = $this->denormalize($child_data, $type, $format, array_merge($context, [self::PARENT_ELEMENT => $entity]));
|
||||
if ($child_entity !== null && !$entity->getChildren()->contains($child_entity)) {
|
||||
$entity->addChild($child_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
//Save the entity in the cache
|
||||
$this->object_cache[$type][$path] = $deserialized_entity;
|
||||
|
||||
//We don't have the entity in the database, so we have to persist it
|
||||
$this->entityManager->persist($entity);
|
||||
$this->entityManager->persist($deserialized_entity);
|
||||
|
||||
return $entity;
|
||||
return $deserialized_entity;
|
||||
}
|
||||
|
||||
public function getSupportedTypes(?string $format): array
|
||||
|
||||
@@ -202,7 +202,6 @@ class KiCadHelper
|
||||
"exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false),
|
||||
"exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false),
|
||||
"exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? false),
|
||||
"description" => $part->getDescription(),
|
||||
"fields" => []
|
||||
];
|
||||
|
||||
|
||||
@@ -219,6 +219,11 @@ class EntityImporter
|
||||
$entities = [$entities];
|
||||
}
|
||||
|
||||
//The serializer has only set the children attributes. We also have to change the parent value (the real value in DB)
|
||||
if ($entities[0] instanceof AbstractStructuralDBElement) {
|
||||
$this->correctParentEntites($entities, null);
|
||||
}
|
||||
|
||||
//Set the parent of the imported elements to the given options
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
@@ -292,14 +297,6 @@ class EntityImporter
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
private function persistRecursively(AbstractStructuralDBElement $entity): void
|
||||
{
|
||||
$this->em->persist($entity);
|
||||
foreach ($entity->getChildren() as $child) {
|
||||
$this->persistRecursively($child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deserializes the given file and writes the entities to the database (and flush the db).
|
||||
* The imported elements will be checked (validated) before written to database.
|
||||
@@ -325,11 +322,7 @@ class EntityImporter
|
||||
|
||||
//Iterate over each $entity write it to DB (the invalid entities were already filtered out).
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
$this->persistRecursively($entity);
|
||||
} else {
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
|
||||
//Save changes to database, when no error happened, or we should continue on error.
|
||||
@@ -407,7 +400,7 @@ class EntityImporter
|
||||
*
|
||||
* @param File $file The Excel file to convert
|
||||
* @param string $delimiter The CSV delimiter to use
|
||||
*
|
||||
*
|
||||
* @return string The CSV data as string
|
||||
*/
|
||||
protected function convertExcelToCsv(File $file, string $delimiter = ';'): string
|
||||
@@ -428,7 +421,7 @@ class EntityImporter
|
||||
]);
|
||||
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
|
||||
for ($row = 1; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
|
||||
@@ -438,7 +431,7 @@ class EntityImporter
|
||||
try {
|
||||
$cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue();
|
||||
$rowData[] = $cellValue ?? '';
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Error reading cell value', [
|
||||
'cell' => "{$col}{$row}",
|
||||
@@ -491,4 +484,21 @@ class EntityImporter
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This functions corrects the parent setting based on the children value of the parent.
|
||||
*
|
||||
* @param iterable $entities the list of entities that should be fixed
|
||||
* @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set
|
||||
*/
|
||||
protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void
|
||||
{
|
||||
foreach ($entities as $entity) {
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
$entity->setParent($parent);
|
||||
//Do the same for the children of entity
|
||||
$this->correctParentEntites($entity->getChildren(), $entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,7 @@ final readonly class BarcodeScanResultHandler
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
//So far we can just copy over our provider info array to the URL parameters:
|
||||
return $this->urlGenerator->generate('info_providers_create_part', $infos);
|
||||
return $this->urlGenerator->generate('info_providers_create_part', ['providerKey' => $infos['providerKey'], 'providerId' => $infos['providerId']]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +146,7 @@ final readonly class BarcodeScanResultHandler
|
||||
|
||||
if ($barcodeScan instanceof AmazonBarcodeScanResult) {
|
||||
return $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->asin)
|
||||
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
|
||||
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -247,7 +246,7 @@ final readonly class BarcodeScanResultHandler
|
||||
* Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function.
|
||||
* It is not necessarily checked that the provider is active, or that the result actually exists on the provider side.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
|
||||
@@ -257,9 +256,6 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'lcsc',
|
||||
'providerId' => $scanResult->lcscCode,
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->orderNumber ?? $scanResult->pickBatchNumber,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -280,7 +276,7 @@ final readonly class BarcodeScanResultHandler
|
||||
|
||||
/**
|
||||
* @param EIGP114BarcodeScanResult $scanResult
|
||||
* * @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
*/
|
||||
private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array
|
||||
{
|
||||
@@ -289,26 +285,23 @@ final readonly class BarcodeScanResultHandler
|
||||
// Mouser: use supplierPartNumber -> search provider -> provider_id
|
||||
if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null
|
||||
) {
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Digi-Key: supplierPartNumber directly
|
||||
@@ -316,9 +309,6 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'digikey',
|
||||
'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Digikey barcode does not contain required supplier part number'),
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->digikeyInvoiceNumber ?? $scanResult->digikeySalesOrderNumber ?? $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -327,9 +317,6 @@ final readonly class BarcodeScanResultHandler
|
||||
return [
|
||||
'providerKey' => 'element14',
|
||||
'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Element14 barcode does not contain required supplier part number'),
|
||||
'lotAmount' => $scanResult->quantity,
|
||||
'lotName' => $scanResult->customerPO,
|
||||
'lotUserBarcode' => $scanResult->rawInput,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
*
|
||||
* @param array<string, string> $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content
|
||||
*/
|
||||
public function __construct(public array $data, public readonly ?string $rawInput = null)
|
||||
public function __construct(public array $data)
|
||||
{
|
||||
//IDs per EIGP 114.2018
|
||||
$this->shipDate = $data['6D'] ?? null;
|
||||
@@ -271,8 +271,6 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
*/
|
||||
public static function parseFormat06Code(string $input): self
|
||||
{
|
||||
$rawInput = $input;
|
||||
|
||||
//Ensure that the input is a valid format06 code
|
||||
if (!self::isFormat06Code($input)) {
|
||||
throw new \InvalidArgumentException("The given input is not a valid format06 code");
|
||||
@@ -308,7 +306,7 @@ readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
$results[$key] = $fieldValue;
|
||||
}
|
||||
|
||||
return new self($results, $rawInput);
|
||||
return new self($results);
|
||||
}
|
||||
|
||||
public function getDecodedForInfoMode(): array
|
||||
|
||||
@@ -114,12 +114,10 @@ final class BarcodeProvider implements PlaceholderProviderInterface
|
||||
return '<b>IPN Barcode ERROR!</b>: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getDefaultPriority(): int
|
||||
{
|
||||
//This provider should be checked before all others, so that nothing is delegated for part lots
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,14 +327,14 @@ readonly class BackupManager
|
||||
*/
|
||||
private function restoreDatabaseFromBackup(string $tempDir): void
|
||||
{
|
||||
// Get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
// Check for SQL dump (MySQL/PostgreSQL)
|
||||
$sqlFile = $tempDir . '/database.sql';
|
||||
if (file_exists($sqlFile)) {
|
||||
// Import SQL using mysql/psql command directly
|
||||
// First, get database connection params from Doctrine
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$params = $connection->getParams();
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
// Use mysql command to import - need to use shell to handle input redirection
|
||||
@@ -403,8 +403,7 @@ readonly class BackupManager
|
||||
// Check for SQLite database file
|
||||
$sqliteFile = $tempDir . '/var/app.db';
|
||||
if (file_exists($sqliteFile)) {
|
||||
// Use the actual configured SQLite path from Doctrine, not a hardcoded path
|
||||
$targetDb = $params['path'] ?? $this->projectDir . '/var/app.db';
|
||||
$targetDb = $this->projectDir . '/var/app.db';
|
||||
$this->filesystem->copy($sqliteFile, $targetDb, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,79 +207,6 @@ class UpdateExecutor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset PHP OPcache for the web server process.
|
||||
*
|
||||
* OPcache in PHP-FPM is separate from CLI. After updating code files,
|
||||
* PHP-FPM may still serve stale cached bytecode, causing constructor
|
||||
* mismatches and 500 errors. This method creates a temporary PHP script
|
||||
* in the public directory, invokes it via HTTP to reset OPcache in the
|
||||
* web server context, then removes the script.
|
||||
*
|
||||
* @return bool Whether OPcache was successfully reset
|
||||
*/
|
||||
private function resetOpcache(): bool
|
||||
{
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$resetScript = $this->project_dir . '/public/_opcache_reset_' . $token . '.php';
|
||||
|
||||
try {
|
||||
// Create a temporary PHP script that resets OPcache
|
||||
$scriptContent = '<?php '
|
||||
. 'if (function_exists("opcache_reset")) { opcache_reset(); echo "OK"; } '
|
||||
. 'else { echo "NO_OPCACHE"; } '
|
||||
. '@unlink(__FILE__);';
|
||||
|
||||
$this->filesystem->dumpFile($resetScript, $scriptContent);
|
||||
|
||||
// Try to invoke it via HTTP on localhost
|
||||
$urls = [
|
||||
'http://127.0.0.1/_opcache_reset_' . $token . '.php',
|
||||
'http://localhost/_opcache_reset_' . $token . '.php',
|
||||
];
|
||||
|
||||
$success = false;
|
||||
foreach ($urls as $url) {
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
if ($response === 'OK') {
|
||||
$this->logger->info('OPcache reset via ' . $url);
|
||||
$success = true;
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Try next URL
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$this->logger->info('OPcache reset via HTTP not available, trying CLI fallback');
|
||||
// CLI opcache_reset() only affects CLI, but try anyway
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('OPcache reset failed: ' . $e->getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
// Ensure the temp script is removed
|
||||
if (file_exists($resetScript)) {
|
||||
@unlink($resetScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that we can perform an update.
|
||||
*
|
||||
@@ -507,20 +434,12 @@ class UpdateExecutor
|
||||
], 'Warmup cache', 120);
|
||||
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 13: Reset OPcache (if available)
|
||||
$stepStart = microtime(true);
|
||||
$opcacheResult = $this->resetOpcache();
|
||||
$log('opcache_reset', $opcacheResult
|
||||
? 'Reset PHP OPcache for web server'
|
||||
: 'OPcache reset skipped (not available or not needed)',
|
||||
true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 14: Disable maintenance mode
|
||||
// Step 13: Disable maintenance mode
|
||||
$stepStart = microtime(true);
|
||||
$this->disableMaintenanceMode();
|
||||
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 15: Release lock
|
||||
// Step 14: Release lock
|
||||
$stepStart = microtime(true);
|
||||
$this->releaseLock();
|
||||
|
||||
@@ -575,9 +494,6 @@ class UpdateExecutor
|
||||
], 'Clear cache after rollback', 120);
|
||||
$log('rollback_cache', 'Cleared cache after rollback', true);
|
||||
|
||||
// Reset OPcache after rollback
|
||||
$this->resetOpcache();
|
||||
|
||||
} catch (\Exception $rollbackError) {
|
||||
$log('rollback_failed', 'Rollback failed: ' . $rollbackError->getMessage(), false);
|
||||
}
|
||||
@@ -686,33 +602,6 @@ class UpdateExecutor
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a specific update log file.
|
||||
*/
|
||||
public function deleteLog(string $filename): bool
|
||||
{
|
||||
// Validate filename pattern for security
|
||||
if (!preg_match('/^update-[\w.\-]+\.log$/', $filename)) {
|
||||
$this->logger->warning('Attempted to delete invalid log filename: ' . $filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
$logPath = $this->project_dir . '/' . self::UPDATE_LOG_DIR . '/' . basename($filename);
|
||||
|
||||
if (!file_exists($logPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->filesystem->remove($logPath);
|
||||
$this->logger->info('Deleted update log: ' . $filename);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to delete update log: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup file with maintenance mode and cache clearing.
|
||||
*
|
||||
@@ -793,17 +682,12 @@ class UpdateExecutor
|
||||
$this->runCommand(['php', 'bin/console', 'cache:warmup'], 'Warm up cache');
|
||||
$log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 6: Reset OPcache
|
||||
$stepStart = microtime(true);
|
||||
$this->resetOpcache();
|
||||
$log('opcache_reset', 'Reset PHP OPcache', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 7: Disable maintenance mode
|
||||
// Step 6: Disable maintenance mode
|
||||
$stepStart = microtime(true);
|
||||
$this->disableMaintenanceMode();
|
||||
$log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart);
|
||||
|
||||
// Step 8: Release lock
|
||||
// Step 7: Release lock
|
||||
$this->releaseLock();
|
||||
|
||||
$totalDuration = microtime(true) - $startTime;
|
||||
@@ -933,7 +817,7 @@ class UpdateExecutor
|
||||
'create_backup' => $createBackup,
|
||||
'started_at' => (new \DateTime())->format('c'),
|
||||
'current_step' => 0,
|
||||
'total_steps' => 15,
|
||||
'total_steps' => 14,
|
||||
'step_name' => 'initializing',
|
||||
'step_message' => 'Starting update process...',
|
||||
'steps' => [],
|
||||
@@ -1006,7 +890,7 @@ class UpdateExecutor
|
||||
bool $createBackup = true,
|
||||
?callable $onProgress = null
|
||||
): array {
|
||||
$totalSteps = 13;
|
||||
$totalSteps = 12;
|
||||
$currentStep = 0;
|
||||
|
||||
$updateProgress = function (string $stepName, string $message, bool $success = true) use (&$currentStep, $totalSteps, $targetVersion, $createBackup): void {
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
<div class="tab-pane" id="eda">
|
||||
{{ form_row(form.eda_info.reference_prefix) }}
|
||||
|
||||
|
||||
{{ form_row(form.eda_info.visibility) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
{{ form_row(form.eda_info.visibility) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="{{ col_input }} {{ offset_label }}">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% import "helper.twig" as helper %}
|
||||
|
||||
{% block title %}Part-DB {% trans %}update_manager.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
@@ -9,60 +7,60 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
<div>
|
||||
<div>
|
||||
|
||||
{# Maintenance Mode Warning #}
|
||||
{% if is_maintenance %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
<strong>{% trans %}update_manager.maintenance_mode_active{% endtrans %}</strong>
|
||||
{% if maintenance_info.reason is defined %}
|
||||
- {{ maintenance_info.reason }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Maintenance Mode Warning #}
|
||||
{% if is_maintenance %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
<strong>{% trans %}update_manager.maintenance_mode_active{% endtrans %}</strong>
|
||||
{% if maintenance_info.reason is defined %}
|
||||
- {{ maintenance_info.reason }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Lock Warning #}
|
||||
{% if is_locked %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<strong>{% trans %}update_manager.update_in_progress{% endtrans %}</strong>
|
||||
{% if lock_info.started_at is defined %}
|
||||
({% trans %}update_manager.started_at{% endtrans %}: {{ lock_info.started_at }})
|
||||
{% endif %}
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="alert-link ms-2">
|
||||
{% trans %}update_manager.view_progress{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Lock Warning #}
|
||||
{% if is_locked %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<strong>{% trans %}update_manager.update_in_progress{% endtrans %}</strong>
|
||||
{% if lock_info.started_at is defined %}
|
||||
({% trans %}update_manager.started_at{% endtrans %}: {{ lock_info.started_at }})
|
||||
{% endif %}
|
||||
<a href="{{ path('admin_update_manager_progress') }}" class="alert-link ms-2">
|
||||
{% trans %}update_manager.view_progress{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Web Updates Disabled Warning #}
|
||||
{% if web_updates_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.web_updates_disabled{% endtrans %}</strong>
|
||||
<p class="mb-0 mt-2">{% trans %}update_manager.web_updates_disabled_hint{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Web Updates Disabled Warning #}
|
||||
{% if web_updates_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.web_updates_disabled{% endtrans %}</strong>
|
||||
<p class="mb-0 mt-2">{% trans %}update_manager.web_updates_disabled_hint{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Backup Restore Disabled Warning #}
|
||||
{% if backup_restore_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.backup_restore_disabled{% endtrans %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Backup Restore Disabled Warning #}
|
||||
{% if backup_restore_disabled %}
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
<strong>{% trans %}update_manager.backup_restore_disabled{% endtrans %}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Current Version Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans %}update_manager.current_installation{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<div class="row">
|
||||
{# Current Version Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans %}update_manager.current_installation{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" style="width: 40%">{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<td>
|
||||
@@ -102,159 +100,153 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.auto_update_supported{% endtrans %}</th>
|
||||
<td>
|
||||
{{ helper.boolean_badge(status.can_auto_update) }}
|
||||
{% if status.can_auto_update %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-check me-1"></i>{% trans %}Yes{% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fas fa-times me-1"></i>{% trans %}No{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.web_updates_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not web_updates_disabled) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.backup_restore_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not backup_restore_disabled) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans %}update_manager.backup_download_allowed{% endtrans %}</th>
|
||||
<td>{{ helper.boolean_badge(not backup_download_disabled) }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<form action="{{ path('admin_update_manager_refresh') }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_refresh') }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.refresh{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Latest Version / Update Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100 {{ status.update_available ? 'border-success' : '' }}">
|
||||
<div class="card-header {{ status.update_available ? 'bg-success text-white' : '' }}">
|
||||
{% if status.update_available %}
|
||||
<i class="fas fa-gift me-2"></i>{% trans %}update_manager.new_version_available.title{% endtrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans %}update_manager.latest_release{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if status.latest_version %}
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-{{ status.update_available ? 'success' : 'primary' }} fs-4 px-4 py-2">
|
||||
{{ status.latest_tag }}
|
||||
</span>
|
||||
{% if not status.update_available %}
|
||||
<p class="text-success mt-2 mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans %}update_manager.already_up_to_date{% endtrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if status.update_available and status.can_auto_update and validation.valid and not web_updates_disabled %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post"
|
||||
data-controller="update-confirm"
|
||||
data-update-confirm-is-downgrade-value="false"
|
||||
data-update-confirm-target-version-value="{{ status.latest_tag }}"
|
||||
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
|
||||
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
|
||||
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ status.latest_tag }}">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% trans %}update_manager.update_to{% endtrans %} {{ status.latest_tag }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="backup" value="1" id="create-backup" checked>
|
||||
<label class="form-check-label" for="create-backup">
|
||||
<i class="fas fa-database me-1"></i> {% trans %}update_manager.create_backup{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if status.published_at %}
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-question-circle fa-3x mb-3"></i>
|
||||
<p>{% trans %}update_manager.could_not_fetch_releases{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if status.latest_tag %}
|
||||
<div class="card-footer">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: status.latest_tag}) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans %}update_manager.view_release_notes{% endtrans %}
|
||||
</a>
|
||||
{% if status.release_url %}
|
||||
<a href="{{ status.release_url }}" class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<form action="{{ path('admin_update_manager_refresh') }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_refresh') }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans %}update_manager.refresh{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Validation Issues #}
|
||||
{% if not validation.valid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans %}update_manager.validation_issues{% endtrans %}
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Latest Version / Update Card #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100 {{ status.update_available ? 'border-success' : '' }}">
|
||||
<div class="card-header {{ status.update_available ? 'bg-success text-white' : '' }}">
|
||||
{% if status.update_available %}
|
||||
<i class="fas fa-gift me-2"></i>{% trans %}update_manager.new_version_available.title{% endtrans %}
|
||||
{% else %}
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans %}update_manager.latest_release{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if status.latest_version %}
|
||||
<div class="text-center mb-3">
|
||||
<span class="badge bg-{{ status.update_available ? 'success' : 'primary' }} fs-4 px-4 py-2">
|
||||
{{ status.latest_tag }}
|
||||
</span>
|
||||
{% if not status.update_available %}
|
||||
<p class="text-success mt-2 mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
{% trans %}update_manager.already_up_to_date{% endtrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Non-auto-update installations info #}
|
||||
{% if not status.can_auto_update %}
|
||||
<div class="alert alert-secondary">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans%}update_manager.cant_auto_update{% endtrans%}: {{ status.installation.type_name }}
|
||||
</h6>
|
||||
<p class="mb-0">{{ status.installation.update_instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if status.update_available and status.can_auto_update and validation.valid and not web_updates_disabled %}
|
||||
<form action="{{ path('admin_update_manager_start') }}" method="post"
|
||||
data-controller="update-confirm"
|
||||
data-update-confirm-is-downgrade-value="false"
|
||||
data-update-confirm-target-version-value="{{ status.latest_tag }}"
|
||||
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
|
||||
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
|
||||
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
|
||||
<input type="hidden" name="version" value="{{ status.latest_tag }}">
|
||||
|
||||
<div class="row">
|
||||
{# Available Versions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags me-2"></i>{% trans %}update_manager.available_versions{% endtrans %}
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% trans %}update_manager.update_to{% endtrans %} {{ status.latest_tag }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="backup" value="1" id="create-backup" checked>
|
||||
<label class="form-check-label" for="create-backup">
|
||||
<i class="fas fa-database me-1"></i> {% trans %}update_manager.create_backup{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if status.published_at %}
|
||||
<p class="text-muted small mt-3 mb-0">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
{% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-question-circle fa-3x mb-3"></i>
|
||||
<p>{% trans %}update_manager.could_not_fetch_releases{% endtrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if status.latest_tag %}
|
||||
<div class="card-footer">
|
||||
<a href="{{ path('admin_update_manager_release', {tag: status.latest_tag}) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans %}update_manager.view_release_notes{% endtrans %}
|
||||
</a>
|
||||
{% if status.release_url %}
|
||||
<a href="{{ status.release_url }}" class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
<i class="fab fa-github me-1"></i> GitHub
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Validation Issues #}
|
||||
{% if not validation.valid %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>{% trans %}update_manager.validation_issues{% endtrans %}
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Non-auto-update installations info #}
|
||||
{% if not status.can_auto_update %}
|
||||
<div class="alert alert-secondary">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans%}update_manager.cant_auto_update{% endtrans%}: {{ status.installation.type_name }}
|
||||
</h6>
|
||||
<p class="mb-0">{{ status.installation.update_instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{# Available Versions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-tags me-2"></i>{% trans %}update_manager.available_versions{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.version{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.released{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for release in all_releases %}
|
||||
<tr{% if release.version == status.current_version %} class="table-active"{% endif %}>
|
||||
<td>
|
||||
@@ -288,8 +280,8 @@
|
||||
<input type="hidden" name="version" value="{{ release.tag }}">
|
||||
<input type="hidden" name="backup" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-{{ release.version > status.current_version ? 'outline-success' : 'outline-warning' }}"
|
||||
title="{% trans %}update_manager.switch_to{% endtrans %} {{ release.tag }}">
|
||||
class="btn btn-{{ release.version > status.current_version ? 'outline-success' : 'outline-warning' }}"
|
||||
title="{% trans %}update_manager.switch_to{% endtrans %} {{ release.tag }}">
|
||||
{% if release.version > status.current_version %}
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
{% else %}
|
||||
@@ -308,69 +300,54 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Update History & Backups #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#logs-tab" type="button">
|
||||
<i class="fas fa-history me-1"></i>{% trans %}update_manager.update_logs{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backups-tab" type="button">
|
||||
<i class="fas fa-archive me-1"></i>{% trans %}update_manager.backups{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="logs-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
{# Update History & Backups #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#logs-tab" type="button">
|
||||
<i class="fas fa-history me-1"></i>{% trans %}update_manager.update_logs{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#backups-tab" type="button">
|
||||
<i class="fas fa-archive me-1"></i>{% trans %}update_manager.backups{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="logs-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.log_file{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in update_logs %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
{{ log.date|date('Y-m-d H:i') }}
|
||||
</td>
|
||||
<td><code class="small">{{ log.file }}</code></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{{ path('admin_update_manager_log', {filename: log.file}) }}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="{% trans %}update_manager.view_log{% endtrans %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_log_delete') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.log.delete.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_delete') }}">
|
||||
<input type="hidden" name="filename" value="{{ log.file }}">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
title="{% trans %}update_manager.delete{% endtrans %}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<td>
|
||||
<a href="{{ path('admin_update_manager_log', {filename: log.file}) }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
@@ -380,39 +357,22 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
{% if is_granted('@system.manage_updates') and not is_locked %}
|
||||
<div class="p-2 border-bottom">
|
||||
<form action="{{ path('admin_update_manager_backup') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.backup.create.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_backup') }}">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-plus me-1"></i>{% trans %}update_manager.backup.create{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_docker %}
|
||||
<div class="alert alert-info alert-sm m-2 mb-0 py-2 small">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{% trans %}update_manager.backup.docker_warning{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
</div>
|
||||
<div class="tab-pane fade" id="backups-tab">
|
||||
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="sticky-top" style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th>{% trans %}update_manager.date{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.file{% endtrans %}</th>
|
||||
<th>{% trans %}update_manager.size{% endtrans %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td class="text-muted small">
|
||||
@@ -423,89 +383,23 @@
|
||||
{{ (backup.size / 1024 / 1024)|number_format(1) }} MB
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#downloadBackupModal-{{ loop.index }}"
|
||||
title="{% trans %}update_manager.backup.download{% endtrans %}">
|
||||
<i class="fas fa-download"></i>
|
||||
{% if status.can_auto_update and validation.valid and not backup_restore_disabled %}
|
||||
<form action="{{ path('admin_update_manager_restore') }}" method="post" class="d-inline"
|
||||
data-controller="backup-restore"
|
||||
data-backup-restore-filename-value="{{ backup.file }}"
|
||||
data-backup-restore-date-value="{{ backup.date|date('Y-m-d H:i') }}"
|
||||
data-backup-restore-confirm-title-value="{{ 'update_manager.restore_confirm_title'|trans }}"
|
||||
data-backup-restore-confirm-message-value="{{ 'update_manager.restore_confirm_message'|trans }}"
|
||||
data-backup-restore-confirm-warning-value="{{ 'update_manager.restore_confirm_warning'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_restore') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="restore_database" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="{% trans %}update_manager.restore_backup{% endtrans %}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if not backup_restore_disabled and is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_restore') }}" method="post" class="d-inline"
|
||||
{{ stimulus_controller('backup-restore') }}
|
||||
data-backup-restore-filename-value="{{ backup.file }}"
|
||||
data-backup-restore-date-value="{{ backup.date|date('Y-m-d H:i') }}"
|
||||
data-backup-restore-confirm-title-value="{{ 'update_manager.restore_confirm_title'|trans }}"
|
||||
data-backup-restore-confirm-message-value="{{ 'update_manager.restore_confirm_message'|trans }}"
|
||||
data-backup-restore-confirm-warning-value="{{ 'update_manager.restore_confirm_warning'|trans }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_restore') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="restore_database" value="1">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-warning"
|
||||
title="{% trans %}update_manager.restore_backup{% endtrans %}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if is_granted('@system.manage_updates') %}
|
||||
<form action="{{ path('admin_update_manager_backup_delete') }}" method="post" class="d-inline"
|
||||
data-turbo-confirm="{% trans %}update_manager.backup.delete.confirm{% endtrans %}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_delete') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
title="{% trans %}update_manager.delete{% endtrans %}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
|
||||
{# Per-backup download modal - no inline JS needed, CSP compatible with Turbo #}
|
||||
<div class="modal fade text-start" id="downloadBackupModal-{{ loop.index }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ path('admin_update_manager_backup_download') }}" method="post" data-turbo="false">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-download me-2"></i>{% trans %}update_manager.backup.download{% endtrans %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
{% trans %}update_manager.backup.download.security_warning{% endtrans %}
|
||||
</div>
|
||||
<p class="text-muted small mb-3">{{ backup.file }}</p>
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_download') }}">
|
||||
<input type="hidden" name="filename" value="{{ backup.file }}">
|
||||
<input type="hidden" name="username" autocomplete="username">
|
||||
<div class="mb-3">
|
||||
<label for="downloadPassword-{{ loop.index }}" class="form-label">
|
||||
{% trans %}update_manager.backup.download.password_label{% endtrans %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="downloadPassword-{{ loop.index }}"
|
||||
name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans %}modal.cancel{% endtrans %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-download me-1"></i>{% trans %}update_manager.backup.download{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -516,9 +410,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,5 +419,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% macro boolean(value) %}
|
||||
{% if value %}
|
||||
{% trans %}Yes{% endtrans %}
|
||||
{% trans %}bool.true{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}No{% endtrans %}
|
||||
{% trans %}bool.false{% endtrans %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
{% macro bool_icon(bool) %}
|
||||
{% if bool %}
|
||||
<i class="fas fa-check fa-fw" title="{% trans %}Yes{% endtrans %}"></i>
|
||||
<i class="fas fa-check-circle fa-fw" title="{% trans %}Yes{% endtrans %}"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-times fa-fw" title="{% trans %}No{% endtrans %}"></i>
|
||||
<i class="fas fa-times-circle fa-fw" title="{% trans %}No{% endtrans %}"></i>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{% if value %}
|
||||
{% set class = class ~ ' bg-success' %}
|
||||
{% else %}
|
||||
{% set class = class ~ ' bg-secondary' %}
|
||||
{% set class = class ~ ' bg-danger' %}
|
||||
{% endif %}
|
||||
|
||||
<span class="{{ class }}">{{ _self.bool_icon(value) }} {{ _self.boolean(value) }}</span>
|
||||
|
||||
@@ -47,32 +47,6 @@ final class PartLotsEndpointTest extends CrudEndpointTestCase
|
||||
$this->_testGetItem(2);
|
||||
}
|
||||
|
||||
public function testFilterByUserBarcode(): void
|
||||
{
|
||||
$response = self::createAuthenticatedClient()->request('GET', '/api/part_lots?user_barcode=lot2_vendor_barcode');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertJsonContains([
|
||||
'hydra:totalItems' => 1,
|
||||
]);
|
||||
|
||||
$json = $response->toArray();
|
||||
self::assertSame('/api/part_lots/2', $json['hydra:member'][0]['@id']);
|
||||
}
|
||||
|
||||
public function testFilterByUserBarcodeUsingWildcard(): void
|
||||
{
|
||||
$response = self::createAuthenticatedClient()->request('GET', '/api/part_lots?user_barcode=lot2_%');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertJsonContains([
|
||||
'hydra:totalItems' => 1,
|
||||
]);
|
||||
|
||||
$json = $response->toArray();
|
||||
self::assertSame('/api/part_lots/2', $json['hydra:member'][0]['@id']);
|
||||
}
|
||||
|
||||
public function testCreateItem(): void
|
||||
{
|
||||
$this->_testPostItem([
|
||||
|
||||
@@ -121,7 +121,6 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
'description' => '',
|
||||
'fields' =>
|
||||
array(
|
||||
'footprint' =>
|
||||
@@ -204,7 +203,6 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
'description' => '',
|
||||
'fields' =>
|
||||
array (
|
||||
'footprint' =>
|
||||
@@ -320,4 +318,4 @@ final class KiCadApiControllerTest extends WebTestCase
|
||||
self::assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\System\BackupManager;
|
||||
use App\Services\System\UpdateExecutor;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
#[Group("slow")]
|
||||
#[Group("DB")]
|
||||
final class UpdateManagerControllerTest extends WebTestCase
|
||||
{
|
||||
private function loginAsAdmin($client): void
|
||||
{
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => 'admin']);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped('Admin user not found');
|
||||
}
|
||||
|
||||
$client->loginUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a CSRF token from the rendered update manager page.
|
||||
*/
|
||||
private function getCsrfTokenFromPage($crawler, string $formAction): string
|
||||
{
|
||||
$form = $crawler->filter('form[action*="' . $formAction . '"]');
|
||||
if ($form->count() === 0) {
|
||||
$this->fail('Form with action containing "' . $formAction . '" not found on page');
|
||||
}
|
||||
|
||||
return $form->filter('input[name="_token"]')->attr('value');
|
||||
}
|
||||
|
||||
// ---- Authentication tests ----
|
||||
|
||||
public function testIndexPageRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager');
|
||||
|
||||
// Should deny access (401 with HTTP Basic auth in test env)
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testIndexPageAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
// ---- Backup creation tests ----
|
||||
|
||||
public function testCreateBackupRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => 'invalid',
|
||||
]);
|
||||
|
||||
// Should redirect with error flash
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testCreateBackupWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Load the page and extract CSRF token from the backup form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => $csrfToken,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
|
||||
// Clean up: delete the backup that was just created
|
||||
$backupManager = $client->getContainer()->get(BackupManager::class);
|
||||
$backups = $backupManager->getBackups();
|
||||
foreach ($backups as $backup) {
|
||||
if (str_contains($backup['file'], 'manual')) {
|
||||
$backupManager->deleteBackup($backup['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCreateBackupBlockedWhenLocked(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Load the page first to get CSRF token before locking
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup');
|
||||
|
||||
// Acquire lock to simulate update in progress
|
||||
$updateExecutor = $client->getContainer()->get(UpdateExecutor::class);
|
||||
$updateExecutor->acquireLock();
|
||||
|
||||
try {
|
||||
$client->request('POST', '/en/system/update-manager/backup', [
|
||||
'_token' => $csrfToken,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
} finally {
|
||||
// Always release lock
|
||||
$updateExecutor->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Backup deletion tests ----
|
||||
|
||||
public function testDeleteBackupRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/delete', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testDeleteBackupWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Create a temporary backup file so the page shows the delete form
|
||||
$backupManager = $client->getContainer()->get(BackupManager::class);
|
||||
$backupDir = $backupManager->getBackupDir();
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
$testFile = 'test-delete-' . uniqid() . '.zip';
|
||||
file_put_contents($backupDir . '/' . $testFile, 'test');
|
||||
|
||||
// Load the page and extract CSRF token from the delete form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'backup/delete');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/delete', [
|
||||
'_token' => $csrfToken,
|
||||
'filename' => $testFile,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
$this->assertFileDoesNotExist($backupDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
// ---- Log deletion tests ----
|
||||
|
||||
public function testDeleteLogRequiresCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/log/delete', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.log',
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testDeleteLogWithValidCsrf(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// Create a temporary log file so the page shows the delete form
|
||||
$projectDir = $client->getContainer()->getParameter('kernel.project_dir');
|
||||
$logDir = $projectDir . '/var/log/updates';
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
$testFile = 'update-test-delete-' . uniqid() . '.log';
|
||||
file_put_contents($logDir . '/' . $testFile, 'test log content');
|
||||
|
||||
// Load the page and extract CSRF token from the log delete form
|
||||
$crawler = $client->request('GET', '/en/system/update-manager');
|
||||
$csrfToken = $this->getCsrfTokenFromPage($crawler, 'log/delete');
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/log/delete', [
|
||||
'_token' => $csrfToken,
|
||||
'filename' => $testFile,
|
||||
]);
|
||||
|
||||
$this->assertResponseRedirects();
|
||||
$this->assertFileDoesNotExist($logDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
// ---- Backup download tests ----
|
||||
|
||||
public function testDownloadBackupBlockedByDefault(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_BACKUP_DOWNLOAD=1 is the default in .env, so this should return 403
|
||||
$client->request('POST', '/en/system/update-manager/backup/download', [
|
||||
'_token' => 'any',
|
||||
'filename' => 'test.zip',
|
||||
'password' => 'test',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testDownloadBackupRequiresPost(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// GET returns 404 since no GET route exists for this path
|
||||
$client->request('GET', '/en/system/update-manager/backup/download');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testDownloadBackupRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/backup/download', [
|
||||
'_token' => 'any',
|
||||
'filename' => 'test.zip',
|
||||
'password' => 'test',
|
||||
]);
|
||||
|
||||
// Should deny access (401 with HTTP Basic auth in test env)
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
// ---- Backup details tests ----
|
||||
|
||||
public function testBackupDetailsReturns404ForNonExistent(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/backup/nonexistent.zip');
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
// ---- Restore tests ----
|
||||
|
||||
public function testRestoreBlockedWhenDisabled(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_BACKUP_RESTORE=1 is the default in .env, so this should return 403
|
||||
$client->request('POST', '/en/system/update-manager/restore', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testRestoreRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/restore', [
|
||||
'_token' => 'invalid',
|
||||
'filename' => 'test.zip',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
// ---- Start update tests ----
|
||||
|
||||
public function testStartUpdateRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('POST', '/en/system/update-manager/start', [
|
||||
'_token' => 'invalid',
|
||||
'version' => 'v1.0.0',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testStartUpdateBlockedWhenWebUpdatesDisabled(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
// DISABLE_WEB_UPDATES=1 is the default in .env
|
||||
$client->request('POST', '/en/system/update-manager/start', [
|
||||
'_token' => 'invalid',
|
||||
'version' => 'v1.0.0',
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
// ---- Status and progress tests ----
|
||||
|
||||
public function testStatusEndpointRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/status');
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testStatusEndpointAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/status');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testProgressStatusEndpointRequiresAuth(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/progress/status');
|
||||
|
||||
$this->assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
public function testProgressStatusEndpointAccessibleByAdmin(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsAdmin($client);
|
||||
|
||||
$client->request('GET', '/en/system/update-manager/progress/status');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
@@ -85,41 +85,4 @@ final class StructuralElementDenormalizerTest extends WebTestCase
|
||||
$result2 = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import']]);
|
||||
$this->assertSame($result, $result2);
|
||||
}
|
||||
|
||||
public function testDenormalizeViaChildren(): void
|
||||
{
|
||||
$data = ['name' => 'Node',
|
||||
'children' => [
|
||||
['name' => 'A', 'children' => [['name' => '1'], ['name' => '2']]],
|
||||
['name' => 'B', 'children' => [['name' => '1'], ['name' => '2']]],
|
||||
['name' => 'C', 'children' => [['name' => '1'], ['name' => '2'], ['name' => '3']]],
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import', 'include_children']]);
|
||||
$this->assertInstanceOf(Category::class, $result);
|
||||
|
||||
$this->assertCount(3, $result->getChildren());
|
||||
$this->assertSame('A', $result->getChildren()[0]->getName());
|
||||
$this->assertSame('B', $result->getChildren()[1]->getName());
|
||||
$this->assertSame('C', $result->getChildren()[2]->getName());
|
||||
//Parents should be set correctly
|
||||
$this->assertSame($result, $result->getChildren()[0]->getParent());
|
||||
$this->assertSame($result, $result->getChildren()[1]->getParent());
|
||||
$this->assertSame($result, $result->getChildren()[2]->getParent());
|
||||
|
||||
$this->assertCount(2, $result->getChildren()[0]->getChildren());
|
||||
$this->assertSame('1', $result->getChildren()[0]->getChildren()[0]->getName());
|
||||
$this->assertSame('2', $result->getChildren()[0]->getChildren()[1]->getName());
|
||||
//Parents should be set correctly
|
||||
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[0]->getParent());
|
||||
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[1]->getParent());
|
||||
|
||||
$this->assertCount(2, $result->getChildren()[1]->getChildren());
|
||||
$this->assertSame('1', $result->getChildren()[1]->getChildren()[0]->getName());
|
||||
$this->assertSame('2', $result->getChildren()[1]->getChildren()[1]->getName());
|
||||
//Must be different instances than the children of A, because we create new elements for the same path, if we don't have them in the DB
|
||||
$this->assertNotSame($result->getChildren()[0]->getChildren()[0], $result->getChildren()[1]->getChildren()[0]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +115,6 @@ final class BarcodeScanHelperTest extends WebTestCase
|
||||
yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 2,BarcodeSourceType::USER_DEFINED),
|
||||
'lot2_vendor_barcode'];
|
||||
|
||||
|
||||
$input = "[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04";
|
||||
$eigp114Result = new EIGP114BarcodeScanResult([
|
||||
'P' => '596-777A1-ND',
|
||||
'1P' => 'XAF4444',
|
||||
@@ -124,9 +122,9 @@ final class BarcodeScanHelperTest extends WebTestCase
|
||||
'10D' => '1452',
|
||||
'1T' => 'BF1103',
|
||||
'4L' => 'US',
|
||||
], $input);
|
||||
]);
|
||||
|
||||
yield [$eigp114Result, $input];
|
||||
yield [$eigp114Result, "[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"];
|
||||
|
||||
$lcscInput = '{pc:C138033,pm:RC0402FR-071ML,qty:10}';
|
||||
$lcscResult = new LCSCBarcodeScanResult(
|
||||
|
||||
@@ -82,16 +82,6 @@ final class BackupManagerTest extends KernelTestCase
|
||||
$this->assertSame('2.6.0', $matches[2]);
|
||||
}
|
||||
|
||||
public function testDeleteBackupReturnsFalseForNonExistentFile(): void
|
||||
{
|
||||
$this->assertFalse($this->backupManager->deleteBackup('non-existent.zip'));
|
||||
}
|
||||
|
||||
public function testDeleteBackupReturnsFalseForNonZipFile(): void
|
||||
{
|
||||
$this->assertFalse($this->backupManager->deleteBackup('not-a-zip.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test version parsing with different filename formats.
|
||||
*/
|
||||
|
||||
@@ -139,38 +139,6 @@ final class UpdateExecutorTest extends KernelTestCase
|
||||
$this->assertFalse($this->updateExecutor->isLocked());
|
||||
}
|
||||
|
||||
public function testDeleteLogRejectsInvalidFilename(): void
|
||||
{
|
||||
// Path traversal attempts should be rejected
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('../../../etc/passwd'));
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('malicious.txt'));
|
||||
$this->assertFalse($this->updateExecutor->deleteLog(''));
|
||||
// Must start with "update-"
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('backup-v1.0.0.log'));
|
||||
}
|
||||
|
||||
public function testDeleteLogReturnsFalseForNonExistentFile(): void
|
||||
{
|
||||
$this->assertFalse($this->updateExecutor->deleteLog('update-nonexistent-file.log'));
|
||||
}
|
||||
|
||||
public function testDeleteLogDeletesExistingFile(): void
|
||||
{
|
||||
// Create a temporary log file in the update logs directory
|
||||
$projectDir = self::getContainer()->getParameter('kernel.project_dir');
|
||||
$logDir = $projectDir . '/var/log/updates';
|
||||
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$testFile = 'update-test-delete-' . uniqid() . '.log';
|
||||
file_put_contents($logDir . '/' . $testFile, 'test log content');
|
||||
|
||||
$this->assertTrue($this->updateExecutor->deleteLog($testFile));
|
||||
$this->assertFileDoesNotExist($logDir . '/' . $testFile);
|
||||
}
|
||||
|
||||
public function testEnableAndDisableMaintenanceMode(): void
|
||||
{
|
||||
// First, ensure maintenance mode is off
|
||||
|
||||
@@ -12489,102 +12489,6 @@ Buerklin-API-Authentication-Server:
|
||||
<target>Backup Wiederherstellungen wurden durch Serverkonfiguration deaktiviert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oAb35wU" name="update_manager.backup.create">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create</source>
|
||||
<target>Backup erstellen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ms26oI0" name="update_manager.backup.create.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create.confirm</source>
|
||||
<target>Jetzt ein vollständiges Backup erstellen? Dies kann einen Moment dauern.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="H9y0eLa" name="update_manager.backup.created">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.created</source>
|
||||
<target>Backup erfolgreich erstellt.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMhXPVB" name="update_manager.backup.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete.confirm</source>
|
||||
<target>Sind Sie sicher, dass Sie dieses Backup löschen möchten?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8tw67c_" name="update_manager.backup.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.deleted</source>
|
||||
<target>Backup erfolgreich gelöscht.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BzBBuqk" name="update_manager.backup.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete_error</source>
|
||||
<target>Backup konnte nicht gelöscht werden.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2olmcSs" name="update_manager.log.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete.confirm</source>
|
||||
<target>Sind Sie sicher, dass Sie dieses Protokoll löschen möchten?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".ZrVHpp" name="update_manager.log.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.deleted</source>
|
||||
<target>Protokoll erfolgreich gelöscht.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="P2JI5Yw" name="update_manager.log.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete_error</source>
|
||||
<target>Protokoll konnte nicht gelöscht werden.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Yos9FWk" name="update_manager.view_log">
|
||||
<segment state="translated">
|
||||
<source>update_manager.view_log</source>
|
||||
<target>Protokoll anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="B9uA2va" name="update_manager.delete">
|
||||
<segment state="translated">
|
||||
<source>update_manager.delete</source>
|
||||
<target>Löschen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZtgvnXB" name="update_manager.backup.download">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download</source>
|
||||
<target>Backup herunterladen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="wxtmrnP" name="update_manager.backup.download.password_label">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.password_label</source>
|
||||
<target>Bestätigen Sie Ihr Passwort zum Herunterladen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MIlTTgL" name="update_manager.backup.download.security_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.security_warning</source>
|
||||
<target>Backups enthalten sensible Daten, einschließlich Passwort-Hashes und Geheimnisse. Bitte bestätigen Sie Ihr Passwort, um mit dem Download fortzufahren.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kZPHBRt" name="update_manager.backup.download.invalid_password">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.invalid_password</source>
|
||||
<target>Ungültiges Passwort. Backup-Download verweigert.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="AZOjnE0" name="update_manager.backup.docker_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.docker_warning</source>
|
||||
<target>Docker-Installation erkannt. Backups werden in var/backups/ gespeichert, was kein persistentes Volume ist. Verwenden Sie die Download-Schaltfläche, um Backups extern zu speichern, oder binden Sie var/backups/ als Volume in Ihrer docker-compose.yml ein.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kHKChQB" name="settings.ips.conrad">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.conrad</source>
|
||||
@@ -12915,35 +12819,5 @@ Buerklin-API-Authentication-Server:
|
||||
<target>Als Klartext anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ehsj93c" name="modal.cancel">
|
||||
<segment state="translated">
|
||||
<source>modal.cancel</source>
|
||||
<target>Abbrechen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdpoFf2" name="update_manager.web_updates_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.web_updates_allowed</source>
|
||||
<target>Web-Updates erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bdWa7is" name="update_manager.backup_restore_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_restore_allowed</source>
|
||||
<target>Backup-Wiederherstellung erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kllGQEN" name="update_manager.backup_download_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_download_allowed</source>
|
||||
<target>Backup-Download erlaubt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="b8JxfcX" name="part.create_from_info_provider.lot_filled_from_barcode">
|
||||
<segment state="translated">
|
||||
<source>part.create_from_info_provider.lot_filled_from_barcode</source>
|
||||
<target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -12491,102 +12491,6 @@ Buerklin-API Authentication server:
|
||||
<target>Backup restore is disabled by server configuration.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oAb35wU" name="update_manager.backup.create">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create</source>
|
||||
<target>Create Backup</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ms26oI0" name="update_manager.backup.create.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.create.confirm</source>
|
||||
<target>Create a full backup now? This may take a moment.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="H9y0eLa" name="update_manager.backup.created">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.created</source>
|
||||
<target>Backup created successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bMhXPVB" name="update_manager.backup.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete.confirm</source>
|
||||
<target>Are you sure you want to delete this backup?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8tw67c_" name="update_manager.backup.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.deleted</source>
|
||||
<target>Backup deleted successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BzBBuqk" name="update_manager.backup.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.delete_error</source>
|
||||
<target>Failed to delete backup.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2olmcSs" name="update_manager.log.delete.confirm">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete.confirm</source>
|
||||
<target>Are you sure you want to delete this log?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".ZrVHpp" name="update_manager.log.deleted">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.deleted</source>
|
||||
<target>Log deleted successfully.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="P2JI5Yw" name="update_manager.log.delete_error">
|
||||
<segment state="translated">
|
||||
<source>update_manager.log.delete_error</source>
|
||||
<target>Failed to delete log.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Yos9FWk" name="update_manager.view_log">
|
||||
<segment state="translated">
|
||||
<source>update_manager.view_log</source>
|
||||
<target>View log</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="B9uA2va" name="update_manager.delete">
|
||||
<segment state="translated">
|
||||
<source>update_manager.delete</source>
|
||||
<target>Delete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ZtgvnXB" name="update_manager.backup.download">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download</source>
|
||||
<target>Download backup</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="wxtmrnP" name="update_manager.backup.download.password_label">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.password_label</source>
|
||||
<target>Confirm your password to download</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="MIlTTgL" name="update_manager.backup.download.security_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.security_warning</source>
|
||||
<target>Backups contain sensitive data including password hashes and secrets. Please confirm your password to proceed with the download.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kZPHBRt" name="update_manager.backup.download.invalid_password">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.download.invalid_password</source>
|
||||
<target>Invalid password. Backup download denied.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="AZOjnE0" name="update_manager.backup.docker_warning">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup.docker_warning</source>
|
||||
<target>Docker installation detected. Backups are stored in var/backups/ which is not a persistent volume. Use the download button to save backups externally, or mount var/backups/ as a volume in your docker-compose.yml.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kHKChQB" name="settings.ips.conrad">
|
||||
<segment state="translated">
|
||||
<source>settings.ips.conrad</source>
|
||||
@@ -12917,35 +12821,5 @@ Buerklin-API Authentication server:
|
||||
<target>View as plain text</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ehsj93c" name="modal.cancel">
|
||||
<segment state="translated">
|
||||
<source>modal.cancel</source>
|
||||
<target>Cancel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdpoFf2" name="update_manager.web_updates_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.web_updates_allowed</source>
|
||||
<target>Web updates allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bdWa7is" name="update_manager.backup_restore_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_restore_allowed</source>
|
||||
<target>Backup restore allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kllGQEN" name="update_manager.backup_download_allowed">
|
||||
<segment state="translated">
|
||||
<source>update_manager.backup_download_allowed</source>
|
||||
<target>Backup download allowed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="b8JxfcX" name="part.create_from_info_provider.lot_filled_from_barcode">
|
||||
<segment state="translated">
|
||||
<source>part.create_from_info_provider.lot_filled_from_barcode</source>
|
||||
<target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -247,11 +247,5 @@
|
||||
<target>Es existiert bereits eine Übersetzung für diesen Typ und Sprache!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="zT_j_oQ" name="validator.invalid_gtin">
|
||||
<segment state="translated">
|
||||
<source>validator.invalid_gtin</source>
|
||||
<target>Dies ist keine gültige GTIN / EAN!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
Reference in New Issue
Block a user