Browse Source

Merge branch 'master' into nearby-filter

pull/162/head
Matthew Raymer 2 days ago
parent
commit
23b4460376
  1. 8
      .env.development
  2. 3
      .env.production
  3. 7
      .env.test
  4. 4
      BUILDING.md
  5. 65
      README.md
  6. 117
      doc/logging-configuration.md
  7. 6
      package-lock.json
  8. 6
      package.json
  9. 2
      scripts/build-android.sh
  10. 2
      scripts/build-electron.sh
  11. 2
      scripts/build-ios.sh
  12. 16
      scripts/build-web.sh
  13. 10
      scripts/common.sh
  14. 36
      scripts/test-env.sh
  15. 65
      src/App.vue
  16. 20
      src/components/ActivityListItem.vue
  17. 2
      src/components/ContactInputForm.vue
  18. 4
      src/components/GiftedDialog.vue
  19. 4
      src/components/ImageMethodDialog.vue
  20. 1
      src/components/ImageViewer.vue
  21. 10
      src/libs/endorserServer.ts
  22. 18
      src/router/index.ts
  23. 2
      src/services/AbsurdSqlDatabaseService.ts
  24. 12
      src/services/deepLinks.ts
  25. 3
      src/services/platforms/WebPlatformService.ts
  26. 2
      src/test/PlatformServiceMixinTest.vue
  27. 38
      src/test/index.ts
  28. 5
      src/utils/PlatformServiceMixin.ts
  29. 76
      src/utils/logger.ts
  30. 3
      src/views/AccountViewView.vue
  31. 4
      src/views/ClaimView.vue
  32. 4
      src/views/ContactQRScanFullView.vue
  33. 3
      src/views/DIDView.vue
  34. 27
      src/views/HomeView.vue
  35. 4
      src/views/ImportDerivedAccountView.vue
  36. 67
      src/views/TestView.vue
  37. 2
      test-playwright/50-record-offer.spec.ts
  38. 4
      test-playwright/60-new-activity.spec.ts

8
.env.development

@ -1,10 +1,14 @@
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# Logging Configuration - Development environment gets maximum visibility
VITE_LOG_LEVEL=debug
# iOS doesn't like spaces in the app title. # iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Dev" TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
VITE_APP_SERVER=http://localhost:8080 VITE_APP_SERVER=http://localhost:8080
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production). # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
# Using shared server by default to ease setup, which works for shared test users. # Using shared server by default to ease setup, which works for shared test users.

3
.env.production

@ -1,6 +1,7 @@
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# Logging Configuration - Production environment gets minimal logging for performance
VITE_LOG_LEVEL=warn
VITE_APP_SERVER=https://timesafari.app VITE_APP_SERVER=https://timesafari.app
# This is the claim ID for actions in the BVC project. # This is the claim ID for actions in the BVC project.

7
.env.test

@ -1,9 +1,14 @@
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# Logging Configuration - Test environment gets balanced logging for debugging
VITE_LOG_LEVEL=info
# iOS doesn't like spaces in the app title. # iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" TIME_SAFARI_APP_TITLE="TimeSafari_Test"
VITE_APP_SERVER=https://test.timesafari.app VITE_APP_SERVER=https://test.timesafari.app
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production). # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
production).
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch

4
BUILDING.md

@ -8,7 +8,9 @@ This guide explains how to build TimeSafari for different platforms using the co
```bash ```bash
# 🖥️ Web Development # 🖥️ Web Development
npm run build:web:dev # Start development server with hot reload npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc
npm run build:web:serve -- --test # Start with test endorser server
npm run build:web:dev # Start development server with hot reload with local endorser server
npm run build:web:prod # Production build npm run build:web:prod # Production build
# 📱 Mobile Development # 📱 Mobile Development

65
README.md

@ -3,36 +3,9 @@
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude [Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
and expand to crowd-fund with time & money, then record and see the impact of contributions. and expand to crowd-fund with time & money, then record and see the impact of contributions.
## Database Migration Status
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
### Migration Progress
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
- ✅ **Platform Service Layer**: Unified database interface across platforms
- ✅ **Settings Migration**: Core user settings transferred
- ✅ **Account Migration**: Identity and key management
- 🔄 **Contact Migration**: User contact data (via import interface)
- 📋 **Code Cleanup**: Remove unused Dexie imports
### Migration Fence
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
**Key Points**:
- Legacy Dexie database is disabled by default
- All database operations go through `PlatformServiceMixin`
- Migration tools provide controlled access to both databases
- Clear separation between legacy and new code
### Migration Documentation
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
## Roadmap ## Roadmap
See [project.task.yaml](project.task.yaml) for current priorities. See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building ## Setup & Building
@ -42,14 +15,45 @@ Quick start:
```bash ```bash
npm install npm install
npm run dev npm run build:web:serve -- --test
``` ```
To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality.
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker). See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
## Development Database Clearing ## Development Database Clearing
TimeSafari provides a simple script-based approach to clear the database for development purposes. TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes.
## Logging Configuration
TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control console output verbosity without modifying code.
### Quick Usage
```bash
# Show only errors
VITE_LOG_LEVEL=error npm run dev
# Show warnings and errors
VITE_LOG_LEVEL=warn npm run dev
# Show info, warnings, and errors (default)
VITE_LOG_LEVEL=info npm run dev
# Show all log levels including debug
VITE_LOG_LEVEL=debug npm run dev
```
### Available Levels
- **`error`**: Critical errors only
- **`warn`**: Warnings and errors (default for production web)
- **`info`**: Info, warnings, and errors (default for development/capacitor)
- **`debug`**: All log levels including verbose debugging
See [Logging Configuration Guide](doc/logging-configuration.md) for complete details.
### Quick Usage ### Quick Usage
```bash ```bash
@ -126,7 +130,6 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
### Documentation ### Documentation
- [Domain Configuration System](docs/domain-configuration.md) - Complete guide
- [Constants and Configuration](src/constants/app.ts) - Core constants - [Constants and Configuration](src/constants/app.ts) - Core constants
## Tests ## Tests

