Compare commits
9 Commits
onboard-al
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eecef6d32 | ||
|
|
9122974fc9 | ||
|
|
fbacf740de | ||
| 810bfa1675 | |||
|
|
cf34952c1f | ||
|
|
7e5c16446f | ||
|
|
d62178bca5 | ||
|
|
1f716100fd | ||
|
|
0493f4f061 |
321
.cursor/rules/component-creation-ideals.mdc
Normal file
321
.cursor/rules/component-creation-ideals.mdc
Normal file
@@ -0,0 +1,321 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
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
|
||||
@@ -202,3 +202,5 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
**Notes for Implementers:**
|
||||
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
|
||||
- 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
Normal file
236
.cursor/rules/historical-comment-management.mdc
Normal file
@@ -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
Normal file
348
.cursor/rules/realistic_time_estimation.mdc
Normal file
@@ -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
Normal file
194
src/components/NotificationSection.vue
Normal file
@@ -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(" ", " ") }}</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(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</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
Normal file
233
src/composables/useNotificationSettings.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -60,91 +60,7 @@
|
||||
@share-info="onShareInfo"
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- 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(" ", " ") }}</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(" ", " ") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||
<NotificationSection />
|
||||
|
||||
<LocationSearchSection :search-box="searchBox" />
|
||||
|
||||
@@ -765,12 +681,13 @@ import { Capacitor } from "@capacitor/core";
|
||||
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import DataExportSection from "../components/DataExportSection.vue";
|
||||
import IdentitySection from "@/components/IdentitySection.vue";
|
||||
import NotificationSection from "@/components/NotificationSection.vue";
|
||||
import RegistrationNotice from "@/components/RegistrationNotice.vue";
|
||||
import LocationSearchSection from "@/components/LocationSearchSection.vue";
|
||||
import UsageLimitsSection from "@/components/UsageLimitsSection.vue";
|
||||
@@ -796,11 +713,7 @@ import {
|
||||
getHeaders,
|
||||
tokenExpiryTimeDescription,
|
||||
} from "../libs/endorserServer";
|
||||
import {
|
||||
DAILY_CHECK_TITLE,
|
||||
DIRECT_PUSH_TITLE,
|
||||
retrieveAccountMetadata,
|
||||
} from "../libs/util";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
@@ -829,7 +742,7 @@ interface UserNameDialogRef {
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
PushNotificationPermission,
|
||||
NotificationSection,
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
@@ -880,12 +793,7 @@ export default class AccountViewView extends Vue {
|
||||
includeUserProfileLocation: boolean = false;
|
||||
savingProfile: boolean = false;
|
||||
|
||||
// Notification properties
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
// Push notification subscription (kept for service worker checks)
|
||||
subscription: PushSubscription | null = null;
|
||||
|
||||
// UI state properties
|
||||
@@ -1003,10 +911,8 @@ export default class AccountViewView extends Vue {
|
||||
const registration = await navigator.serviceWorker?.ready;
|
||||
this.subscription = await registration.pushManager.getSubscription();
|
||||
if (!this.subscription) {
|
||||
if (this.notifyingNewActivity || this.notifyingReminder) {
|
||||
// the app thought there was a subscription but there isn't, so fix the settings
|
||||
this.turnOffNotifyingFlags();
|
||||
}
|
||||
// Notification settings cleanup is now handled by the NotificationSection component
|
||||
// which manages its own state lifecycle and persistence
|
||||
}
|
||||
} catch (error) {
|
||||
this.notify.warning(
|
||||
@@ -1046,11 +952,6 @@ export default class AccountViewView extends Vue {
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.isSearchAreasSet = !!settings.searchBoxes;
|
||||
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.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> {
|
||||
const newSetting = !this.hideRegisterPromptOnNewContact;
|
||||
await this.$saveSettings({
|
||||
@@ -1229,19 +1049,8 @@ export default class AccountViewView extends Vue {
|
||||
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
||||
}
|
||||
|
||||
public async turnOffNotifyingFlags(): Promise<void> {
|
||||
// should tell the push server as well
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
}
|
||||
// Note: Notification state management has been migrated to NotificationSection component
|
||||
// which handles its own lifecycle and persistence via PlatformServiceMixin
|
||||
|
||||
/**
|
||||
* Asynchronously exports the database into a downloadable JSON file.
|
||||
|
||||
Reference in New Issue
Block a user