Extract notification section into reusable component #150

Open
anomalist wants to merge 9 commits from notification-section into master
  1. 321
      .cursor/rules/component-creation-ideals.mdc
  2. 2
      .cursor/rules/harbor_pilot_universal.mdc
  3. 236
      .cursor/rules/historical-comment-management.mdc
  4. 348
      .cursor/rules/realistic_time_estimation.mdc
  5. 194
      src/components/NotificationSection.vue
  6. 233
      src/composables/useNotificationSettings.ts
  7. 211
      src/views/AccountViewView.vue

321
.cursor/rules/component-creation-ideals.mdc

@ -0,0 +1,321 @@
---
alwaysApply: true
Review

This one does make some sense to always include.

This just reminds me that we have others that probably shouldn't be included on every convo:
.cursor/rules/legacy_dexie.mdc:alwaysApply: true
.cursor/rules/documentation.mdc:alwaysApply: true
.cursor/rules/absurd-sql.mdc:alwaysApply: true
.cursor/rules/version_control.mdc:alwaysApply: true

BTW, these ones might be consolidated. (... or if the decision record is just historical then maybe it's not important every time.)
.cursor/rules/timesafari.mdc:alwaysApply: true
.cursor/rules/architectural_decision_record.mdc:alwaysApply: true

I'd support that refactor here because we see it now or logged as a future issue.

This one does make some sense to always include. This just reminds me that we have others that probably shouldn't be included on every convo: .cursor/rules/legacy_dexie.mdc:alwaysApply: true .cursor/rules/documentation.mdc:alwaysApply: true .cursor/rules/absurd-sql.mdc:alwaysApply: true .cursor/rules/version_control.mdc:alwaysApply: true BTW, these ones might be consolidated. (... or if the decision record is just historical then maybe it's not important every time.) .cursor/rules/timesafari.mdc:alwaysApply: true .cursor/rules/architectural_decision_record.mdc:alwaysApply: true I'd support that refactor here because we see it now or logged as a future issue.
version: "2.0.0"
lastUpdated: "2025-08-15"
priority: "critical"
---
# Component Creation Ideals — TimeSafari Architecture (Directive MDC, v2)
> **Agent role**: Apply these rules when creating, refactoring, or reviewing Vue components (Vue 3 with **vue-facing-decorator**). Prioritize **self-contained** components that hydrate from and persist to **PlatformServiceMixin** settings. Minimize parent–child coupling and prop drilling. Prefer **concision** where a separate component would add more API surface than value.
## 📚 Cross-References
- **Migration Example**: See `src/components/NotificationSection.vue` for successful refactor implementation
- **Parent Component**: See `src/views/AccountViewView.vue` for before/after comparison
- **Settings Infrastructure**: See `src/utils/PlatformServiceMixin.ts` for available methods
- **Related Rules**: See `.cursor/rules/` for other architectural guidelines
## Golden Rules (Enforce)
### Priority Levels
- 🔴 **CRITICAL**: Must be followed - breaking these creates architectural problems
- 🟡 **HIGH**: Strongly recommended - important for maintainability
- 🟢 **MEDIUM**: Good practice - improves code quality
1. **Self-Contained Components** 🔴 **CRITICAL**
- Do **not** require parent props for internal state or behavior.
- Hydrate on `mounted()` via `this.$accountSettings()`; persist with `this.$saveSettings()`.
- Encapsulate business logic inside the component; avoid delegating core logic to the parent.
- Prefer **computed** getters for derived values; avoid stored duplicates of derived state.
2. **Settings-First Architecture** 🔴 **CRITICAL**
- Use `PlatformServiceMixin` for reading/writing settings. Do **not** introduce new state managers (Pinia, custom stores) unless the state is *truly global*.
- Prefer **fetch-on-mount** over passing values via props.
3. **Single Responsibility** 🟡 **HIGH**
- Each component owns **one clear purpose** (UI + related logic + settings persistence). Avoid splitting UI/logic across multiple components unless reusability clearly benefits.
4. **Internal State Lifecycle** 🟡 **HIGH**
- Pattern: **defaults → hydrate on mount → computed for derived → persist on change**.
- Handle hydration errors gracefully (keep safe defaults; surface actionable UI states as needed).
5. **Minimal Props** 🔴 **CRITICAL**
- Props are for **pure configuration** (labels, limits, feature flags). Do not pass data that can be loaded internally.
- **Never** pass props that mirror settings values (e.g., `isRegistered`, `notifying*`). Load those from settings.
6. **Communication & Events** 🟡 **HIGH**
- Children may emit events for *user interactions* (e.g., `submitted`, `closed`) but **not** to offload core logic to the parent.
- Do not emit events solely to persist settings; the child handles persistence.
7. **Testing** 🟢 **MEDIUM**
- Unit tests mount components in isolation; mock `this.$accountSettings`/`this.$saveSettings`.
- Verify hydration on mount, persistence on mutation, and graceful failure on settings errors.
---
## Concision-First Decision Framework
> **Goal:** Avoid unnecessary components when a concise script, composable, or helper suffices.
**Prefer NOT making a component when:**
- **One-off UI**: used in exactly one view, unlikely to repeat.
- **Small scope**: ~≤100–150 LOC, ≤3 reactive fields, ≤2 handlers.
- **Purely presentational** with trivial logic.
- **Local invariants**: behavior depends entirely on the view’s context.
- **Abstraction cost > benefit**: would create an anemic component with props mirroring parent state.
- **Better fit as code reuse**: logic works as a **composable/service/helper** without introducing new UI.
**Concise alternatives:**
- **Composable**: `useFeature()` encapsulates settings I/O and state.
- **Service/Module**: plain TS helpers for formatting/validation.
- **Directive**: tiny DOM behaviors that don’t need a lifecycle boundary.
**When to make a component (even without reuse yet):**
- **Isolation boundary**: async side effects, permission prompts, or recoverable error states.
- **Stateful widget**: internal settings persistence, media controls, complex a11y.
- **Slots/composition**: needs flexible children or layout.
- **Different change rate**: sub-tree churns independently of the parent.
- **Testability/ownership**: clear, ownable surface that’s easier to unit-test in isolation.
**Rule of Three (guardrail):**
- 1st time: inline or composable.
- 2nd time: consider shared abstraction.
- 3rd time: extract a component.
**PR language (use when choosing concision):**
- “One-off, ~80 LOC, no expected reuse. A component would add an API surface with no consumer. Keeping it local reduces cognitive load. If we see a second usage, promote to a composable; third usage, a component.”
- “Logic lives in `useX()` to keep the view concise without prop plumbing; settings stay via `PlatformServiceMixin`.”
---
## Anti-Patterns (Reject)
- Props that duplicate internal/derivable state: `:is-registered`, `:notifying-*`, etc.
- Child components that are **UI-only** while parents hold the business logic for that feature.
- Introducing Pinia/custom stores for per-component state.
- Emitting `update:*` events to push settings responsibilities to parents.
- Scattering a feature across multiple micro-components without a clear reuse reason.
- Using props for computed/derived values (e.g., `showAdvancedFeatures` as a prop).
---
## Quick Examples
### ✅ DO (Self-Contained, Settings-First)
```vue
<!-- Parent renders with no props -->
<NotificationSection />
```
```ts
// NotificationSection.vue (vue-facing-decorator + PlatformServiceMixin)
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
mixins: [PlatformServiceMixin],
})
export default class NotificationSection extends Vue {
private notifyingNewActivity: boolean = false;
async mounted(): Promise<void> {
await this.hydrateFromSettings();
}
private async hydrateFromSettings(): Promise<void> {
try {
const s = await this.$accountSettings();
this.notifyingNewActivity = !!s.notifyingNewActivityTime;
} catch (err) {
// Keep defaults; optionally surface a non-blocking UI notice
}
}
private async updateNotifying(state: boolean): Promise<void> {
await this.$saveSettings({ notifyingNewActivityTime: state ? Date.now() : null });
this.notifyingNewActivity = state;
}
}
```
### ❌ DON'T (Prop-Driven Coupling)
```vue
<!-- Parent passes internal state down -->
<NotificationSection
:is-registered="isRegistered"
:notifying-new-activity="notifyingNewActivity"
/>
```
```ts
// Child receives props (anti-pattern)
export default class NotificationSection extends Vue {
isRegistered: boolean = false;
notifyingNewActivity: boolean = false;
}
```
---
## Component Structure Template (Drop-In)
```ts
/**
* ComponentName.vue — Purpose
* Owns: UI + logic + settings persistence (PlatformServiceMixin).
*/
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import type { Router } from "vue-router";
@Component({
components: {
// child components here
},
mixins: [PlatformServiceMixin], // Settings access
})
export default class ComponentName extends Vue {
// Internal state
private myState: boolean = false;
private myData: string = "";
// Derived
private get isEnabled(): boolean {
return this.myState && this.hasPermissions;
}
async mounted(): Promise<void> {
await this.hydrateFromSettings();
}
private async hydrateFromSettings(): Promise<void> {
try {
const s = await this.$accountSettings();
this.myState = !!s.mySetting;
this.myData = s.myData ?? "";
} catch (err) {
// keep defaults
}
}
private async updateState(v: boolean): Promise<void> {
await this.$saveSettings({ mySetting: v });
this.myState = v;
}
async handleUserAction(): Promise<void> {
await this.updateState(true);
}
}
```
---
## Migration Playbook (Props → Self-Contained)
> **Agent, when you see a component receiving `@Prop()` values that mirror settings or internal state, apply this playbook.**
1. **Detect Anti-Props**
- Flag props matching: `is*`, `has*`, `notifying*`, or mirroring settings keys.
- Search for parent usage of these props and events.
2. **Inline State**
- Remove anti-props and `@Prop()` declarations.
- Add private fields for state inside the child.
- Add `mounted()` hydration and persistence helpers via `PlatformServiceMixin`.
3. **Parent Simplification**
- Replace `<Child :foo="..." :bar="..." @update="..."/>` with `<Child />`.
- Delete now-unused local state, watchers, and computed values that existed only to feed the child.
4. **Events**
- Keep only user-interaction events (e.g., `submitted`). Remove persistence/logic events that the child now owns.
5. **Tests**
- Update unit tests to mount child in isolation; mock settings I/O.
- Verify: hydrate on mount, persist on change, safe fallback on error.
---
## When Exceptions Are Acceptable
- **Truly Global State** across distant components → use Pinia/service *sparingly*.
- **Reusable Form Inputs** → accept `value` prop and emit `input`/`update:modelValue`; keep validation/business logic internal.
- **Configuration-Only Props** for labels, visual variants, or limits — not for state that can be fetched.
---
## Definition of Done (Checklist)
- [ ] No props required for internal state or settings-backed values.
- [ ] Uses `PlatformServiceMixin` for all settings I/O.
- [ ] Hydrates on `mounted()`, persists on mutations.
- [ ] Single-responsibility: UI + logic + persistence together.
- [ ] Computed getters for derived state.
- [ ] Unit tests mock settings and cover hydrate/persist/failure paths.
- [ ] Parent components contain no leftover `notifying-*` or similar prop wiring.
---
## Testing Snippets
```ts
// Hydration on mount
test("hydrates from settings on mount", async () => {
const wrap = mount(MyComponent);
await wrap.vm.$nextTick();
expect((wrap.vm as any).myState).toBe(true);
});
```
```ts
// Mock settings methods
jest.spyOn(wrapper.vm as any, "$accountSettings").mockResolvedValue({ mySetting: true });
jest.spyOn(wrapper.vm as any, "$saveSettings").mockResolvedValue(void 0);
```
```ts
// Graceful failure
test("handles settings load failure", async () => {
jest.spyOn(wrapper.vm as any, "$accountSettings").mockRejectedValue(new Error("DB Error"));
const wrap = mount(MyComponent);
await wrap.vm.$nextTick();
expect((wrap.vm as any).myState).toBe(false); // default
});
```
---
## Rationale (Short)
- **Concision first where appropriate** → avoid unnecessary components and API surfaces.
- **Reduced coupling** → portability and reuse when boundaries are justified.
- **Maintainability** → changes localized to the owning component.
- **Consistency** → one canonical path for settings-backed features.
**Rule of thumb:** _Can this feature operate independently, and does a component materially improve isolation or testability?_ If not, keep it concise (inline or composable).
---
## 📝 Version History
### v2.0.0 (2025-08-15)
- **Major Enhancement**: Added Concision-First Decision Framework
- **New**: Rule of Three guardrail for component extraction
- **New**: PR language guidance for code reviews
- **Enhanced**: Agent role includes concision preference
- **Refined**: Anti-patterns and examples updated
### v1.0.0 (2025-08-15)
- Initial creation based on successful NotificationSection refactor
- Established core architectural principles for self-contained components
- Added comprehensive migration playbook and testing guidelines
- Included practical examples and anti-pattern detection
### Future Enhancements
- Additional migration patterns for complex components
- Integration with automated refactoring tools
- Performance benchmarking guidelines
- Advanced testing strategies for complex state management

