Release v1.0.4 - Add logging system, lock button, and emoji navigation

- Comprehensive logging system with chrome.storage.session persistence
- NIP-07 action logging in background scripts with standalone functions
- Vault operation logging (unlock, lock, create, reset, import, export)
- Profile and bookmark operation logging
- Logs page with refresh functionality and category icons
- Lock button (🔒) in navigation bar to quickly lock vault
- Reduced nav bar size (40px height, 16px font) with emoji icons
- Reordered navigation: You, Permissions, Bookmarks, Logs, About, Lock
- Bookmarks functionality for saving frequently used Nostr apps
- Fixed lock/unlock flow by properly clearing in-memory session data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 12:42:19 +01:00
parent b535a7b967
commit 45b1fb58e9
51 changed files with 1267 additions and 158 deletions

View File

@@ -7,6 +7,7 @@ import { IdentityComponent } from './components/home/identity/identity.component
import { InfoComponent } from './components/home/info/info.component';
import { SettingsComponent } from './components/home/settings/settings.component';
import { LogsComponent } from './components/home/logs/logs.component';
import { BookmarksComponent } from './components/home/bookmarks/bookmarks.component';
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
@@ -71,6 +72,10 @@ export const routes: Routes = [
path: 'logs',
component: LogsComponent,
},
{
path: 'bookmarks',
component: BookmarksComponent,
},
],
},
{

View File

@@ -28,7 +28,7 @@
</div>
<div class="info-banner">
<i class="bi bi-info-circle"></i>
<span class="emoji">💡</span>
<span>These relays are fetched from your NIP-65 relay list (kind 10002). To update your relay list, use a Nostr client that supports NIP-65.</span>
</div>

View File

@@ -0,0 +1,36 @@
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
<div class="bookmarks-header">
<span class="bookmarks-title">Bookmarks</span>
<button class="btn btn-primary btn-sm" (click)="onBookmarkThisPage()">
<span class="emoji">🔖</span> Bookmark This Page
</button>
</div>
<div class="bookmarks-container">
@if (isLoading) {
<div class="loading-state">Loading...</div>
} @else if (bookmarks.length === 0) {
<div class="empty-state">
<span class="sam-text-muted">
No bookmarks yet. Click "Bookmark This Page" to add the current page.
</span>
</div>
} @else {
@for (bookmark of bookmarks; track bookmark.id) {
<div class="bookmark-item" (click)="openBookmark(bookmark)">
<div class="bookmark-info">
<span class="bookmark-title">{{ bookmark.title }}</span>
<span class="bookmark-url">{{ getDomain(bookmark.url) }}</span>
</div>
<button
class="remove-btn"
title="Remove bookmark"
(click)="onRemoveBookmark(bookmark); $event.stopPropagation()"
>
<span class="emoji"></span>
</button>
</div>
}
}
</div>

View File

@@ -0,0 +1,92 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
padding: var(--size);
overflow: hidden;
}
.bookmarks-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--size);
flex-shrink: 0;
}
.bookmarks-title {
font-weight: 600;
font-size: 1.1rem;
}
.bookmarks-container {
flex: 1;
overflow-y: auto;
}
.empty-state,
.loading-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: var(--muted-foreground);
}
.bookmark-item {
display: flex;
align-items: center;
gap: var(--size-h);
padding: var(--size-h) var(--size);
margin-bottom: var(--size-hh);
background: var(--background-light);
border-radius: var(--radius-md);
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background: var(--background-light-hover);
}
}
.bookmark-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.bookmark-title {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-url {
font-size: 0.75rem;
color: var(--muted-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.remove-btn {
all: unset;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
color: var(--muted-foreground);
transition: background-color 0.15s ease, color 0.15s ease;
&:hover {
background: var(--destructive);
color: var(--destructive-foreground);
}
}

View File

@@ -0,0 +1,91 @@
import { Component, inject, OnInit } from '@angular/core';
import { Bookmark, LoggerService, SignerMetaData } from '@common';
import { FirefoxMetaHandler } from '../../../common/data/firefox-meta-handler';
import browser from 'webextension-polyfill';
@Component({
selector: 'app-bookmarks',
templateUrl: './bookmarks.component.html',
styleUrl: './bookmarks.component.scss',
imports: [],
})
export class BookmarksComponent implements OnInit {
readonly #logger = inject(LoggerService);
readonly #metaHandler = new FirefoxMetaHandler();
bookmarks: Bookmark[] = [];
isLoading = true;
async ngOnInit() {
await this.loadBookmarks();
}
async loadBookmarks() {
this.isLoading = true;
try {
const metaData = await this.#metaHandler.loadFullData() as SignerMetaData;
this.#metaHandler.setFullData(metaData);
this.bookmarks = this.#metaHandler.getBookmarks();
} catch (error) {
console.error('Failed to load bookmarks:', error);
} finally {
this.isLoading = false;
}
}
async onBookmarkThisPage() {
try {
// Get the current tab URL and title
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.url || !tab?.title) {
console.error('Could not get current tab info');
return;
}
// Check if already bookmarked
if (this.bookmarks.some(b => b.url === tab.url)) {
console.log('Page already bookmarked');
return;
}
const newBookmark: Bookmark = {
id: crypto.randomUUID(),
url: tab.url,
title: tab.title,
createdAt: Date.now(),
};
this.bookmarks = [newBookmark, ...this.bookmarks];
await this.saveBookmarks();
this.#logger.logBookmarkAdded(newBookmark.url, newBookmark.title);
} catch (error) {
console.error('Failed to bookmark page:', error);
}
}
async onRemoveBookmark(bookmark: Bookmark) {
this.bookmarks = this.bookmarks.filter(b => b.id !== bookmark.id);
await this.saveBookmarks();
this.#logger.logBookmarkRemoved(bookmark.url, bookmark.title);
}
async saveBookmarks() {
try {
await this.#metaHandler.setBookmarks(this.bookmarks);
} catch (error) {
console.error('Failed to save bookmarks:', error);
}
}
openBookmark(bookmark: Bookmark) {
browser.tabs.create({ url: bookmark.url });
}
getDomain(url: string): string {
try {
return new URL(url).hostname;
} catch {
return url;
}
}
}

View File

@@ -9,7 +9,7 @@
routerLinkActive="active"
title="Your selected identity"
>
<i class="bi bi-person-circle"></i>
<span class="emoji">👤</span>
</a>
<a
@@ -18,7 +18,7 @@
routerLinkActive="active"
title="Identities"
>
<i class="bi bi-people-fill"></i>
<span class="emoji">👥</span>
</a>
<a
@@ -27,14 +27,22 @@
routerLinkActive="active"
title="Settings"
>
<i class="bi bi-gear"></i>
<span class="emoji">⚙️</span>
</a>
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
<i class="bi bi-info-circle"></i>
<a class="tab" routerLink="/home/bookmarks" routerLinkActive="active" title="Bookmarks">
<span class="emoji">🔖</span>
</a>
<a class="tab" routerLink="/home/logs" routerLinkActive="active" title="Logs">
<span style="font-size: 1.2rem">🪵</span>
<span class="emoji">🪵</span>
</a>
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
<span class="emoji">💡</span>
</a>
<button class="tab" (click)="onClickLock()" title="Lock">
<span class="emoji">🔒</span>
</button>
</div>

View File

@@ -4,17 +4,17 @@
flex-direction: column;
.tab-content {
height: calc(100% - 60px);
height: calc(100% - 40px);
}
.tabs {
height: 60px;
min-height: 60px;
height: 40px;
min-height: 40px;
background: var(--background-light);
display: flex;
flex-direction: row;
a {
a, button {
all: unset;
}
@@ -23,10 +23,10 @@
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-size: 16px;
color: var(--muted-foreground);
border-top: 3px solid transparent;
border-top: 2px solid transparent;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
cursor: pointer;
@@ -38,7 +38,7 @@
&.active {
color: var(--foreground);
border-top: 3px solid var(--primary);
border-top: 2px solid var(--primary);
}
}
}

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { RouterModule, RouterOutlet } from '@angular/router';
import { Component, inject } from '@angular/core';
import { Router, RouterModule, RouterOutlet } from '@angular/router';
import { LoggerService, StorageService } from '@common';
@Component({
selector: 'app-home',
@@ -7,4 +8,14 @@ import { RouterModule, RouterOutlet } from '@angular/router';
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {}
export class HomeComponent {
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
readonly #logger = inject(LoggerService);
async onClickLock() {
this.#logger.logVaultLock();
await this.#storage.lockVault();
this.#router.navigateByUrl('/vault-login');
}
}

View File

@@ -31,7 +31,7 @@
title="Manage whitelisted apps"
(click)="onClickWhitelistedApps()"
>
<i class="bi bi-gear"></i>
<span class="emoji">⚙️</span>
</button>
</div>
@@ -62,7 +62,7 @@
/>
<span class="name">{{ getDisplayName(identity) }}</span>
<lib-icon-button
icon="gear"
icon="⚙️"
title="Identity settings"
(click)="onClickEditIdentity(identity.id, $event)"
></lib-icon-button>

View File

@@ -2,6 +2,7 @@ import { Component, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
Identity_DECRYPTED,
LoggerService,
NostrHelper,
ProfileMetadata,
ProfileMetadataService,
@@ -29,6 +30,7 @@ export class IdentityComponent implements OnInit {
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
readonly #profileMetadata = inject(ProfileMetadataService);
readonly #logger = inject(LoggerService);
ngOnInit(): void {
this.#loadData();
@@ -136,13 +138,16 @@ export class IdentityComponent implements OnInit {
const result = await validateNip05(nip05, pubkey);
this.nip05isValidated = result.valid;
if (!result.valid) {
console.log('NIP-05 validation failed:', result.error);
if (result.valid) {
this.#logger.logNip05ValidationSuccess(nip05, pubkey);
} else {
this.#logger.logNip05ValidationError(nip05, result.error ?? 'Unknown error');
}
this.validating = false;
} catch (error) {
console.error('NIP-05 validation failed:', error);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
this.#logger.logNip05ValidationError(nip05, errorMsg);
this.nip05isValidated = false;
this.validating = false;
}

View File

@@ -1,20 +1,20 @@
<div class="logs-header">
<span class="logs-title">Logs</span>
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear Log</button>
<div class="logs-actions">
<button class="btn btn-sm btn-secondary" (click)="onRefresh()">Refresh</button>
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear</button>
</div>
</div>
<div class="logs-container">
@if (logs.length === 0) {
<div class="logs-empty">No logs yet</div>
<div class="logs-empty">No activity logged yet</div>
}
@for (log of logs; track log.timestamp) {
<div class="log-entry" [class]="getLevelClass(log.level)">
<span class="log-icon emoji">{{ log.icon }}</span>
<span class="log-time">{{ log.timestamp | date:'HH:mm:ss' }}</span>
<span class="log-level">{{ log.level }}</span>
<span class="log-message">{{ log.message }}</span>
@if (log.data) {
<pre class="log-data">{{ log.data | json }}</pre>
}
</div>
}
</div>

View File

@@ -14,6 +14,11 @@
flex-shrink: 0;
}
.logs-actions {
display: flex;
gap: 8px;
}
.logs-title {
font-weight: 600;
font-size: 1.1rem;
@@ -34,15 +39,14 @@
}
.log-entry {
font-family: monospace;
font-family: var(--font-sans);
font-size: 11px;
padding: 4px 8px;
padding: 6px 8px;
border-radius: 4px;
margin-bottom: 2px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
align-items: center;
&.log-error {
background: rgba(220, 53, 69, 0.15);
@@ -65,29 +69,20 @@
}
}
.log-icon {
font-size: 14px;
flex-shrink: 0;
width: 18px;
text-align: center;
}
.log-time {
color: var(--muted-foreground);
flex-shrink: 0;
}
.log-level {
font-weight: 600;
text-transform: uppercase;
width: 40px;
flex-shrink: 0;
font-size: 10px;
}
.log-message {
flex: 1;
word-break: break-word;
}
.log-data {
width: 100%;
margin: 4px 0 0 0;
padding: 4px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
font-size: 10px;
overflow-x: auto;
}

View File

@@ -1,22 +1,31 @@
import { Component, inject } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { LoggerService, LogEntry } from '@common';
import { DatePipe, JsonPipe } from '@angular/common';
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-logs',
templateUrl: './logs.component.html',
styleUrl: './logs.component.scss',
imports: [DatePipe, JsonPipe],
imports: [DatePipe],
})
export class LogsComponent {
export class LogsComponent implements OnInit {
readonly #logger = inject(LoggerService);
get logs(): LogEntry[] {
return this.#logger.logs;
}
onClear() {
this.#logger.clear();
ngOnInit() {
// Refresh logs from storage to get background script logs
this.#logger.refreshLogs();
}
async onRefresh() {
await this.#logger.refreshLogs();
}
async onClear() {
await this.#logger.clear();
}
getLevelClass(level: LogEntry['level']): string {

View File

@@ -3,6 +3,7 @@ import {
BrowserSyncFlow,
ConfirmComponent,
DateHelper,
LoggerService,
NavComponent,
StartupService,
StorageService,
@@ -20,6 +21,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
readonly #logger = inject(LoggerService);
ngOnInit(): void {
const vault = JSON.stringify(
@@ -43,6 +45,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
async onResetExtension() {
try {
this.#logger.logVaultReset();
await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
@@ -58,6 +61,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
const fileName = `Plebeian Signer Firefox - Vault Export - ${dateTimeString}.json`;
this.#downloadJson(jsonVault, fileName);
this.#logger.logVaultExport(fileName);
}
#downloadJson(jsonString: string, fileName: string) {

View File

@@ -1,7 +1,7 @@
import { Component, inject, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { NavComponent, StorageService, DerivingModalComponent } from '@common';
import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common';
@Component({
selector: 'app-new',
@@ -16,6 +16,7 @@ export class NewComponent extends NavComponent {
readonly #router = inject(Router);
readonly #storage = inject(StorageService);
readonly #logger = inject(LoggerService);
toggleType(element: HTMLInputElement) {
if (element.type === 'password') {
@@ -35,6 +36,7 @@ export class NewComponent extends NavComponent {
try {
await this.#storage.createNewVault(this.password);
this.derivingModal.hide();
this.#logger.logVaultCreated();
this.#router.navigateByUrl('/home/identities');
} catch (error) {
this.derivingModal.hide();

View File

@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
import {
ConfirmComponent,
DerivingModalComponent,
LoggerService,
NostrHelper,
ProfileMetadataService,
StartupService,
@@ -28,6 +29,7 @@ export class VaultLoginComponent implements AfterViewInit {
readonly #router = inject(Router);
readonly #startup = inject(StartupService);
readonly #profileMetadata = inject(ProfileMetadataService);
readonly #logger = inject(LoggerService);
ngAfterViewInit() {
this.passwordInput.nativeElement.focus();
@@ -69,6 +71,7 @@ export class VaultLoginComponent implements AfterViewInit {
// Unlock succeeded - hide modal and navigate
console.log('[login] Hiding modal and navigating');
this.derivingModal.hide();
this.#logger.logVaultUnlock();
// Fetch profile metadata for all identities in the background
this.#fetchAllProfiles();
@@ -102,6 +105,7 @@ export class VaultLoginComponent implements AfterViewInit {
async onClickResetExtension() {
try {
this.#logger.logVaultReset();
await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {

View File

@@ -1,5 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NostrHelper } from '@common';
import {
backgroundLogNip07Action,
backgroundLogPermissionStored,
NostrHelper,
} from '@common';
import {
BackgroundRequestMessage,
checkPermissions,
@@ -109,17 +113,28 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
});
debug(response);
if (response === 'approve' || response === 'reject') {
const policy = response === 'approve' ? 'allow' : 'deny';
await storePermission(
browserSessionData,
currentIdentity,
req.host,
req.method,
response === 'approve' ? 'allow' : 'deny',
policy,
req.params?.kind
);
await backgroundLogPermissionStored(
req.host,
req.method,
policy,
req.params?.kind
);
}
if (['reject', 'reject-once'].includes(response)) {
await backgroundLogNip07Action(req.method, req.host, false, false, {
kind: req.params?.kind,
peerPubkey: req.params?.peerPubkey,
});
throw new Error('Permission denied');
}
} else {
@@ -128,46 +143,71 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
}
const relays: Relays = {};
let result: any;
switch (req.method) {
case 'getPublicKey':
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
result = NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
return result;
case 'signEvent':
return signEvent(req.params, currentIdentity.privkey);
result = signEvent(req.params, currentIdentity.privkey);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
kind: req.params?.kind,
});
return result;
case 'getRelays':
browserSessionData.relays.forEach((x) => {
relays[x.url] = { read: x.read, write: x.write };
});
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
return relays;
case 'nip04.encrypt':
return await nip04Encrypt(
result = await nip04Encrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.plaintext
);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
peerPubkey: req.params.peerPubkey,
});
return result;
case 'nip44.encrypt':
return await nip44Encrypt(
result = await nip44Encrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.plaintext
);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
peerPubkey: req.params.peerPubkey,
});
return result;
case 'nip04.decrypt':
return await nip04Decrypt(
result = await nip04Decrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.ciphertext
);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
peerPubkey: req.params.peerPubkey,
});
return result;
case 'nip44.decrypt':
return await nip44Decrypt(
result = await nip44Decrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.ciphertext
);
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
peerPubkey: req.params.peerPubkey,
});
return result;
default:
throw new Error(`Not supported request method '${req.method}'.`);