mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
docs: cleanup
This commit is contained in:
parent
6577b82392
commit
cd5b38a6c9
11 changed files with 0 additions and 2001 deletions
|
|
@ -1,480 +0,0 @@
|
|||
# Super Productivity Plugin API Overview
|
||||
|
||||
This document provides a comprehensive overview of the Super Productivity plugin system, its architecture, and API.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Plugin Architecture](#plugin-architecture)
|
||||
2. [Plugin Types](#plugin-types)
|
||||
3. [Plugin API Reference](#plugin-api-reference)
|
||||
4. [Security Model](#security-model)
|
||||
5. [Plugin Development](#plugin-development)
|
||||
6. [Examples](#examples)
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
Super Productivity uses a sophisticated plugin system that allows extending the application's functionality through custom plugins. The architecture consists of several key components:
|
||||
|
||||
### Core Components
|
||||
|
||||
- **PluginService** (`/src/app/plugins/plugin.service.ts`): Main orchestrator for plugin lifecycle
|
||||
- **PluginBridgeService** (`/src/app/plugins/plugin-bridge.service.ts`): Communication bridge between plugins and the app
|
||||
- **PluginAPI** (`/src/app/plugins/plugin-api.ts`): API interface exposed to plugins
|
||||
- **PluginRunner** (`/src/app/plugins/plugin-runner.ts`): Executes plugin code in sandboxed environments
|
||||
|
||||
### Plugin Loading Process
|
||||
|
||||
1. **Discovery**: Built-in plugins from `/assets/` and uploaded plugins from cache
|
||||
2. **Validation**: Manifest validation and permission checking
|
||||
3. **Sandboxing**: Code execution in isolated JavaScript environments
|
||||
4. **Registration**: UI components and hooks registration
|
||||
5. **Initialization**: Plugin startup and configuration loading
|
||||
|
||||
## Plugin Types
|
||||
|
||||
### 1. JavaScript Plugins (plugin.js)
|
||||
|
||||
Traditional plugins that run JavaScript code in a sandboxed environment.
|
||||
|
||||
```javascript
|
||||
// Example plugin.js
|
||||
class MyPlugin {
|
||||
constructor() {
|
||||
this.config = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Register UI components
|
||||
PluginAPI.registerHeaderButton({
|
||||
label: 'My Plugin',
|
||||
icon: 'extension',
|
||||
onClick: () => this.showDialog(),
|
||||
});
|
||||
|
||||
// Register hooks
|
||||
PluginAPI.registerHook('taskCreated', (task) => {
|
||||
console.log('New task created:', task.title);
|
||||
});
|
||||
}
|
||||
|
||||
showDialog() {
|
||||
PluginAPI.openDialog({
|
||||
title: 'My Plugin Dialog',
|
||||
message: 'Hello from my plugin!',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize plugin
|
||||
const plugin = new MyPlugin();
|
||||
plugin.init();
|
||||
```
|
||||
|
||||
### 2. Iframe Plugins (index.html)
|
||||
|
||||
Plugins that render HTML interfaces in sandboxed iframes.
|
||||
|
||||
```html
|
||||
<!-- index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My Plugin UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My Plugin Interface</h1>
|
||||
<button onclick="createTask()">Create Task</button>
|
||||
|
||||
<script>
|
||||
async function createTask() {
|
||||
await window.PluginAPI.addTask({
|
||||
title: 'Task from plugin',
|
||||
projectId: null,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3. Hybrid Plugins
|
||||
|
||||
Plugins that combine both JavaScript execution and iframe interfaces.
|
||||
|
||||
## Plugin API Reference
|
||||
|
||||
### Core Methods
|
||||
|
||||
#### Data Operations
|
||||
|
||||
- `getTasks()` - Retrieve all tasks
|
||||
- `getTaskById(id)` - Get specific task
|
||||
- `addTask(task)` - Create new task
|
||||
- `updateTask(id, changes)` - Update existing task
|
||||
- `removeTask(id)` - Delete task
|
||||
|
||||
#### Project Operations
|
||||
|
||||
- `getProjects()` - Get all projects
|
||||
- `getActiveProject()` - Get currently active project
|
||||
- `addProject(project)` - Create new project
|
||||
|
||||
#### Data Persistence
|
||||
|
||||
- `persistDataSynced(data)` - Save plugin data with sync
|
||||
- `loadPersistedData()` - Load saved plugin data
|
||||
|
||||
#### UI Operations
|
||||
|
||||
- `showSnack(config)` - Show notification snackbar
|
||||
- `notify(config)` - Show system notification
|
||||
- `openDialog(config)` - Display modal dialog
|
||||
|
||||
#### Navigation
|
||||
|
||||
- `showIndexHtmlAsView()` - Show plugin iframe in full view
|
||||
- `showIndexHtmlInSidePanel()` - Show plugin iframe in side panel
|
||||
|
||||
### UI Registration Methods (plugin.js only)
|
||||
|
||||
These methods are restricted to main plugin code for security:
|
||||
|
||||
#### Header Buttons
|
||||
|
||||
```javascript
|
||||
PluginAPI.registerHeaderButton({
|
||||
label: 'My Button',
|
||||
icon: 'extension',
|
||||
onClick: () => {
|
||||
// Button click handler
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Menu Entries
|
||||
|
||||
```javascript
|
||||
PluginAPI.registerMenuEntry({
|
||||
label: 'My Plugin',
|
||||
icon: 'extension',
|
||||
onClick: () => {
|
||||
// Menu item click handler
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Side Panel Buttons
|
||||
|
||||
```javascript
|
||||
PluginAPI.registerSidePanelButton({
|
||||
label: 'My Panel',
|
||||
icon: 'extension',
|
||||
onClick: () => {
|
||||
PluginAPI.showIndexHtmlInSidePanel();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Keyboard Shortcuts
|
||||
|
||||
```javascript
|
||||
PluginAPI.registerShortcut({
|
||||
keys: 'ctrl+alt+m',
|
||||
description: 'My Plugin Shortcut',
|
||||
callback: () => {
|
||||
// Shortcut handler
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Hook System
|
||||
|
||||
Plugins can register hooks to respond to application events:
|
||||
|
||||
```javascript
|
||||
PluginAPI.registerHook('taskCreated', (task) => {
|
||||
// React to task creation
|
||||
});
|
||||
|
||||
PluginAPI.registerHook('taskUpdated', (task, changes) => {
|
||||
// React to task updates
|
||||
});
|
||||
|
||||
PluginAPI.registerHook('beforeTaskDelete', (taskId) => {
|
||||
// React before task deletion
|
||||
});
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
The plugin system implements a multi-layered security model:
|
||||
|
||||
### 1. Code Sandboxing
|
||||
|
||||
- JavaScript plugins run in isolated VM contexts
|
||||
- Limited access to Node.js APIs (desktop app only)
|
||||
- No direct file system access without permissions
|
||||
|
||||
### 2. Iframe Sandboxing
|
||||
|
||||
- HTML content runs in sandboxed iframes
|
||||
- Standard iframe restrictions apply
|
||||
- Communication only via postMessage
|
||||
|
||||
### 3. API Access Control
|
||||
|
||||
Certain methods are restricted based on context:
|
||||
|
||||
#### Restricted in Iframe Context
|
||||
|
||||
- `registerHeaderButton`
|
||||
- `registerMenuEntry`
|
||||
- `registerSidePanelButton`
|
||||
- `registerShortcut`
|
||||
- `registerHook`
|
||||
- `executeNodeScript`
|
||||
- `onMessage`
|
||||
|
||||
#### Available in Iframe Context
|
||||
|
||||
- All data operations (getTasks, addTask, etc.)
|
||||
- UI operations (showSnack, notify, openDialog)
|
||||
- Data persistence (persistDataSynced, loadPersistedData)
|
||||
|
||||
### 4. Permission System
|
||||
|
||||
Plugins can request specific permissions in their manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": ["nodeExecution", "fileSystem", "network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### Manifest Structure
|
||||
|
||||
Every plugin requires a `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample plugin",
|
||||
"author": "Developer Name",
|
||||
"hooks": ["taskCreated", "taskUpdated"],
|
||||
"permissions": ["nodeExecution"],
|
||||
"iFrame": true,
|
||||
"isSkipMenuEntry": false,
|
||||
"minSupVersion": "8.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── manifest.json # Plugin metadata
|
||||
├── plugin.js # Main plugin code (optional)
|
||||
├── index.html # UI interface (optional)
|
||||
├── iframe-script.js # Iframe-specific code (optional)
|
||||
└── icon.svg # Plugin icon (optional)
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Create Manifest**: Define plugin metadata and requirements
|
||||
2. **Implement Logic**: Write plugin.js for core functionality
|
||||
3. **Create UI**: Design index.html for user interface
|
||||
4. **Test Integration**: Use development tools to test plugin
|
||||
5. **Package**: Create ZIP file for distribution
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Error Handling**: Always wrap API calls in try-catch blocks
|
||||
2. **Async Operations**: Use async/await for all API calls
|
||||
3. **Resource Cleanup**: Properly clean up timers and listeners
|
||||
4. **User Feedback**: Provide clear feedback for user actions
|
||||
5. **Performance**: Minimize impact on app performance
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Task Counter Plugin
|
||||
|
||||
```javascript
|
||||
// plugin.js
|
||||
class TaskCounterPlugin {
|
||||
constructor() {
|
||||
this.taskCount = 0;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Register UI
|
||||
PluginAPI.registerHeaderButton({
|
||||
label: `Tasks: ${this.taskCount}`,
|
||||
icon: 'assignment',
|
||||
onClick: () => this.showDetails(),
|
||||
});
|
||||
|
||||
// Listen for task changes
|
||||
PluginAPI.registerHook('taskCreated', () => this.updateCount());
|
||||
PluginAPI.registerHook('taskDeleted', () => this.updateCount());
|
||||
|
||||
// Initial count
|
||||
await this.updateCount();
|
||||
}
|
||||
|
||||
async updateCount() {
|
||||
const tasks = await PluginAPI.getTasks();
|
||||
this.taskCount = tasks.length;
|
||||
// Update button label...
|
||||
}
|
||||
|
||||
showDetails() {
|
||||
PluginAPI.openDialog({
|
||||
title: 'Task Statistics',
|
||||
message: `Total tasks: ${this.taskCount}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new TaskCounterPlugin().init();
|
||||
```
|
||||
|
||||
### 2. Quick Note Plugin (Iframe)
|
||||
|
||||
```html
|
||||
<!-- index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Quick Notes</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Quick Notes</h2>
|
||||
<textarea
|
||||
id="noteText"
|
||||
placeholder="Enter your note..."
|
||||
></textarea>
|
||||
<br />
|
||||
<button onclick="saveNote()">Save as Task</button>
|
||||
<button onclick="clearNote()">Clear</button>
|
||||
|
||||
<script>
|
||||
async function saveNote() {
|
||||
const text = document.getElementById('noteText').value;
|
||||
if (!text.trim()) return;
|
||||
|
||||
try {
|
||||
await window.PluginAPI.addTask({
|
||||
title: text,
|
||||
projectId: null,
|
||||
});
|
||||
|
||||
window.PluginAPI.showSnack({
|
||||
message: 'Note saved as task!',
|
||||
type: 'SUCCESS',
|
||||
});
|
||||
|
||||
clearNote();
|
||||
} catch (error) {
|
||||
window.PluginAPI.showSnack({
|
||||
message: 'Failed to save note',
|
||||
type: 'ERROR',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearNote() {
|
||||
document.getElementById('noteText').value = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3. Sync Plugin (Hybrid)
|
||||
|
||||
Combines both plugin.js for logic and index.html for configuration:
|
||||
|
||||
```javascript
|
||||
// plugin.js
|
||||
class SyncPlugin {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.config = await this.loadConfig();
|
||||
|
||||
PluginAPI.registerSidePanelButton({
|
||||
label: 'Sync Settings',
|
||||
icon: 'sync',
|
||||
onClick: () => PluginAPI.showIndexHtmlInSidePanel(),
|
||||
});
|
||||
|
||||
// Start sync if configured
|
||||
if (this.config && this.config.enabled) {
|
||||
this.startSync();
|
||||
}
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
const data = await PluginAPI.loadPersistedData();
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async saveConfig(config) {
|
||||
await PluginAPI.persistDataSynced(JSON.stringify(config));
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
startSync() {
|
||||
// Implement sync logic
|
||||
}
|
||||
}
|
||||
|
||||
new SyncPlugin().init();
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Plugin Not Loading**: Check manifest.json syntax and required fields
|
||||
2. **API Calls Failing**: Ensure proper async/await usage and error handling
|
||||
3. **UI Not Registering**: Verify registerUi methods are called from plugin.js, not iframe
|
||||
4. **Permission Errors**: Check if plugin has required permissions in manifest
|
||||
|
||||
### Development Tools
|
||||
|
||||
- Use browser DevTools for iframe debugging
|
||||
- Check plugin service logs for loading issues
|
||||
- Use the plugin management UI to reload plugins during development
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check existing plugin examples in `/assets/` directory
|
||||
- Review plugin service source code for API implementation details
|
||||
- Test with minimal plugin examples before adding complexity
|
||||
|
||||
---
|
||||
|
||||
This overview provides the foundation for understanding and developing plugins for Super Productivity. For the most up-to-date API reference, always refer to the source code and existing plugin examples.
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# CalDAV Features and Roadmap
|
||||
|
||||
## Overview
|
||||
|
||||
Super Productivity offers basic CalDAV integration for importing and syncing tasks. This document outlines the current capabilities and limitations of the integration. If you're a developer and would like to improve the integration help would be very welcome.
|
||||
|
||||
## Feature Support Matrix
|
||||
|
||||
| Feature | Import | Sync SP → CalDAV | Sync CalDAV → SP |
|
||||
| --------------------------------------------- | ---------- | ---------------- | ---------------- |
|
||||
| Title | ✅ | ✅ | ❌ |
|
||||
| Description | ✅ | ❌ | ❌ |
|
||||
| Tags | ✅ | ❌ | ❌ |
|
||||
| Complete/Incomplete Status | ✅ | ✅\* | ❌ |
|
||||
| New Tasks | ✅ | ❌ [^3017] | ✅\* |
|
||||
| Subtasks | ✅ [^2876] | ❌ | ❌ |
|
||||
| Deleted Tasks | ❌ | ❌ | ❌ [^2915] |
|
||||
| Start Date | ❌ | ❌ | ❌ |
|
||||
| Due Date | ❌ | ❌ | ❌ |
|
||||
| Attachments | ❌ | ❌ | ❌ |
|
||||
| Recurrence Rules | ❌ | ❌ | ❌ |
|
||||
| Created Time | ❌ | ❌ | ❌ |
|
||||
| Last Modified Time | ❌ | ❌ | ❌ |
|
||||
| Status (Needs Action, In Progress, Cancelled) | ❌ | ❌ | ❌ |
|
||||
| Priority | ❌ | ❌ | ❌ |
|
||||
|
||||
\*Requires enabling in advanced configuration
|
||||
|
||||
## Current Features
|
||||
|
||||
### Advanced Config Options
|
||||
|
||||
- Auto import to Default Project (configurable per calendar)
|
||||
- Poll imported for changes and notify (only supports new tasks, see above table)
|
||||
- Automatically complete CalDAV todos on task completion
|
||||
|
||||
## Limitations
|
||||
|
||||
### Configuration Restrictions
|
||||
|
||||
- Each CalDAV calendar/list requires a separate configuration
|
||||
- Cannot import from multiple calendars with a single configuration
|
||||
|
||||
## Future Development
|
||||
|
||||
### Note on Integration Roadmap
|
||||
|
||||
As [stated](https://github.com/johannesjo/super-productivity/issues/3017#issuecomment-2577469193) by the repository maintainer in Jan 2025:
|
||||
|
||||
> "It's important to understand that this will always be limited since CalDAV is likely not completely mappable to the model of Super Productivity. First step for this would be a feasibility analysis to check what parts of the model can be mapped and which not. Next step would be making a concept how this all could work."
|
||||
|
||||
### Developer Contributions Welcome
|
||||
|
||||
If you have experience with CalDAV development and are interested in improving this integration, your help would be greatly appreciated. Key areas that need attention include:
|
||||
|
||||
- Feasibility analysis of CalDAV-to-SP model mapping
|
||||
- Integration architecture design
|
||||
- Implementation of additional sync capabilities
|
||||
|
||||
Feel free to reach out if you'd like to contribute.
|
||||
|
||||
## Related Issues
|
||||
|
||||
[^2876]: [Import Multilevel Subtasks from CalDAV](https://github.com/johannesjo/super-productivity/issues/2876)
|
||||
|
||||
[^2915]: [CalDav task status and deleted tasks are not updated](https://github.com/johannesjo/super-productivity/issues/2915)
|
||||
|
||||
[^3017]: [Unable to upload new local tasks to CalDav](https://github.com/johannesjo/super-productivity/issues/3017)
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
# Mobile Logging Strategy for Super Productivity
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Logging Infrastructure
|
||||
|
||||
- **Console Logging**: Uses `pfLog` system with configurable levels (level 2 in production)
|
||||
- **Native Bridge Logging**: Capacitor enables Android Log class usage for native logging
|
||||
- **Global Error Handler**: Captures unhandled errors and can generate GitHub issues
|
||||
- **Development Logging**: Console logs visible via `adb logcat` (Android) and Xcode console (iOS)
|
||||
|
||||
### Current Limitations
|
||||
|
||||
- **No log persistence** on mobile devices for production releases
|
||||
- **No log export mechanism** for end users or support
|
||||
- **No crash reporting service** for remote log collection
|
||||
- **Limited debugging capabilities** for production mobile releases
|
||||
- **No structured logging** with device metadata
|
||||
|
||||
## Problem Statement
|
||||
|
||||
When users experience issues with mobile releases, there's currently no way to:
|
||||
|
||||
1. Access application logs from production builds
|
||||
2. Export logs for support or debugging
|
||||
3. Collect crash information automatically
|
||||
4. Debug issues that only occur on specific devices/OS versions
|
||||
|
||||
## Recommended Solutions
|
||||
|
||||
### Phase 1: Local Log Collection & Export (High Priority)
|
||||
|
||||
#### 1.1 Mobile Log Collection Service
|
||||
|
||||
```typescript
|
||||
// New service: MobileLogService
|
||||
- Intercept and store console.log, console.error, console.warn
|
||||
- Add device metadata (OS version, app version, device model)
|
||||
- Implement circular buffer with size limits (e.g., 1000 entries, 5MB max)
|
||||
- Include timestamps and log levels
|
||||
- Store in device's persistent storage (Capacitor Preferences or Filesystem)
|
||||
```
|
||||
|
||||
#### 1.2 Log Export Feature
|
||||
|
||||
```typescript
|
||||
// Settings menu addition
|
||||
- "Export Logs" button in Debug/Support section
|
||||
- Generate ZIP file with:
|
||||
- Filtered logs (remove sensitive data)
|
||||
- Device info summary
|
||||
- App configuration (non-sensitive)
|
||||
- Use Capacitor Share plugin for native sharing
|
||||
- Allow email export or cloud storage upload
|
||||
```
|
||||
|
||||
#### 1.3 Privacy-Aware Filtering
|
||||
|
||||
```typescript
|
||||
// Log sanitization
|
||||
- Remove or hash potential PII (task names, project names)
|
||||
- Filter out authentication tokens
|
||||
- Exclude sync data content
|
||||
- Maintain error stack traces and system info
|
||||
```
|
||||
|
||||
### Phase 2: Enhanced Error Reporting (Medium Priority)
|
||||
|
||||
#### 2.1 Mobile-Specific Error Handler
|
||||
|
||||
```typescript
|
||||
// Extend existing GlobalErrorHandler
|
||||
- Detect mobile-specific errors (Capacitor plugin failures)
|
||||
- Include device capabilities and plugin versions
|
||||
- Store critical errors separately for priority export
|
||||
- Add network connectivity status to error context
|
||||
```
|
||||
|
||||
#### 2.2 Crash Recovery Information
|
||||
|
||||
```typescript
|
||||
// On app restart after crash
|
||||
- Detect unclean shutdown
|
||||
- Prompt user to export crash logs
|
||||
- Include memory usage and app state before crash
|
||||
```
|
||||
|
||||
### Phase 3: Development & Debug Improvements (Medium Priority)
|
||||
|
||||
#### 3.1 In-App Log Viewer
|
||||
|
||||
```typescript
|
||||
// Debug-only component
|
||||
- Scrollable log view with filtering
|
||||
- Real-time log streaming
|
||||
- Export selected logs
|
||||
- Available only in development/debug builds
|
||||
```
|
||||
|
||||
#### 3.2 Remote Development Logging
|
||||
|
||||
```typescript
|
||||
// Development environment only
|
||||
- WebSocket log streaming to development server
|
||||
- Real-time debugging on actual devices
|
||||
- Network request/response logging for mobile-specific issues
|
||||
```
|
||||
|
||||
### Phase 4: Optional Remote Logging (Low Priority)
|
||||
|
||||
#### 4.1 Crash Reporting Service Integration
|
||||
|
||||
```typescript
|
||||
// Optional third-party integration
|
||||
- Sentry, Bugsnag, or similar service
|
||||
- User consent required (privacy-first approach)
|
||||
- Configurable in settings (off by default)
|
||||
- Minimal data collection (errors only, no analytics)
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Technical Approach
|
||||
|
||||
#### Log Storage Architecture
|
||||
|
||||
```
|
||||
Mobile Device Storage:
|
||||
├── logs/
|
||||
│ ├── app-logs.json # Application logs
|
||||
│ ├── error-logs.json # Critical errors
|
||||
│ └── device-info.json # Static device metadata
|
||||
```
|
||||
|
||||
#### Log Entry Structure
|
||||
|
||||
```typescript
|
||||
interface LogEntry {
|
||||
timestamp: string;
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
message: string;
|
||||
source?: string; // Component/service name
|
||||
metadata?: {
|
||||
deviceInfo?: DeviceInfo;
|
||||
appVersion: string;
|
||||
buildType: 'dev' | 'prod';
|
||||
stackTrace?: string; // For errors
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Dependencies Required
|
||||
|
||||
- `@capacitor/share` - For log export sharing
|
||||
- `@capacitor/filesystem` - For log file management
|
||||
- `@capacitor/device` - For device information collection
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
- **Local-first approach**: Logs stored locally, exported manually
|
||||
- **User consent**: Clear disclosure of what data is collected
|
||||
- **Data minimization**: Only collect essential debugging information
|
||||
- **Sanitization**: Remove or hash potentially sensitive content
|
||||
- **User control**: Easy log deletion and export management
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Log collection service functionality
|
||||
- Privacy filtering effectiveness
|
||||
- Log rotation and size management
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Mobile platform log export flow
|
||||
- Error capture and storage
|
||||
- Share functionality across different apps
|
||||
|
||||
### Manual Testing Scenarios
|
||||
|
||||
- Log export on various devices/OS versions
|
||||
- Large log file handling
|
||||
- App crash recovery and log preservation
|
||||
- Share dialog integration with email/cloud apps
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
### Phase 1 (Immediate)
|
||||
|
||||
1. Implement MobileLogService
|
||||
2. Add basic log export in settings
|
||||
3. Test on development builds
|
||||
|
||||
### Phase 2 (Next Release)
|
||||
|
||||
1. Enhanced error reporting
|
||||
2. Privacy filtering refinements
|
||||
3. User documentation
|
||||
|
||||
### Phase 3 (Future)
|
||||
|
||||
1. In-app log viewer for debug builds
|
||||
2. Remote development logging
|
||||
3. Optional crash reporting integration
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- **User Support**: Reduced time to diagnose mobile-specific issues
|
||||
- **Bug Resolution**: Faster identification of device-specific problems
|
||||
- **Privacy Compliance**: Zero privacy violations related to logging
|
||||
- **Performance**: No significant impact on app performance or storage
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
### Remote-First Logging
|
||||
|
||||
**Rejected**: Conflicts with privacy-first philosophy and requires server infrastructure
|
||||
|
||||
### Always-On Crash Reporting
|
||||
|
||||
**Rejected**: Privacy concerns and user consent complexity
|
||||
|
||||
### File-Based Log Export
|
||||
|
||||
**Considered**: Less user-friendly than native share integration
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- Integration with existing GitHub issue creation workflow
|
||||
- Automated log collection triggers (e.g., repeated crashes)
|
||||
- Integration with upcoming sync diagnostics
|
||||
- Cross-platform log format standardization
|
||||
|
||||
---
|
||||
|
||||
**Document Created**: 2025-07-07
|
||||
**Author**: Claude (AI Assistant)
|
||||
**Status**: Planning Phase
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
# Plugin API deleteTask Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Added the `deleteTask` method implementation to the PluginAPI, allowing plugins to delete tasks programmatically.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. PluginAPI Class (`/src/app/plugins/plugin-api.ts`)
|
||||
|
||||
Added the `deleteTask` method that delegates to the PluginBridgeService:
|
||||
|
||||
```typescript
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
console.log(`Plugin ${this._pluginId} requested to delete task ${taskId}`);
|
||||
return this._pluginBridge.deleteTask(taskId);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. PluginBridgeService (`/src/app/plugins/plugin-bridge.service.ts`)
|
||||
|
||||
Implemented the actual deletion logic:
|
||||
|
||||
```typescript
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
typia.assert<string>(taskId);
|
||||
|
||||
try {
|
||||
// Get the task with its subtasks
|
||||
const taskWithSubTasks = await this._store
|
||||
.select(selectTaskByIdWithSubTaskData, { id: taskId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (!taskWithSubTasks) {
|
||||
throw new Error(
|
||||
this._translateService.instant(T.PLUGINS.TASK_NOT_FOUND, { taskId }),
|
||||
);
|
||||
}
|
||||
|
||||
// Use the TaskService remove method which handles deletion properly
|
||||
this._taskService.remove(taskWithSubTasks);
|
||||
|
||||
console.log('PluginBridge: Task deleted successfully', {
|
||||
taskId,
|
||||
hadSubTasks: taskWithSubTasks.subTasks.length > 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('PluginBridge: Failed to delete task:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Task Validation**: Checks if the task exists before attempting deletion
|
||||
2. **Subtask Handling**: Automatically handles deletion of subtasks when a parent task is deleted
|
||||
3. **Error Handling**: Provides clear error messages when task is not found
|
||||
4. **Logging**: Logs successful deletions with information about subtasks
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's how a plugin would use the deleteTask method:
|
||||
|
||||
```typescript
|
||||
// In a plugin's code
|
||||
async function cleanupOldTasks(api: PluginAPI) {
|
||||
try {
|
||||
// Get all tasks
|
||||
const tasks = await api.getTasks();
|
||||
|
||||
// Find tasks older than 30 days that are marked as done
|
||||
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
const oldDoneTasks = tasks.filter(
|
||||
(task) => task.isDone && task.doneOn && task.doneOn < thirtyDaysAgo,
|
||||
);
|
||||
|
||||
// Delete each old task
|
||||
for (const task of oldDoneTasks) {
|
||||
await api.deleteTask(task.id);
|
||||
console.log(`Deleted old task: ${task.title}`);
|
||||
}
|
||||
|
||||
api.showSnack({
|
||||
msg: `Cleaned up ${oldDoneTasks.length} old tasks`,
|
||||
type: 'SUCCESS',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup tasks:', error);
|
||||
api.showSnack({
|
||||
msg: 'Failed to cleanup old tasks',
|
||||
type: 'ERROR',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- The method uses the existing `TaskService.remove()` method which properly handles all the NgRx state updates
|
||||
- The `selectTaskByIdWithSubTaskData` selector is used to get the task with all its subtasks
|
||||
- The method is async to handle the observable conversion properly
|
||||
- Type validation is performed using Typia to ensure the taskId is a string
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
# Super Productivity Theme Colors Overview
|
||||
|
||||
This document provides a comprehensive overview of all background colors and colors used in Super Productivity's theming system.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Theme Architecture](#theme-architecture)
|
||||
- [Color Categories](#color-categories)
|
||||
- [Light Theme Colors](#light-theme-colors)
|
||||
- [Dark Theme Colors](#dark-theme-colors)
|
||||
- [Component-Specific Colors](#component-specific-colors)
|
||||
- [File References](#file-references)
|
||||
|
||||
## Theme Architecture
|
||||
|
||||
The theming system is built on CSS custom properties (CSS variables) that are defined in:
|
||||
|
||||
- `/src/styles/_css-variables.scss` - Main variable definitions
|
||||
- `/src/styles/themes.scss` - Theme classes and utilities
|
||||
|
||||
Themes are applied via body classes:
|
||||
|
||||
- Default (Light theme): No class needed
|
||||
- Dark theme: `body.isDarkTheme`
|
||||
|
||||
## Color Categories
|
||||
|
||||
### 1. Theme-Independent Colors (Always the Same)
|
||||
|
||||
#### Semantic Colors
|
||||
|
||||
- `--color-success: #4caf50` - Success/positive actions
|
||||
- `--color-warning: #ff9800` - Warning states
|
||||
- `--color-danger: #f44336` - Error/danger states
|
||||
- `--success-green: #4fa758` - Alternative success color
|
||||
- `--yellow: #fff400` (overridden to `#ffc107` in light theme)
|
||||
|
||||
#### Dark Elevation Colors
|
||||
|
||||
Used for elevation in dark theme surfaces:
|
||||
|
||||
- `--dark0: rgb(0, 0, 0)` - Pure black
|
||||
- `--dark1: rgb(30, 30, 30)`
|
||||
- `--dark2: rgb(34, 34, 34)`
|
||||
- `--dark3: rgb(36, 36, 36)`
|
||||
- `--dark4: rgb(39, 39, 39)`
|
||||
- `--dark4-5: rgb(40, 40, 40)`
|
||||
- `--dark5: rgb(42, 42, 42)`
|
||||
- `--dark6: rgb(44, 44, 44)`
|
||||
- `--dark8: rgb(46, 46, 46)`
|
||||
- `--dark10: rgb(48, 48, 48)`
|
||||
- `--dark12: rgb(51, 51, 51)`
|
||||
- `--dark16: rgb(53, 53, 53)`
|
||||
- `--dark24: rgb(56, 56, 56)`
|
||||
|
||||
#### Overlay Colors
|
||||
|
||||
- Dark overlays: `--color-overlay-dark-10` through `--color-overlay-dark-90`
|
||||
- Light overlays: `--color-overlay-light-05` through `--color-overlay-light-90`
|
||||
|
||||
#### Material Design Integration
|
||||
|
||||
- `--c-primary: var(--palette-primary-500)` - Primary theme color
|
||||
- `--c-accent: var(--palette-accent-500)` - Accent color
|
||||
- `--c-warn: var(--palette-warn-500)` - Warning color
|
||||
|
||||
## Light Theme Colors
|
||||
|
||||
### Background Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| -------------------- | -------------------- | ------------------------- |
|
||||
| `--theme-bg` | `#f8f8f7` | Main background |
|
||||
| `--theme-bg-darker` | `rgb(235, 235, 235)` | Darker background variant |
|
||||
| `--theme-card-bg` | `#ffffff` | Card/surface background |
|
||||
| `--theme-sidebar-bg` | `var(--theme-bg)` | Sidebar background |
|
||||
| `--task-c-bg` | `#fff` | Task background |
|
||||
| `--standard-note-bg` | `#ffffff` | Note background |
|
||||
|
||||
### Text Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| --------------------------------- | ----------------------- | -------------------- |
|
||||
| `--theme-text-color` | `rgb(44, 44, 44)` | Primary text |
|
||||
| `--theme-text-color-less-intense` | `rgba(44, 44, 44, 0.9)` | Slightly dimmed text |
|
||||
| `--theme-text-color-muted` | `rgba(44, 44, 44, 0.6)` | Muted/secondary text |
|
||||
| `--theme-text-color-most-intense` | `rgb(0, 0, 0)` | High contrast text |
|
||||
|
||||
### Border & Separator Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| ---------------------------- | --------------------- | ------------- |
|
||||
| `--theme-extra-border-color` | `#dddddd` | Extra borders |
|
||||
| `--theme-separator-color` | `#d0d0d0` | Separators |
|
||||
| `--theme-divider-color` | `rgba(0, 0, 0, 0.12)` | Dividers |
|
||||
| `--theme-grid-color` | `#dadce0` | Grid lines |
|
||||
|
||||
### UI Element Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| ------------------------------- | ------------------------------ | ----------------------- |
|
||||
| `--theme-scrollbar-thumb` | `#888` | Scrollbar thumb |
|
||||
| `--theme-scrollbar-thumb-hover` | `#555` | Scrollbar thumb hover |
|
||||
| `--theme-scrollbar-track` | `#f1f1f1` | Scrollbar track |
|
||||
| `--theme-chip-outline-color` | `rgba(125, 125, 125, 0.4)` | Chip outlines |
|
||||
| `--theme-progress-bg` | `rgba(127, 127, 127, 0.2)` | Progress bar background |
|
||||
| `--theme-select-hover-bg` | `var(--color-overlay-dark-10)` | Select hover background |
|
||||
|
||||
## Dark Theme Colors
|
||||
|
||||
### Background Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| -------------------- | --------------- | ---------------------------- |
|
||||
| `--theme-bg` | `var(--dark0)` | Main background (pure black) |
|
||||
| `--theme-bg-darker` | `var(--dark0)` | Darker variant |
|
||||
| `--theme-card-bg` | `var(--dark2)` | Card/surface background |
|
||||
| `--theme-sidebar-bg` | `var(--dark8)` | Sidebar background |
|
||||
| `--task-c-bg` | `var(--dark3)` | Task background |
|
||||
| `--task-c-bg-done` | `var(--dark1)` | Completed task background |
|
||||
| `--standard-note-bg` | `var(--dark16)` | Note background |
|
||||
|
||||
### Text Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| --------------------------------- | -------------------------- | -------------------- |
|
||||
| `--theme-text-color` | `rgb(235, 235, 235)` | Primary text |
|
||||
| `--theme-text-color-less-intense` | `rgba(235, 235, 235, 0.9)` | Slightly dimmed text |
|
||||
| `--theme-text-color-muted` | `rgba(235, 235, 235, 0.6)` | Muted/secondary text |
|
||||
| `--theme-text-color-most-intense` | `rgb(255, 255, 255)` | High contrast text |
|
||||
| `--standard-note-fg` | `#eeeeee` | Note text color |
|
||||
|
||||
### Border & Separator Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| ---------------------------- | ------------------------------- | ------------- |
|
||||
| `--theme-extra-border-color` | `rgba(255, 255, 255, 0.12)` | Extra borders |
|
||||
| `--theme-separator-color` | `rgba(255, 255, 255, 0.1)` | Separators |
|
||||
| `--theme-divider-color` | `rgba(255, 255, 255, 0.12)` | Dividers |
|
||||
| `--theme-grid-color` | `var(--color-overlay-light-10)` | Grid lines |
|
||||
|
||||
### UI Element Colors
|
||||
|
||||
| Variable | Value | Usage |
|
||||
| ------------------------------- | ------------------------------- | ----------------------- |
|
||||
| `--theme-scrollbar-thumb` | `#333` | Scrollbar thumb |
|
||||
| `--theme-scrollbar-thumb-hover` | `#444` | Scrollbar thumb hover |
|
||||
| `--theme-scrollbar-track` | `#222` | Scrollbar track |
|
||||
| `--theme-chip-outline-color` | `rgba(125, 125, 125, 0.4)` | Chip outlines |
|
||||
| `--theme-select-hover-bg` | `var(--color-overlay-light-10)` | Select hover background |
|
||||
|
||||
## Component-Specific Colors
|
||||
|
||||
### Global Error Alert (`/src/styles/components/global-error-alert.scss`)
|
||||
|
||||
- Red border: `border: 8px solid red`
|
||||
- Blue spinner: `border-top: 4px solid #3498db`
|
||||
- Black border: `border: 1px solid black`
|
||||
|
||||
### CDK Drag & Drop
|
||||
|
||||
- Drag outline: `outline: 1px dashed var(--c-primary)`
|
||||
- Drag preview background: `background: var(--theme-bg-lightest)`
|
||||
|
||||
### Links
|
||||
|
||||
- Link color: `color: var(--c-accent)`
|
||||
|
||||
### Blockquotes
|
||||
|
||||
- Left border: `border-left: 4px solid rgba(var(--c-accent), 1)`
|
||||
|
||||
### Task States
|
||||
|
||||
- Current task shadow: `var(--whiteframe-shadow-3dp)` (light) / `var(--whiteframe-shadow-8dp)` (dark)
|
||||
- Selected task shadow: `var(--whiteframe-shadow-3dp)` (light) / `var(--whiteframe-shadow-4dp)` (dark)
|
||||
|
||||
### Date/Time Picker Colors
|
||||
|
||||
Light theme:
|
||||
|
||||
- `--owl-text-color-strong: var(--color-overlay-dark-90)`
|
||||
- `--owl-text-color: rgba(0, 0, 0, 0.75)`
|
||||
- `--owl-light-selected-bg: rgb(238, 238, 238)`
|
||||
|
||||
Dark theme:
|
||||
|
||||
- `--owl-text-color-strong: var(--color-overlay-light-90)`
|
||||
- `--owl-text-color: rgba(255, 255, 255, 0.75)`
|
||||
- `--owl-light-selected-bg: rgba(49, 49, 49, 1)`
|
||||
|
||||
## File References
|
||||
|
||||
### Core Theme Files
|
||||
|
||||
- `/src/styles/_css-variables.scss` - All CSS custom properties definitions
|
||||
- `/src/styles/themes.scss` - Theme utility classes and material integration
|
||||
|
||||
### Component Examples Using Theme Colors
|
||||
|
||||
- `/src/app/app.component.scss` - Main app container styling
|
||||
- `/src/app/core-ui/side-nav/side-nav.component.scss` - Navigation styling
|
||||
- `/src/app/pages/*/` - Various page components
|
||||
- `/src/app/features/tasks/` - Task-related components
|
||||
- `/src/app/ui/` - UI components
|
||||
|
||||
### Material Design Integration
|
||||
|
||||
The app uses Angular Material CSS variables through the `angular-material-css-vars` library, which provides automatic theme generation based on the primary, accent, and warn colors.
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
1. **Always use CSS variables** for colors to ensure theme consistency
|
||||
2. **Never hardcode colors** except for special cases (like error states)
|
||||
3. **Test both themes** when adding new components
|
||||
4. **Use semantic color names** (e.g., `--theme-text-color` instead of specific color values)
|
||||
5. **Leverage existing overlay colors** for hover states and overlays
|
||||
|
||||
## Adding New Colors
|
||||
|
||||
When adding new colors:
|
||||
|
||||
1. Define them in `/src/styles/_css-variables.scss`
|
||||
2. Provide both light and dark theme values
|
||||
3. Use semantic naming that describes the purpose, not the color
|
||||
4. Document the usage in this file
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
# Timezone Testing Best Practices for Super Productivity
|
||||
|
||||
## Current State
|
||||
|
||||
Super Productivity currently uses a simple but effective approach:
|
||||
|
||||
- All tests run with `TZ='Europe/Berlin'` environment variable
|
||||
- This is set in both `package.json` test scripts and `karma.conf.js`
|
||||
- Ensures consistent test results across different developer machines and CI environments
|
||||
|
||||
## Identified Issues
|
||||
|
||||
1. **Limited timezone coverage**: Only testing in one timezone (Europe/Berlin) may miss bugs that occur in:
|
||||
|
||||
- Negative UTC offset timezones (e.g., America/Los_Angeles)
|
||||
- Timezones near the International Date Line
|
||||
- During DST transitions
|
||||
|
||||
2. **The specific bug from issue #4653**: Day of week mismatch in negative UTC offset timezones was not caught by tests
|
||||
|
||||
## Recommended Testing Strategy
|
||||
|
||||
### 1. **Multi-Timezone Test Suite**
|
||||
|
||||
Create a dedicated test suite that runs critical date-related tests in multiple timezones:
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test:tz:la": "cross-env TZ='America/Los_Angeles' ng test --include='**/*.tz.spec.ts'",
|
||||
"test:tz:tokyo": "cross-env TZ='Asia/Tokyo' ng test --include='**/*.tz.spec.ts'",
|
||||
"test:tz:sydney": "cross-env TZ='Australia/Sydney' ng test --include='**/*.tz.spec.ts'",
|
||||
"test:tz:all": "npm run test:tz:la && npm run test:tz:tokyo && npm run test:tz:sydney"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Critical Functions to Test Across Timezones**
|
||||
|
||||
Based on the codebase analysis, these functions should have timezone-specific tests:
|
||||
|
||||
1. `dateStrToUtcDate()` - Core utility for parsing date strings
|
||||
2. `formatDayStr()` - Formats day names (where the bug was found)
|
||||
3. `formatDayMonthStr()` - Formats day and month strings
|
||||
4. `getWorklogStr()` - Generates worklog date strings
|
||||
5. Date range utilities (day/week/month calculations)
|
||||
|
||||
### 3. **Example Timezone Test Pattern**
|
||||
|
||||
```typescript
|
||||
// format-day-str.tz.spec.ts
|
||||
describe('formatDayStr timezone tests', () => {
|
||||
const testCases = [
|
||||
{ dateStr: '2024-01-15', expectedDayBerlin: 'Mon', expectedDayLA: 'Mon' },
|
||||
{ dateStr: '2024-12-31', expectedDayBerlin: 'Tue', expectedDayLA: 'Tue' },
|
||||
// Edge cases near midnight
|
||||
{ dateStr: '2024-01-01', expectedDayBerlin: 'Mon', expectedDayLA: 'Mon' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ dateStr, expectedDayBerlin, expectedDayLA }) => {
|
||||
it(`should format ${dateStr} correctly in current timezone`, () => {
|
||||
const result = formatDayStr(dateStr, 'en-US');
|
||||
const expectedDay =
|
||||
process.env.TZ === 'America/Los_Angeles' ? expectedDayLA : expectedDayBerlin;
|
||||
expect(result).toBe(expectedDay);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **DST Transition Testing**
|
||||
|
||||
Test dates around Daylight Saving Time transitions:
|
||||
|
||||
```typescript
|
||||
describe('DST transition tests', () => {
|
||||
const dstTransitionDates = [
|
||||
'2024-03-10', // Spring forward in US
|
||||
'2024-11-03', // Fall back in US
|
||||
'2024-03-31', // Spring forward in EU
|
||||
'2024-10-27', // Fall back in EU
|
||||
];
|
||||
|
||||
dstTransitionDates.forEach((date) => {
|
||||
it(`should handle DST transition on ${date}`, () => {
|
||||
// Test date parsing and formatting around DST transitions
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 5. **Mock Timezone Testing (Alternative Approach)**
|
||||
|
||||
For more dynamic timezone testing without changing environment variables:
|
||||
|
||||
```bash
|
||||
npm install --save-dev timezone-mock
|
||||
```
|
||||
|
||||
```typescript
|
||||
import * as timezoneMock from 'timezone-mock';
|
||||
|
||||
describe('Timezone mock tests', () => {
|
||||
afterEach(() => {
|
||||
timezoneMock.unregister();
|
||||
});
|
||||
|
||||
it('should work in Los Angeles timezone', () => {
|
||||
timezoneMock.register('US/Pacific');
|
||||
const result = formatDayStr('2024-01-15', 'en-US');
|
||||
expect(result).toBe('Mon');
|
||||
});
|
||||
|
||||
it('should work in Tokyo timezone', () => {
|
||||
timezoneMock.register('Asia/Tokyo');
|
||||
const result = formatDayStr('2024-01-15', 'en-US');
|
||||
expect(result).toBe('Mon');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6. **CI/CD Integration**
|
||||
|
||||
Add timezone testing to the CI pipeline:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
test-timezones:
|
||||
strategy:
|
||||
matrix:
|
||||
timezone: ['America/Los_Angeles', 'Europe/Berlin', 'Asia/Tokyo']
|
||||
steps:
|
||||
- name: Run tests in ${{ matrix.timezone }}
|
||||
env:
|
||||
TZ: ${{ matrix.timezone }}
|
||||
run: npm test
|
||||
```
|
||||
|
||||
### 7. **Key Test Scenarios**
|
||||
|
||||
1. **Date string parsing**: Test `YYYY-MM-DD` format parsing in different timezones
|
||||
2. **Day boundaries**: Test dates at 23:59 and 00:01 in different timezones
|
||||
3. **Week boundaries**: Test Sunday/Monday transitions
|
||||
4. **Month boundaries**: Test last/first day of month
|
||||
5. **Year boundaries**: Test Dec 31/Jan 1
|
||||
6. **DST transitions**: Test dates during spring forward/fall back
|
||||
7. **Leap years**: Test Feb 28/29 in leap years
|
||||
|
||||
### 8. **Debugging Timezone Issues**
|
||||
|
||||
When debugging timezone-related test failures:
|
||||
|
||||
```typescript
|
||||
console.log({
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
offset: new Date().getTimezoneOffset(),
|
||||
envTZ: process.env.TZ,
|
||||
date: new Date('2024-01-15').toString(),
|
||||
utcDate: new Date('2024-01-15').toUTCString(),
|
||||
});
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority**: Add timezone tests for the fixed bug functions (`formatDayStr`, `formatDayMonthStr`)
|
||||
2. **Medium Priority**: Create a timezone test suite for core date utilities
|
||||
3. **Low Priority**: Consider timezone-mock library for more comprehensive testing
|
||||
|
||||
## Conclusion
|
||||
|
||||
The current approach of fixing timezone to 'Europe/Berlin' is good for consistency, but adding targeted timezone tests for critical date functions would catch timezone-specific bugs like issue #4653 before they reach production.
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
# getWorklogStr() Occurrences with Parameters
|
||||
|
||||
This file lists all occurrences of `getWorklogStr()` called with parameters, which might have timezone issues.
|
||||
|
||||
## List of Occurrences
|
||||
|
||||
### Completed
|
||||
|
||||
1. **task-repeat-cfg.service.ts** ✓ (No fix needed - tested and works correctly)
|
||||
|
||||
- `src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts`: `dueDay: getWorklogStr(targetCreated),`
|
||||
|
||||
2. **short-syntax.effects.ts** ✓ (No fix needed - tested and works correctly)
|
||||
|
||||
- `src/app/features/tasks/store/short-syntax.effects.ts`: `const plannedDayInIsoFormat = getWorklogStr(plannedDay);`
|
||||
|
||||
3. **dialog-schedule-task.component.ts** ✓ (No fix needed - already uses dateStrToUtcDate)
|
||||
|
||||
- `src/app/features/planner/dialog-schedule-task/dialog-schedule-task.component.ts`: `const newDay = getWorklogStr(newDayDate);`
|
||||
|
||||
4. **work-context.service.ts** ✓ (No fix needed - works correctly)
|
||||
|
||||
- `src/app/features/work-context/work-context.service.ts`: `(t) => !!t && !t.parentId && t.doneOn && getWorklogStr(t.doneOn) === day,`
|
||||
|
||||
5. **gitlab-common-interfaces.service.ts** ✓ (Fixed - use date string directly)
|
||||
|
||||
- `src/app/features/issue/providers/gitlab/gitlab-common-interfaces.service.ts`: `dueDay: issue.due_date ? getWorklogStr(issue.due_date) : undefined,`
|
||||
|
||||
6. **open-project-common-interfaces.service.ts** ✓ (Fixed - use date string directly)
|
||||
|
||||
- `src/app/features/issue/providers/open-project/open-project-common-interfaces.service.ts`: `dueDay: issue.startDate ? getWorklogStr(issue.startDate) : undefined,`
|
||||
|
||||
7. **task-view-customizer.service.ts** ✓ (Already fixed in previous session)
|
||||
|
||||
- `src/app/features/task-view-customizer/task-view-customizer.service.ts`: Multiple occurrences for date calculations
|
||||
|
||||
8. **add-tasks-for-tomorrow.service.ts** ✓ (Already fixed in previous session)
|
||||
|
||||
- `src/app/features/add-tasks-for-tomorrow/add-tasks-for-tomorrow.service.ts`: Multiple occurrences
|
||||
|
||||
9. **dialog-edit-task-repeat-cfg.component.ts** ✓ (No fix needed - converts timestamp to local date string correctly)
|
||||
|
||||
- `src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts`: `startDate: getWorklogStr(this._data.task.dueWithTime || undefined),`
|
||||
|
||||
10. **date.service.ts** ✓ (No fix needed - utility wrapper for converting timestamps to local date strings)
|
||||
|
||||
- `src/app/core/date/date.service.ts`: `return getWorklogStr(date);`
|
||||
|
||||
11. **metric.util.ts** ✓ (No fix needed - converts timestamp to local date string for metrics display)
|
||||
|
||||
- `src/app/features/metric/metric.util.ts`: `start: getWorklogStr(s.start),`
|
||||
|
||||
12. **archive.service.ts** ✓ (No fix needed - converts current timestamp to today string for archiving)
|
||||
|
||||
- `src/app/features/time-tracking/archive.service.ts`: `todayStr: getWorklogStr(now),`
|
||||
|
||||
13. **map-archive-to-worklog-weeks.ts** ✓ (No fix needed - converts timestamps to local date strings for worklog)
|
||||
|
||||
- `src/app/features/worklog/util/map-archive-to-worklog-weeks.ts`: `getWorklogStr(entities[task.parentId].created)`
|
||||
- `src/app/features/worklog/util/map-archive-to-worklog-weeks.ts`: `return { [getWorklogStr(task.created)]: 1 };`
|
||||
|
||||
14. **map-archive-to-worklog.ts** ✓ (No fix needed - converts doneOn/created timestamps to local date strings)
|
||||
|
||||
- `src/app/features/worklog/util/map-archive-to-worklog.ts`: `getWorklogStr(entities[task.parentId].doneOn || entities[task.parentId].created)`
|
||||
- `src/app/features/worklog/util/map-archive-to-worklog.ts`: `return { [getWorklogStr(task.doneOn || task.created)]: 1 };`
|
||||
|
||||
15. **worklog-export.component.ts** ✓ (No fix needed - converts Date objects to date strings for filename)
|
||||
|
||||
- `src/app/features/worklog/worklog-export/worklog-export.component.ts`: `'tasks' + getWorklogStr(rangeStart) + '-' + getWorklogStr(rangeEnd) + '.csv';`
|
||||
|
||||
16. **task-context-menu-inner.component.ts** ✓ (No fix needed - converts Date objects for task scheduling)
|
||||
|
||||
- `src/app/features/tasks/task-context-menu/task-context-menu-inner/task-context-menu-inner.component.ts`: `const newDay = getWorklogStr(newDayDate);`
|
||||
|
||||
17. **get-today-str.ts** ✓ (No fix needed - utility function for today's date string)
|
||||
|
||||
- `src/app/features/tasks/util/get-today-str.ts`: `export const getTodayStr = (): string => getWorklogStr(new Date());`
|
||||
|
||||
18. **dialog-view-task-reminders.component.ts** ✓ (No fix needed - converts tomorrow Date to date string)
|
||||
|
||||
- `src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts`: `day: getWorklogStr(getTomorrow()),`
|
||||
|
||||
### Remaining
|
||||
|
||||
1. **dialog-time-estimate.component.ts**
|
||||
|
||||
- `src/app/features/tasks/dialog-time-estimate/dialog-time-estimate.component.ts`: `[getWorklogStr(result.date)]: result.timeSpent,`
|
||||
|
||||
2. **tasks-by-tag.component.ts**
|
||||
|
||||
- `src/app/features/tasks/tasks-by-tag/tasks-by-tag.component.ts`: `const yesterdayDayStr = getWorklogStr(yesterdayDate);`
|
||||
|
||||
3. **issue-panel-calendar-agenda.component.ts**
|
||||
|
||||
- `src/app/features/issue-panel/issue-panel-calendar-agenda/issue-panel-calendar-agenda.component.ts`: `const date = getWorklogStr((item.issueData as ICalIssueReduced).start);`
|
||||
|
||||
4. **planner.service.ts**
|
||||
|
||||
- `src/app/features/planner/planner.service.ts`: `if (days[1]?.dayDate === getWorklogStr(tomorrow)) {`
|
||||
|
||||
5. **create-task-placeholder.component.ts**
|
||||
|
||||
- `src/app/features/schedule/create-task-placeholder/create-task-placeholder.component.ts`: `day: getWorklogStr(this.due()),`
|
||||
|
||||
6. **create-blocked-blocks-by-day-map.ts**
|
||||
|
||||
- `src/app/features/schedule/map-schedule-data/create-blocked-blocks-by-day-map.ts`: `const dayStartDateStr = getWorklogStr(block.start);`
|
||||
- `src/app/features/schedule/map-schedule-data/create-blocked-blocks-by-day-map.ts`: `const dayStr = getWorklogStr(curDateTs);`
|
||||
|
||||
7. **get-simple-counter-streak-duration.ts**
|
||||
|
||||
- `src/app/features/simple-counter/get-simple-counter-streak-duration.ts`: Multiple occurrences
|
||||
|
||||
8. **navigate-to-task.service.ts**
|
||||
- `src/app/core-ui/navigate-to-task/navigate-to-task.service.ts`: `return dateStr ?? getWorklogStr(parentTask.created);`
|
||||
- `src/app/core-ui/navigate-to-task/navigate-to-task.service.ts`: `return getWorklogStr(task.created);`
|
||||
|
||||
## Analysis Plan
|
||||
|
||||
For each occurrence, we need to:
|
||||
|
||||
1. Understand if the date parameter is a timestamp (number) or Date object
|
||||
2. Determine if timezone conversion could cause issues
|
||||
3. Write tests that demonstrate any timezone bugs
|
||||
4. Apply fixes if needed
|
||||
|
||||
## Priority
|
||||
|
||||
High priority items (likely to cause user-visible bugs):
|
||||
|
||||
- task-repeat-cfg.service.ts (affects repeating tasks)
|
||||
- short-syntax.effects.ts (affects task creation)
|
||||
- dialog-schedule-task.component.ts (affects scheduling)
|
||||
- work-context.service.ts (affects done tasks)
|
||||
- Issue provider services (gitlab, open-project)
|
||||
|
||||
Medium priority (data display/export):
|
||||
|
||||
- worklog utilities
|
||||
- metric.util.ts
|
||||
- navigate-to-task.service.ts
|
||||
|
||||
Low priority (utility functions that might be correct):
|
||||
|
||||
- date.service.ts (wrapper function)
|
||||
- archive.service.ts
|
||||
- get-today-str.ts
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
# Wayland Idle Detection Fix
|
||||
|
||||
## Problem
|
||||
|
||||
The Electron `powerMonitor.getSystemIdleTime()` API doesn't work properly on Wayland-based Linux systems. This is a known upstream issue ([electron/electron#27912](https://github.com/electron/electron/issues/27912)) where:
|
||||
|
||||
- The function returns 0 or doesn't detect keyboard-only activity
|
||||
- This affects idle time tracking in Super Productivity on modern Linux distributions that use Wayland by default (Ubuntu 22.04+, Fedora, etc.)
|
||||
|
||||
## Solution
|
||||
|
||||
This fix implements a multi-layered approach to detect idle time on Wayland systems:
|
||||
|
||||
### 1. Environment Detection
|
||||
|
||||
The system automatically detects if it's running on Wayland by checking:
|
||||
|
||||
- `XDG_SESSION_TYPE` environment variable
|
||||
- `WAYLAND_DISPLAY` environment variable
|
||||
- Desktop environment (GNOME, KDE, etc.)
|
||||
|
||||
### 2. Multiple Fallback Methods
|
||||
|
||||
When running on Wayland, the system tries these methods in order:
|
||||
|
||||
1. **GNOME DBus API** (for GNOME Wayland sessions):
|
||||
|
||||
```bash
|
||||
dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor \
|
||||
/org/gnome/Mutter/IdleMonitor/Core \
|
||||
org.gnome.Mutter.IdleMonitor.GetIdletime
|
||||
```
|
||||
|
||||
2. **xprintidle** (if XWayland is available):
|
||||
|
||||
- Works on some Wayland systems that have XWayland support
|
||||
- Requires `xprintidle` package to be installed
|
||||
|
||||
3. **loginctl** (systemd-based systems):
|
||||
|
||||
```bash
|
||||
loginctl show-session -p IdleSinceHint
|
||||
```
|
||||
|
||||
If all methods fail, the system returns 0 (not idle) to avoid false idle detection.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Modified/Added:
|
||||
|
||||
1. **`electron/idle-time-handler.ts`** - New file
|
||||
|
||||
- Core idle detection logic with multiple fallback methods
|
||||
- Environment detection
|
||||
- Unified API for idle time retrieval
|
||||
|
||||
2. **`electron/start-app.ts`** - Modified
|
||||
|
||||
- Integrates the new idle handler
|
||||
- Replaces direct `powerMonitor.getSystemIdleTime()` calls
|
||||
- Uses `getIdleTimeWithFallbacks()` for robust idle detection
|
||||
|
||||
## Testing
|
||||
|
||||
To test if idle detection is working on your Wayland system:
|
||||
|
||||
1. **Verify D-Bus availability** (GNOME systems):
|
||||
|
||||
```bash
|
||||
dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor \
|
||||
/org/gnome/Mutter/IdleMonitor/Core \
|
||||
org.gnome.Mutter.IdleMonitor.GetIdletime
|
||||
```
|
||||
|
||||
2. **Check environment detection** in Electron logs:
|
||||
|
||||
```
|
||||
Environment detection: {
|
||||
sessionType: 'wayland',
|
||||
currentDesktop: 'ubuntu:GNOME',
|
||||
waylandDisplay: ':0',
|
||||
gnomeShellVersion: 'ubuntu',
|
||||
isWayland: true,
|
||||
isGnomeWayland: true
|
||||
}
|
||||
```
|
||||
|
||||
3. **Test idle detection**:
|
||||
- Let the system idle for the configured time (default: 15 seconds)
|
||||
- The idle dialog should appear
|
||||
- Check logs for which method was used for idle detection
|
||||
|
||||
## Workarounds for Users
|
||||
|
||||
If idle detection still doesn't work:
|
||||
|
||||
1. **Switch to X11/Xorg session** (most reliable):
|
||||
|
||||
- Log out
|
||||
- Select "Ubuntu on Xorg" or similar from the login screen
|
||||
- Log back in
|
||||
|
||||
2. **Install additional packages** (may help):
|
||||
|
||||
```bash
|
||||
sudo apt install xprintidle # For xprintidle fallback
|
||||
```
|
||||
|
||||
3. **Adjust idle detection settings**:
|
||||
- Go to Settings → Time Tracking
|
||||
- Increase the minimum idle time if false positives occur
|
||||
- Or disable idle detection if it's not working reliably
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. Add support for KDE's idle protocol on Wayland
|
||||
2. Implement native Wayland idle detection when Electron adds support
|
||||
3. Add user-configurable detection method selection
|
||||
4. Support for more Wayland compositors (Sway, Hyprland, etc.)
|
||||
|
||||
## References
|
||||
|
||||
- [Electron Issue #27912](https://github.com/electron/electron/issues/27912)
|
||||
- [Stretchly Issue #1261](https://github.com/hovancik/stretchly/issues/1261)
|
||||
- [Wayland Idle Protocols](https://wayland.app/protocols/kde-idle)
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
# WebDAV Conditional Headers Analysis
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Analysis of ETag vs Last-Modified conditional header mappings for WebDAV implementation, including verification against RFC 7232 (HTTP Conditional Requests) and RFC 4918 (WebDAV specification).
|
||||
|
||||
**Key Finding**: The proposed mapping contains a critical conceptual error for resource creation operations.
|
||||
|
||||
## Original Proposed Mapping (INCORRECT)
|
||||
|
||||
| ETag Operation | Current Header | Last-Modified Equivalent |
|
||||
| ----------------- | ----------------------- | ---------------------------------- | --- |
|
||||
| Create new file | `If-None-Match: *` | `If-None-Match: *` | ❌ |
|
||||
| Update existing | `If-Match: <etag>` | `If-Unmodified-Since: <timestamp>` | ✅ |
|
||||
| Check for changes | `If-None-Match: <etag>` | `If-Modified-Since: <timestamp>` | ✅ |
|
||||
| Success response | `ETag: <etag>` | `Last-Modified: <timestamp>` | ✅ |
|
||||
| Not modified | `304 + ETag` | `304 + Last-Modified` | ✅ |
|
||||
| Conflict | `412` (ETag mismatch) | `412` (timestamp mismatch) | ✅ |
|
||||
|
||||
## Corrected Analysis
|
||||
|
||||
### Critical Error Identified
|
||||
|
||||
The "Create new file" row contains a fundamental conceptual error:
|
||||
|
||||
- `If-None-Match: *` is an **ETag-based** header, not a Last-Modified equivalent
|
||||
- Last-Modified headers cannot provide safe resource creation functionality
|
||||
|
||||
### Corrected Mapping
|
||||
|
||||
| ETag Operation | ETag Header | Last-Modified Equivalent |
|
||||
| ----------------- | ----------------------- | -------------------------------------------- |
|
||||
| Create new file | `If-None-Match: *` | **❌ NO EQUIVALENT** (concept doesn't apply) |
|
||||
| Update existing | `If-Match: <etag>` | `If-Unmodified-Since: <timestamp>` |
|
||||
| Check for changes | `If-None-Match: <etag>` | `If-Modified-Since: <timestamp>` |
|
||||
| Success response | `ETag: <etag>` | `Last-Modified: <timestamp>` |
|
||||
| Not modified | `304 + ETag` | `304 + Last-Modified` |
|
||||
| Conflict | `412` (ETag mismatch) | `412` (timestamp mismatch) |
|
||||
|
||||
## RFC Analysis
|
||||
|
||||
### RFC 7232 (HTTP Conditional Requests)
|
||||
|
||||
1. **Header Evaluation Order**:
|
||||
|
||||
- If-Match (if present)
|
||||
- If-Unmodified-Since (if If-Match not present)
|
||||
- If-None-Match
|
||||
- If-Modified-Since (only for GET/HEAD, if If-None-Match not present)
|
||||
|
||||
2. **Validator Precedence**:
|
||||
|
||||
- ETags take precedence over Last-Modified when both present
|
||||
- Strong ETags preferred over weak ETags
|
||||
|
||||
3. **Method Restrictions**:
|
||||
- `If-Modified-Since` and `If-Unmodified-Since` only valid for GET/HEAD requests
|
||||
- `If-None-Match` and `If-Match` valid for all HTTP methods
|
||||
|
||||
### RFC 4918 (WebDAV Specification)
|
||||
|
||||
1. **Security Requirements**:
|
||||
|
||||
- "The server MUST do authorization checks before checking any HTTP conditional header"
|
||||
|
||||
2. **ETag Importance**:
|
||||
|
||||
- "Correct use of ETags is even more important in a distributed authoring environment"
|
||||
- "ETags are necessary along with locks to avoid the lost-update problem"
|
||||
- "Strong ETags are much more useful for authoring use cases than weak ETags"
|
||||
|
||||
3. **Namespace Operations**:
|
||||
- For COPY/MOVE operations, servers must ensure ETag values are not reused
|
||||
- ETag semantics must be preserved across namespace operations
|
||||
|
||||
### Current WebDAV Implementation Verification
|
||||
|
||||
The current implementation in `webdav-api.ts:197-212` correctly follows RFC standards:
|
||||
|
||||
```typescript
|
||||
private _createConditionalHeaders(
|
||||
isOverwrite?: boolean,
|
||||
expectedEtag?: string | null,
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (!isOverwrite) {
|
||||
if (expectedEtag) {
|
||||
headers['If-Match'] = expectedEtag; // Update existing
|
||||
} else {
|
||||
headers['If-None-Match'] = '*'; // Create new
|
||||
}
|
||||
} else if (expectedEtag) {
|
||||
headers['If-Match'] = expectedEtag; // Force overwrite
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
```
|
||||
|
||||
## Why Last-Modified Cannot Handle Resource Creation
|
||||
|
||||
1. **Existence vs. Temporal Comparison**:
|
||||
|
||||
- `If-None-Match: *` checks for resource **existence**
|
||||
- Last-Modified headers only handle **temporal comparisons** of existing resources
|
||||
|
||||
2. **Method Limitations**:
|
||||
|
||||
- Last-Modified conditionals (If-Modified-Since/If-Unmodified-Since) are restricted to GET/HEAD
|
||||
- Resource creation typically uses PUT/POST which cannot use these headers
|
||||
|
||||
3. **Conceptual Mismatch**:
|
||||
- You cannot compare timestamps for non-existent resources
|
||||
- Resource creation requires existence checking, not temporal validation
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **For ETag-capable servers**: Use `If-None-Match: *` for safe resource creation
|
||||
2. **For Last-Modified-only servers**: Research alternative approaches (see separate analysis)
|
||||
3. **Implementation priority**: ETags are strongly preferred for WebDAV authoring scenarios
|
||||
4. **Fallback strategy**: Implement alternative creation safety mechanisms for Last-Modified-only environments
|
||||
|
||||
## References
|
||||
|
||||
- RFC 7232: Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests
|
||||
- RFC 4918: HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
|
||||
- Current WebDAV implementation: `src/app/pfapi/api/sync/providers/webdav/webdav-api.ts`
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
# WebDAV Resource Creation Alternatives to `If-None-Match: *`
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Analysis of alternative approaches for safe resource creation in WebDAV environments where `If-None-Match: *` is not supported or unreliable, particularly focusing on servers that only support Last-Modified headers or have implementation issues with ETags.
|
||||
|
||||
## Background
|
||||
|
||||
The standard approach for safe resource creation is using `If-None-Match: *` with PUT requests to prevent overwriting existing resources. However, several scenarios require alternatives:
|
||||
|
||||
1. **ETag-unsupported servers**: Some WebDAV servers only support Last-Modified headers
|
||||
2. **Implementation bugs**: Various servers (Nextcloud/Sabre, Lighttpd) have issues with `If-None-Match: *`
|
||||
3. **Legacy compatibility**: Older WebDAV implementations may not fully support conditional headers
|
||||
|
||||
## Alternative Approaches
|
||||
|
||||
### 1. WebDAV-Specific If Header
|
||||
|
||||
**Description**: WebDAV defines an enhanced `If` header that provides more powerful conditional logic than standard HTTP headers.
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- More flexible than `If-Match`/`If-None-Match`
|
||||
- Supports complex conditional logic
|
||||
- Can reference multiple resources
|
||||
- Allows custom flags and extensions
|
||||
|
||||
**Syntax Examples**:
|
||||
|
||||
```http
|
||||
If: (["etag-value"]) # Equivalent to If-Match: "etag-value"
|
||||
If: (NOT ["etag-value"]) # Equivalent to If-None-Match: "etag-value"
|
||||
If: </resource1> (["etag1"]) </resource2> (["etag2"]) # Multi-resource conditions
|
||||
```
|
||||
|
||||
**Implementation Status**: ✅ Standards-based (RFC 4918)
|
||||
**Server Support**: Varies by implementation
|
||||
|
||||
### 2. WebDAV LOCK/UNLOCK Mechanism
|
||||
|
||||
**Description**: Use WebDAV locking to reserve resource names before creation.
|
||||
|
||||
**Process**:
|
||||
|
||||
1. Send `LOCK` request to unmapped URL to reserve the name
|
||||
2. If lock succeeds, resource doesn't exist - proceed with creation
|
||||
3. Create resource with `PUT`
|
||||
4. `UNLOCK` the resource
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Prevents race conditions
|
||||
- Clear resource reservation semantics
|
||||
- Supported by most WebDAV servers
|
||||
|
||||
**Disadvantages**:
|
||||
|
||||
- Requires multiple requests
|
||||
- Lock timeout management complexity
|
||||
- Not all servers support locking on unmapped URLs
|
||||
|
||||
**Implementation Status**: ✅ Standards-based (RFC 4918)
|
||||
**Server Support**: Wide but not universal
|
||||
|
||||
### 3. Overwrite Header (Limited Scope)
|
||||
|
||||
**Description**: WebDAV `Overwrite: F` header prevents overwriting during COPY/MOVE operations.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```http
|
||||
COPY /source HTTP/1.1
|
||||
Destination: /target
|
||||
Overwrite: F
|
||||
```
|
||||
|
||||
**Limitations**:
|
||||
|
||||
- ⚠️ **Only works with COPY/MOVE, not PUT**
|
||||
- Cannot be used for direct resource creation
|
||||
- Useful for safe resource relocation/duplication
|
||||
|
||||
**Implementation Status**: ✅ Standards-based (RFC 4918)
|
||||
**Server Support**: Wide
|
||||
|
||||
### 4. HEAD-then-PUT Pattern
|
||||
|
||||
**Description**: Check resource existence before creation using HEAD request.
|
||||
|
||||
**Process**:
|
||||
|
||||
1. Send `HEAD` request to target URL
|
||||
2. If 404 (not found), proceed with `PUT`
|
||||
3. If 200 (exists), handle conflict appropriately
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Works with any HTTP server
|
||||
- Simple implementation
|
||||
- No conditional header dependencies
|
||||
|
||||
**Disadvantages**:
|
||||
|
||||
- ⚠️ **Race condition vulnerability** (resource could be created between HEAD and PUT)
|
||||
- Two requests required
|
||||
- Not atomic
|
||||
|
||||
**Implementation Status**: ✅ Standard HTTP
|
||||
**Server Support**: Universal
|
||||
|
||||
### 5. Last-Modified-Based Approaches
|
||||
|
||||
**Description**: Use timestamp-based conditional headers for servers without ETag support.
|
||||
|
||||
**Limitations**:
|
||||
|
||||
- ❌ **Cannot prevent initial resource creation** (no timestamp for non-existent resources)
|
||||
- `If-Modified-Since`/`If-Unmodified-Since` only work with GET/HEAD requests
|
||||
- Only useful for updates, not creation
|
||||
|
||||
**Verdict**: Not viable for resource creation scenarios
|
||||
|
||||
### 6. Custom Application-Level Protocols
|
||||
|
||||
**Description**: Implement application-specific safety mechanisms.
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Unique filename generation (UUIDs, timestamps)
|
||||
- Application-level locks/reservations
|
||||
- Database-backed existence checking
|
||||
- Custom HTTP headers
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Complete control over behavior
|
||||
- Can work around server limitations
|
||||
- Application-specific optimization
|
||||
|
||||
**Disadvantages**:
|
||||
|
||||
- Non-standard approaches
|
||||
- Increased complexity
|
||||
- Limited interoperability
|
||||
|
||||
## Implementation Recommendations
|
||||
|
||||
### Priority Order
|
||||
|
||||
1. **First Choice**: `If-None-Match: *` (if server supports it properly)
|
||||
2. **Fallback 1**: WebDAV `If` header with `NOT` operator
|
||||
3. **Fallback 2**: WebDAV LOCK/UNLOCK mechanism
|
||||
4. **Fallback 3**: HEAD-then-PUT with conflict handling
|
||||
5. **Last Resort**: Application-specific approaches
|
||||
|
||||
### Detection Strategy
|
||||
|
||||
```typescript
|
||||
// Pseudo-code for capability detection
|
||||
async function detectSafeCreationCapabilities(server: WebDAVServer) {
|
||||
try {
|
||||
// Test If-None-Match: * support
|
||||
await server.testConditionalHeaders();
|
||||
return 'if-none-match';
|
||||
} catch (error) {
|
||||
if (error.status === 412) {
|
||||
// Server supports conditional headers but may have bugs
|
||||
return 'webdav-if-header';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Test WebDAV If header support
|
||||
await server.testWebDAVIfHeader();
|
||||
return 'webdav-if-header';
|
||||
} catch (error) {
|
||||
// Fall back to LOCK mechanism
|
||||
return 'lock-unlock';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Common error responses and handling strategies:
|
||||
|
||||
| Status Code | Meaning | Handling Strategy |
|
||||
| ----------- | ------------------- | ------------------------------------------------------------- |
|
||||
| 412 | Precondition Failed | Resource exists or conditional failed - expected for safety |
|
||||
| 423 | Locked | Resource is locked - retry with backoff or fail |
|
||||
| 405 | Method Not Allowed | Server doesn't support method - try alternative |
|
||||
| 501 | Not Implemented | Server doesn't support feature - fallback to simpler approach |
|
||||
|
||||
## Known Server Limitations
|
||||
|
||||
### Nextcloud/Sabre DAV
|
||||
|
||||
- Issues with `If-None-Match: *` returning 412 errors
|
||||
- Generally supports WebDAV `If` header
|
||||
- Strong ETag support
|
||||
|
||||
### Lighttpd mod_webdav
|
||||
|
||||
- Known 412 Precondition Failed issues with conditional headers
|
||||
- Limited WebDAV feature support
|
||||
- Consider HEAD-then-PUT approach
|
||||
|
||||
### Microsoft IIS WebDAV
|
||||
|
||||
- Variable conditional header support depending on version
|
||||
- Good LOCK/UNLOCK support
|
||||
- Test thoroughly for specific IIS versions
|
||||
|
||||
### Apache mod_dav
|
||||
|
||||
- Generally good conditional header support
|
||||
- Strong WebDAV compliance
|
||||
- Reliable `If-None-Match: *` support
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authorization First**: RFC 4918 requires authorization checks before conditional header evaluation
|
||||
2. **Race Condition Awareness**: Multi-request approaches have inherent timing vulnerabilities
|
||||
3. **Lock Timeout Management**: Proper cleanup of failed lock operations
|
||||
4. **Error Information Disclosure**: Avoid exposing internal server state in error messages
|
||||
|
||||
## Conclusion
|
||||
|
||||
While `If-None-Match: *` remains the preferred standard approach, WebDAV implementations often require fallback strategies. The WebDAV `If` header provides the most powerful alternative, followed by the LOCK/UNLOCK mechanism for broader compatibility. HEAD-then-PUT should be used cautiously due to race condition risks, and Last-Modified-based approaches cannot solve the resource creation safety problem.
|
||||
|
||||
The key is implementing a robust capability detection and fallback system that gracefully handles various server implementations and their limitations.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# concept for 2 file sync in super productivity
|
||||
|
||||
2 files:
|
||||
1 for archive
|
||||
1 for everything else including metadata
|
||||
(can be later expanded, if needed)
|
||||
|
||||
main file stores metadata and content
|
||||
|
||||
metadata includes:
|
||||
|
||||
- etag of main file
|
||||
- checksum for archive (and maybe for main file too)
|
||||
- (timestamp of last update to archive)
|
||||
- (timestamp of last update to main file)
|
||||
- (timestamp of last completed sync)
|
||||
- model versions are included by the data itself
|
||||
|
||||
We also need to save any incomplete syncs, so that we can warn the user if closing the app
|
||||
|
||||
## sync flow
|
||||
|
||||
### how to determine if local archive was updated?
|
||||
|
||||
- calculate checksum of local archive or (faster) just save a timestamp every time the archive is updated und use that as a checksum
|
||||
- for local change check the timestamp should be enough
|
||||
- for comparing with remote the checksum might be better
|
||||
|
||||
### out
|
||||
|
||||
#### only main file was updated
|
||||
|
||||
1. update metadata and upload main file
|
||||
|
||||
#### archive was updated
|
||||
|
||||
1. upload archive file (and get new etag ?)
|
||||
2. update metadata (in main file) and upload main file
|
||||
|
||||
### in
|
||||
|
||||
#### only main file was updated
|
||||
|
||||
1. app checks remote state of main file
|
||||
- first via etag
|
||||
- then again via all metadata
|
||||
2. download main file (and check metadata)
|
||||
3. check if metadata checksum for archive matches local archive checksum
|
||||
4. all done
|
||||
|
||||
#### archive was updated
|
||||
|
||||
1. app checks remote state of main file
|
||||
|
||||
- first via etag
|
||||
- than again via all metadata
|
||||
|
||||
2. download main file (and check metadata)
|
||||
3. check if metadata checksum for archive matches local archive checksum
|
||||
4. if not, download archive file
|
||||
5. re-check checksum
|
||||
6. all done
|
||||
|
||||
### edge cases
|
||||
|
||||
#### checksum miss-match for archive after download of main file
|
||||
|
||||
- try to download new archive file
|
||||
- (reattempt once if checksum check still fails (remote update might not be completed yet???))
|
||||
- (if checksum check still fails, check once for new main file (via etag))
|
||||
- if any of the downloads fail show error message that sync couldn't be completed and ask if the user wants to try again
|
||||
- if checksum check still fails after successful download, show error message and ask if local data should be used instead and if remote data should be overwritten
|
||||
-
|
||||
|
||||
### more edge case considerations
|
||||
|
||||
- what if the main file is updated and the archive is updated at the same time?
|
||||
- since we always check for matching checksums, we always should have a complete state
|
||||
|
||||
## further considerations
|
||||
|
||||
- to make the sync more robust we could try to handle archive data more independent of the main file data e.g. by removing tags and projects as needed or by creating new ones if needed when restoring or showing archive data
|
||||
Loading…
Add table
Add a link
Reference in a new issue