mirror of
https://github.com/zadam/trilium.git
synced 2025-12-06 07:24:25 +01:00
feat: implement centralized logging system
Components: - CentralizedLogger static class for log aggregation - Logger class with source context (background/content/popup/options) - Persistent storage in chrome.storage.local (up to 1000 entries) - Log viewer UI with filtering, search, and export - Survives service worker restarts Critical for MV3 debugging where service workers terminate frequently. Provides unified debugging across all extension contexts.
This commit is contained in:
parent
acbd5c8bcf
commit
6811b91a17
280
apps/web-clipper-manifestv3/src/logs/index.html
Normal file
280
apps/web-clipper-manifestv3/src/logs/index.html
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Trilium Web Clipper - Log Viewer</title>
|
||||||
|
<style>
|
||||||
|
/* Reset and base styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #2d2d2d;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple controls */
|
||||||
|
.controls {
|
||||||
|
background: #3d3d3d;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls label {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #f0f0f0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, input {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding-right: 30px;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
background-size: 16px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #007cba;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #005a87;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple log container */
|
||||||
|
.logs-container {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple log entry */
|
||||||
|
.log-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-meta {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
color: #e0e0e0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.info { background: #17a2b8; }
|
||||||
|
.log-level.debug { background: #6c757d; }
|
||||||
|
.log-level.warn { background: #ffc107; color: #000; }
|
||||||
|
.log-level.error { background: #dc3545; }
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
background: #555;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-refresh indicators */
|
||||||
|
#auto-refresh-interval {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-refresh-status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-logs-indicator {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ccc;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border: 1px solid #444;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand/Collapse all buttons container */
|
||||||
|
.expand-collapse-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#expand-all-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
#collapse-all-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls > * {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-collapse-controls {
|
||||||
|
margin-left: 0;
|
||||||
|
order: -1; /* Show expand/collapse buttons at top on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Extension Log Viewer</h1>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label>Level:</label>
|
||||||
|
<select id="level-filter">
|
||||||
|
<option value="">All Levels</option>
|
||||||
|
<option value="debug">Debug</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="warn">Warning</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Source:</label>
|
||||||
|
<select id="source-filter">
|
||||||
|
<option value="">All Sources</option>
|
||||||
|
<option value="background">Background</option>
|
||||||
|
<option value="content">Content</option>
|
||||||
|
<option value="popup">Popup</option>
|
||||||
|
<option value="options">Options</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="search" id="search-box" placeholder="Search logs...">
|
||||||
|
|
||||||
|
<label>Auto-refresh:</label>
|
||||||
|
<select id="auto-refresh-interval">
|
||||||
|
<option value="0">Off</option>
|
||||||
|
<option value="1000">1 second</option>
|
||||||
|
<option value="2000">2 seconds</option>
|
||||||
|
<option value="5000" selected>5 seconds</option>
|
||||||
|
<option value="10000">10 seconds</option>
|
||||||
|
<option value="30000">30 seconds</option>
|
||||||
|
<option value="60000">1 minute</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button id="refresh-btn">Refresh</button>
|
||||||
|
<button id="export-btn">Export</button>
|
||||||
|
<button id="clear-btn" style="background: #dc3545;">Clear All</button>
|
||||||
|
|
||||||
|
<div class="expand-collapse-controls">
|
||||||
|
<button id="expand-all-btn" style="background: #28a745;">Expand All</button>
|
||||||
|
<button id="collapse-all-btn" style="background: #6c757d;">Collapse All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs-container">
|
||||||
|
<div id="logs-list">Loading logs...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="logs.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
495
apps/web-clipper-manifestv3/src/logs/logs.css
Normal file
495
apps/web-clipper-manifestv3/src/logs/logs.css
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
/*
|
||||||
|
* Clean, simple log viewer CSS - no complex layouts
|
||||||
|
* This file is now unused - styles are inline in index.html
|
||||||
|
* Keeping this file for compatibility but styles are embedded
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force normal text layout for all log elements */
|
||||||
|
.log-entry * {
|
||||||
|
writing-mode: horizontal-tb !important;
|
||||||
|
text-orientation: mixed !important;
|
||||||
|
direction: ltr !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force vertical stacking - override any inherited flexbox/grid/column layouts */
|
||||||
|
.log-entries, #logs-list {
|
||||||
|
display: block !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
grid-template-columns: none !important;
|
||||||
|
column-count: 1 !important;
|
||||||
|
columns: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nuclear option - force all log entries to stack vertically */
|
||||||
|
.log-entries .log-entry {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
float: none !important;
|
||||||
|
position: relative !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
top: auto !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure no flexbox/grid on any parent containers */
|
||||||
|
.log-entries * {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: var(--color-surface);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--color-surface-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
input[type="text"],
|
||||||
|
input[type="search"] {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: var(--theme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus,
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="search"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--theme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
background: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover {
|
||||||
|
background: var(--color-error-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log entries */
|
||||||
|
.log-entries {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logs-list {
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
column-count: unset !important;
|
||||||
|
columns: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
float: none !important;
|
||||||
|
position: static !important;
|
||||||
|
flex: none !important;
|
||||||
|
clear: both !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.debug {
|
||||||
|
background: var(--color-surface-secondary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.info {
|
||||||
|
background: var(--color-info-bg);
|
||||||
|
color: var(--color-info-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.warn {
|
||||||
|
background: var(--color-warning-bg);
|
||||||
|
color: var(--color-warning-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.error {
|
||||||
|
background: var(--color-error-bg);
|
||||||
|
color: var(--color-error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message-text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message-text.truncated {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-data {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--color-surface-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics */
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--color-surface-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle */
|
||||||
|
.theme-toggle {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp,
|
||||||
|
.log-level,
|
||||||
|
.log-source {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--color-border-primary);
|
||||||
|
border-top: 2px solid var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.log-entries::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entries::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-surface-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entries::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entries::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export dialog styling */
|
||||||
|
.export-dialog {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-content {
|
||||||
|
background: var(--color-surface);
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-content h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--theme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-option:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-option input[type="radio"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
294
apps/web-clipper-manifestv3/src/logs/logs.ts
Normal file
294
apps/web-clipper-manifestv3/src/logs/logs.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { CentralizedLogger, LogEntry } from '@/shared/utils';
|
||||||
|
|
||||||
|
class SimpleLogViewer {
|
||||||
|
private logs: LogEntry[] = [];
|
||||||
|
private autoRefreshTimer: number | null = null;
|
||||||
|
private lastLogCount: number = 0;
|
||||||
|
private autoRefreshEnabled: boolean = true;
|
||||||
|
private expandedLogs: Set<string> = new Set(); // Track which logs are expanded
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
this.setupEventHandlers();
|
||||||
|
await this.loadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventHandlers(): void {
|
||||||
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
const exportBtn = document.getElementById('export-btn');
|
||||||
|
const clearBtn = document.getElementById('clear-btn');
|
||||||
|
const expandAllBtn = document.getElementById('expand-all-btn');
|
||||||
|
const collapseAllBtn = document.getElementById('collapse-all-btn');
|
||||||
|
const levelFilter = document.getElementById('level-filter') as HTMLSelectElement;
|
||||||
|
const sourceFilter = document.getElementById('source-filter') as HTMLSelectElement;
|
||||||
|
const searchBox = document.getElementById('search-box') as HTMLInputElement;
|
||||||
|
const autoRefreshSelect = document.getElementById('auto-refresh-interval') as HTMLSelectElement;
|
||||||
|
|
||||||
|
refreshBtn?.addEventListener('click', () => this.loadLogs());
|
||||||
|
exportBtn?.addEventListener('click', () => this.exportLogs());
|
||||||
|
clearBtn?.addEventListener('click', () => this.clearLogs());
|
||||||
|
expandAllBtn?.addEventListener('click', () => this.expandAllLogs());
|
||||||
|
collapseAllBtn?.addEventListener('click', () => this.collapseAllLogs());
|
||||||
|
levelFilter?.addEventListener('change', () => this.renderLogs());
|
||||||
|
sourceFilter?.addEventListener('change', () => this.renderLogs());
|
||||||
|
searchBox?.addEventListener('input', () => this.renderLogs());
|
||||||
|
autoRefreshSelect?.addEventListener('change', (e) => this.handleAutoRefreshChange(e));
|
||||||
|
|
||||||
|
// Start auto-refresh with default interval (5 seconds)
|
||||||
|
this.startAutoRefresh(5000);
|
||||||
|
|
||||||
|
// Pause auto-refresh when tab is not visible
|
||||||
|
this.setupVisibilityHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupVisibilityHandling(): void {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
this.autoRefreshEnabled = !document.hidden;
|
||||||
|
|
||||||
|
// If tab becomes visible again, refresh immediately
|
||||||
|
if (!document.hidden) {
|
||||||
|
this.loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const newLogs = await CentralizedLogger.getLogs();
|
||||||
|
const hasNewLogs = newLogs.length !== this.lastLogCount;
|
||||||
|
|
||||||
|
this.logs = newLogs;
|
||||||
|
this.lastLogCount = newLogs.length;
|
||||||
|
|
||||||
|
this.renderLogs();
|
||||||
|
|
||||||
|
// Show notification if new logs arrived during auto-refresh
|
||||||
|
if (hasNewLogs && this.logs.length > 0) {
|
||||||
|
this.showNewLogsIndicator();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error);
|
||||||
|
this.showError('Failed to load logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAutoRefreshChange(event: Event): void {
|
||||||
|
const select = event.target as HTMLSelectElement;
|
||||||
|
const interval = parseInt(select.value);
|
||||||
|
|
||||||
|
if (interval === 0) {
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
} else {
|
||||||
|
this.startAutoRefresh(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAutoRefresh(intervalMs: number): void {
|
||||||
|
this.stopAutoRefresh(); // Clear any existing timer
|
||||||
|
|
||||||
|
if (intervalMs > 0) {
|
||||||
|
this.autoRefreshTimer = window.setInterval(() => {
|
||||||
|
if (this.autoRefreshEnabled) {
|
||||||
|
this.loadLogs();
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopAutoRefresh(): void {
|
||||||
|
if (this.autoRefreshTimer) {
|
||||||
|
clearInterval(this.autoRefreshTimer);
|
||||||
|
this.autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showNewLogsIndicator(): void {
|
||||||
|
// Flash the refresh button to indicate new logs
|
||||||
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.style.background = '#28a745';
|
||||||
|
refreshBtn.textContent = 'New logs!';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshBtn.style.background = '#007cba';
|
||||||
|
refreshBtn.textContent = 'Refresh';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLogs(): void {
|
||||||
|
const logsList = document.getElementById('logs-list');
|
||||||
|
if (!logsList) return;
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
const levelFilter = (document.getElementById('level-filter') as HTMLSelectElement).value;
|
||||||
|
const sourceFilter = (document.getElementById('source-filter') as HTMLSelectElement).value;
|
||||||
|
const searchQuery = (document.getElementById('search-box') as HTMLInputElement).value.toLowerCase();
|
||||||
|
|
||||||
|
let filteredLogs = this.logs.filter(log => {
|
||||||
|
if (levelFilter && log.level !== levelFilter) return false;
|
||||||
|
if (sourceFilter && log.source !== sourceFilter) return false;
|
||||||
|
if (searchQuery) {
|
||||||
|
const searchText = `${log.context} ${log.message}`.toLowerCase();
|
||||||
|
if (!searchText.includes(searchQuery)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by timestamp (newest first)
|
||||||
|
filteredLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||||
|
|
||||||
|
if (filteredLogs.length === 0) {
|
||||||
|
logsList.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">No logs found</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render simple log entries
|
||||||
|
logsList.innerHTML = filteredLogs.map(log => this.renderLogItem(log)).join('');
|
||||||
|
|
||||||
|
// Add event listeners for expand buttons
|
||||||
|
this.setupExpandButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupExpandButtons(): void {
|
||||||
|
const expandButtons = document.querySelectorAll('.expand-btn');
|
||||||
|
expandButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target as HTMLButtonElement;
|
||||||
|
const logId = btn.getAttribute('data-log-id');
|
||||||
|
if (!logId) return;
|
||||||
|
|
||||||
|
const details = document.getElementById(`details-${logId}`);
|
||||||
|
if (!details) return;
|
||||||
|
|
||||||
|
if (this.expandedLogs.has(logId)) {
|
||||||
|
// Collapse
|
||||||
|
details.style.display = 'none';
|
||||||
|
btn.textContent = 'Expand';
|
||||||
|
this.expandedLogs.delete(logId);
|
||||||
|
} else {
|
||||||
|
// Expand
|
||||||
|
details.style.display = 'block';
|
||||||
|
btn.textContent = 'Collapse';
|
||||||
|
this.expandedLogs.add(logId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLogItem(log: LogEntry): string {
|
||||||
|
const timestamp = new Date(log.timestamp).toLocaleString();
|
||||||
|
const message = this.escapeHtml(`[${log.context}] ${log.message}`);
|
||||||
|
|
||||||
|
// Handle additional data
|
||||||
|
let details = '';
|
||||||
|
if (log.args && log.args.length > 0) {
|
||||||
|
details += `<div class="log-details">${JSON.stringify(log.args, null, 2)}</div>`;
|
||||||
|
}
|
||||||
|
if (log.error) {
|
||||||
|
details += `<div class="log-details">Error: ${log.error.name}: ${log.error.message}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsExpand = message.length > 200 || details;
|
||||||
|
const displayMessage = needsExpand ? message.substring(0, 200) + '...' : message;
|
||||||
|
|
||||||
|
// Check if this log is currently expanded
|
||||||
|
const isExpanded = this.expandedLogs.has(log.id);
|
||||||
|
const displayStyle = isExpanded ? 'block' : 'none';
|
||||||
|
const buttonText = isExpanded ? 'Collapse' : 'Expand';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="log-item">
|
||||||
|
<div class="log-meta">
|
||||||
|
${timestamp}
|
||||||
|
<span class="log-level ${log.level}">${log.level}</span>
|
||||||
|
<span style="color: #007cba;">${log.source}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-content">
|
||||||
|
${displayMessage}
|
||||||
|
${needsExpand ? `<button class="expand-btn" data-log-id="${log.id}">${buttonText}</button>` : ''}
|
||||||
|
${needsExpand ? `<div class="log-details" id="details-${log.id}" style="display: ${displayStyle};">${message}${details}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private escapeHtml(text: string): string {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async exportLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const logsJson = await CentralizedLogger.exportLogs();
|
||||||
|
const blob = new Blob([logsJson], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `trilium-logs-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearLogs(): Promise<void> {
|
||||||
|
if (confirm('Are you sure you want to clear all logs?')) {
|
||||||
|
try {
|
||||||
|
await CentralizedLogger.clearLogs();
|
||||||
|
this.logs = [];
|
||||||
|
this.expandedLogs.clear(); // Clear expanded state when clearing logs
|
||||||
|
this.renderLogs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private expandAllLogs(): void {
|
||||||
|
// Get all currently visible logs that can be expanded
|
||||||
|
const expandButtons = document.querySelectorAll('.expand-btn');
|
||||||
|
expandButtons.forEach(button => {
|
||||||
|
const logId = button.getAttribute('data-log-id');
|
||||||
|
if (logId) {
|
||||||
|
this.expandedLogs.add(logId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-render to apply the expanded state
|
||||||
|
this.renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private collapseAllLogs(): void {
|
||||||
|
// Clear all expanded states
|
||||||
|
this.expandedLogs.clear();
|
||||||
|
|
||||||
|
// Re-render to apply the collapsed state
|
||||||
|
this.renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private showError(message: string): void {
|
||||||
|
const logsList = document.getElementById('logs-list');
|
||||||
|
if (logsList) {
|
||||||
|
logsList.innerHTML = `<div style="padding: 20px; color: #dc3545; text-align: center;">${message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new SimpleLogViewer();
|
||||||
|
});
|
||||||
344
apps/web-clipper-manifestv3/src/shared/utils.ts
Normal file
344
apps/web-clipper-manifestv3/src/shared/utils.ts
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
/**
|
||||||
|
* Log entry interface for centralized logging
|
||||||
|
*/
|
||||||
|
export interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
context: string;
|
||||||
|
message: string;
|
||||||
|
args?: unknown[];
|
||||||
|
error?: {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
};
|
||||||
|
source: 'background' | 'content' | 'popup' | 'options';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized logging system for the extension
|
||||||
|
* Aggregates logs from all contexts and provides unified access
|
||||||
|
*/
|
||||||
|
export class CentralizedLogger {
|
||||||
|
private static readonly MAX_LOGS = 1000;
|
||||||
|
private static readonly STORAGE_KEY = 'extension_logs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a log entry to centralized storage
|
||||||
|
*/
|
||||||
|
static async addLog(entry: Omit<LogEntry, 'id' | 'timestamp'>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const logEntry: LogEntry = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...entry,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get existing logs
|
||||||
|
const result = await chrome.storage.local.get(this.STORAGE_KEY);
|
||||||
|
const logs: LogEntry[] = result[this.STORAGE_KEY] || [];
|
||||||
|
|
||||||
|
// Add new log and maintain size limit
|
||||||
|
logs.push(logEntry);
|
||||||
|
if (logs.length > this.MAX_LOGS) {
|
||||||
|
logs.splice(0, logs.length - this.MAX_LOGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store updated logs
|
||||||
|
await chrome.storage.local.set({ [this.STORAGE_KEY]: logs });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to store centralized log:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all logs from centralized storage
|
||||||
|
*/
|
||||||
|
static async getLogs(): Promise<LogEntry[]> {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get(this.STORAGE_KEY);
|
||||||
|
return result[this.STORAGE_KEY] || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to retrieve logs:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all logs
|
||||||
|
*/
|
||||||
|
static async clearLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.remove(this.STORAGE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export logs as JSON string
|
||||||
|
*/
|
||||||
|
static async exportLogs(): Promise<string> {
|
||||||
|
const logs = await this.getLogs();
|
||||||
|
return JSON.stringify(logs, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get logs filtered by level
|
||||||
|
*/
|
||||||
|
static async getLogsByLevel(level: LogEntry['level']): Promise<LogEntry[]> {
|
||||||
|
const logs = await this.getLogs();
|
||||||
|
return logs.filter(log => log.level === level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get logs filtered by context
|
||||||
|
*/
|
||||||
|
static async getLogsByContext(context: string): Promise<LogEntry[]> {
|
||||||
|
const logs = await this.getLogs();
|
||||||
|
return logs.filter(log => log.context === context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get logs filtered by source
|
||||||
|
*/
|
||||||
|
static async getLogsBySource(source: LogEntry['source']): Promise<LogEntry[]> {
|
||||||
|
const logs = await this.getLogs();
|
||||||
|
return logs.filter(log => log.source === source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced logging system for the extension with centralized storage
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
private context: string;
|
||||||
|
private source: LogEntry['source'];
|
||||||
|
private isDebugMode: boolean = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
constructor(context: string, source: LogEntry['source'] = 'background') {
|
||||||
|
this.context = context;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(context: string, source: LogEntry['source'] = 'background'): Logger {
|
||||||
|
return new Logger(context, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async logToStorage(level: LogEntry['level'], message: string, args?: unknown[], error?: Error): Promise<void> {
|
||||||
|
const logEntry: Omit<LogEntry, 'id' | 'timestamp'> = {
|
||||||
|
level,
|
||||||
|
context: this.context,
|
||||||
|
message,
|
||||||
|
source: this.source,
|
||||||
|
args: args && args.length > 0 ? args : undefined,
|
||||||
|
error: error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
} : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await CentralizedLogger.addLog(logEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(level: string, message: string, ...args: unknown[]): void {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const prefix = `[${timestamp}] [${this.source}:${this.context}] [${level.toUpperCase()}]`;
|
||||||
|
|
||||||
|
if (this.isDebugMode || level === 'ERROR') {
|
||||||
|
const consoleMethod = console[level as keyof typeof console] as (...args: unknown[]) => void;
|
||||||
|
if (typeof consoleMethod === 'function') {
|
||||||
|
consoleMethod(prefix, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, ...args: unknown[]): void {
|
||||||
|
this.formatMessage('debug', message, ...args);
|
||||||
|
this.logToStorage('debug', message, args).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, ...args: unknown[]): void {
|
||||||
|
this.formatMessage('info', message, ...args);
|
||||||
|
this.logToStorage('info', message, args).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, ...args: unknown[]): void {
|
||||||
|
this.formatMessage('warn', message, ...args);
|
||||||
|
this.logToStorage('warn', message, args).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, error?: Error, ...args: unknown[]): void {
|
||||||
|
this.formatMessage('error', message, error, ...args);
|
||||||
|
this.logToStorage('error', message, args, error).catch(console.error);
|
||||||
|
|
||||||
|
// In production, you might want to send errors to a logging service
|
||||||
|
if (!this.isDebugMode && error) {
|
||||||
|
this.reportError(error, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reportError(error: Error, context: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Store error details for debugging
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[`error_${Date.now()}`]: {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
context,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to store error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions
|
||||||
|
*/
|
||||||
|
export const Utils = {
|
||||||
|
/**
|
||||||
|
* Generate a random string of specified length
|
||||||
|
*/
|
||||||
|
randomString(length: number): string {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base URL of the current page
|
||||||
|
*/
|
||||||
|
getBaseUrl(url: string = window.location.href): string {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return `${urlObj.protocol}//${urlObj.host}`;
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a relative URL to absolute
|
||||||
|
*/
|
||||||
|
makeAbsoluteUrl(relativeUrl: string, baseUrl: string): string {
|
||||||
|
try {
|
||||||
|
return new URL(relativeUrl, baseUrl).href;
|
||||||
|
} catch (error) {
|
||||||
|
return relativeUrl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML content
|
||||||
|
*/
|
||||||
|
sanitizeHtml(html: string): string {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = html;
|
||||||
|
return div.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function calls
|
||||||
|
*/
|
||||||
|
debounce<T extends (...args: unknown[]) => void>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), wait);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep for specified milliseconds
|
||||||
|
*/
|
||||||
|
sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry a function with exponential backoff
|
||||||
|
*/
|
||||||
|
async retry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxAttempts: number = 3,
|
||||||
|
baseDelay: number = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: Error;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
|
||||||
|
if (attempt === maxAttempts) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = baseDelay * Math.pow(2, attempt - 1);
|
||||||
|
await this.sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message handling utilities
|
||||||
|
*/
|
||||||
|
export const MessageUtils = {
|
||||||
|
/**
|
||||||
|
* Send a message with automatic retry and error handling
|
||||||
|
*/
|
||||||
|
async sendMessage<T>(message: unknown, tabId?: number): Promise<T> {
|
||||||
|
const logger = Logger.create('MessageUtils');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = tabId
|
||||||
|
? await chrome.tabs.sendMessage(tabId, message)
|
||||||
|
: await chrome.runtime.sendMessage(message);
|
||||||
|
|
||||||
|
return response as T;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send message', error as Error, { message, tabId });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a message response handler
|
||||||
|
*/
|
||||||
|
createResponseHandler<T>(
|
||||||
|
handler: (message: unknown, sender: chrome.runtime.MessageSender) => Promise<T> | T,
|
||||||
|
source: LogEntry['source'] = 'background'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
message: unknown,
|
||||||
|
sender: chrome.runtime.MessageSender,
|
||||||
|
sendResponse: (response: T) => void
|
||||||
|
): boolean => {
|
||||||
|
const logger = Logger.create('MessageHandler', source);
|
||||||
|
|
||||||
|
Promise.resolve(handler(message, sender))
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch(error => {
|
||||||
|
logger.error('Message handler failed', error as Error, { message, sender });
|
||||||
|
sendResponse({ error: error.message } as T);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true; // Indicates async response
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user