Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Raymer
ff3901c796 feat: add development build to dist without HMR
Add new build option `npm run build:web:dev:dist` that creates development
configuration build in dist folder without starting HMR server. This enables
testing development settings in static environments and CI/CD pipelines.

Use cases:
- Test development configuration without dev server overhead
- Deploy development builds to static hosting services
- CI/CD pipelines requiring development environment variables
- Performance testing with development settings
- Debugging production-like builds with source maps enabled

Changes:
- Add --build-dev-to-dist flag to build-web.sh script
- Create npm script build:web:dev:dist for easy access
- Update documentation with new workflow and examples
- Maintain development environment variables and source maps
- Include PWA support for testing in static builds
2025-07-29 04:52:37 +00:00
36 changed files with 477 additions and 891 deletions

3
.gitignore vendored
View File

@@ -127,5 +127,4 @@ electron/out/
# Gradle cache files
android/.gradle/file-system.probe
android/.gradle/caches/
coverage
android/.gradle/caches/

View File

@@ -20,6 +20,9 @@ The web build system is fully integrated into `package.json` with the following
# Development (starts dev server)
npm run build:web:dev # Development server with hot reload
# Development build to dist (no HMR)
npm run build:web:dev:dist # Development configuration built to dist folder
# Production builds
npm run build:web:test # Testing environment build
npm run build:web:prod # Production environment build
@@ -56,6 +59,7 @@ The `build-web.sh` script supports comprehensive command-line options:
# Environment-specific builds
./scripts/build-web.sh --dev # Development server
./scripts/build-web.sh --dev --build-dev-to-dist # Development build to dist
./scripts/build-web.sh --test # Testing build
./scripts/build-web.sh --prod # Production build
@@ -73,6 +77,7 @@ The `build-web.sh` script supports comprehensive command-line options:
| Option | Description | Default |
|--------|-------------|---------|
| `--dev`, `--development` | Development mode (starts dev server) | ✅ |
| `--build-dev-to-dist` | Build development configuration to dist folder | |
| `--test` | Testing environment build | |
| `--prod`, `--production` | Production environment build | |
| `--docker` | Build and create Docker image | |
@@ -92,6 +97,14 @@ The `build-web.sh` script supports comprehensive command-line options:
4. **Hot Reload**: Enable live reload and HMR
5. **PWA Setup**: Configure PWA for development
### Development Build to Dist Flow
1. **Environment Setup**: Load development environment variables
2. **Validation**: Check for required dependencies
3. **Build Process**: Run Vite build with development configuration
4. **Output**: Create static files in dist folder with development settings
5. **No HMR**: No development server started
### Production Mode Flow
1. **Environment Setup**: Load production environment variables
@@ -278,7 +291,30 @@ docker push timesafari-web:production
- **Hot Reload**: Enabled with Vite HMR
- **Source Maps**: Enabled for debugging
### Production Mode
### Development Build to Dist
```bash
dist/
├── index.html # Main HTML file
├── manifest.webmanifest # PWA manifest
├── sw.js # Service worker
├── workbox-*.js # Workbox library
└── assets/
├── index-*.js # Main application bundle (development config)
├── index-*.css # Stylesheet bundle (development config)
├── icons/ # PWA icons
└── images/ # Optimized images
```
**Features**:
- Development environment variables
- Source maps enabled
- No minification
- PWA enabled for testing
- No HMR server running
### Production Mode File Structure
```bash
dist/
@@ -347,6 +383,20 @@ npm run build:web:dev
# PWA features available
```
### Development Build Workflow
```bash
# Build development configuration to dist
npm run build:web:dev:dist
# Serve the development build locally
npm run build:web:serve
# Access at http://localhost:8080
# No hot reload, but development configuration
# Useful for testing development settings without HMR
```
### Testing Workflow
```bash

View File

@@ -1,178 +0,0 @@
# Debug Logging Control
## Overview
Debug logging in TimeSafari can be controlled via environment variables to reduce console noise during development and production.
## Current Behavior
By default, debug logging is **disabled** to reduce console noise. Debug logs are very verbose and include detailed information about:
- Camera operations (ImageMethodDialog, PhotoDialog)
- Database operations (CapacitorPlatformService)
- QR Scanner operations
- Platform service operations
- Component lifecycle events
## How to Enable Debug Logging
### Option 1: Environment Variable (Recommended)
Set the `VITE_DEBUG_LOGGING` environment variable to `true`:
```bash
# For development
VITE_DEBUG_LOGGING=true npm run dev
# For web builds
VITE_DEBUG_LOGGING=true npm run build:web:dev
# For Electron builds
VITE_DEBUG_LOGGING=true npm run build:electron:dev
```
### Option 2: .env File
Create or modify `.env.local` file:
```bash
# Enable debug logging
VITE_DEBUG_LOGGING=true
```
### Option 3: Package.json Scripts
Add debug variants to your package.json scripts:
```json
{
"scripts": {
"dev:debug": "VITE_DEBUG_LOGGING=true npm run dev",
"build:web:debug": "VITE_DEBUG_LOGGING=true npm run build:web:dev",
"build:electron:debug": "VITE_DEBUG_LOGGING=true npm run build:electron:dev"
}
}
```
## Debug Logging Rules
Debug logging follows these rules:
1. **Only shows in development mode** (not production)
2. **Only shows for web platform** (not Electron)
3. **Must be explicitly enabled** via `VITE_DEBUG_LOGGING=true`
4. **Never logged to database** (to reduce noise)
5. **Very verbose** - includes detailed component state and operations
## Components with Debug Logging
The following components include debug logging:
- **ImageMethodDialog.vue** - Camera operations, preview state
- **PhotoDialog.vue** - Camera operations, video setup
- **AmountInput.vue** - Input validation, increment/decrement
- **GiftedDialog.vue** - Amount updates, form state
- **CapacitorPlatformService.ts** - Database operations, migrations
- **QRScanner services** - Camera permissions, scanner state
- **PlatformServiceMixin.ts** - Service initialization
## Example Debug Output
When enabled, you'll see output like:
```
[ImageMethodDialog] open called
[ImageMethodDialog] Camera facing mode: user
[ImageMethodDialog] Should mirror video: true
[ImageMethodDialog] Platform capabilities: {isMobile: false, hasCamera: true}
[ImageMethodDialog] Starting camera preview from open()
[ImageMethodDialog] startCameraPreview called
[ImageMethodDialog] Current showCameraPreview state: true
[ImageMethodDialog] MediaDevices available: true
[ImageMethodDialog] getUserMedia constraints: {video: {facingMode: "user"}}
[ImageMethodDialog] Setting video element srcObject
[ImageMethodDialog] Video metadata loaded, starting playback
[ImageMethodDialog] Video element started playing successfully
```
## Disabling Debug Logging
To disable debug logging:
1. **Remove the environment variable:**
```bash
unset VITE_DEBUG_LOGGING
```
2. **Or set it to false:**
```bash
VITE_DEBUG_LOGGING=false npm run dev
```
3. **Or remove from .env file:**
```bash
# Comment out or remove this line
# VITE_DEBUG_LOGGING=true
```
## Production Behavior
In production builds, debug logging is **always disabled** regardless of the environment variable setting. This ensures:
- No debug output in production
- No performance impact from debug logging
- Clean console output for end users
## Troubleshooting
### Debug Logging Not Working
1. **Check environment variable:**
```bash
echo $VITE_DEBUG_LOGGING
```
2. **Verify it's set to "true":**
```bash
VITE_DEBUG_LOGGING=true npm run dev
```
3. **Check if you're in development mode:**
- Debug logging only works in development (`NODE_ENV !== "production"`)
- Production builds always disable debug logging
### Too Much Debug Output
If debug logging is too verbose:
1. **Disable it completely:**
```bash
unset VITE_DEBUG_LOGGING
```
2. **Or modify specific components** to use `logger.log` instead of `logger.debug`
3. **Or add conditional logging** in components:
```typescript
if (process.env.VITE_DEBUG_LOGGING === "true") {
logger.debug("Detailed debug info");
}
```
## Best Practices
1. **Use debug logging sparingly** - only for troubleshooting
2. **Disable in production** - debug logging is automatically disabled
3. **Use specific component prefixes** - makes it easier to filter output
4. **Consider log levels** - use `logger.log` for important info, `logger.debug` for verbose details
5. **Test without debug logging** - ensure your app works without debug output
## Future Improvements
Potential enhancements to the debug logging system:
1. **Component-specific debug flags** - enable debug for specific components only
2. **Log level filtering** - show only certain types of debug messages
3. **Debug UI panel** - in-app debug information display
4. **Structured logging** - JSON format for better parsing
5. **Performance monitoring** - track impact of debug logging

View File

@@ -1,113 +0,0 @@
# $updateSettings to $saveSettings Consolidation Plan
## Overview
Consolidate `$updateSettings` method into `$saveSettings` to eliminate code duplication and improve maintainability. The `$updateSettings` method is currently just a thin wrapper around `$saveSettings` and `$saveUserSettings`, providing no additional functionality.
## Current State Analysis
### Current Implementation
```typescript
// Current $updateSettings - just a wrapper
async $updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean> {
try {
if (did) {
return await this.$saveUserSettings(did, changes);
} else {
return await this.$saveSettings(changes);
}
} catch (error) {
logger.error("[PlatformServiceMixin] Error updating settings:", error);
return false;
}
}
```
### Usage Statistics
- **$updateSettings**: 42 references across codebase
- **$saveSettings**: 38 references across codebase
- **$saveUserSettings**: 12 references across codebase
## Migration Strategy
### Phase 1: Documentation and Planning ✅
- [x] Document current usage patterns
- [x] Identify all call sites
- [x] Create migration plan
### Phase 2: Implementation
- [ ] Update `$saveSettings` to accept optional `did` parameter
- [ ] Add error handling to `$saveSettings` (currently missing)
- [ ] Deprecate `$updateSettings` with migration notice
- [ ] Update all call sites to use `$saveSettings` directly
### Phase 3: Cleanup
- [ ] Remove `$updateSettings` method
- [ ] Update documentation
- [ ] Update tests
## Implementation Details
### Enhanced $saveSettings Method
```typescript
async $saveSettings(changes: Partial<Settings>, did?: string): Promise<boolean> {
try {
// Convert settings for database storage
const convertedChanges = this._convertSettingsForStorage(changes);
if (did) {
// User-specific settings
return await this.$saveUserSettings(did, convertedChanges);
} else {
// Default settings
return await this.$saveSettings(convertedChanges);
}
} catch (error) {
logger.error("[PlatformServiceMixin] Error saving settings:", error);
return false;
}
}
```
### Migration Benefits
1. **Reduced Code Duplication**: Single method handles both use cases
2. **Improved Maintainability**: One place to fix issues
3. **Consistent Error Handling**: Unified error handling approach
4. **Better Type Safety**: Single method signature to maintain
### Risk Assessment
- **Low Risk**: `$updateSettings` is just a wrapper, no complex logic
- **Backward Compatible**: Can maintain both methods during transition
- **Testable**: Existing tests can be updated incrementally
## Call Site Migration Examples
### Before (using $updateSettings)
```typescript
await this.$updateSettings({ searchBoxes: [newSearchBox] });
await this.$updateSettings({ filterFeedByNearby: false }, userDid);
```
### After (using $saveSettings)
```typescript
await this.$saveSettings({ searchBoxes: [newSearchBox] });
await this.$saveSettings({ filterFeedByNearby: false }, userDid);
```
## Testing Strategy
1. **Unit Tests**: Update existing tests to use `$saveSettings`
2. **Integration Tests**: Verify both default and user-specific settings work
3. **Migration Tests**: Ensure searchBoxes conversion still works
4. **Performance Tests**: Verify no performance regression
## Timeline
- **Phase 1**: ✅ Complete
- **Phase 2**: 1-2 days
- **Phase 3**: 1 day
- **Total**: 2-3 days
## Success Criteria
- [ ] All existing functionality preserved
- [ ] No performance regression
- [ ] All tests passing
- [ ] Reduced code duplication
- [ ] Improved maintainability

View File

@@ -14,10 +14,7 @@ import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher }
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// Debug logging - only show when VITE_DEBUG_LOGGING is enabled
if (process.env.VITE_DEBUG_LOGGING === 'true') {
console.log('[Electron] Another instance is already running. Exiting immediately...');
}
console.log('[Electron] Another instance is already running. Exiting immediately...');
process.exit(0);
}
@@ -93,10 +90,7 @@ if (electronIsDev) {
// Handle second instance launch (focus existing window and show dialog)
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Debug logging - only show when VITE_DEBUG_LOGGING is enabled
if (process.env.VITE_DEBUG_LOGGING === 'true') {
console.log('[Electron] Second instance attempted to launch');
}
console.log('[Electron] Second instance attempted to launch');
// Someone tried to run a second instance, we should focus our window instead
const mainWindow = myCapacitorApp.getMainWindow();
@@ -169,10 +163,7 @@ ipcMain.handle('export-data-to-downloads', async (_event, fileName: string, data
// Write the file to the Downloads directory
await fs.writeFile(filePath, data, 'utf-8');
// Debug logging - only show when VITE_DEBUG_LOGGING is enabled
if (process.env.VITE_DEBUG_LOGGING === 'true') {
console.log(`[Electron Main] File exported successfully: ${filePath}`);
}
console.log(`[Electron Main] File exported successfully: ${filePath}`);
return {
success: true,

View File

@@ -3,10 +3,7 @@ import { contextBridge, ipcRenderer } from 'electron';
require('./rt/electron-rt');
//////////////////////////////
// User Defined Preload scripts below
// Debug logging - only show when VITE_DEBUG_LOGGING is enabled
if (process.env.VITE_DEBUG_LOGGING === 'true') {
console.log('User Preload!');
}
console.log('User Preload!');
/**
* Expose secure IPC APIs to the renderer process.

View File

@@ -42,6 +42,7 @@
"build:ios:deploy": "./scripts/build-ios.sh --deploy",
"build:web": "./scripts/build-web.sh",
"build:web:dev": "./scripts/build-web.sh --dev",
"build:web:dev:dist": "./scripts/build-web.sh --dev --build-dev-to-dist",
"build:web:test": "./scripts/build-web.sh --test",
"build:web:prod": "./scripts/build-web.sh --prod",
"build:web:docker": "./scripts/build-web.sh --docker",

View File

@@ -186,8 +186,8 @@ clean_ios_build() {
log_debug "Cleaned ios/App/DerivedData/"
fi
# Clean Capacitor (using npm script instead of invalid cap clean command)
npm run clean:ios || true
# Clean Capacitor
npx cap clean ios || true
log_success "iOS build cleaned"
}

View File

@@ -44,6 +44,7 @@ BUILD_MODE="development"
BUILD_ACTION="build"
DOCKER_BUILD=false
SERVE_BUILD=false
BUILD_DEV_TO_DIST=false
# Function to show usage
show_usage() {
@@ -61,6 +62,7 @@ OPTIONS:
--help Show this help message
--verbose Enable verbose logging
--env Show environment variables
--build-dev-to-dist Build development configuration to dist folder
EXAMPLES:
$0 # Development build
@@ -70,6 +72,7 @@ EXAMPLES:
$0 --docker:test # Test + Docker
$0 --docker:prod # Production + Docker
$0 --serve # Build and serve
$0 --build-dev-to-dist # Build development configuration to dist folder
BUILD MODES:
development: Starts Vite development server with hot reload (default)
@@ -125,6 +128,10 @@ parse_web_args() {
print_env_vars "VITE_"
exit 0
;;
--build-dev-to-dist)
BUILD_DEV_TO_DIST=true
shift
;;
*)
log_warn "Unknown option: $1"
shift
@@ -321,10 +328,28 @@ setup_web_environment
# Handle different build modes
if [ "$BUILD_MODE" = "development" ] && [ "$DOCKER_BUILD" = false ] && [ "$SERVE_BUILD" = false ]; then
# Development mode: Start dev server
log_info "Development mode detected - starting development server"
start_dev_server
# Note: start_dev_server doesn't return, it runs the server
# Check if we want to build development to dist instead of starting dev server
if [ "$BUILD_DEV_TO_DIST" = true ]; then
# Development build mode: Build to dist with development configuration
log_info "Development build mode detected - building to dist with development configuration"
# Step 1: Clean dist directory
log_info "Cleaning dist directory..."
clean_build_artifacts "dist"
# Step 2: Execute Vite build with development configuration
safe_execute "Vite development build" "execute_vite_build development" || exit 3
log_success "Development build completed successfully!"
log_info "Development build output available in: dist/"
print_footer "Web Development Build"
exit 0
else
# Development mode: Start dev server
log_info "Development mode detected - starting development server"
start_dev_server
# Note: start_dev_server doesn't return, it runs the server
fi
elif [ "$SERVE_BUILD" = true ]; then
# Serve mode: Build then serve
log_info "Serve mode detected - building then serving"

View File

@@ -52,7 +52,7 @@
<a
class="cursor-pointer"
data-testid="circle-info-link"
@click="emitLoadClaim(record.jwtId)"
@click="$emit('loadClaim', record.jwtId)"
>
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
@@ -67,7 +67,7 @@
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="emitViewImage(record.image)"
@click="$emit('viewImage', record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
@@ -80,7 +80,7 @@
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
@@ -248,7 +248,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { Component, Prop, Vue } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
@@ -340,19 +340,7 @@ export default class ActivityListItem extends Vue {
return true;
}
// Emit methods using @Emit decorator
@Emit("viewImage")
emitViewImage(imageUrl: string) {
return imageUrl;
}
@Emit("loadClaim")
emitLoadClaim(jwtId: string) {
return jwtId;
}
@Emit("confirmClaim")
emitConfirmClaim() {
handleConfirmClick() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
@@ -364,11 +352,7 @@ export default class ActivityListItem extends Vue {
);
return;
}
return this.record;
}
handleConfirmClick() {
this.emitConfirmClaim();
this.$emit("confirmClaim", this.record);
}
get friendlyDate(): string {

View File

@@ -6,13 +6,13 @@
:checked="allContactsSelected"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
@click="emitToggleAllSelection"
@click="$emit('toggle-all-selection')"
/>
<button
v-if="!showGiveNumbers"
:class="copyButtonClass"
:disabled="copyButtonDisabled"
@click="emitCopySelected"
@click="$emit('copy-selected')"
>
Copy
</button>
@@ -20,7 +20,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
/**
* ContactBulkActions - Contact bulk actions component
@@ -38,16 +38,5 @@ export default class ContactBulkActions extends Vue {
@Prop({ required: true }) allContactsSelected!: boolean;
@Prop({ required: true }) copyButtonClass!: string;
@Prop({ required: true }) copyButtonDisabled!: boolean;
// Emit methods using @Emit decorator
@Emit("toggle-all-selection")
emitToggleAllSelection() {
// No parameters needed
}
@Emit("copy-selected")
emitCopySelected() {
// No parameters needed
}
}
</script>

View File

@@ -64,7 +64,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
/**
* ContactInputForm - Contact input form component
@@ -165,9 +165,9 @@ export default class ContactInputForm extends Vue {
* Handle QR scan button click
* Emits qr-scan event for parent handling
*/
@Emit("qr-scan")
private handleQRScan(): void {
// QR scan button clicked - event emitted to parent
console.log("[ContactInputForm] QR scan button clicked");
this.$emit("qr-scan");
}
}
</script>

View File

@@ -8,21 +8,21 @@
:checked="allContactsSelected"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
@click="emitToggleAllSelection"
@click="$emit('toggle-all-selection')"
/>
<button
v-if="!showGiveNumbers"
:class="copyButtonClass"
:disabled="copyButtonDisabled"
data-testId="copySelectedContactsButtonTop"
@click="emitCopySelected"
@click="$emit('copy-selected')"
>
Copy
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
@click="emitShowCopyInfo"
@click="$emit('show-copy-info')"
/>
</div>
</div>
@@ -33,7 +33,7 @@
v-if="showGiveNumbers"
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
:class="giveAmountsButtonClass"
@click="emitToggleGiveTotals"
@click="$emit('toggle-give-totals')"
>
{{ giveAmountsButtonText }}
<font-awesome icon="left-right" class="fa-fw" />
@@ -41,7 +41,7 @@
<button
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="emitToggleShowActions"
@click="$emit('toggle-show-actions')"
>
{{ showActionsButtonText }}
</button>
@@ -50,7 +50,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
/**
* ContactListHeader - Contact list header component
@@ -71,31 +71,5 @@ export default class ContactListHeader extends Vue {
@Prop({ required: true }) giveAmountsButtonText!: string;
@Prop({ required: true }) showActionsButtonText!: string;
@Prop({ required: true }) giveAmountsButtonClass!: Record<string, boolean>;
// Emit methods using @Emit decorator
@Emit("toggle-all-selection")
emitToggleAllSelection() {
// No parameters needed
}
@Emit("copy-selected")
emitCopySelected() {
// No parameters needed
}
@Emit("show-copy-info")
emitShowCopyInfo() {
// No parameters needed
}
@Emit("toggle-give-totals")
emitToggleGiveTotals() {
// No parameters needed
}
@Emit("toggle-show-actions")
emitToggleShowActions() {
// No parameters needed
}
}
</script>

View File

@@ -9,14 +9,14 @@
:checked="isSelected"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="emitToggleSelection(contact.did)"
@click="$emit('toggle-selection', contact.did)"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="emitShowIdenticon(contact)"
@click="$emit('show-identicon', contact)"
/>
<div class="overflow-hidden">
@@ -63,7 +63,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-l-md"
:title="getGiveDescriptionForContact(contact.did, true)"
@click="emitShowGiftedDialog(contact.did, activeDid)"
@click="$emit('show-gifted-dialog', contact.did, activeDid)"
>
{{ getGiveAmountForContact(contact.did, true) }}
</button>
@@ -71,7 +71,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-r-md border-l"
:title="getGiveDescriptionForContact(contact.did, false)"
@click="emitShowGiftedDialog(activeDid, contact.did)"
@click="$emit('show-gifted-dialog', activeDid, contact.did)"
>
{{ getGiveAmountForContact(contact.did, false) }}
</button>
@@ -81,7 +81,7 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
data-testId="offerButton"
@click="emitOpenOfferDialog(contact.did, contact.name)"
@click="$emit('open-offer-dialog', contact.did, contact.name)"
>
Offer
</button>
@@ -102,7 +102,7 @@
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import { Contact } from "../db/tables/contacts";
import { AppString } from "../constants/app";
@@ -140,27 +140,6 @@ export default class ContactListItem extends Vue {
// Constants
AppString = AppString;
// Emit methods using @Emit decorator
@Emit("toggle-selection")
emitToggleSelection(did: string) {
return did;
}
@Emit("show-identicon")
emitShowIdenticon(contact: Contact) {
return contact;
}
@Emit("show-gifted-dialog")
emitShowGiftedDialog(fromDid: string, toDid: string) {
return { fromDid, toDid };
}
@Emit("open-offer-dialog")
emitOpenOfferDialog(did: string, name: string | undefined) {
return { did, name };
}
/**
* Format contact name with non-breaking spaces
*/

View File

@@ -55,7 +55,10 @@
aria-label="Delete profile image"
@click="deleteImage"
>
<font-awesome icon="trash-can" aria-hidden="true" />
<font-awesome
icon="trash-can"
aria-hidden="true"
/>
</button>
</span>
<div v-else class="text-center">

View File

@@ -25,7 +25,7 @@
<div class="flex-1 flex items-center justify-center p-2">
<div class="w-full h-full flex items-center justify-center">
<img
:src="imageUrl"
:src="transformedImageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click="close"

View File

@@ -7,14 +7,14 @@
:contact="contact"
:icon-size="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="emitClose"
@click="$emit('close')"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import { Contact } from "../db/tables/contacts";
@@ -34,11 +34,5 @@ import { Contact } from "../db/tables/contacts";
})
export default class LargeIdenticonModal extends Vue {
@Prop({ required: true }) contact!: Contact | undefined;
// Emit methods using @Emit decorator
@Emit("close")
emitClose() {
// No parameters needed
}
}
</script>

View File

@@ -162,6 +162,8 @@
/* TODO: Human Testing Required - PlatformServiceMixin Migration */
// Priority: High | Migrated: 2025-07-06 | Author: Matthew Raymer
//
// TESTING NEEDED: Component migrated from legacy logConsoleAndDb to PlatformServiceMixin
// but requires human validation due to meeting component accessibility limitations.
//
// Test Scenarios Required:
// 1. Load members list with valid meeting password
@@ -172,10 +174,11 @@
// 6. Cross-platform testing: web, mobile, desktop
//
// Reference: docs/migration-testing/migration-checklist-MembersList.md
// Migration Details: Replaced 3 logConsoleAndDb() calls with this.$logAndConsole()
// Validation: Passes lint checks and TypeScript compilation
// Navigation: Contacts → Chair Icon → Start/Join Meeting → Members List
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { Component, Vue, Prop } from "vue-facing-decorator";
import {
errorStringForLog,
@@ -219,12 +222,6 @@ export default class MembersList extends Vue {
@Prop({ required: true }) password!: string;
@Prop({ default: false }) showOrganizerTools!: boolean;
// Emit methods using @Emit decorator
@Emit("error")
emitError(message: string) {
return message;
}
decryptedMembers: DecryptedMember[] = [];
firstName = "";
isLoading = true;
@@ -265,7 +262,10 @@ export default class MembersList extends Vue {
"Error fetching members: " + errorStringForLog(error),
true,
);
this.emitError(serverMessageForUser(error) || "Failed to fetch members.");
this.$emit(
"error",
serverMessageForUser(error) || "Failed to fetch members.",
);
} finally {
this.isLoading = false;
}
@@ -478,7 +478,8 @@ export default class MembersList extends Vue {
"Error toggling admission: " + errorStringForLog(error),
true,
);
this.emitError(
this.$emit(
"error",
serverMessageForUser(error) ||
"Failed to update member admission status.",
);

View File

@@ -180,7 +180,7 @@
>
Let's go!
<br />
See & record things you've received.
See & record gratitude.
</button>
<button
type="button"

View File

@@ -67,7 +67,7 @@
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this week.
Your image counter resets at
<b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime)
readableDate(imageLimits?.nextWeekBeginDateTime || "")
}}</b>
</p>
</div>

View File

@@ -273,8 +273,8 @@ export async function logToDb(
// Prevent infinite logging loops - if we're already trying to log to database,
// just log to console instead to break circular dependency
if (isLoggingToDatabase) {
// Use logger.debug for controlled debug output instead of direct console.log
logger.debug(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`);
// eslint-disable-next-line no-console
console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`);
return;
}

View File

@@ -6,9 +6,7 @@ export type ContactMethod = {
export type Contact = {
//
// When adding a property:
// - Consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
// When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
did: string;
contactMethods?: Array<ContactMethod>;

View File

@@ -34,7 +34,6 @@ import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
// Consolidate this with src/utils/PlatformServiceMixin._parseJsonField
function parseJsonField<T>(value: unknown, defaultValue: T): T {
if (typeof value === "string") {
try {
@@ -502,7 +501,15 @@ export function findAllVisibleToDids(
import * as R from 'ramda';
//import { findAllVisibleToDids } from './src/libs/util'; // doesn't work because other dependencies fail so gotta copy-and-paste function
// Test/debug console.log statements removed - use logger.debug() if needed
console.log(R.equals(findAllVisibleToDids(null), {}));
console.log(R.equals(findAllVisibleToDids(9), {}));
console.log(R.equals(findAllVisibleToDids([]), {}));
console.log(R.equals(findAllVisibleToDids({}), {}));
console.log(R.equals(findAllVisibleToDids({ issuer: "abc" }), {}));
console.log(R.equals(findAllVisibleToDids({ issuerVisibleToDids: ["abc"] }), { ".issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids([{ issuerVisibleToDids: ["abc"] }]), { "[0].issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] } }]), { "[1].fluff.issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] }, stuff: [ { did: "HIDDEN", agentDidVisibleToDids: ["def", "ghi"] } ] }]), { "[1].fluff.issuer": ["abc"], "[1].stuff[0].agentDid": ["def", "ghi"] }));
*
**/
@@ -966,28 +973,28 @@ export async function importFromMnemonic(
if (isTestUser0) {
// Set up Test User #0 specific settings with enhanced error handling
const platformService = await getPlatformService();
try {
// First, ensure the DID-specific settings record exists
await platformService.insertDidSpecificSettings(newId.did);
// Then update with Test User #0 specific settings
await platformService.updateDidSpecificSettings(newId.did, {
firstName: "User Zero",
isRegistered: true,
});
// Verify the settings were saved correctly
const verificationResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (verificationResult?.values?.length) {
const settings = verificationResult.values[0];
const firstName = settings[0];
const isRegistered = settings[1];
logger.info("[importFromMnemonic] Test User #0 settings verification", {
did: newId.did,
firstName,
@@ -995,50 +1002,40 @@ export async function importFromMnemonic(
expectedFirstName: "User Zero",
expectedIsRegistered: true,
});
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
logger.warn(
"[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates",
);
logger.warn("[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates");
await platformService.dbExec(
"UPDATE settings SET firstName = ? WHERE accountDid = ?",
["User Zero", newId.did],
);
await platformService.dbExec(
"UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
[1, newId.did],
);
// Verify again
const retryResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.info(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
isRegistered: retrySettings[1],
},
);
logger.info("[importFromMnemonic] Test User #0 settings after retry", {
firstName: retrySettings[0],
isRegistered: retrySettings[1],
});
}
}
} else {
logger.error(
"[importFromMnemonic] Failed to verify Test User #0 settings - no record found",
);
logger.error("[importFromMnemonic] Failed to verify Test User #0 settings - no record found");
}
} catch (error) {
logger.error(
"[importFromMnemonic] Error setting up Test User #0 settings:",
error,
);
logger.error("[importFromMnemonic] Error setting up Test User #0 settings:", error);
// Don't throw - allow the import to continue even if settings fail
}
}

View File

@@ -250,12 +250,6 @@ onerror = function (error) {
* Auto-initialize on worker startup (removed to prevent circular dependency)
* Initialization now happens on first database operation
*/
// Use logger.debug for controlled debug output instead of direct console.log
// Note: This is a worker context, so we use a simple debug message
if (typeof self !== "undefined" && self.name) {
// Worker context - use simple debug output
self.postMessage({
type: "debug",
message: "[SQLWorker] Worker loaded, ready to receive messages",
});
}
// Use console for critical startup message to avoid circular dependency
// eslint-disable-next-line no-console
console.log("[SQLWorker] Worker loaded, ready to receive messages");

View File

@@ -42,8 +42,9 @@ export class PlatformServiceFactory {
const platform = process.env.VITE_PLATFORM || "web";
if (!PlatformServiceFactory.creationLogged) {
// Use logger.debug for controlled debug output instead of direct console.log
logger.debug(
// Use console for critical startup message to avoid circular dependency
// eslint-disable-next-line no-console
console.log(
`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`,
);
PlatformServiceFactory.creationLogged = true;

View File

@@ -174,7 +174,7 @@ export class DeepLinkHandler {
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name;
} catch (error) {
logger.error(`[DeepLink] Invalid route path: ${path}`);
console.error(`[DeepLink] Invalid route path: ${path}`);
// Redirect to error page with information about the invalid link
await this.router.replace({
@@ -201,7 +201,7 @@ export class DeepLinkHandler {
validatedQuery = await schema.parseAsync(query);
} catch (error) {
// For parameter validation errors, provide specific error feedback
logger.error(
console.error(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
);
await this.router.replace({
@@ -226,7 +226,7 @@ export class DeepLinkHandler {
query: validatedQuery,
});
} catch (error) {
logger.error(
console.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)} ... and validated query: ${JSON.stringify(validatedQuery)}`,
);
// For parameter validation errors, provide specific error feedback
@@ -260,7 +260,7 @@ export class DeepLinkHandler {
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logger.error(
console.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
);

View File

@@ -97,8 +97,9 @@ export class WebPlatformService implements PlatformService {
}
} else {
// We're in a worker context - skip initBackend call
// Use logger.debug for controlled debug output instead of direct console.log
logger.debug(
// Use console for critical startup message to avoid circular dependency
// eslint-disable-next-line no-console
console.log(
"[WebPlatformService] Skipping initBackend call in worker context",
);
}
@@ -692,7 +693,11 @@ export class WebPlatformService implements PlatformService {
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did];
// Debug logging removed - use logger.debug() if needed
console.log(
"[WebPlatformService] updateDidSpecificSettings",
sql,
JSON.stringify(params, null, 2),
);
await this.dbExec(sql, params);
}