2
.cursor/rules/harbor_pilot_universal.mdc

@ -202,3 +202,5 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
**Notes for Implementers:** **Notes for Implementers:**
- Respect Base *Do-Not* (no filler, no invented facts, no censorship). - Respect Base *Do-Not* (no filler, no invented facts, no censorship).
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly. - Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)

236
.cursor/rules/historical-comment-management.mdc

@ -0,0 +1,236 @@
---
description: when comments are generated by the model
alwaysApply: false
---
# Historical Comment Management — Harbor Pilot Directive
> **Agent role**: When encountering historical comments about removed methods, deprecated patterns, or architectural changes, apply these guidelines to maintain code clarity and developer guidance.
## 🎯 Purpose
Historical comments should either be **removed entirely** or **transformed into actionable guidance** for future developers. Avoid keeping comments that merely state what was removed without explaining why or what to do instead.
## 📋 Decision Framework
### Remove Historical Comments When:
- **Obsolete Information**: Comment describes functionality that no longer exists
- **No Action Required**: Comment doesn't help future developers make decisions
- **Outdated Context**: Comment refers to old patterns that are no longer relevant
- **Self-Evident**: The current code clearly shows the current approach
### Transform Historical Comments When:
- **Architectural Context**: The change represents a significant pattern shift
- **Migration Guidance**: Future developers might need to understand the evolution
- **Decision Rationale**: The "why" behind the change is still relevant
- **Alternative Approaches**: The comment can guide future implementation choices
## 🔄 Transformation Patterns
### 1. From Removal Notice to Migration Note
```typescript
// ❌ REMOVE THIS
// turnOffNotifyingFlags method removed - notification state is now managed by NotificationSection component
// ✅ TRANSFORM TO THIS
// Note: Notification state management has been migrated to NotificationSection component
// which handles its own lifecycle and persistence via PlatformServiceMixin
```
### 2. From Deprecation Notice to Implementation Guide
```typescript
// ❌ REMOVE THIS
// This will be handled by the NewComponent now
// No need to call oldMethod() as it's no longer needed
// ✅ TRANSFORM TO THIS
// Note: This functionality has been migrated to NewComponent
// which provides better separation of concerns and testability
```
### 3. From Historical Note to Architectural Context
```typescript
// ❌ REMOVE THIS
// Old approach: used direct database calls
// New approach: uses service layer
// ✅ TRANSFORM TO THIS
// Note: Database access has been abstracted through service layer
// for better testability and platform independence
```
## 🚫 Anti-Patterns to Remove
- Comments that only state what was removed
- Comments that don't explain the current approach
- Comments that reference non-existent methods
- Comments that are self-evident from the code
- Comments that don't help future decision-making
## ✅ Best Practices
### When Keeping Historical Context:
1. **Explain the "Why"**: Why was the change made?
2. **Describe the "What"**: What is the current approach?
3. **Provide Context**: When might this information be useful?
4. **Use Actionable Language**: Guide future decisions, not just document history
### When Removing Historical Context:
1. **Verify Obsoleteness**: Ensure the information is truly outdated
2. **Check for Dependencies**: Ensure no other code references the old approach
3. **Update Related Docs**: If removing from code, consider adding to documentation
4. **Preserve in Git History**: The change is preserved in version control
## 🔍 Implementation Checklist
- [ ] Identify historical comments about removed/deprecated functionality
- [ ] Determine if comment provides actionable guidance
- [ ] Transform useful comments into migration notes or architectural context
- [ ] Remove comments that are purely historical without guidance value
- [ ] Ensure remaining comments explain current approach and rationale
- [ ] Update related documentation if significant context is removed
## 📚 Examples
### Good Historical Comment (Keep & Transform)
```typescript
// Note: Database access has been migrated from direct IndexedDB calls to PlatformServiceMixin
// This provides better platform abstraction and consistent error handling across web/mobile/desktop
// When adding new database operations, use this.$getContact(), this.$saveSettings(), etc.
```
### Bad Historical Comment (Remove)
```typescript
// Old method getContactFromDB() removed - now handled by PlatformServiceMixin
// No need to call the old method anymore
```
## 🎯 Integration with Harbor Pilot
This rule works in conjunction with:
- **Component Creation Ideals**: Maintains architectural consistency
- **Migration Patterns**: Documents evolution of patterns
- **Code Review Guidelines**: Ensures comments provide value
## 📝 Version History
### v1.0.0 (2025-08-21)
- Initial creation based on notification system cleanup
- Established decision framework for historical comment management
- Added transformation patterns and anti-patterns
- Integrated with existing Harbor Pilot architecture rules
# Historical Comment Management — Harbor Pilot Directive
> **Agent role**: When encountering historical comments about removed methods, deprecated patterns, or architectural changes, apply these guidelines to maintain code clarity and developer guidance.
## 🎯 Purpose
Historical comments should either be **removed entirely** or **transformed into actionable guidance** for future developers. Avoid keeping comments that merely state what was removed without explaining why or what to do instead.
## 📋 Decision Framework
### Remove Historical Comments When:
- **Obsolete Information**: Comment describes functionality that no longer exists
- **No Action Required**: Comment doesn't help future developers make decisions
- **Outdated Context**: Comment refers to old patterns that are no longer relevant
- **Self-Evident**: The current code clearly shows the current approach
### Transform Historical Comments When:
- **Architectural Context**: The change represents a significant pattern shift
- **Migration Guidance**: Future developers might need to understand the evolution
- **Decision Rationale**: The "why" behind the change is still relevant
- **Alternative Approaches**: The comment can guide future implementation choices
## 🔄 Transformation Patterns
### 1. From Removal Notice to Migration Note
```typescript
// ❌ REMOVE THIS
// turnOffNotifyingFlags method removed - notification state is now managed by NotificationSection component
// ✅ TRANSFORM TO THIS
// Note: Notification state management has been migrated to NotificationSection component
// which handles its own lifecycle and persistence via PlatformServiceMixin
```
### 2. From Deprecation Notice to Implementation Guide
```typescript
// ❌ REMOVE THIS
// This will be handled by the NewComponent now
// No need to call oldMethod() as it's no longer needed
// ✅ TRANSFORM TO THIS
// Note: This functionality has been migrated to NewComponent
// which provides better separation of concerns and testability
```
### 3. From Historical Note to Architectural Context
```typescript
// ❌ REMOVE THIS
// Old approach: used direct database calls
// New approach: uses service layer
// ✅ TRANSFORM TO THIS
// Note: Database access has been abstracted through service layer
// for better testability and platform independence
```
## 🚫 Anti-Patterns to Remove
- Comments that only state what was removed
- Comments that don't explain the current approach
- Comments that reference non-existent methods
- Comments that are self-evident from the code
- Comments that don't help future decision-making
## ✅ Best Practices
### When Keeping Historical Context:
1. **Explain the "Why"**: Why was the change made?
2. **Describe the "What"**: What is the current approach?
3. **Provide Context**: When might this information be useful?
4. **Use Actionable Language**: Guide future decisions, not just document history
### When Removing Historical Context:
1. **Verify Obsoleteness**: Ensure the information is truly outdated
2. **Check for Dependencies**: Ensure no other code references the old approach
3. **Update Related Docs**: If removing from code, consider adding to documentation
4. **Preserve in Git History**: The change is preserved in version control
## 🔍 Implementation Checklist
- [ ] Identify historical comments about removed/deprecated functionality
- [ ] Determine if comment provides actionable guidance
- [ ] Transform useful comments into migration notes or architectural context
- [ ] Remove comments that are purely historical without guidance value
- [ ] Ensure remaining comments explain current approach and rationale
- [ ] Update related documentation if significant context is removed
## 📚 Examples
### Good Historical Comment (Keep & Transform)
```typescript
// Note: Database access has been migrated from direct IndexedDB calls to PlatformServiceMixin
// This provides better platform abstraction and consistent error handling across web/mobile/desktop
// When adding new database operations, use this.$getContact(), this.$saveSettings(), etc.
```
### Bad Historical Comment (Remove)
```typescript
// Old method getContactFromDB() removed - now handled by PlatformServiceMixin
// No need to call the old method anymore
```
## 🎯 Integration with Harbor Pilot
This rule works in conjunction with:
- **Component Creation Ideals**: Maintains architectural consistency
- **Migration Patterns**: Documents evolution of patterns
- **Code Review Guidelines**: Ensures comments provide value
## 📝 Version History
### v1.0.0 (2025-08-21)
- Initial creation based on notification system cleanup
- Established decision framework for historical comment management
- Added transformation patterns and anti-patterns
- Integrated with existing Harbor Pilot architecture rules