117
doc/logging-configuration.md

@ -0,0 +1,117 @@
# Logging Configuration Guide
## Overview
TimeSafari now supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control the verbosity of console output without modifying code.
## Available Log Levels
| Level | Value | Description | Console Output |
|-------|-------|-------------|----------------|
| `error` | 0 | Errors only | Critical errors only |
| `warn` | 1 | Warnings and errors | Warnings and errors |
| `info` | 2 | Info, warnings, and errors | General information, warnings, and errors |
| `debug` | 3 | All log levels | Verbose debugging information |
## Environment Variable
Set the `VITE_LOG_LEVEL` environment variable to control logging:
```bash
# Show only errors
VITE_LOG_LEVEL=error
# Show warnings and errors (default for production web)
VITE_LOG_LEVEL=warn
# Show info, warnings, and errors (default for development/capacitor)
VITE_LOG_LEVEL=info
# Show all log levels including debug
VITE_LOG_LEVEL=debug
```
## Default Behavior by Platform
The logger automatically selects appropriate default log levels based on your platform and environment:
- **Production Web**: `warn` (warnings and errors only)
- **Electron**: `error` (errors only - very quiet)
- **Development/Capacitor**: `info` (info and above)
## Usage Examples
### Setting Log Level in Development
```bash
# In your terminal before running the app
export VITE_LOG_LEVEL=debug
npm run dev
# Or inline
VITE_LOG_LEVEL=debug npm run dev
```
### Setting Log Level in Production
```bash
# For verbose production logging
VITE_LOG_LEVEL=info npm run build:web
# For minimal production logging
VITE_LOG_LEVEL=warn npm run build:web
```
### Programmatic Access
The logger provides methods to check current configuration:
```typescript
import { logger } from '@/utils/logger';
// Get current log level
const currentLevel = logger.getCurrentLevel(); // 'info'
// Check if a level is enabled
const debugEnabled = logger.isLevelEnabled('debug'); // false if level < debug
// Get available levels
const levels = logger.getAvailableLevels(); // ['error', 'warn', 'info', 'debug']
```
## Database Logging
Database logging continues to work regardless of console log level settings. All log messages are still stored in the database for debugging and audit purposes.
## Migration Notes
- **Existing code**: No changes required - logging behavior remains the same
- **New feature**: Use `VITE_LOG_LEVEL` to override default behavior
- **Backward compatible**: All existing logging calls work as before
## Best Practices
1. **Development**: Use `VITE_LOG_LEVEL=debug` for maximum visibility
2. **Testing**: Use `VITE_LOG_LEVEL=info` for balanced output
3. **Production**: Use `VITE_LOG_LEVEL=warn` for minimal noise
4. **Debugging**: Temporarily set `VITE_LOG_LEVEL=debug` to troubleshoot issues
## Troubleshooting
### No Logs Appearing
Check your `VITE_LOG_LEVEL` setting:
```bash
echo $VITE_LOG_LEVEL
```
### Too Many Logs
Reduce verbosity by setting a lower log level:
```bash
VITE_LOG_LEVEL=warn
```
### Platform-Specific Issues
Remember that Electron is very quiet by default. Use `VITE_LOG_LEVEL=info` to see more output in Electron builds.

6
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@ -96,7 +96,7 @@
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.54.2",
"@types/dom-webcodecs": "^0.1.7", "@types/dom-webcodecs": "^0.1.7",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

6
package.json

@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@ -53,6 +53,8 @@
"build:web:docker:test": "./scripts/build-web.sh --docker:test", "build:web:docker:test": "./scripts/build-web.sh --docker:test",
"build:web:docker:prod": "./scripts/build-web.sh --docker:prod", "build:web:docker:prod": "./scripts/build-web.sh --docker:prod",
"build:web:serve": "./scripts/build-web.sh --serve", "build:web:serve": "./scripts/build-web.sh --serve",
"build:web:serve:test": "./scripts/build-web.sh --serve --test",
"build:web:serve:prod": "./scripts/build-web.sh --serve --prod",
"docker:up": "docker-compose up", "docker:up": "docker-compose up",
"docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test", "docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test",
"docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production", "docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production",
@ -204,7 +206,7 @@
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.54.2",
"@types/dom-webcodecs": "^0.1.7", "@types/dom-webcodecs": "^0.1.7",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

2
scripts/build-android.sh

@ -184,7 +184,7 @@ log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build # Setup environment for Capacitor build
setup_build_env "capacitor" setup_build_env "capacitor" "$BUILD_MODE"
# Override API servers for Android development # Override API servers for Android development
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then

2
scripts/build-electron.sh