View File

@@ -1,92 +1,26 @@
<template>
<div>
<h2>PlatformServiceMixin Test</h2>
<div class="space-y-2">
<button
:class="
activeTest === 'insert'
? 'bg-green-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
"
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testInsert"
>
Test Insert
</button>
<button
:class="
activeTest === 'update'
? 'bg-green-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
"
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testUpdate"
>
Test Update
</button>
<button
:class="
activeTest === 'searchBoxes'
? 'bg-green-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
"
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testSearchBoxesConversion"
>
Test SearchBoxes Conversion
</button>
<button
:class="
activeTest === 'database'
? 'bg-green-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
"
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testDatabaseStorage"
>
Test Database Storage Format
</button>
<button
:class="
activeTest === 'userZero'
? 'bg-green-500 text-white'
: primaryButtonClasses
"
class="transition-colors"
@click="testUserZeroSettings"
>
Test User #0 Settings
</button>
</div>
<div
v-if="userZeroTestResult"
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
<button @click="testInsert">Test Insert</button>
<button @click="testUpdate">Test Update</button>
<button
:class="primaryButtonClasses"
@click="testUserZeroSettings"
>
<h4 class="font-semibold mb-2">User #0 Settings Test Result:</h4>
<pre class="text-sm">{{
JSON.stringify(userZeroTestResult, null, 2)
}}</pre>
</div>
Test User #0 Settings
</button>
<div v-if="result" class="mt-4">
<div class="p-4 border border-blue-300 rounded-md bg-blue-50">
<h4 class="font-semibold mb-2 text-blue-800">Test Results:</h4>
<div
class="whitespace-pre-wrap text-sm font-mono bg-white p-3 rounded border"
>
{{ result }}
</div>
</div>
<div v-if="userZeroTestResult" class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50">
<h4 class="font-semibold mb-2">User #0 Settings Test Result:</h4>
<pre class="text-sm">{{ JSON.stringify(userZeroTestResult, null, 2) }}</pre>
</div>
<pre>{{ result }}</pre>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { logger } from "@/utils/logger";
@Component({
mixins: [PlatformServiceMixin],
@@ -94,15 +28,8 @@ import { logger } from "@/utils/logger";
export default class PlatformServiceMixinTest extends Vue {
result: string = "";
userZeroTestResult: any = null;
activeTest: string = ""; // Track which test is currently active
// Add the missing computed property
get primaryButtonClasses() {
return "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded";
}
testInsert() {
this.activeTest = "insert";
const contact = {
name: "Alice",
age: 30,
@@ -114,7 +41,6 @@ export default class PlatformServiceMixinTest extends Vue {
}
testUpdate() {
this.activeTest = "update";
const changes = { name: "Bob", isActive: false };
const { sql, params } = this.$generateUpdateStatement(
changes,
@@ -125,138 +51,20 @@ export default class PlatformServiceMixinTest extends Vue {
this.result = `SQL: ${sql}\nParams: ${JSON.stringify(params)}`;
}
testSearchBoxesConversion() {
this.activeTest = "searchBoxes";
// Test the _convertSettingsForStorage helper method
const testSettings = {
firstName: "John",
searchBoxes: [
{
name: "Test Area",
bbox: {
eastLong: 1.0,
maxLat: 1.0,
minLat: 0.0,
westLong: 0.0,
},
},
],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const converted = (this as any)._convertSettingsForStorage(testSettings);
this.result = `# 🔄 SearchBoxes Conversion Test (Helper Method)
## 📥 Input Settings
\`\`\`json
{
"firstName": "John",
"searchBoxes": ${JSON.stringify(testSettings.searchBoxes, null, 2)}
}
\`\`\`
## 🔧 After _convertSettingsForStorage()
\`\`\`json
{
"firstName": "John",
"searchBoxes": "${converted.searchBoxes}"
}
\`\`\`
## ✅ Conversion Results
- **Original Type**: \`${typeof testSettings.searchBoxes}\`
- **Converted Type**: \`${typeof converted.searchBoxes}\`
- **Conversion**: Array → JSON String ✅
## 📝 Note
This tests the helper method only - no database interaction`;
}
async testDatabaseStorage() {
this.activeTest = "database";
try {
this.result = "🔄 Testing database storage format...";
// Create test settings with searchBoxes array
const testSettings = {
searchBoxes: [
{
name: "Test Area",
bbox: {
eastLong: 1.0,
maxLat: 1.0,
minLat: 0.0,
westLong: 0.0,
},
},
],
};
// Save to database using our fixed method
const success = await this.$saveSettings(testSettings);
if (success) {
// Now query the raw database to see how it's actually stored
const rawResult = await this.$dbQuery(
"SELECT searchBoxes FROM settings WHERE id = ?",
[MASTER_SETTINGS_KEY],
);
if (rawResult?.values?.length) {
const rawSearchBoxes = rawResult.values[0][0]; // First column of first row
this.result = `# 🔧 Database Storage Format Test (Full Database Cycle)
## 📥 Input (JavaScript Array)
\`\`\`json
${JSON.stringify(testSettings.searchBoxes, null, 2)}
\`\`\`
## 💾 Database Storage (JSON String)
\`\`\`sql
"${rawSearchBoxes}"
\`\`\`
## ✅ Verification
- **Type**: \`${typeof rawSearchBoxes}\`
- **Is JSON String**: \`${typeof rawSearchBoxes === "string" && rawSearchBoxes.startsWith("[")}\`
- **Conversion Working**: ✅ **YES** - Array converted to JSON string for database storage
## 🔄 Process Flow
1. **Input**: JavaScript array with bounding box coordinates
2. **Conversion**: \`_convertSettingsForStorage()\` converts array to JSON string
3. **Storage**: JSON string saved to database using \`$saveSettings()\`
4. **Retrieval**: JSON string parsed back to array for application use
## 📝 Note
This tests the complete save → retrieve cycle with actual database interaction`;
} else {
this.result = "❌ No data found in database";
}
} else {
this.result = "❌ Failed to save settings";
}
} catch (error) {
this.result = `❌ Error: ${error}`;
}
}
async testUserZeroSettings() {
this.activeTest = "userZero";
try {
// User #0's DID
const userZeroDid = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
this.result = "Testing User #0 settings...";
// Test the debug methods
await this.$debugMergedSettings(userZeroDid);
// Get the actual settings
const didSettings = await this.$debugDidSettings(userZeroDid);
const accountSettings = await this.$accountSettings(userZeroDid);
this.userZeroTestResult = {
didSettings,
accountSettings,
@@ -264,11 +72,11 @@ This tests the complete save → retrieve cycle with actual database interaction
firstName: accountSettings.firstName,
timestamp: new Date().toISOString(),
};
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
} catch (error) {
this.result = `Error testing User #0 settings: ${error}`;
logger.error("Error testing User #0 settings:", error);
console.error("Error testing User #0 settings:", error);
}
}
}

83
src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,83 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module '@jlongster/sql.js' {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => Database;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface Database {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
prepare: (sql: string) => Promise<Statement>;
close: () => void;
}
interface Statement {
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (params?: unknown[]) => Promise<SqlValue[]>;
all: (params?: unknown[]) => Promise<SqlValue[][]>;
finalize: () => void;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}
/**
* Electron API types for the main world context bridge.
*
* These types define the secure IPC APIs exposed by the preload script
* to the renderer process for native Electron functionality.
*/
interface ElectronAPI {
/**
* Export data to the user's Downloads folder.
*
* @param fileName - The name of the file to save (e.g., 'backup-2025-07-06.json')
* @param data - The content to write to the file (string)
* @returns Promise with success status, file path, or error message
*/
exportData: (fileName: string, data: string) => Promise<{
success: boolean;
path?: string;
error?: string;
}>;
}
/**
* Global window interface extension for Electron APIs.
*
* This makes the electronAPI available on the window object
* in TypeScript without type errors.
*/
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
/**
* Vue instance interface extension for global properties.
*
* This makes global properties available on Vue instances
* in TypeScript without type errors.
*/
declare module 'vue' {
interface ComponentCustomProperties {
$notify: (notification: any, timeout?: number) => void;
$route: import('vue-router').RouteLocationNormalizedLoaded;
$router: import('vue-router').Router;
}
}

67
src/types/modules.d.ts vendored Normal file
View File

@@ -0,0 +1,67 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module '@jlongster/sql.js' {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => Database;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface Database {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
prepare: (sql: string) => Promise<Statement>;
close: () => void;
}
interface Statement {
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (params?: unknown[]) => Promise<SqlValue[]>;
all: (params?: unknown[]) => Promise<SqlValue[][]>;
finalize: () => void;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}
declare module 'absurd-sql' {
import type { SQL } from '@jlongster/sql.js';
export class SQLiteFS {
constructor(fs: any, backend: any);
}
}
declare module 'absurd-sql/dist/indexeddb-backend' {
export default class IndexedDBBackend {
constructor();
}
}
declare module 'absurd-sql/dist/indexeddb-main-thread' {
import type { QueryExecResult } from './database';
export interface SQLiteOptions {
filename?: string;
autoLoad?: boolean;
debug?: boolean;
}
export interface SQLiteDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
close: () => Promise<void>;
}
export function initSqlJs(options?: any): Promise<any>;
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
}

View File

@@ -44,11 +44,7 @@ import type {
PlatformService,
PlatformCapabilities,
} from "@/services/PlatformService";
import {
MASTER_SETTINGS_KEY,
type Settings,
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings";
import { logger } from "@/utils/logger";
import { Contact } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
@@ -63,14 +59,14 @@ import {
// TYPESCRIPT INTERFACES
// =================================================
// /**
// * Cache entry interface for storing data with TTL
// */
// interface CacheEntry<T> {
// data: T;
// timestamp: number;
// ttl: number; // milliseconds
// }
/**
* Cache entry interface for storing data with TTL
*/
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number; // milliseconds
}
/**
* Vue component interface that uses the PlatformServiceMixin
@@ -83,21 +79,21 @@ interface VueComponentWithMixin {
platformService(): PlatformService;
}
// /**
// * Global cache store for mixin instances
// * Uses WeakMap to avoid memory leaks when components are destroyed
// */
// const componentCaches = new WeakMap<
// VueComponentWithMixin,
// Map<string, CacheEntry<unknown>>
// >();
//
// /**
// * Cache configuration constants
// */
// const CACHE_DEFAULTS = {
// default: 15000, // 15 seconds default TTL
// } as const;
/**
* Global cache store for mixin instances
* Uses WeakMap to avoid memory leaks when components are destroyed
*/
const componentCaches = new WeakMap<
VueComponentWithMixin,
Map<string, CacheEntry<unknown>>
>();
/**
* Cache configuration constants
*/
const CACHE_DEFAULTS = {
default: 15000, // 15 seconds default TTL
} as const;
const _memoryLogs: string[] = [];
@@ -182,8 +178,8 @@ export const PlatformServiceMixin = {
logger.debug(
`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`,
);
// // Clear caches that might be affected by the change
// (this as any).$clearAllCaches();
// Clear caches that might be affected by the change
(this as any).$clearAllCaches();
}
},
immediate: true,
@@ -207,8 +203,8 @@ export const PlatformServiceMixin = {
logger.debug(
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
);
// // Clear caches that might be affected by the change
// this.$clearAllCaches();
// Clear caches that might be affected by the change
this.$clearAllCaches();
}
},
@@ -224,20 +220,13 @@ export const PlatformServiceMixin = {
const obj: Record<string, unknown> = {};
columns.forEach((column, index) => {
let value = row[index];
// Convert SQLite integer booleans to JavaScript booleans
if (
column === "isRegistered" ||
column === "finishedOnboarding" ||
column === "filterFeedByVisible" ||
column === "filterFeedByNearby" ||
column === "hideRegisterPromptOnNewContact" ||
column === "showContactGivesInline" ||
column === "showGeneralAdvanced" ||
column === "showShortcutBvc" ||
column === "warnIfProdServer" ||
column === "warnIfTestServer"
) {
if (column === 'isRegistered' || column === 'finishedOnboarding' ||
column === 'filterFeedByVisible' || column === 'filterFeedByNearby' ||
column === 'hideRegisterPromptOnNewContact' || column === 'showContactGivesInline' ||
column === 'showGeneralAdvanced' || column === 'showShortcutBvc' ||
column === 'warnIfProdServer' || column === 'warnIfTestServer') {
if (value === 1) {
value = true;
} else if (value === 0) {
@@ -245,7 +234,7 @@ export const PlatformServiceMixin = {
}
// Keep null values as null
}
obj[column] = value;
});
return obj;
@@ -255,8 +244,6 @@ export const PlatformServiceMixin = {
/**
* Self-contained implementation of parseJsonField
* Safely parses JSON strings with fallback to default value
*
* Consolidate this with src/libs/util.ts parseJsonField
*/
_parseJsonField<T>(value: unknown, defaultValue: T): T {
if (typeof value === "string") {
@@ -269,92 +256,71 @@ export const PlatformServiceMixin = {
return (value as T) || defaultValue;
},
// =================================================
// CACHING UTILITY METHODS
// =================================================
/**
* Convert Settings object to SettingsWithJsonStrings for database storage
* Handles conversion of complex objects like searchBoxes to JSON strings
* @param settings Settings object to convert
* @returns SettingsWithJsonStrings object ready for database storage
* Get or initialize cache for this component instance
*/
_convertSettingsForStorage(
settings: Partial<Settings>,
): Partial<SettingsWithJsonStrings> {
const converted = { ...settings } as Partial<SettingsWithJsonStrings>;
// Convert searchBoxes array to JSON string if present
if (settings.searchBoxes !== undefined) {
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes);
_getCache(): Map<string, CacheEntry<unknown>> {
let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
if (!cache) {
cache = new Map();
componentCaches.set(this as unknown as VueComponentWithMixin, cache);
}
return converted;
return cache;
},
// // =================================================
// // CACHING UTILITY METHODS
// // =================================================
/**
* Check if cache entry is valid (not expired)
*/
_isCacheValid(entry: CacheEntry<unknown>): boolean {
return Date.now() - entry.timestamp < entry.ttl;
},
// /**
// * Get or initialize cache for this component instance
// */
// _getCache(): Map<string, CacheEntry<unknown>> {
// let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
// if (!cache) {
// cache = new Map();
// componentCaches.set(this as unknown as VueComponentWithMixin, cache);
// }
// return cache;
// },
/**
* Get data from cache if valid, otherwise return null
*/
_getCached<T>(key: string): T | null {
const cache = this._getCache();
const entry = cache.get(key);
if (entry && this._isCacheValid(entry)) {
return entry.data as T;
}
cache.delete(key); // Clean up expired entries
return null;
},
// /**
// * Check if cache entry is valid (not expired)
// */
// _isCacheValid(entry: CacheEntry<unknown>): boolean {
// return Date.now() - entry.timestamp < entry.ttl;
// },
/**
* Store data in cache with TTL
*/
_setCached<T>(key: string, data: T, ttl?: number): T {
const cache = this._getCache();
const actualTtl = ttl || CACHE_DEFAULTS.default;
cache.set(key, {
data,
timestamp: Date.now(),
ttl: actualTtl,
});
return data;
},
// /**
// * Get data from cache if valid, otherwise return null
// */
// _getCached<T>(key: string): T | null {
// const cache = this._getCache();
// const entry = cache.get(key);
// if (entry && this._isCacheValid(entry)) {
// return entry.data as T;
// }
// cache.delete(key); // Clean up expired entries
// return null;
// },
/**
* Invalidate specific cache entry
*/
_invalidateCache(key: string): void {
const cache = this._getCache();
cache.delete(key);
},
// /**
// * Store data in cache with TTL
// */
// _setCached<T>(key: string, data: T, ttl?: number): T {
// const cache = this._getCache();
// const actualTtl = ttl || CACHE_DEFAULTS.default;
// cache.set(key, {
// data,
// timestamp: Date.now(),
// ttl: actualTtl,
// });
// return data;
// },
// /**
// * Invalidate specific cache entry
// */
// _invalidateCache(key: string): void {
// const cache = this._getCache();
// cache.delete(key);
// },
// /**
// * Clear all cache entries for this component
// */
// _clearCache(): void {
// const cache = this._getCache();
// cache.clear();
// },
/**
* Clear all cache entries for this component
*/
_clearCache(): void {
const cache = this._getCache();
cache.clear();
},
// =================================================
// ENHANCED DATABASE METHODS (with error handling)
@@ -757,7 +723,10 @@ export const PlatformServiceMixin = {
// Merge with any provided defaults (these take highest precedence)
const finalSettings = { ...mergedSettings, ...defaults };
// Debug logging removed - use logger.debug() if needed
console.log(
"[PlatformServiceMixin] $accountSettings",
JSON.stringify(finalSettings, null, 2),
);
return finalSettings;
} catch (error) {
logger.error(
@@ -777,9 +746,6 @@ export const PlatformServiceMixin = {
/**
* Save default settings - $saveSettings()
* Ultra-concise shortcut for updateDefaultSettings
*
* ✅ KEEP: This method will be the primary settings save method after consolidation
*
* @param changes Settings changes to save
* @returns Promise<boolean> Success status
*/
@@ -794,13 +760,10 @@ export const PlatformServiceMixin = {
if (Object.keys(safeChanges).length === 0) return true;
// Convert settings for database storage (handles searchBoxes conversion)
const convertedChanges = this._convertSettingsForStorage(safeChanges);
const setParts: string[] = [];
const params: unknown[] = [];
Object.entries(convertedChanges).forEach(([key, value]) => {
Object.entries(safeChanges).forEach(([key, value]) => {
if (value !== undefined) {
setParts.push(`${key} = ?`);
params.push(value);
@@ -847,13 +810,10 @@ export const PlatformServiceMixin = {
if (Object.keys(safeChanges).length === 0) return true;
// Convert settings for database storage (handles searchBoxes conversion)
const convertedChanges = this._convertSettingsForStorage(safeChanges);
const setParts: string[] = [];
const params: unknown[] = [];
Object.entries(convertedChanges).forEach(([key, value]) => {
Object.entries(safeChanges).forEach(([key, value]) => {
if (value !== undefined) {
setParts.push(`${key} = ?`);
params.push(value);
@@ -912,13 +872,13 @@ export const PlatformServiceMixin = {
return await this.$contacts();
},
// /**
// * Clear all caches for this component - $clearAllCaches()
// * Useful for manual cache management
// */
// $clearAllCaches(): void {
// this._clearCache();
// },
/**
* Clear all caches for this component - $clearAllCaches()
* Useful for manual cache management
*/
$clearAllCaches(): void {
this._clearCache();
},
// =================================================
// HIGH-LEVEL ENTITY OPERATIONS (eliminate verbose SQL patterns)
@@ -1224,17 +1184,6 @@ export const PlatformServiceMixin = {
* @param did Optional DID for user-specific settings
* @returns Promise<boolean> Success status
*/
/**
* Update settings - $updateSettings()
* Ultra-concise shortcut for updating settings (default or user-specific)
*
* ⚠️ DEPRECATED: This method will be removed in favor of $saveSettings()
* Use $saveSettings(changes, did?) instead for better consistency
*
* @param changes Settings changes to save
* @param did Optional DID for user-specific settings
* @returns Promise<boolean> Success status
*/
async $updateSettings(
changes: Partial<Settings>,
did?: string,
@@ -1452,9 +1401,7 @@ export const PlatformServiceMixin = {
);
if (!result?.values?.length) {
logger.warn(
`[PlatformServiceMixin] No settings found for DID: ${did}`,
);
logger.warn(`[PlatformServiceMixin] No settings found for DID: ${did}`);
return null;
}
@@ -1464,9 +1411,7 @@ export const PlatformServiceMixin = {
);
if (!mappedResults.length) {
logger.warn(
`[PlatformServiceMixin] Failed to map settings for DID: ${did}`,
);
logger.warn(`[PlatformServiceMixin] Failed to map settings for DID: ${did}`);
return null;
}
@@ -1481,10 +1426,7 @@ export const PlatformServiceMixin = {
return settings;
} catch (error) {
logger.error(
`[PlatformServiceMixin] Error debugging settings for DID ${did}:`,
error,
);
logger.error(`[PlatformServiceMixin] Error debugging settings for DID ${did}:`, error);
return null;
}
},
@@ -1498,24 +1440,14 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
{},
);
logger.info(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
);
const defaultSettings = await this.$getSettings(MASTER_SETTINGS_KEY, {});
logger.info(`[PlatformServiceMixin] Default settings:`, defaultSettings);
// Get DID-specific settings
const didSettings = await this.$debugDidSettings(did);
// Get merged settings
const mergedSettings = await this.$getMergedSettings(
MASTER_SETTINGS_KEY,
did,
defaultSettings || {},
);
const mergedSettings = await this.$getMergedSettings(MASTER_SETTINGS_KEY, did, defaultSettings || {});
logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
defaultSettings,
@@ -1524,10 +1456,7 @@ export const PlatformServiceMixin = {
isRegistered: mergedSettings.isRegistered,
});
} catch (error) {
logger.error(
`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`,
error,
);
logger.error(`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`, error);
}
},
},
@@ -1701,7 +1630,7 @@ declare module "@vue/runtime-core" {
// Cache management methods
$refreshSettings(): Promise<Settings>;
$refreshContacts(): Promise<Contact[]>;
// $clearAllCaches(): void;
$clearAllCaches(): void;
// High-level entity operations (eliminate verbose SQL patterns)
$mapResults<T>(

View File

@@ -45,7 +45,6 @@ export function safeStringify(obj: unknown) {
// Determine if we should suppress verbose logging (for Electron)
const isElectron = process.env.VITE_PLATFORM === "electron";
const isProduction = process.env.NODE_ENV === "production";
const isDebugEnabled = process.env.VITE_DEBUG_LOGGING === "true";
// Track initialization state to prevent circular dependencies
let isInitializing = true;
@@ -109,8 +108,8 @@ async function logToDatabase(
// Enhanced logger with self-contained database methods
export const logger = {
debug: (message: string, ...args: unknown[]) => {
// Debug logs are very verbose - only show when explicitly enabled
if (isDebugEnabled && !isProduction && !isElectron) {
// Debug logs are very verbose - only show in development mode for web
if (!isProduction && !isElectron) {
// eslint-disable-next-line no-console
console.debug(message, ...args);
}

View File

@@ -1396,6 +1396,10 @@ export default class AccountViewView extends Vue {
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
await this.$saveSettings({
profileImageUrl: "",
});
this.profileImageUrl = "";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
return;
@@ -1410,14 +1414,17 @@ export default class AccountViewView extends Vue {
if (endorserResp.status === 200) {
this.endorserLimits = endorserResp.data;
} else {
await this.$saveSettings({
profileImageUrl: "",
});
this.profileImageUrl = "";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
return;
}
} catch (error) {
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Error retrieving limits: ", error);
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
console.log("error: ", error);
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
} finally {
this.loadingLimits = false;
@@ -1476,7 +1483,7 @@ export default class AccountViewView extends Vue {
async deleteImage(): Promise<void> {
try {
// Extract the image ID from the full URL
const imageId = this.profileImageUrl?.split("/").pop();
const imageId = this.profileImageUrl?.split('/').pop();
if (!imageId) {
this.notify.error("Invalid image URL");
return;

View File

@@ -49,6 +49,7 @@ import {
VALID_DEEP_LINK_ROUTES,
deepLinkSchemas,
} from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger";
const route = useRoute();
@@ -105,8 +106,9 @@ const reportIssue = () => {
// Log the error for analytics
onMounted(() => {
logger.error(
logConsoleAndDb(
`[DeepLinkError] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`,
true,
);
});
</script>

View File

@@ -314,7 +314,6 @@ import {
} from "@/constants/notifications";
import * as Package from "../../package.json";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim {
claim?: Claim; // For nested claims in Verifiable Credentials
agent?: {

View File

@@ -310,9 +310,10 @@ export default class SearchAreaView extends Vue {
},
};
const searchBoxes = JSON.stringify([newSearchBox]);
// Store search box configuration using platform service
// searchBoxes will be automatically converted to JSON string by $updateSettings
await this.$updateSettings({ searchBoxes: [newSearchBox] });
await this.$updateSettings({ searchBoxes: searchBoxes as any });
this.searchBox = newSearchBox;
this.isChoosingSearchBox = false;
@@ -346,7 +347,7 @@ export default class SearchAreaView extends Vue {
try {
// Clear search box settings and disable nearby filtering
await this.$updateSettings({
searchBoxes: [],
searchBoxes: "[]" as any,
filterFeedByNearby: false,
});