348
.cursor/rules/realistic_time_estimation.mdc

@ -0,0 +1,348 @@
---
description: when generating text that has project task work estimates
alwaysApply: false
---
# No Time Estimates — Harbor Pilot Directive
> **Agent role**: **DO NOT MAKE TIME ESTIMATES**. Instead, use phases, milestones, and complexity levels. Time estimates are consistently wrong and create unrealistic expectations.
## 🎯 Purpose
Development time estimates are consistently wrong and create unrealistic expectations. This rule ensures we focus on phases, milestones, and complexity rather than trying to predict specific timeframes.
## 🚨 Critical Rule
**DO NOT MAKE TIME ESTIMATES**
- **Never provide specific time estimates** - they are always wrong
- **Use phases and milestones** instead of days/weeks
- **Focus on complexity and dependencies** rather than time
- **Set expectations based on progress, not deadlines**
## 📊 Planning Framework (Not Time Estimates)
### **Complexity Categories**
- **Simple**: Text changes, styling updates, minor bug fixes
- **Medium**: New features, refactoring, component updates
- **Complex**: Architecture changes, integrations, cross-platform work
- **Unknown**: New technologies, APIs, or approaches
### **Platform Complexity**
- **Single platform**: Web-only or mobile-only changes
- **Two platforms**: Web + mobile or web + desktop
- **Three platforms**: Web + mobile + desktop
- **Cross-platform consistency**: Ensuring behavior matches across all platforms
### **Testing Complexity**
- **Basic**: Unit tests for new functionality
- **Comprehensive**: Integration tests, cross-platform testing
- **User acceptance**: User testing, feedback integration
## 🔍 Planning Process (No Time Estimates)
### **Step 1: Break Down the Work**
- Identify all subtasks and dependencies
- Group related work into logical phases
- Identify critical path and blockers
### **Step 2: Define Phases and Milestones**
- **Phase 1**: Foundation work (basic fixes, core functionality)
- **Phase 2**: Enhancement work (new features, integrations)
- **Phase 3**: Polish work (testing, user experience, edge cases)
### **Step 3: Identify Dependencies**
- **Technical dependencies**: What must be built first
- **Platform dependencies**: What works on which platforms
- **Testing dependencies**: What can be tested when
### **Step 4: Set Progress Milestones**
- **Milestone 1**: Basic functionality working
- **Milestone 2**: All platforms supported
- **Milestone 3**: Fully tested and polished
## 📋 Planning Checklist (No Time Estimates)
- [ ] Work broken down into logical phases
- [ ] Dependencies identified and mapped
- [ ] Milestones defined with clear criteria
- [ ] Complexity levels assigned to each phase
- [ ] Platform requirements identified
- [ ] Testing strategy planned
- [ ] Risk factors identified
- [ ] Success criteria defined
## 🎯 Example Planning (No Time Estimates)
### **Example 1: Simple Feature**
```
Phase 1: Core implementation
- Basic functionality
- Single platform support
- Unit tests
Phase 2: Platform expansion
- Multi-platform support
- Integration tests
Phase 3: Polish
- User testing
- Edge case handling
```
### **Example 2: Complex Cross-Platform Feature**
```
Phase 1: Foundation
- Architecture design
- Core service implementation
- Basic web platform support
Phase 2: Platform Integration
- Mobile platform support
- Desktop platform support
- Cross-platform consistency
Phase 3: Testing & Polish
- Comprehensive testing
- Error handling
- User experience refinement
```
## 🚫 Anti-Patterns to Avoid
- **"This should take X days"** - Red flag for time estimation
- **"Just a few hours"** - Ignores complexity and testing
- **"Similar to X"** - Without considering differences
- **"Quick fix"** - Nothing is ever quick in software
- **"No testing needed"** - Testing always takes effort
## ✅ Best Practices
### **When Planning:**
1. **Break down everything** - no work is too small to plan
2. **Consider all platforms** - web, mobile, desktop differences
3. **Include testing strategy** - unit, integration, and user testing
4. **Account for unknowns** - there are always surprises
5. **Focus on dependencies** - what blocks what
### **When Presenting Plans:**
1. **Show the phases** - explain the logical progression
2. **Highlight dependencies** - what could block progress
3. **Define milestones** - clear success criteria
4. **Identify risks** - what could go wrong
5. **Suggest alternatives** - ways to reduce scope or complexity
## 🔄 Continuous Improvement
### **Track Progress**
- Record planned vs. actual phases completed
- Identify what took longer than expected
- Learn from complexity misjudgments
- Adjust planning process based on experience
### **Learn from Experience**
- **Underestimated complexity**: Increase complexity categories
- **Missed dependencies**: Improve dependency mapping
- **Platform surprises**: Better platform research upfront
## 🎯 Integration with Harbor Pilot
This rule works in conjunction with:
- **Project Planning**: Focuses on phases and milestones
- **Resource Allocation**: Based on complexity, not time
- **Risk Management**: Identifies blockers and dependencies
- **Stakeholder Communication**: Sets progress-based expectations
## 📝 Version History
### v2.0.0 (2025-08-21)
- **Major Change**: Completely removed time estimation approach
- **New Focus**: Phases, milestones, and complexity-based planning
- **Eliminated**: All time multipliers, estimates, and calculations
- **Added**: Dependency mapping and progress milestone framework
### v1.0.0 (2025-08-21)
- Initial creation based on user feedback about estimation accuracy
- ~~Established realistic estimation multipliers and process~~
- ~~Added comprehensive estimation checklist and examples~~
- Integrated with Harbor Pilot planning and risk management
---
## 🚨 Remember
**DO NOT MAKE TIME ESTIMATES. Use phases, milestones, and complexity instead. Focus on progress, not deadlines.**
## 🚨 Remember
**Your first estimate is wrong. Your second estimate is probably still wrong. Focus on progress, not deadlines.**
# No Time Estimates — Harbor Pilot Directive
> **Agent role**: **DO NOT MAKE TIME ESTIMATES**. Instead, use phases, milestones, and complexity levels. Time estimates are consistently wrong and create unrealistic expectations.
## 🎯 Purpose
Development time estimates are consistently wrong and create unrealistic expectations. This rule ensures we focus on phases, milestones, and complexity rather than trying to predict specific timeframes.
## 🚨 Critical Rule
**DO NOT MAKE TIME ESTIMATES**
- **Never provide specific time estimates** - they are always wrong
- **Use phases and milestones** instead of days/weeks
- **Focus on complexity and dependencies** rather than time
- **Set expectations based on progress, not deadlines**
## 📊 Planning Framework (Not Time Estimates)
### **Complexity Categories**
- **Simple**: Text changes, styling updates, minor bug fixes
- **Medium**: New features, refactoring, component updates
- **Complex**: Architecture changes, integrations, cross-platform work
- **Unknown**: New technologies, APIs, or approaches
### **Platform Complexity**
- **Single platform**: Web-only or mobile-only changes
- **Two platforms**: Web + mobile or web + desktop
- **Three platforms**: Web + mobile + desktop
- **Cross-platform consistency**: Ensuring behavior matches across all platforms
### **Testing Complexity**
- **Basic**: Unit tests for new functionality
- **Comprehensive**: Integration tests, cross-platform testing
- **User acceptance**: User testing, feedback integration
## 🔍 Planning Process (No Time Estimates)
### **Step 1: Break Down the Work**
- Identify all subtasks and dependencies
- Group related work into logical phases
- Identify critical path and blockers
### **Step 2: Define Phases and Milestones**
- **Phase 1**: Foundation work (basic fixes, core functionality)
- **Phase 2**: Enhancement work (new features, integrations)
- **Phase 3**: Polish work (testing, user experience, edge cases)
### **Step 3: Identify Dependencies**
- **Technical dependencies**: What must be built first
- **Platform dependencies**: What works on which platforms
- **Testing dependencies**: What can be tested when
### **Step 4: Set Progress Milestones**
- **Milestone 1**: Basic functionality working
- **Milestone 2**: All platforms supported
- **Milestone 3**: Fully tested and polished
## 📋 Planning Checklist (No Time Estimates)
- [ ] Work broken down into logical phases
- [ ] Dependencies identified and mapped
- [ ] Milestones defined with clear criteria
- [ ] Complexity levels assigned to each phase
- [ ] Platform requirements identified
- [ ] Testing strategy planned
- [ ] Risk factors identified
- [ ] Success criteria defined
## 🎯 Example Planning (No Time Estimates)
### **Example 1: Simple Feature**
```
Phase 1: Core implementation
- Basic functionality
- Single platform support
- Unit tests
Phase 2: Platform expansion
- Multi-platform support
- Integration tests
Phase 3: Polish
- User testing
- Edge case handling
```
### **Example 2: Complex Cross-Platform Feature**
```
Phase 1: Foundation
- Architecture design
- Core service implementation
- Basic web platform support
Phase 2: Platform Integration
- Mobile platform support
- Desktop platform support
- Cross-platform consistency
Phase 3: Testing & Polish
- Comprehensive testing
- Error handling
- User experience refinement
```
## 🚫 Anti-Patterns to Avoid
- **"This should take X days"** - Red flag for time estimation
- **"Just a few hours"** - Ignores complexity and testing
- **"Similar to X"** - Without considering differences
- **"Quick fix"** - Nothing is ever quick in software
- **"No testing needed"** - Testing always takes effort
## ✅ Best Practices
### **When Planning:**
1. **Break down everything** - no work is too small to plan
2. **Consider all platforms** - web, mobile, desktop differences
3. **Include testing strategy** - unit, integration, and user testing
4. **Account for unknowns** - there are always surprises
5. **Focus on dependencies** - what blocks what
### **When Presenting Plans:**
1. **Show the phases** - explain the logical progression
2. **Highlight dependencies** - what could block progress
3. **Define milestones** - clear success criteria
4. **Identify risks** - what could go wrong
5. **Suggest alternatives** - ways to reduce scope or complexity
## 🔄 Continuous Improvement
### **Track Progress**
- Record planned vs. actual phases completed
- Identify what took longer than expected
- Learn from complexity misjudgments
- Adjust planning process based on experience
### **Learn from Experience**
- **Underestimated complexity**: Increase complexity categories
- **Missed dependencies**: Improve dependency mapping
- **Platform surprises**: Better platform research upfront
## 🎯 Integration with Harbor Pilot
This rule works in conjunction with:
- **Project Planning**: Focuses on phases and milestones
- **Resource Allocation**: Based on complexity, not time
- **Risk Management**: Identifies blockers and dependencies
- **Stakeholder Communication**: Sets progress-based expectations
## 📝 Version History
### v2.0.0 (2025-08-21)
- **Major Change**: Completely removed time estimation approach
- **New Focus**: Phases, milestones, and complexity-based planning
- **Eliminated**: All time multipliers, estimates, and calculations
- **Added**: Dependency mapping and progress milestone framework
### v1.0.0 (2025-08-21)
- Initial creation based on user feedback about estimation accuracy
- ~~Established realistic estimation multipliers and process~~
- ~~Added comprehensive estimation checklist and examples~~
- Integrated with Harbor Pilot planning and risk management
---
## 🚨 Remember
**DO NOT MAKE TIME ESTIMATES. Use phases, milestones, and complexity instead. Focus on progress, not deadlines.**
## 🚨 Remember
**Your first estimate is wrong. Your second estimate is probably still wrong. Focus on progress, not deadlines.**