@ -339,7 +339,7 @@ main_electron_build() {
fi fi
# Setup environment # Setup environment
setup_build_env "electron" setup_build_env "electron" "$BUILD_MODE"
setup_app_directories setup_app_directories
load_env_file ".env" load_env_file ".env"

2
scripts/build-ios.sh

@ -311,7 +311,7 @@ log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build # Setup environment for Capacitor build
setup_build_env "capacitor" setup_build_env "capacitor" "$BUILD_MODE"
# Override API servers for iOS development when custom IP is specified # Override API servers for iOS development when custom IP is specified
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then

16
scripts/build-web.sh

@ -300,18 +300,20 @@ serve_build() {
exit 5 exit 5
fi fi
# Use a simple HTTP server to serve the build # Use a server that supports SPA routing (serves index.html for all routes)
if command -v python3 &> /dev/null; then if command -v npx &> /dev/null; then
log_info "Starting npx serve with SPA support on port 8080..."
npx serve -s dist -l 8080
elif command -v python3 &> /dev/null; then
log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404."
log_info "Starting Python HTTP server on port 8080..." log_info "Starting Python HTTP server on port 8080..."
cd dist && python3 -m http.server 8080 cd dist && python3 -m http.server 8080
elif command -v python &> /dev/null; then elif command -v python &> /dev/null; then
log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404."
log_info "Starting Python HTTP server on port 8080..." log_info "Starting Python HTTP server on port 8080..."
cd dist && python -m SimpleHTTPServer 8080 cd dist && python -m SimpleHTTPServer 8080
elif command -v npx &> /dev/null; then
log_info "Starting npx serve on port 8080..."
npx serve -s dist -l 8080
else else
log_error "No suitable HTTP server found. Install Python or npx serve." log_error "No suitable HTTP server found. Install npx serve or Python."
exit 5 exit 5
fi fi
} }
@ -330,7 +332,7 @@ log_info "Serve build: $SERVE_BUILD"
validate_web_environment validate_web_environment
# Setup environment for web build # Setup environment for web build
setup_build_env "web" setup_build_env "web" "$BUILD_MODE"
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories

10
scripts/common.sh

