Système
- -- Affiché le = date('d/m/Y à H:i:s') ?> — - Rafraîchir — - Forcer actualisation -
- - -diff --git a/.phpunit.result.cache b/.phpunit.result.cache
index 390efcf..01fbdde 100644
--- a/.phpunit.result.cache
+++ b/.phpunit.result.cache
@@ -1 +1 @@
-{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.026,"CryptoTest::testEncryptDecryptWithUnicode":0.002,"CryptoTest::testEncryptDecryptMultiline":0.002,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0.001,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.002,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.002,"CryptoTest::testIsEncryptedRejectsPlaintext":0.002,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0.001,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0.001,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0.001,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0.002,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0.001,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.002,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.002,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.014,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.002,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.002,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.002,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.002,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.002,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.002,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.004,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.002,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.002,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0.002,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.002,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.002,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.002,"EmailObfuscatorTest::testEmailWithPlusSign":0.002,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.023,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.003,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.002,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.002,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.002,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.002,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.002,"SystemControllerHelpersTest::testHumanBytesZero":0.096,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.003,"SystemControllerHelpersTest::testHumanBytesOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneMB":0.002,"SystemControllerHelpersTest::testHumanBytesOneGB":0.002,"SystemControllerHelpersTest::testHumanBytes1523MB":0.002,"SystemControllerHelpersTest::testHumanBytes2500GB":0.003,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.002,"SystemControllerHelpersTest::testDiskColorWarning":0.002,"SystemControllerHelpersTest::testDiskColorCritical":0.002,"SystemControllerHelpersTest::testLogLineClassCrit":0.002,"SystemControllerHelpersTest::testLogLineClassError":0.002,"SystemControllerHelpersTest::testLogLineClassWarn":0.003,"SystemControllerHelpersTest::testLogLineClassNotice":0.002,"SystemControllerHelpersTest::testLogLineClassHttp500":0.002,"SystemControllerHelpersTest::testLogLineClassHttp300":0.002,"SystemControllerHelpersTest::testLogLineClassDefault":0.002,"SystemControllerHelpersTest::testNginxLineClassComment":0.002,"SystemControllerHelpersTest::testNginxLineClassBlock":0.003,"SystemControllerHelpersTest::testNginxLineClassDirective":0.002,"SystemControllerHelpersTest::testStatusLabelActive":0.002,"SystemControllerHelpersTest::testStatusLabelInactive":0.002,"SystemControllerHelpersTest::testStatusLabelFailed":0.002,"SystemControllerHelpersTest::testStatusLabelWarn":0.002,"SystemControllerHelpersTest::testStatusLabelUnknown":0.002,"SystemControllerHelpersTest::testStatusClassOk":0.002,"SystemControllerHelpersTest::testStatusClassWarn":0.002,"SystemControllerHelpersTest::testStatusClassError":0.002,"SystemControllerHelpersTest::testStatusClassUnknown":0.002,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.003,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.003,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.002,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.002,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.002,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.002,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.002,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.002,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.002,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.002,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.002,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.003,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.002,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.002,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.003,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.004,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.004,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0.003,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.004,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.002,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.002,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.002,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.003,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.015,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.003,"DatabaseExtendedTest::testRenameTagUpdatesName":0.002,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.002,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.033,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.002,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0.002,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.002,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.002,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.002,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.34,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.291,"ShareLinkExtendedTest::testFindBySlugHit":0.293,"ShareLinkExtendedTest::testFindBySlugMiss":0.002,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.286,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.002,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.287,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.312,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.306,"ShareLinkExtendedTest::testCreateWithLockedYear":0.292,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.292,"ShareLinkExtendedTest::testUpdateLockedYear":0.293,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.324,"ShareLinkExtendedTest::testIncrementUsage":0.292,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.286,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.29,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.002,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.002,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.002,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":1.01,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0.004,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0.004,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.003,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.139,"AutofocusFieldForErrorTest::testEditAutofocusYear":0.002,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0.002,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0.003,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.002,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.003,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.002,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.004,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.003,"ThesisCreateValidationTest::testYearZeroRejected":0.002,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.002,"ThesisCreateValidationTest::testFarFutureYearRejected":0.002,"ThesisCreateValidationTest::testCurrentYearAccepted":0.003,"ThesisCreateValidationTest::testMalformedUrlRejected":0.003,"ThesisCreateValidationTest::testValidUrlAccepted":0.002,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0.002,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.004,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.002,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.002,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.002,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.003,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.004,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.003,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.004,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.003,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.002,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.002,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.002,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.002,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.002,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.002,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.002,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.002,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.027,"ErrorHandlerTest::testFkApPrograms":0.002,"ErrorHandlerTest::testFkFinalityTypes":0.002,"ErrorHandlerTest::testFkThesisLanguages":0.002,"ErrorHandlerTest::testFkThesisFormats":0.002,"ErrorHandlerTest::testFkThesisTags":0.002,"ErrorHandlerTest::testFkThesisSupervisors":0.002,"ErrorHandlerTest::testFkAccessTypes":0.002,"ErrorHandlerTest::testFkLicenseTypes":0.001,"ErrorHandlerTest::testFkAuthors":0.002,"ErrorHandlerTest::testFkQuotedTableName":0.002,"ErrorHandlerTest::testFkQuotedLanguages":0.002,"ErrorHandlerTest::testFkQuotedFormatTypes":0.002,"ErrorHandlerTest::testFkReferencesTags":0.002,"ErrorHandlerTest::testFkReferencesOrientations":0.002,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.002,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.002,"ErrorHandlerTest::testUniqueConstraint":0.002,"ErrorHandlerTest::testNotNullConstraint":0.002,"ErrorHandlerTest::testGenericPdoError":0.002,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.005,"ErrorHandlerTest::testValidationExceptionPassesThrough":0.002,"ErrorHandlerTest::testGenericExceptionPassesThrough":0.002,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0.002,"ErrorHandlerTest::testLogWithContext":0.002,"ErrorHandlerTest::testLogWithNullValues":0.003,"ErrorHandlerTest::testLogWithEmptyExtra":0.002,"ErrorHandlerTest::testFkQuotedColumnNames":0.002,"ErrorHandlerTest::testFkUpdateStatement":0.001,"ErrorHandlerTest::testFkWithReferencesAndInsert":0.002,"PureLogicTest::testSplitJuryByRoleAllRoles":0.043,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0.002,"PureLogicTest::testSplitJuryByRoleEmptyJury":0.002,"PureLogicTest::testCollectCaptionPathsVttByMime":0.002,"PureLogicTest::testCollectCaptionPathsVttByExtension":0.002,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0.002,"PureLogicTest::testDetectFileTypeByMime":0.003,"PureLogicTest::testDetectFileTypeByExtensionFallback":0.003,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.055,"SearchControllerTest::testCoverMapContainsKnownThesis":0.004}}
\ No newline at end of file
+{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.035,"CryptoTest::testEncryptDecryptWithUnicode":0.002,"CryptoTest::testEncryptDecryptMultiline":0.002,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0.002,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.002,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.01,"CryptoTest::testIsEncryptedRejectsPlaintext":0.003,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0.003,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0.002,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0.002,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0.002,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0.002,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.002,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.005,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.015,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.002,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.002,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.007,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.003,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.004,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.002,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.003,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.002,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.002,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0.001,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.001,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.002,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.002,"EmailObfuscatorTest::testEmailWithPlusSign":0.011,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.053,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.006,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.006,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.008,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.003,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.004,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.005,"SystemControllerHelpersTest::testHumanBytesZero":0.219,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.004,"SystemControllerHelpersTest::testHumanBytesOneKB":0.003,"SystemControllerHelpersTest::testHumanBytesOneMB":0.003,"SystemControllerHelpersTest::testHumanBytesOneGB":0.004,"SystemControllerHelpersTest::testHumanBytes1523MB":0.003,"SystemControllerHelpersTest::testHumanBytes2500GB":0.003,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.005,"SystemControllerHelpersTest::testDiskColorWarning":0.005,"SystemControllerHelpersTest::testDiskColorCritical":0.005,"SystemControllerHelpersTest::testLogLineClassCrit":0.003,"SystemControllerHelpersTest::testLogLineClassError":0.003,"SystemControllerHelpersTest::testLogLineClassWarn":0.003,"SystemControllerHelpersTest::testLogLineClassNotice":0.009,"SystemControllerHelpersTest::testLogLineClassHttp500":0.003,"SystemControllerHelpersTest::testLogLineClassHttp300":0.002,"SystemControllerHelpersTest::testLogLineClassDefault":0.003,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0.004,"SystemControllerHelpersTest::testStatusLabelInactive":0.003,"SystemControllerHelpersTest::testStatusLabelFailed":0.003,"SystemControllerHelpersTest::testStatusLabelWarn":0.004,"SystemControllerHelpersTest::testStatusLabelUnknown":0.003,"SystemControllerHelpersTest::testStatusClassOk":0.005,"SystemControllerHelpersTest::testStatusClassWarn":0.003,"SystemControllerHelpersTest::testStatusClassError":0.009,"SystemControllerHelpersTest::testStatusClassUnknown":0.004,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.003,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.006,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.003,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.003,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.007,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.004,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.007,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.007,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.011,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.004,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.004,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.003,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.003,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.003,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.002,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.002,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0.002,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.008,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.004,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.004,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.003,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.009,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.005,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.01,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0.007,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.003,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.003,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.053,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.003,"DatabaseExtendedTest::testRenameTagUpdatesName":0.008,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.003,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.061,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.004,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0.003,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.007,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.003,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.003,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.372,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.295,"ShareLinkExtendedTest::testFindBySlugHit":0.302,"ShareLinkExtendedTest::testFindBySlugMiss":0.002,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.3,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.002,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.323,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.334,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.326,"ShareLinkExtendedTest::testCreateWithLockedYear":0.312,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.309,"ShareLinkExtendedTest::testUpdateLockedYear":0.33,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.31,"ShareLinkExtendedTest::testIncrementUsage":0.316,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.332,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.322,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.002,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.003,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.002,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":1.349,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0.001,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.007,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.215,"AutofocusFieldForErrorTest::testEditAutofocusYear":0.003,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0.006,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0.003,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0.003,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0.004,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.011,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.006,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.006,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.004,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.014,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.013,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.006,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.006,"ThesisCreateValidationTest::testYearZeroRejected":0.005,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.005,"ThesisCreateValidationTest::testFarFutureYearRejected":0.006,"ThesisCreateValidationTest::testCurrentYearAccepted":0.005,"ThesisCreateValidationTest::testMalformedUrlRejected":0.003,"ThesisCreateValidationTest::testValidUrlAccepted":0.002,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0.005,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.004,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.007,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.005,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.007,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.009,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.007,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.005,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.002,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.002,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.002,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.002,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.002,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.002,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.003,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.008,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.005,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.003,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.794,"ErrorHandlerTest::testFkApPrograms":0.002,"ErrorHandlerTest::testFkFinalityTypes":0.001,"ErrorHandlerTest::testFkThesisLanguages":0.001,"ErrorHandlerTest::testFkThesisFormats":0.003,"ErrorHandlerTest::testFkThesisTags":0.005,"ErrorHandlerTest::testFkThesisSupervisors":0.002,"ErrorHandlerTest::testFkAccessTypes":0.002,"ErrorHandlerTest::testFkLicenseTypes":0.002,"ErrorHandlerTest::testFkAuthors":0.002,"ErrorHandlerTest::testFkQuotedTableName":0.002,"ErrorHandlerTest::testFkQuotedLanguages":0.002,"ErrorHandlerTest::testFkQuotedFormatTypes":0.001,"ErrorHandlerTest::testFkReferencesTags":0.001,"ErrorHandlerTest::testFkReferencesOrientations":0.001,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.001,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.002,"ErrorHandlerTest::testUniqueConstraint":0.002,"ErrorHandlerTest::testNotNullConstraint":0.003,"ErrorHandlerTest::testGenericPdoError":0.004,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.009,"ErrorHandlerTest::testValidationExceptionPassesThrough":0.003,"ErrorHandlerTest::testGenericExceptionPassesThrough":0.002,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0.002,"ErrorHandlerTest::testLogWithContext":0.024,"ErrorHandlerTest::testLogWithNullValues":0.003,"ErrorHandlerTest::testLogWithEmptyExtra":0.002,"ErrorHandlerTest::testFkQuotedColumnNames":0.002,"ErrorHandlerTest::testFkUpdateStatement":0.002,"ErrorHandlerTest::testFkWithReferencesAndInsert":0.002,"PureLogicTest::testSplitJuryByRoleAllRoles":0.064,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0.004,"PureLogicTest::testSplitJuryByRoleEmptyJury":0.002,"PureLogicTest::testCollectCaptionPathsVttByMime":0.002,"PureLogicTest::testCollectCaptionPathsVttByExtension":0.002,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0.002,"PureLogicTest::testDetectFileTypeByMime":0.002,"PureLogicTest::testDetectFileTypeByExtensionFallback":0.002,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.058,"SearchControllerTest::testCoverMapContainsKnownThesis":0.007}}
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 97ee59b..edb6198 100644
--- a/TODO.md
+++ b/TODO.md
@@ -225,8 +225,12 @@
- [x] Step 4 — Update templates (data-queue-type on all inputs, data-existing-files in edit)
- [x] Step 5 — Update upload-progress.js (new collectFileNames, pending-uploads guard)
- [ ] Step 6 — QA / integration testing
+- [x] Logs accessible via Paramètres: app, admin, error, audit tabs (JSON parsed to readable lines), nginx tabs kept
+- [x] Remove nginx config tab and PHP-FPM error log tab from UI
- [ ] Step 7 — Cleanup: remove transition flags, remove INPUT_ID_TO_TYPE
+
+
# CSP & Deploy Fixes (May 2026)
- [x] Track vendor JS files in jj (they were moved to vendor/ but never `jj file track`ed)
diff --git a/app/public/admin/parametres.php b/app/public/admin/parametres.php
index dda265b..586e264 100644
--- a/app/public/admin/parametres.php
+++ b/app/public/admin/parametres.php
@@ -48,11 +48,9 @@ $diskPct = $diskInfo['pct'];
$diskColor = SystemController::diskColor($diskPct);
// ── Logs section ──────────────────────────────────────────────────────────────
-$activeTab = $_GET['tab'] ?? 'nginx_access';
-if ($activeTab === 'status') {
- $activeTab = 'nginx_access';
-} elseif ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
- $activeTab = 'nginx_access';
+$activeTab = $_GET['tab'] ?? 'app';
+if ($activeTab === 'status' || !array_key_exists($activeTab, SystemController::LOG_FILES)) {
+ $activeTab = 'app';
}
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
@@ -60,27 +58,12 @@ if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
$selectedN = 100;
}
-$logLines = null;
-$logError = null;
-$logFileMeta = null;
-
-$nginxConfigLines = null;
-$nginxConfigSource = null;
-$nginxConfigError = null;
-$nginxConfigMeta = null;
-
-if ($activeTab === 'nginx_config') {
- $nginxData = $_controller->getNginxConfigData();
- $nginxConfigLines = $nginxData['lines'];
- $nginxConfigSource = $nginxData['source'];
- $nginxConfigMeta = $nginxData['meta'];
- $nginxConfigError = $nginxData['error'];
-} else {
- $logData = $_controller->getLogData($activeTab, $selectedN);
- $logLines = $logData['lines'];
- $logError = $logData['error'];
- $logFileMeta = $logData['meta'];
-}
+$logData = $_controller->getLogData($activeTab, $selectedN);
+$logLines = $logData['lines'];
+$logError = $logData['error'];
+$logFileMeta = $logData['meta'];
+$logIsJson = $logData['isJson'] ?? false;
+$notYet = $logData['notYet'] ?? false;
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
$statusInitiallyCollapsed = $collapsed === '1';
diff --git a/app/public/admin/system-fragment.php b/app/public/admin/system-fragment.php
index 7001fd7..682a180 100644
--- a/app/public/admin/system-fragment.php
+++ b/app/public/admin/system-fragment.php
@@ -2,9 +2,9 @@
/**
* system-fragment.php — returns only the tab-panel HTML for the admin system page.
*
- * Called by fetch() from system.php JS when switching tabs or changing line count.
+ * Called by fetch() from parametres.php JS when switching tabs or changing line count.
* With JS disabled the user never hits this URL directly; the tab hrefs still
- * point at system.php?tab=… so navigation degrades gracefully.
+ * point at parametres.php?tab=… so navigation degrades gracefully.
*
* Response: text/html fragment (no // wrapper).
* On any auth failure or bad request: 403 / 400 with a plain-text body.
@@ -23,9 +23,9 @@ if (!AdminAuth::isAuthenticated()) {
}
// ── Validate inputs ────────────────────────────────────────────────────────
-$activeTab = $_GET['tab'] ?? 'nginx_access';
-if ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
- $activeTab = 'nginx_access';
+$activeTab = $_GET['tab'] ?? 'app';
+if (!array_key_exists($activeTab, SystemController::LOG_FILES)) {
+ $activeTab = 'app';
}
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
@@ -42,19 +42,11 @@ $_cache = new SystemCache($_db->getPDO());
$_controller = new SystemController($_db, $_cache);
// ── Render ─────────────────────────────────────────────────────────────────
-if ($activeTab === 'nginx_config') {
- $nginxData = $_controller->getNginxConfigData();
- $nginxConfigLines = $nginxData['lines'];
- $nginxConfigSource = $nginxData['source'];
- $nginxConfigMeta = $nginxData['meta'];
- $nginxConfigError = $nginxData['error'];
+$logData = $_controller->getLogData($activeTab, $selectedN);
+$logLines = $logData['lines'];
+$logError = $logData['error'];
+$logFileMeta = $logData['meta'];
+$logIsJson = $logData['isJson'] ?? false;
+$notYet = $logData['notYet'] ?? false;
- include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php';
-} else {
- $logData = $_controller->getLogData($activeTab, $selectedN);
- $logLines = $logData['lines'];
- $logError = $logData['error'];
- $logFileMeta = $logData['meta'];
-
- include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
-}
+include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
diff --git a/app/public/assets/css/system.css b/app/public/assets/css/system.css
index bdc7d51..6dff4ff 100644
--- a/app/public/assets/css/system.css
+++ b/app/public/assets/css/system.css
@@ -352,29 +352,4 @@
border: 1px solid var(--success-muted-border);
}
-/* ── Nginx config viewer ───────────────────────────────────────────────── */
-.nginx-source-badge {
- display: inline-block;
- font-size: var(--step--2);
- font-family: ui-monospace, monospace;
- padding: var(--space-3xs) var(--space-2xs);
- border-radius: var(--radius);
- margin-left: var(--space-2xs);
- vertical-align: middle;
-}
-.nginx-source-badge--live {
- background: var(--success-muted-bg);
- color: var(--success);
- border: 1px solid var(--success-muted-border);
-}
-.nginx-source-badge--local {
- background: var(--warning-muted-bg);
- color: var(--warning);
- border: 1px solid var(--warning-muted-border);
-}
-/* Nginx syntax highlight layers inside .log-output */
-.nginx-comment { color: var(--sys-syntax-comment); font-style: italic; }
-.nginx-directive { color: var(--sys-syntax-directive); }
-.nginx-block { color: var(--sys-syntax-block); font-weight: 600; }
-.nginx-value { color: var(--sys-syntax-value); }
-.nginx-location { color: var(--sys-syntax-location); }
+
diff --git a/app/src/AdminLogger.php b/app/src/AdminLogger.php
index 7abd489..e63cfc2 100644
--- a/app/src/AdminLogger.php
+++ b/app/src/AdminLogger.php
@@ -14,21 +14,10 @@
*/
class AdminLogger
{
- private string $logFile;
private ?Database $db;
public function __construct(?Database $db = null)
{
- if (php_sapi_name() === 'cli-server') {
- $dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs';
- if (!is_dir($dir)) {
- mkdir($dir, 0755, true);
- }
- $this->logFile = $dir . '/admin.log';
- } else {
- $this->logFile = '/var/log/xamxam.log';
- }
-
$this->db = $db;
}
@@ -256,10 +245,10 @@ class AdminLogger
$entry['context'] = $context;
}
- $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
- if (is_writable($this->logFile) || (!file_exists($this->logFile) && is_writable(dirname($this->logFile)))) {
- error_log($line, 3, $this->logFile);
- }
+ $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+ // File output — delegates to Monolog 'admin' channel
+ Logger::get('admin')->info($line);
if ($this->db !== null) {
$this->insertDb($resource, $action, $status, $context);
diff --git a/app/src/AppLogger.php b/app/src/AppLogger.php
index f7e7072..73453fd 100644
--- a/app/src/AppLogger.php
+++ b/app/src/AppLogger.php
@@ -3,9 +3,8 @@
/**
* Structured application logger for form submissions.
*
- * Writes JSON-lines to a log file in storage/logs/.
- * Each entry contains: timestamp, source (admin|partage), action,
- * status (success|error), context (IP, UA, thesis ID, error message, etc.).
+ * Thin facade over Monolog channel 'app'.
+ * Delegates all file I/O — keeps existing public API unchanged.
*/
class AppLogger
{
@@ -16,10 +15,7 @@ class AppLogger
{
$this->logDir = $logDir ?? (defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs');
- if (!is_dir($this->logDir)) {
- mkdir($this->logDir, 0755, true);
- }
-
+ // Keep for backward compat — actual file I/O is now handled by Monolog via Logger::get('app')
$this->logFile = $this->logDir . '/form-submissions.log';
}
@@ -88,7 +84,7 @@ class AppLogger
}
/**
- * Write a structured log line.
+ * Write a structured log line — delegates to Monolog 'app' channel.
*/
private function write(array $entry): void
{
@@ -96,7 +92,8 @@ class AppLogger
$entry['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$entry['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
- $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
- error_log($line, 3, $this->logFile);
+ $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+ Logger::get('app')->info($line);
}
}
diff --git a/app/src/Audit.php b/app/src/Audit.php
index 6e77cd6..52d1564 100644
--- a/app/src/Audit.php
+++ b/app/src/Audit.php
@@ -34,6 +34,7 @@ class Audit
?array $oldData = null,
?array $newData = null
): void {
+ // DB write is the primary path — best-effort, never crash.
try {
$stmt = $db->getConnection()->prepare(
'INSERT INTO audit_log (actor, action, table_name, record_id, old_data, new_data)
@@ -49,7 +50,33 @@ class Audit
]);
} catch (\Throwable $e) {
// Audit logging is best-effort — never crash the app over it.
- error_log('[Audit] write failed: ' . $e->getMessage());
+ error_log('[Audit] DB write failed: ' . $e->getMessage());
+ }
+
+ // File shadow — structured JSON-line log for debuggability
+ // (Option A from monolog-plan: keep Audit DB logic as-is, add file trace)
+ try {
+ $entry = [
+ 'timestamp' => date('c'),
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ 'actor' => $actor,
+ 'action' => $action,
+ 'table' => $tableName,
+ 'record_id' => $recordId,
+ ];
+ if ($oldData !== null) {
+ $entry['old_data'] = $oldData;
+ }
+ if ($newData !== null) {
+ $entry['new_data'] = $newData;
+ }
+
+ $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ Logger::get('audit')->info($line);
+ } catch (\Throwable $e) {
+ // File shadow is also best-effort
+ error_log('[Audit] file shadow write failed: ' . $e->getMessage());
}
}
diff --git a/app/src/Controllers/SystemController.php b/app/src/Controllers/SystemController.php
index a0a8128..59e835b 100644
--- a/app/src/Controllers/SystemController.php
+++ b/app/src/Controllers/SystemController.php
@@ -11,9 +11,8 @@
* maintenance mode) with SystemCache TTL caching
* - PHP environment info (1-hour TTL)
* - Disk usage info (5-minute TTL)
- * - Log file reading (tail, meta)
- * - Nginx config file reading
- * - Log/nginx line classifiers used by both system.php and system-fragment.php
+ * - Log file reading (tail, meta, JSON-line parsing)
+ * - Log line classifiers used by both system.php and system-fragment.php
*
* Both system.php (full page) and system-fragment.php (AJAX panel) delegate
* here so helpers are never duplicated.
@@ -23,17 +22,39 @@ class SystemController
// ── Constants ─────────────────────────────────────────────────────────────
public const LOG_FILES = [
- 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log'],
- 'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/xamxam_error.log'],
- 'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'],
+ 'app' => ['label' => 'App — soumissions', 'path' => null, 'json' => true],
+ 'admin' => ['label' => 'Admin — actions', 'path' => null, 'json' => true],
+ 'error' => ['label' => 'Erreurs — application', 'path' => null, 'json' => true],
+ 'audit' => ['label' => 'Audit — données', 'path' => null, 'json' => true],
+ 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/xamxam_access.log', 'json' => false],
+ 'nginx_error' => ['label' => 'nginx — erreurs','path' => '/var/log/nginx/xamxam_error.log', 'json' => false],
];
- public const ALLOWED_LINES = [50, 100, 200, 500];
+ /**
+ * Resolve a log file path — app logs live under STORAGE_ROOT, system logs
+ * have hard-coded paths (only valid in production).
+ */
+ private static function resolveLogPath(string $tab): string
+ {
+ $def = self::LOG_FILES[$tab];
+ if ($def['path'] !== null) {
+ return $def['path'];
+ }
+ // App logs: storage/logs/{channel}.log (Monolog RotatingFileHandler uses
+ // this as base name; the current log is always at {channel}-YYYY-MM-DD.log)
+ $dir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : APP_ROOT . '/storage/logs';
+ // Find the most recent dated log file for this channel
+ $base = $dir . '/' . $tab;
+ $dated = glob($base . '-20[0-9][0-9]-[0-9][0-9]-[0-9][0-9].log');
+ if (!empty($dated)) {
+ rsort($dated); // newest first
+ return $dated[0];
+ }
+ // Fall back to the bare name (pre-existing or non-rotated)
+ return $base . '.log';
+ }
- /** Live deployed nginx config path. */
- public const NGINX_CONFIG_LIVE = '/etc/nginx/sites-available/xamxam';
- /** Local reference copy used as fallback in dev. */
- public const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/xamxam.conf';
+ public const ALLOWED_LINES = [50, 100, 200, 500];
// ── TTLs ──────────────────────────────────────────────────────────────────
private const TTL_STATUS = 120; // 2 minutes
@@ -140,11 +161,37 @@ class SystemController
*/
public function getLogData(string $tab, int $n): array
{
- $logPath = self::LOG_FILES[$tab]['path'];
+ $logPath = self::resolveLogPath($tab);
+ $isJson = self::LOG_FILES[$tab]['json'] ?? false;
$error = null;
- $lines = $this->readLogTail($logPath, $n, $error);
- $meta = null;
+ $rawLines = null;
+ if (!file_exists($logPath)) {
+ // App logs are rotated by Monolog; a missing file just means no
+ // events have been logged yet. Show a friendly empty-state message
+ // instead of a scary "fichier introuvable" error.
+ if ($isJson) {
+ return [
+ 'lines' => [],
+ 'error' => null,
+ 'meta' => null,
+ 'isJson' => true,
+ 'notYet' => true,
+ ];
+ }
+ // System logs genuinely missing — show an error
+ $error = 'Fichier introuvable : ' . htmlspecialchars($logPath);
+ } else {
+ $rawLines = $this->readLogTail($logPath, $n, $error);
+ }
+
+ // Parse JSON lines into display strings
+ $lines = null;
+ if ($rawLines !== null) {
+ $lines = $isJson ? self::formatJsonLines($rawLines) : $rawLines;
+ }
+
+ $meta = null;
if (file_exists($logPath)) {
$sz = filesize($logPath);
$meta = [
@@ -156,43 +203,102 @@ class SystemController
];
}
- return ['lines' => $lines, 'error' => $error, 'meta' => $meta];
+ return ['lines' => $lines, 'error' => $error, 'meta' => $meta, 'isJson' => $isJson, 'notYet' => false];
}
- // ── Nginx config tab ──────────────────────────────────────────────────────
-
/**
- * Read and return data for the nginx config tab.
+ * Format JSON log lines into human-readable display strings.
*
- * @return array{lines: ?array, source: ?string, meta: ?array, error: ?string}
+ * Each line is a flat JSON object. We extract key fields and build a
+ * compact one-line representation with emoji status indicators.
+ *
+ * @param string[] $jsonLines Raw JSON strings (newest first).
+ * @return string[] Formatted display lines.
*/
- public function getNginxConfigData(): array
+ private static function formatJsonLines(array $jsonLines): array
{
- $livePath = self::NGINX_CONFIG_LIVE;
- $localPath = self::NGINX_CONFIG_LOCAL;
-
- foreach ([[$livePath, 'live'], [$localPath, 'local']] as [$path, $src]) {
- if (file_exists($path) && is_readable($path)) {
- $raw = file($path, FILE_IGNORE_NEW_LINES);
- if ($raw !== false) {
- $sz = filesize($path);
- $meta = [
- 'path' => $path,
- 'size' => $sz > 1048576
- ? number_format($sz / 1048576, 2) . ' MB'
- : number_format($sz / 1024, 1) . ' KB',
- 'mtime' => date('d/m/Y H:i:s', filemtime($path)),
- ];
- return ['lines' => $raw, 'source' => $src, 'meta' => $meta, 'error' => null];
- }
+ $formatted = [];
+ foreach ($jsonLines as $raw) {
+ $entry = json_decode($raw, true);
+ if (!is_array($entry)) {
+ $formatted[] = $raw; // not valid JSON — show raw
+ continue;
}
+
+ $parts = [];
+
+ // Timestamp
+ $ts = $entry['timestamp'] ?? '';
+ if ($ts !== '') {
+ $parts[] = substr($ts, 0, 19); // YYYY-MM-DDTHH:MM:SS
+ }
+
+ // Status emoji
+ $status = $entry['status'] ?? '';
+ if ($status === 'success' || $status === 'active') {
+ $parts[] = '✓';
+ } elseif ($status === 'error' || $status === 'duplicate') {
+ $parts[] = '✗';
+ }
+
+ // Key identifying fields (vary by channel)
+ $id = $entry['resource']
+ ?? $entry['source']
+ ?? $entry['context']
+ ?? '';
+ if ($id !== '') {
+ $parts[] = $id;
+ }
+
+ // Action
+ $action = $entry['action'] ?? '';
+ if ($action !== '') {
+ $parts[] = $action;
+ }
+
+ // Status text
+ if ($status !== '' && $status !== 'success') {
+ $parts[] = $status;
+ }
+
+ // Actor / IP
+ $actor = $entry['actor'] ?? '';
+ if ($actor !== '') {
+ $parts[] = $actor;
+ } elseif (isset($entry['ip'])) {
+ $parts[] = $entry['ip'];
+ }
+
+ // User agent (compact)
+ $ua = $entry['user_agent'] ?? '';
+ if ($ua !== '' && $ua !== 'unknown' && $ua !== 'cli') {
+ // Truncate UA: first meaningful segment
+ $uaShort = preg_match('#^([^(]+)#', $ua, $m) ? trim($m[1]) : $ua;
+ if (mb_strlen($uaShort) > 60) {
+ $uaShort = mb_substr($uaShort, 0, 57) . '…';
+ }
+ $parts[] = $uaShort;
+ }
+
+ // For error log: exception + message
+ if (isset($entry['exception']) && isset($entry['message'])) {
+ $msg = $entry['exception'] . ': ' . $entry['message'];
+ if (mb_strlen($msg) > 120) {
+ $msg = mb_substr($msg, 0, 117) . '…';
+ }
+ $parts[] = $msg;
+ }
+
+ // For audit log: table + record_id
+ $table = $entry['table'] ?? '';
+ if ($table !== '') {
+ $parts[] = $table . (isset($entry['record_id']) ? '#' . $entry['record_id'] : '');
+ }
+
+ $formatted[] = implode(' ', $parts);
}
- $error = file_exists($livePath)
- ? 'Fichier non lisible (permissions insuffisantes) : ' . htmlspecialchars($livePath)
- : 'Config live introuvable (' . htmlspecialchars($livePath) . ') et config locale introuvable (' . htmlspecialchars($localPath) . ').';
-
- return ['lines' => null, 'source' => null, 'meta' => null, 'error' => $error];
+ return $formatted;
}
// ── Line classifiers (used by both system.php and system-fragment.php) ────
@@ -223,21 +329,6 @@ class SystemController
return '';
}
- /**
- * Return the CSS class for a nginx config line.
- */
- public static function nginxLineClass(string $line): string
- {
- $trimmed = ltrim($line);
- if ($trimmed === '' || str_starts_with($trimmed, '#')) {
- return 'nginx-comment';
- }
- if (preg_match('/^\s*(location|server|upstream|events|http|geo|map|types)\b/', $line)) {
- return 'nginx-block';
- }
- return 'nginx-directive';
- }
-
// ── View helpers ──────────────────────────────────────────────────────────
/**
@@ -399,15 +490,14 @@ class SystemController
/**
* Read the tail of a log file, newest-first. Returns null on error.
+ *
+ * Prefers tail(1) for efficiency on large files; falls back to a pure-PHP
+ * read for app log files when exec() is unavailable or tail fails.
*/
private function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array
{
$errorMsg = null;
- if (!function_exists('exec')) {
- $errorMsg = 'exec() est désactivé sur ce serveur.';
- return null;
- }
if (!file_exists($logPath)) {
$errorMsg = 'Fichier introuvable : ' . htmlspecialchars($logPath);
return null;
@@ -417,16 +507,25 @@ class SystemController
return null;
}
- $output = [];
- $rc = 0;
- exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
+ // Try tail(1) first — fast on large log files
+ if (function_exists('exec')) {
+ $output = [];
+ $rc = 0;
+ @exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
+ if ($rc === 0) {
+ return array_reverse($output); // newest first
+ }
+ }
- if ($rc !== 0) {
+ // PHP fallback — reads the whole file, returns last N lines
+ $raw = @file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ if ($raw === false) {
$errorMsg = 'Erreur lors de la lecture du fichier journal.';
return null;
}
- return array_reverse($output); // newest first
+ $slice = array_slice($raw, -min($n, count($raw)));
+ return array_reverse($slice);
}
/**
diff --git a/app/src/ErrorHandler.php b/app/src/ErrorHandler.php
index 2f97f25..0eefdcd 100644
--- a/app/src/ErrorHandler.php
+++ b/app/src/ErrorHandler.php
@@ -154,7 +154,7 @@ class ErrorHandler
}
/**
- * Write a structured error log entry.
+ * Write a structured error log entry — delegates to Monolog 'error' channel.
*
* @param string $context e.g. 'thesis_edit', 'partage_submit', 'csv_import'
* @param \Throwable $e
@@ -162,22 +162,22 @@ class ErrorHandler
*/
public static function log(string $context, \Throwable $e, array $extra = []): void
{
- $parts = [
- 'context=' . $context,
- 'exception=' . get_class($e),
- 'message=' . $e->getMessage(),
+ $entry = [
+ 'timestamp' => date('c'),
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ 'context' => $context,
+ 'exception' => get_class($e),
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
];
- foreach ($extra as $k => $v) {
- if (is_scalar($v) || $v === null) {
- $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
- } elseif (is_array($v)) {
- $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
- } else {
- $parts[] = $k . '=' . gettype($v);
- }
- }
- $parts[] = 'trace=' . $e->getTraceAsString();
- error_log(implode(' | ', $parts));
+ if (!empty($extra)) {
+ $entry['extra'] = $extra;
+ }
+
+ $line = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+ Logger::get('error')->error($line);
}
}
diff --git a/app/src/Logger.php b/app/src/Logger.php
new file mode 100644
index 0000000..b441389
--- /dev/null
+++ b/app/src/Logger.php
@@ -0,0 +1,105 @@
+info('message', [...]);
+ * Logger::get('admin')->warning('message', [...]);
+ */
+class Logger
+{
+ /** @var array
/etc/nginx/sites-available/xamxam n'existe pas.
- La config de référence se trouve dans nginx/xamxam.conf.
- - Affiché le = date('d/m/Y à H:i:s') ?> — - Rafraîchir — - Forcer actualisation -
- - -