194
src/components/NotificationSection.vue

@ -0,0 +1,194 @@
<template>
<section
v-if="isRegistered"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"
>
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
<div class="flex items-center justify-between">
<div>
Reminder Notification
<button
class="text-slate-400 fa-fw cursor-pointer"
aria-label="Learn more about reminder notifications"
@click.stop="showReminderNotificationInfo"
>
<font-awesome-icon icon="question-circle" aria-hidden="true" />
</button>
</div>
<div
class="relative ml-2 cursor-pointer"
role="switch"
:aria-checked="notifyingReminder"
aria-label="Toggle reminder notifications"
tabindex="0"
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingReminder" class="w-full flex justify-between">
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
<span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<!-- label -->
<div>
New Activity Notification
<font-awesome-icon
icon="question-circle"
class="text-slate-400 fa-fw cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingNewActivity" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
</div>
<div class="mt-2 text-center">
<router-link class="text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notifications&hellip;
</router-link>
</div>
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import PushNotificationPermission from "./PushNotificationPermission.vue";
import { createNotifyHelpers } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import {
createNotificationSettingsService,
type NotificationSettingsService,
} from "@/composables/useNotificationSettings";
/**
* NotificationSection.vue - Notification UI component with service-based logic
*
* This component handles the UI presentation of notification functionality:
* - Reminder notifications with custom messages
* - New activity notifications
* - Notification permission management
* - Help and troubleshooting links
*
* Business logic is delegated to NotificationSettingsService for:
* - Settings hydration and persistence
* - Registration status checking
* - Notification permission management
*
* The component maintains its lifecycle boundary while keeping logic separate.
*
* @author Matthew Raymer
* @component NotificationSection
* @vue-facing-decorator
*/
@Component({
components: {
FontAwesomeIcon,
PushNotificationPermission,
},
mixins: [PlatformServiceMixin],
})
export default class NotificationSection extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
// Notification settings service - handles all business logic
private notificationService!: NotificationSettingsService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
this.notify = createNotifyHelpers(this.$notify);
this.notificationService = createNotificationSettingsService(
this,
this.notify,
);
}
/**
* Computed property to determine if user is registered
* Reads from the notification service
*/
private get isRegistered(): boolean {
return this.notificationService.isRegistered;
}
/**
* Initialize component state from persisted settings
* Called when component is mounted
*/
async mounted(): Promise<void> {
await this.notificationService.hydrateFromSettings();
}
// Computed properties for template access
private get notifyingNewActivity(): boolean {
return this.notificationService.notifyingNewActivity;
}
private get notifyingNewActivityTime(): string {
return this.notificationService.notifyingNewActivityTime;
}
private get notifyingReminder(): boolean {
return this.notificationService.notifyingReminder;
}
private get notifyingReminderMessage(): string {
return this.notificationService.notifyingReminderMessage;
}
private get notifyingReminderTime(): string {
return this.notificationService.notifyingReminderTime;
}
async showNewActivityNotificationInfo(): Promise<void> {
await this.notificationService.showNewActivityNotificationInfo(
this.$router,
);
}
async showNewActivityNotificationChoice(): Promise<void> {
await this.notificationService.showNewActivityNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
async showReminderNotificationInfo(): Promise<void> {
await this.notificationService.showReminderNotificationInfo(this.$router);
}
async showReminderNotificationChoice(): Promise<void> {
await this.notificationService.showReminderNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
}
</script>

233
src/composables/useNotificationSettings.ts

@ -0,0 +1,233 @@
/**
* useNotificationSettings.ts - Notification settings and permissions service
*
* This service handles all notification-related business logic including:
* - Settings hydration and persistence
* - Registration status checking
* - Notification permission management
* - Settings state management
*
* Separates business logic from UI components while maintaining
* the lifecycle boundary and settings access patterns.
*
* @author Matthew Raymer
* @service NotificationSettingsService
*/
import { createNotifyHelpers } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE } from "@/libs/util";
import type { ComponentPublicInstance } from "vue";
/**
* Interface for notification settings state
*/
export interface NotificationSettingsState {
isRegistered: boolean;
notifyingNewActivity: boolean;
notifyingNewActivityTime: string;
notifyingReminder: boolean;
notifyingReminderMessage: string;
notifyingReminderTime: string;
}
/**
* Interface for notification settings actions
*/
export interface NotificationSettingsActions {
hydrateFromSettings: () => Promise<void>;
updateNewActivityNotification: (
enabled: boolean,
timeText?: string,
) => Promise<void>;
updateReminderNotification: (
enabled: boolean,
timeText?: string,
message?: string,
) => Promise<void>;
showNewActivityNotificationInfo: (router: any) => Promise<void>;
showReminderNotificationInfo: (router: any) => Promise<void>;
showNewActivityNotificationChoice: (
pushNotificationPermissionRef: any,
) => Promise<void>;
showReminderNotificationChoice: (
pushNotificationPermissionRef: any,
) => Promise<void>;
}
/**
* Service class for managing notification settings and permissions
*
* @param platformService - PlatformServiceMixin instance for settings access
* @param notify - Notification helper functions
*/
export class NotificationSettingsService
implements NotificationSettingsState, NotificationSettingsActions
{
// State properties
public isRegistered: boolean = false;
public notifyingNewActivity: boolean = false;
public notifyingNewActivityTime: string = "";
public notifyingReminder: boolean = false;
public notifyingReminderMessage: string = "";
public notifyingReminderTime: string = "";
constructor(
private platformService: ComponentPublicInstance & {
$accountSettings: () => Promise<any>;
$saveSettings: (changes: any) => Promise<boolean>;
},
private notify: ReturnType<typeof createNotifyHelpers>,
) {}
/**
* Load notification settings from database and hydrate internal state
* Uses the existing settings mechanism for consistency
*/
public async hydrateFromSettings(): Promise<void> {
try {
const settings = await this.platformService.$accountSettings();
// Hydrate registration status
this.isRegistered = !!settings?.isRegistered;
// Hydrate boolean flags from time presence
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || "";
} catch (error) {
console.error("Failed to hydrate notification settings:", error);
// Keep default values on error
}
}
/**
* Update new activity notification settings
*/
public async updateNewActivityNotification(
enabled: boolean,
timeText: string = "",
): Promise<void> {
await this.platformService.$saveSettings({
notifyingNewActivityTime: timeText,
});
this.notifyingNewActivity = enabled;
this.notifyingNewActivityTime = timeText;
}
/**
* Update reminder notification settings
*/
public async updateReminderNotification(
enabled: boolean,
timeText: string = "",
message: string = "",
): Promise<void> {
await this.platformService.$saveSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
this.notifyingReminder = enabled;
this.notifyingReminderMessage = message;
this.notifyingReminderTime = timeText;
}
/**
* Show new activity notification info dialog
*/
public async showNewActivityNotificationInfo(router: any): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
async () => {
await router.push({
name: "help-notification-types",
});
},
);
}
/**
* Show reminder notification info dialog
*/
public async showReminderNotificationInfo(router: any): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await router.push({
name: "help-notification-types",
});
},
);
}
/**
* Handle new activity notification choice (enable/disable)
*/
public async showNewActivityNotificationChoice(
pushNotificationPermissionRef: any,
): Promise<void> {
if (!this.notifyingNewActivity) {
// Enable notification
pushNotificationPermissionRef.open(
DAILY_CHECK_TITLE,
async (success: boolean, timeText: string) => {
if (success) {
await this.updateNewActivityNotification(true, timeText);
}
},
);
} else {
// Disable notification
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.updateNewActivityNotification(false);
}
});
}
}
/**
* Handle reminder notification choice (enable/disable)
*/
public async showReminderNotificationChoice(
pushNotificationPermissionRef: any,
): Promise<void> {
if (!this.notifyingReminder) {
// Enable notification
pushNotificationPermissionRef.open(
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await this.updateReminderNotification(true, timeText, message);
}
},
);
} else {
// Disable notification
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.updateReminderNotification(false);
}
});
}
}
}
/**
* Factory function to create a NotificationSettingsService instance
*
* @param platformService - PlatformServiceMixin instance
* @param notify - Notification helper functions
* @returns NotificationSettingsService instance
*/
export function createNotificationSettingsService(
platformService: ComponentPublicInstance & {
$accountSettings: () => Promise<any>;
$saveSettings: (changes: any) => Promise<boolean>;
},
notify: ReturnType<typeof createNotifyHelpers>,
): NotificationSettingsService {
return new NotificationSettingsService(platformService, notify);
}