@ -174,9 +174,9 @@ validate_env_vars() {
# Function to set environment variables for different build types # Function to set environment variables for different build types
setup_build_env() { setup_build_env() {
local build_type="$1" local build_type="$1"
local production="${2:-false}" local build_mode="${2:-development}"
log_info "Setting up environment for $build_type build" log_info "Setting up environment for $build_type build (mode: $build_mode)"
# Get git hash for versioning # Get git hash for versioning
local git_hash=$(get_git_hash) local git_hash=$(get_git_hash)
@ -204,19 +204,19 @@ setup_build_env() {
esac esac
# Set API server environment variables based on build mode # Set API server environment variables based on build mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$build_mode" = "development" ]; then
# For Capacitor development, use localhost by default # For Capacitor development, use localhost by default
# Android builds will override this in build-android.sh # Android builds will override this in build-android.sh
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000" export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000" export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
log_debug "Development mode: Using localhost for Endorser and Partner APIs" log_debug "Development mode: Using localhost for Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app" export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$build_mode" = "test" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://test-api.endorser.ch" export VITE_DEFAULT_ENDORSER_API_SERVER="https://test-api.endorser.ch"
export VITE_DEFAULT_PARTNER_API_SERVER="https://test-partner-api.endorser.ch" export VITE_DEFAULT_PARTNER_API_SERVER="https://test-partner-api.endorser.ch"
log_debug "Test mode: Using test Endorser and Partner APIs" log_debug "Test mode: Using test Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app" export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
elif [ "$BUILD_MODE" = "production" ]; then elif [ "$build_mode" = "production" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://api.endorser.ch" export VITE_DEFAULT_ENDORSER_API_SERVER="https://api.endorser.ch"
export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch" export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch"
log_debug "Production mode: Using production API servers" log_debug "Production mode: Using production API servers"

36
scripts/test-env.sh

@ -17,34 +17,40 @@ parse_args "$@"
print_header "Environment Variable Test" print_header "Environment Variable Test"
log_info "Testing environment variable handling at $(date)" log_info "Testing environment variable handling at $(date)"
# Test 1: Capacitor environment # Test 1: Capacitor environment (development)
log_info "Test 1: Setting up Capacitor environment..." log_info "Test 1: Setting up Capacitor environment (development mode)..."
setup_build_env "capacitor" setup_build_env "capacitor" "development"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 2: Web environment # Test 2: Web environment (development)
log_info "Test 2: Setting up Web environment..." log_info "Test 2: Setting up Web environment (development mode)..."
setup_build_env "web" setup_build_env "web" "development"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 3: Production Capacitor environment # Test 3: Capacitor test environment
log_info "Test 3: Setting up Production Capacitor environment..." log_info "Test 3: Setting up Capacitor environment (test mode)..."
setup_build_env "capacitor" "true" setup_build_env "capacitor" "test"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 4: Application directories # Test 4: Capacitor production environment
log_info "Test 4: Setting up application directories..." log_info "Test 4: Setting up Capacitor environment (production mode)..."
setup_build_env "capacitor" "production"
print_env_vars "VITE_"
echo ""
# Test 5: Application directories
log_info "Test 5: Setting up application directories..."
setup_app_directories setup_app_directories
# Test 5: Load .env file (if it exists) # Test 6: Load .env file (if it exists)
log_info "Test 5: Loading .env file..." log_info "Test 6: Loading .env file..."
load_env_file ".env" load_env_file ".env"
# Test 6: Git hash # Test 7: Git hash
log_info "Test 6: Getting git hash..." log_info "Test 7: Getting git hash..."
GIT_HASH=$(get_git_hash) GIT_HASH=$(get_git_hash)
log_info "Git hash: $GIT_HASH" log_info "Git hash: $GIT_HASH"

65
src/App.vue

@ -27,9 +27,13 @@
v-if="notification.type === 'toast'" v-if="notification.type === 'toast'"
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md" class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
> >
<div class="w-full px-4 py-3"> <div class="w-full px-4 py-3 overflow-hidden">
<span class="font-semibold">{{ notification.title }}</span> <h4 class="font-semibold text-ellipsis overflow-hidden">
<p class="text-sm">{{ notification.text }}</p> {{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
</div> </div>
</div> </div>
@ -46,9 +50,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-slate-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@ -72,9 +82,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-emerald-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@ -98,9 +114,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-amber-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@ -124,9 +146,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-rose-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@ -349,13 +377,6 @@ export default class App extends Vue {
stopAsking = false; stopAsking = false;
truncateLongWords(sentence: string) {
return sentence
.split(" ")
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
.join(" ");
}
async turnOffNotifications( async turnOffNotifications(
notification: NotificationIface, notification: NotificationIface,
): Promise<boolean> { ): Promise<boolean> {

20
src/components/ActivityListItem.vue

@ -73,7 +73,6 @@
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md" class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image" :src="record.image"
alt="Activity image" alt="Activity image"
@load="handleImageLoad(record.image)"
/> />
</a> </a>
</div> </div>
@ -253,7 +252,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer"; import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify"; import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_PERSON_HIDDEN, NOTIFY_PERSON_HIDDEN,
NOTIFY_UNKNOWN_PERSON, NOTIFY_UNKNOWN_PERSON,
@ -272,16 +271,9 @@ export default class ActivityListItem extends Vue {
@Prop() isRegistered!: boolean; @Prop() isRegistered!: boolean;
@Prop() activeDid!: string; @Prop() activeDid!: string;
/**
* Function prop for handling image caching
* Called when an image loads successfully, allowing parent to control caching behavior
*/
@Prop({ type: Function, default: () => {} })
onImageCache!: (imageUrl: string) => void | Promise<void>;
isHiddenDid = isHiddenDid; isHiddenDid = isHiddenDid;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
@ -295,14 +287,6 @@ export default class ActivityListItem extends Vue {
this.notify.warning(NOTIFY_UNKNOWN_PERSON.message, TIMEOUTS.STANDARD); this.notify.warning(NOTIFY_UNKNOWN_PERSON.message, TIMEOUTS.STANDARD);
} }
/**
* Handle image load event - call function prop for caching
* Allows parent to control caching behavior and validation
*/
handleImageLoad(imageUrl: string): void {
this.onImageCache(imageUrl);
}
get fetchAmount(): string { get fetchAmount(): string {
const claim = const claim =
(this.record.fullClaim as any)?.claim || this.record.fullClaim; (this.record.fullClaim as any)?.claim || this.record.fullClaim;

2
src/components/ContactInputForm.vue

@ -167,7 +167,7 @@ export default class ContactInputForm extends Vue {
*/ */
@Emit("qr-scan") @Emit("qr-scan")
private handleQRScan(): void { private handleQRScan(): void {
console.log("[ContactInputForm] QR scan button clicked"); // QR scan button clicked - event emitted for parent handling
} }
} }
</script> </script>

4
src/components/GiftedDialog.vue

@ -80,7 +80,7 @@ import EntitySelectionStep from "../components/EntitySelectionStep.vue";
import GiftDetailsStep from "../components/GiftDetailsStep.vue"; import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT, NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION, NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@ -98,7 +98,7 @@ import {
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
/** /**

4
src/components/ImageMethodDialog.vue

@ -282,7 +282,7 @@ import {
NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT, NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT,
createImageDialogCameraErrorMessage, createImageDialogCameraErrorMessage,
} from "../constants/notifications"; } from "../constants/notifications";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "../utils/notify";
const inputImageFileNameRef = ref<Blob>(); const inputImageFileNameRef = ref<Blob>();
@ -291,7 +291,7 @@ const inputImageFileNameRef = ref<Blob>();
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
notify = createNotifyHelpers(this.$notify); notify = createNotifyHelpers(this.$notify);

1
src/components/ImageViewer.vue

@ -45,7 +45,6 @@ import { logger } from "../utils/logger";
@Component({ emits: ["update:isOpen"] }) @Component({ emits: ["update:isOpen"] })
export default class ImageViewer extends Vue { export default class ImageViewer extends Vue {
@Prop() imageUrl!: string; @Prop() imageUrl!: string;
@Prop() imageData!: Blob | null;
@Prop() isOpen!: boolean; @Prop() isOpen!: boolean;
userAgent = new UAParser(); userAgent = new UAParser();

10
src/libs/endorserServer.ts

@ -1348,12 +1348,12 @@ export async function createEndorserJwtVcFromClaim(
} }
/** /**
* Create a JWT for a RegisterAction claim. * Create a JWT for a RegisterAction claim, used for registrations & invites.
* *
* @param activeDid - The DID of the user creating the invite * @param activeDid - The DID of the user creating the invite
* @param contact - The contact to register, with a 'did' field (all optional for invites) * @param contact - Optional - The contact to register, with a 'did' field (all optional for invites)
* @param identifier - The identifier for the invite, usually random * @param identifier - Optional - The identifier for the invite, usually random
* @param expiresIn - The number of seconds until the invite expires * @param expiresIn - Optional - The number of seconds until the invite expires
* @returns The JWT for the RegisterAction claim * @returns The JWT for the RegisterAction claim
*/ */
export async function createInviteJwt( export async function createInviteJwt(
@ -1367,7 +1367,7 @@ export async function createInviteJwt(
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: activeDid }, agent: { identifier: activeDid },
object: SERVICE_ID, object: SERVICE_ID,
identifier: identifier, identifier: identifier, // not sent if undefined
}; };
if (contact?.did) { if (contact?.did) {
vcClaim.participant = { identifier: contact.did }; vcClaim.participant = { identifier: contact.did };

18
src/router/index.ts

@ -82,6 +82,15 @@ const routes: Array<RouteRecordRaw> = [
name: "database-migration", name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"), component: () => import("../views/DatabaseMigration.vue"),
}, },
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
{ {
path: "/did/:did?", path: "/did/:did?",
name: "did", name: "did",
@ -276,15 +285,6 @@ const routes: Array<RouteRecordRaw> = [
name: "user-profile", name: "user-profile",
component: () => import("../views/UserProfileView.vue"), component: () => import("../views/UserProfileView.vue"),
}, },
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
]; ];
const isElectron = window.location.protocol === "file:"; const isElectron = window.location.protocol === "file:";

2
src/services/AbsurdSqlDatabaseService.ts

@ -1,7 +1,9 @@
// **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top // **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top
// This prevents "crypto is not defined" errors when running in worker context // This prevents "crypto is not defined" errors when running in worker context
if (typeof window === "undefined" && typeof crypto === "undefined") { if (typeof window === "undefined" && typeof crypto === "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).crypto = { (globalThis as any).crypto = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRandomValues: (array: any) => { getRandomValues: (array: any) => {
// Simple fallback for worker context // Simple fallback for worker context
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {

12
src/services/deepLinks.ts

@ -53,6 +53,7 @@ import {
DeepLinkRoute, DeepLinkRoute,
} from "../interfaces/deepLinks"; } from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger";
// Helper function to extract the first key from a Zod object schema // Helper function to extract the first key from a Zod object schema
function getFirstKeyFromZodObject( function getFirstKeyFromZodObject(
@ -178,7 +179,7 @@ export class DeepLinkHandler {
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name; routeName = ROUTE_MAP[validRoute].name;
} catch (error) { } catch (error) {
console.error(`[DeepLink] Invalid route path: ${path}`); logger.error(`[DeepLink] Invalid route path: ${path}`);
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({
@ -204,9 +205,8 @@ export class DeepLinkHandler {
validatedParams = await schema.parseAsync(params); validatedParams = await schema.parseAsync(params);
} catch (error) { } catch (error) {
// For parameter validation errors, provide specific error feedback // For parameter validation errors, provide specific error feedback
logConsoleAndDb( logger.error(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, `[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
true,
); );
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
@ -229,9 +229,8 @@ export class DeepLinkHandler {
params: validatedParams, params: validatedParams,
}); });
} catch (error) { } catch (error) {
logConsoleAndDb( logger.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`, `[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
true,
); );
// For parameter validation errors, provide specific error feedback // For parameter validation errors, provide specific error feedback
await this.router.replace({ await this.router.replace({
@ -263,9 +262,8 @@ export class DeepLinkHandler {
await this.validateAndRoute(path, sanitizedParams, query); await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) { } catch (error) {
const deepLinkError = error as DeepLinkError; const deepLinkError = error as DeepLinkError;
logConsoleAndDb( logger.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`, `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true,
); );
throw { throw {

3
src/services/platforms/WebPlatformService.ts

@ -693,7 +693,8 @@ export class WebPlatformService implements PlatformService {
const setClause = keys.map((key) => `${key} = ?`).join(", "); const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did]; const params = [...keys.map((key) => settings[key]), did];
console.log( // Log update operation for debugging
logger.debug(
"[WebPlatformService] updateDidSpecificSettings", "[WebPlatformService] updateDidSpecificSettings",
sql, sql,
JSON.stringify(params, null, 2), JSON.stringify(params, null, 2),

2
src/test/PlatformServiceMixinTest.vue

@ -92,6 +92,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
}) })
export default class PlatformServiceMixinTest extends Vue { export default class PlatformServiceMixinTest extends Vue {
result: string = ""; result: string = "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
userZeroTestResult: any = null; userZeroTestResult: any = null;
activeTest: string = ""; // Track which test is currently active activeTest: string = ""; // Track which test is currently active
@ -267,6 +268,7 @@ This tests the complete save → retrieve cycle with actual database interaction
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`; this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
} catch (error) { } catch (error) {
this.result = `Error testing User #0 settings: ${error}`; this.result = `Error testing User #0 settings: ${error}`;
// eslint-disable-next-line no-console
console.error("Error testing User #0 settings:", error); console.error("Error testing User #0 settings:", error);
} }
} }

38
src/test/index.ts

@ -1,9 +1,34 @@
import axios from "axios"; import axios from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { SERVICE_ID } from "../libs/endorserServer"; import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import {
DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { AppString } from "../constants/app"; import { AppString } from "../constants/app";
import { saveNewIdentity } from "@/libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
const TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
export async function testBecomeUser0() {
const [addr, privateHex, publicHex, deriPath] =
deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await saveNewIdentity(
identity0,
TEST_USER_0_MNEMONIC,
DEFAULT_ROOT_DERIVATION_PATH,
);
const platformService = await PlatformServiceFactory.getInstance();
await platformService.updateDidSpecificSettings(identity0.did, {
isRegistered: true,
});
}
/** /**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid. * Get User #0 to sign & submit a RegisterAction for the user's activeDid.
@ -15,10 +40,8 @@ import { AppString } from "../constants/app";
* @throws Error if registration fails or database access fails * @throws Error if registration fails or database access fails
*/ */
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = const [addr, privateHex, publicHex, deriPath] =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; deriveAddress(TEST_USER_0_MNEMONIC);
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
@ -32,9 +55,9 @@ export async function testServerRegisterUser() {
const vcClaim = { const vcClaim = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { did: identity0.did }, agent: { identifier: identity0.did },
object: SERVICE_ID, object: SERVICE_ID,
participant: { did: settings.activeDid }, participant: { identifier: settings.activeDid },
}; };
// Make a payload for the claim // Make a payload for the claim
@ -71,4 +94,5 @@ export async function testServerRegisterUser() {
const resp = await axios.post(url, payload, { headers }); const resp = await axios.post(url, payload, { headers });
logger.log("User registration result:", resp); logger.log("User registration result:", resp);
return resp;
} }

5
src/utils/PlatformServiceMixin.ts

@ -133,6 +133,7 @@ export const PlatformServiceMixin = {
* Used for change detection and component updates * Used for change detection and component updates
*/ */
currentActiveDid(): string | null { currentActiveDid(): string | null {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any)._currentActiveDid; return (this as any)._currentActiveDid;
}, },
@ -200,7 +201,9 @@ export const PlatformServiceMixin = {
* This method should be called when the user switches identities * This method should be called when the user switches identities
*/ */
async $updateActiveDid(newDid: string | null): Promise<void> { async $updateActiveDid(newDid: string | null): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const oldDid = (this as any)._currentActiveDid; const oldDid = (this as any)._currentActiveDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)._currentActiveDid = newDid; (this as any)._currentActiveDid = newDid;
if (newDid !== oldDid) { if (newDid !== oldDid) {
@ -291,6 +294,7 @@ export const PlatformServiceMixin = {
// Convert searchBoxes array to JSON string if present // Convert searchBoxes array to JSON string if present
if (settings.searchBoxes !== undefined) { if (settings.searchBoxes !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes) (converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
? JSON.stringify(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes); : String(settings.searchBoxes);
@ -692,6 +696,7 @@ export const PlatformServiceMixin = {
typeof method.value === "string"; typeof method.value === "string";
if (!isValid && method !== undefined) { if (!isValid && method !== undefined) {
// eslint-disable-next-line no-console
console.warn( console.warn(
"[ContactNormalization] Invalid contact method:", "[ContactNormalization] Invalid contact method:",
method, method,

76
src/utils/logger.ts

@ -2,9 +2,10 @@
* Enhanced logger with self-contained database logging * Enhanced logger with self-contained database logging
* *
* Provides comprehensive logging with console and database output. * Provides comprehensive logging with console and database output.
* Supports configurable log levels via VITE_LOG_LEVEL environment variable.
* *
* @author Matthew Raymer * @author Matthew Raymer
* @version 2.0.0 * @version 2.1.0
* @since 2025-01-25 * @since 2025-01-25
*/ */
@ -46,6 +47,42 @@ export function safeStringify(obj: unknown) {
const isElectron = process.env.VITE_PLATFORM === "electron"; const isElectron = process.env.VITE_PLATFORM === "electron";
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
// Log level configuration via environment variable
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
} as const;
type LogLevel = keyof typeof LOG_LEVELS;
// Parse VITE_LOG_LEVEL environment variable
const getLogLevel = (): LogLevel => {
const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase();
if (envLogLevel && envLogLevel in LOG_LEVELS) {
return envLogLevel as LogLevel;
}
// Default log levels based on environment
if (isProduction && !isElectron) {
return "warn"; // Production web: warnings and errors only
} else if (isElectron) {
return "error"; // Electron: errors only
} else {
return "info"; // Development/Capacitor: info and above
}
};
const currentLogLevel = getLogLevel();
const currentLevelValue = LOG_LEVELS[currentLogLevel];
// Helper function to check if a log level should be displayed
const shouldLog = (level: LogLevel): boolean => {
return LOG_LEVELS[level] <= currentLevelValue;
};
// Track initialization state to prevent circular dependencies // Track initialization state to prevent circular dependencies
let isInitializing = true; let isInitializing = true;
@ -105,11 +142,11 @@ async function logToDatabase(
} }
} }
// Enhanced logger with self-contained database methods // Enhanced logger with self-contained database methods and log level control
export const logger = { export const logger = {
debug: (message: string, ...args: unknown[]) => { debug: (message: string, ...args: unknown[]) => {
// Debug logs are very verbose - only show in development mode for web // Debug logs only show if VITE_LOG_LEVEL allows it
if (!isProduction && !isElectron) { if (shouldLog("debug")) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.debug(message, ...args); console.debug(message, ...args);
} }
@ -117,11 +154,8 @@ export const logger = {
}, },
log: (message: string, ...args: unknown[]) => { log: (message: string, ...args: unknown[]) => {
// Regular logs - show in development or for capacitor, but quiet for Electron // Regular logs - show if VITE_LOG_LEVEL allows info level
if ( if (shouldLog("info")) {
(!isProduction && !isElectron) ||
process.env.VITE_PLATFORM === "capacitor"
) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(message, ...args); console.log(message, ...args);
} }
@ -132,11 +166,7 @@ export const logger = {
}, },
info: (message: string, ...args: unknown[]) => { info: (message: string, ...args: unknown[]) => {
if ( if (shouldLog("info")) {
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.info(message, ...args); console.info(message, ...args);
} }
@ -147,8 +177,7 @@ export const logger = {
}, },
warn: (message: string, ...args: unknown[]) => { warn: (message: string, ...args: unknown[]) => {
// Always show warnings, but for Electron, suppress routine database warnings if (shouldLog("warn")) {
if (!isElectron || !message.includes("[CapacitorPlatformService]")) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(message, ...args); console.warn(message, ...args);
} }
@ -159,9 +188,10 @@ export const logger = {
}, },
error: (message: string, ...args: unknown[]) => { error: (message: string, ...args: unknown[]) => {
// Errors will always be logged to console if (shouldLog("error")) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(message, ...args); console.error(message, ...args);
}
// Database logging // Database logging
const messageString = safeStringify(message); const messageString = safeStringify(message);
@ -175,11 +205,11 @@ export const logger = {
}, },
toConsoleAndDb: async (message: string, isError = false): Promise<void> => { toConsoleAndDb: async (message: string, isError = false): Promise<void> => {
// Console output // Console output based on log level
if (isError) { if (isError && shouldLog("error")) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(message); console.error(message);
} else { } else if (!isError && shouldLog("info")) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(message); console.log(message);
} }
@ -194,6 +224,12 @@ export const logger = {
error: (message: string) => error: (message: string) =>
logToDatabase(`[${componentName}] ${message}`, "error"), logToDatabase(`[${componentName}] ${message}`, "error"),
}), }),
// Log level information methods
getCurrentLevel: (): LogLevel => currentLogLevel,
getCurrentLevelValue: (): number => currentLevelValue,
isLevelEnabled: (level: LogLevel): boolean => shouldLog(level),
getAvailableLevels: (): LogLevel[] => Object.keys(LOG_LEVELS) as LogLevel[],
}; };
// Function to manually mark initialization as complete // Function to manually mark initialization as complete

3
src/views/AccountViewView.vue

@ -61,7 +61,8 @@
/> />
<!-- Notifications --> <!-- Notifications -->
<!-- Currently disabled because it doesn't work, even on Chrome. If restored, make sure it works or doesn't show on mobile/electron. --> <!-- Currently disabled because it doesn't work, even on Chrome.
If restored, make sure it works or doesn't show on mobile/electron. -->
<section <section
v-if="false" v-if="false"
id="sectionNotifications" id="sectionNotifications"

4
src/views/ClaimView.vue

@ -724,6 +724,8 @@ export default class ClaimView extends Vue {
} }
async created() { async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
@ -754,8 +756,6 @@ export default class ClaimView extends Vue {
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
this.notify = createNotifyHelpers(this.$notify);
} }
// insert a space before any capital letters except the initial letter // insert a space before any capital letters except the initial letter

4
src/views/ContactQRScanFullView.vue

@ -143,7 +143,7 @@ import {
QR_TIMEOUT_STANDARD, QR_TIMEOUT_STANDARD,
QR_TIMEOUT_LONG, QR_TIMEOUT_LONG,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { createNotifyHelpers } from "../utils/notify"; import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
interface QRScanResult { interface QRScanResult {
rawValue?: string; rawValue?: string;
@ -191,7 +191,7 @@ interface IUserNameDialog {
* @since 2024 * @since 2024
*/ */
export default class ContactQRScanFull extends Vue { export default class ContactQRScanFull extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
// Notification helper system // Notification helper system

3
src/views/DIDView.vue

@ -7,7 +7,8 @@
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7"> <h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page (and going back there is annoying). --> <!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page
(and going back there is annoying). -->
<router-link <router-link
:to="{ name: 'contacts' }" :to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"

27
src/views/HomeView.vue

@ -234,7 +234,6 @@ Raymer * @version 1.0.0 */
:last-viewed-claim-id="feedLastViewedClaimId" :last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isRegistered" :is-registered="isRegistered"
:active-did="activeDid" :active-did="activeDid"
:on-image-cache="cacheImageData"
@load-claim="onClickLoadClaim" @load-claim="onClickLoadClaim"
@view-image="openImageViewer" @view-image="openImageViewer"
/> />
@ -255,11 +254,7 @@ Raymer * @version 1.0.0 */
<ChoiceButtonDialog ref="choiceButtonDialog" /> <ChoiceButtonDialog ref="choiceButtonDialog" />
<ImageViewer <ImageViewer v-model:is-open="isImageViewerOpen" :image-url="selectedImage" />
v-model:is-open="isImageViewerOpen"
:image-url="selectedImage"
:image-data="selectedImageData"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -434,9 +429,7 @@ export default class HomeView extends Vue {
showShortcutBvc = false; showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = ""; selectedImage = "";
selectedImageData: Blob | null = null;
isImageViewerOpen = false; isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false; showProjectsDialog = false;
/** /**
@ -1712,23 +1705,6 @@ export default class HomeView extends Vue {
}); });
} }
/**
* Caches image data for sharing
*
* @public
* Called by ActivityListItem component function prop
* @param imageUrl URL of image to cache
*/
async cacheImageData(imageUrl: string) {
try {
// For images that might fail CORS, just store the URL
// The Web Share API will handle sharing the URL appropriately
this.imageCache.set(imageUrl, null);
} catch (error) {
logger.warn("Failed to cache image:", error);
}
}
/** /**
* Opens image viewer dialog * Opens image viewer dialog
* *
@ -1737,7 +1713,6 @@ export default class HomeView extends Vue {
* @param imageUrl URL of image to display * @param imageUrl URL of image to display
*/ */
async openImageViewer(imageUrl: string) { async openImageViewer(imageUrl: string) {
this.selectedImageData = this.imageCache.get(imageUrl) ?? null;
this.selectedImage = imageUrl; this.selectedImage = imageUrl;
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }

4
src/views/ImportDerivedAccountView.vue

@ -87,7 +87,7 @@ import {
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Account, AccountEncrypted } from "../db/tables/accounts"; import { Account, AccountEncrypted } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_ACCOUNT_DERIVATION_SUCCESS, NOTIFY_ACCOUNT_DERIVATION_SUCCESS,
NOTIFY_ACCOUNT_DERIVATION_ERROR, NOTIFY_ACCOUNT_DERIVATION_ERROR,
@ -100,7 +100,7 @@ import {
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded; $route!: RouteLocationNormalizedLoaded;
$router!: Router; $router!: Router;
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;

67
src/views/TestView.vue

@ -21,7 +21,17 @@
</h1> </h1>
</div> </div>
<div> <div v-if="isNotProdServer">
<h2 class="text-xl font-bold mb-4">User Registration</h2>
<button :class="primaryButtonClasses" @click="registerMe()">
Register Yourself
</button>
<button :class="primaryButtonClasses" @click="becomeUser0()">
Become User 0 (who can register others)
</button>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2> <h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2>
<!-- Notification test buttons using computed configuration --> <!-- Notification test buttons using computed configuration -->
@ -99,7 +109,7 @@
<div> <div>
Register Passkey Register Passkey
<button :class="primaryButtonClasses" @click="register()"> <button :class="primaryButtonClasses" @click="registerPasskey()">
Simplewebauthn Simplewebauthn
</button> </button>
</div> </div>
@ -235,6 +245,7 @@ import {
registerAndSavePasskey, registerAndSavePasskey,
SHARED_PHOTO_BASE64_KEY, SHARED_PHOTO_BASE64_KEY,
} from "../libs/util"; } from "../libs/util";
import { testBecomeUser0, testServerRegisterUser } from "@/test";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts"; import { Account } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@ -300,6 +311,7 @@ export default class Help extends Vue {
// for passkeys // for passkeys
credIdHex?: string; credIdHex?: string;
activeDid?: string; activeDid?: string;
apiServer?: string;
jwt?: string; jwt?: string;
peerSetup?: PeerSetup; peerSetup?: PeerSetup;
userName?: string; userName?: string;
@ -521,17 +533,6 @@ export default class Help extends Vue {
]; ];
} }
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/** /**
* Component initialization * Component initialization
* *
@ -541,6 +542,7 @@ export default class Help extends Vue {
async mounted() { async mounted() {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName; this.userName = settings.firstName;
const account = await retrieveAccountMetadata(this.activeDid); const account = await retrieveAccountMetadata(this.activeDid);
@ -553,6 +555,43 @@ export default class Help extends Vue {
} }
} }
/**
* Checks if running on production server
*
* @returns True if not on production server (enables test utilities)
*/
public isNotProdServer() {
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
}
async registerMe() {
const response = await testServerRegisterUser();
if (response.status === 201) {
alert("Registration successful.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
} else {
logger.error("Registration failure response:", response);
alert("Registration failed: " + (response.data.error || response.data));
}
}
async becomeUser0() {
await testBecomeUser0();
alert("You are now User 0.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
}
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/** /**
* Handles file upload for image sharing tests * Handles file upload for image sharing tests
* *
@ -609,7 +648,7 @@ export default class Help extends Vue {
* Includes validation and user confirmation workflow * Includes validation and user confirmation workflow
* Uses notification helpers for consistent messaging * Uses notification helpers for consistent messaging
*/ */
public async register() { public async registerPasskey() {
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) { if (!this.userName) {
const modalConfig = createPasskeyNameModal( const modalConfig = createPasskeyNameModal(

2
test-playwright/50-record-offer.spec.ts

@ -23,7 +23,7 @@ test('Record an offer', async ({ page }) => {
await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss) await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss)
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(description); await page.getByTestId('inputDescription').fill(description);
await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString()); await page.getByTestId('inputOfferAmount').locator('input').fill(randomNonZeroNumber.toString());
expect(page.getByRole('button', { name: 'Sign & Send' })); expect(page.getByRole('button', { name: 'Sign & Send' }));
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();

4
test-playwright/60-new-activity.spec.ts

@ -36,7 +36,7 @@ test('New offers for another user', async ({ page }) => {
const randomString1 = Math.random().toString(36).substring(2, 5); const randomString1 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`); await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`);
await page.getByTestId('inputOfferAmount').fill('1'); await page.getByTestId('inputOfferAmount').locator('input').fill('1');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
@ -46,7 +46,7 @@ test('New offers for another user', async ({ page }) => {
const randomString2 = Math.random().toString(36).substring(2, 5); const randomString2 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`); await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`);
await page.getByTestId('inputOfferAmount').fill('3'); await page.getByTestId('inputOfferAmount').locator('input').fill('3');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert

Loading…
Cancel
Save