211
src/views/AccountViewView.vue

@ -60,91 +60,7 @@
@share-info="onShareInfo" @share-info="onShareInfo"
/> />
<!-- Notifications --> <NotificationSection />
<!-- Currently disabled because it doesn't work, even on Chrome.
If restored, make sure it works or doesn't show on mobile/electron. -->
<section
v-if="false"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"
>
<h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
<div class="flex items-center justify-between">
<div>
Reminder Notification
<button
class="text-slate-400 fa-fw cursor-pointer"
aria-label="Learn more about reminder notifications"
@click.stop="showReminderNotificationInfo"
>
<font-awesome
icon="question-circle"
aria-hidden="true"
></font-awesome>
</button>
</div>
<div
class="relative ml-2 cursor-pointer"
role="switch"
:aria-checked="notifyingReminder"
aria-label="Toggle reminder notifications"
tabindex="0"
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingReminder" class="w-full flex justify-between">
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
<span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<!-- label -->
<div>
New Activity Notification
<font-awesome
icon="question-circle"
class="text-slate-400 fa-fw cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<!-- input -->
<input
v-model="notifyingNewActivity"
type="checkbox"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</div>
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
</div>
<div class="mt-2 text-center">
<router-link class="text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notifications&hellip;
</router-link>
</div>
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
<LocationSearchSection :search-box="searchBox" /> <LocationSearchSection :search-box="searchBox" />
@ -765,12 +681,13 @@ import { Capacitor } from "@capacitor/core";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
import ImageMethodDialog from "../components/ImageMethodDialog.vue"; import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue"; import DataExportSection from "../components/DataExportSection.vue";
import IdentitySection from "@/components/IdentitySection.vue"; import IdentitySection from "@/components/IdentitySection.vue";
import NotificationSection from "@/components/NotificationSection.vue";
import RegistrationNotice from "@/components/RegistrationNotice.vue"; import RegistrationNotice from "@/components/RegistrationNotice.vue";
import LocationSearchSection from "@/components/LocationSearchSection.vue"; import LocationSearchSection from "@/components/LocationSearchSection.vue";
import UsageLimitsSection from "@/components/UsageLimitsSection.vue"; import UsageLimitsSection from "@/components/UsageLimitsSection.vue";
@ -796,11 +713,7 @@ import {
getHeaders, getHeaders,
tokenExpiryTimeDescription, tokenExpiryTimeDescription,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { import { retrieveAccountMetadata } from "../libs/util";
DAILY_CHECK_TITLE,
DIRECT_PUSH_TITLE,
retrieveAccountMetadata,
} from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
@ -829,7 +742,7 @@ interface UserNameDialogRef {
LMap, LMap,
LMarker, LMarker,
LTileLayer, LTileLayer,
PushNotificationPermission, NotificationSection,
QuickNav, QuickNav,
TopMessage, TopMessage,
UserNameDialog, UserNameDialog,
@ -880,12 +793,7 @@ export default class AccountViewView extends Vue {
includeUserProfileLocation: boolean = false; includeUserProfileLocation: boolean = false;
savingProfile: boolean = false; savingProfile: boolean = false;
// Notification properties // Push notification subscription (kept for service worker checks)
notifyingNewActivity: boolean = false;
notifyingNewActivityTime: string = "";
notifyingReminder: boolean = false;
notifyingReminderMessage: string = "";
notifyingReminderTime: string = "";
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
// UI state properties // UI state properties
@ -1003,10 +911,8 @@ export default class AccountViewView extends Vue {
const registration = await navigator.serviceWorker?.ready; const registration = await navigator.serviceWorker?.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
if (!this.subscription) { if (!this.subscription) {
if (this.notifyingNewActivity || this.notifyingReminder) { // Notification settings cleanup is now handled by the NotificationSection component
// the app thought there was a subscription but there isn't, so fix the settings // which manages its own state lifecycle and persistence
this.turnOffNotifyingFlags();
}
} }
} catch (error) { } catch (error) {
this.notify.warning( this.notify.warning(
@ -1046,11 +952,6 @@ export default class AccountViewView extends Vue {
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.isSearchAreasSet = !!settings.searchBoxes; this.isSearchAreasSet = !!settings.searchBoxes;
this.searchBox = settings.searchBoxes?.[0] || null; this.searchBox = settings.searchBoxes?.[0] || null;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || "";
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer; this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
this.partnerApiServerInput = this.partnerApiServerInput =
settings.partnerApiServer || this.partnerApiServerInput; settings.partnerApiServer || this.partnerApiServerInput;
@ -1132,87 +1033,6 @@ export default class AccountViewView extends Vue {
} }
} }
async showNewActivityNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
);
}
async showNewActivityNotificationChoice(): Promise<void> {
if (!this.notifyingNewActivity) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: timeText,
});
this.notifyingNewActivity = true;
this.notifyingNewActivityTime = timeText;
}
});
} else {
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
});
}
}
async showReminderNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
);
}
async showReminderNotificationChoice(): Promise<void> {
if (!this.notifyingReminder) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
this.notifyingReminder = true;
this.notifyingReminderMessage = message || "";
this.notifyingReminderTime = timeText;
}
},
);
} else {
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
});
}
}
public async toggleHideRegisterPromptOnNewContact(): Promise<void> { public async toggleHideRegisterPromptOnNewContact(): Promise<void> {
const newSetting = !this.hideRegisterPromptOnNewContact; const newSetting = !this.hideRegisterPromptOnNewContact;
await this.$saveSettings({ await this.$saveSettings({
@ -1229,19 +1049,8 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription(); this.passkeyExpirationDescription = tokenExpiryTimeDescription();
} }
public async turnOffNotifyingFlags(): Promise<void> { // Note: Notification state management has been migrated to NotificationSection component
// should tell the push server as well // which handles its own lifecycle and persistence via PlatformServiceMixin
await this.$saveSettings({
notifyingNewActivityTime: "",
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
/** /**
* Asynchronously exports the database into a downloadable JSON file. * Asynchronously exports the database into a downloadable JSON file.

Loading…
Cancel
Save