Compare commits
7 Commits
1.1.2
...
wip_new_no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464a825a7b | ||
|
|
0dcb1d029e | ||
|
|
79b226e7d2 | ||
|
|
74e4a20ef9 | ||
|
|
cfeb920493 | ||
|
|
a427a9e66f | ||
|
|
5110c17fba |
@@ -21,7 +21,7 @@ alwaysApply: false
|
||||
|
||||
## Purpose
|
||||
|
||||
All interactions must *increase the human's competence over time* while
|
||||
All interactions must _increase the human's competence over time_ while
|
||||
completing the task efficiently. The model may handle menial work and memory
|
||||
extension, but must also promote learning, autonomy, and healthy work habits.
|
||||
The model should also **encourage human interaction and collaboration** rather
|
||||
@@ -31,7 +31,7 @@ machine-driven steps.
|
||||
|
||||
## Principles
|
||||
|
||||
1. Competence over convenience: finish the task *and* leave the human more
|
||||
1. Competence over convenience: finish the task _and_ leave the human more
|
||||
|
||||
capable next time.
|
||||
|
||||
@@ -75,7 +75,7 @@ assumptions if unanswered.
|
||||
|
||||
### timebox_minutes
|
||||
|
||||
*integer or null* — When set to a positive integer (e.g., `5`), this acts
|
||||
_integer or null_ — When set to a positive integer (e.g., `5`), this acts
|
||||
as a **time budget** guiding the model to prioritize delivering the most
|
||||
essential parts of the task within that constraint.
|
||||
|
||||
@@ -91,7 +91,7 @@ Behavior when set:
|
||||
|
||||
3. **Signal Skipped Depth** — Omitted details should be listed under
|
||||
|
||||
*Deferred for depth*.
|
||||
_Deferred for depth_.
|
||||
|
||||
4. **Order by Value** — Start with blocking or high-value items, then
|
||||
|
||||
@@ -198,7 +198,7 @@ Default: Doer + short Mentor notes.
|
||||
|
||||
## Self-Check (model, before responding)
|
||||
|
||||
- [ ] Task done *and* at least one competence lever included (≤120 words
|
||||
- [ ] Task done _and_ at least one competence lever included (≤120 words
|
||||
total)
|
||||
- [ ] At least one collaboration/discussion hook present
|
||||
- [ ] Output follows the **Output Contract** sections
|
||||
|
||||
@@ -53,7 +53,7 @@ evidence-backed steps**.
|
||||
- **Verifiable Outputs**: Include expected results, status codes, or
|
||||
error messages
|
||||
|
||||
- **Cite evidence** for *Works/Doesn't* items (timestamps, filenames,
|
||||
- **Cite evidence** for _Works/Doesn't_ items (timestamps, filenames,
|
||||
line numbers, IDs/status codes, or logs).
|
||||
|
||||
## Required Sections
|
||||
@@ -181,8 +181,8 @@ Before publishing, verify:
|
||||
|
||||
---
|
||||
|
||||
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human
|
||||
Competence First*
|
||||
**Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human
|
||||
Competence First_
|
||||
|
||||
**Priority**: Critical
|
||||
**Estimated Effort**: Ongoing reference
|
||||
|
||||
@@ -12,12 +12,11 @@ language: Match repository languages and conventions
|
||||
|
||||
## Rules
|
||||
|
||||
0. **Principle:** just the facts m'am.
|
||||
1. **Default to the least complex solution.** Fix the problem directly
|
||||
where it occurs; avoid new layers, indirection, or patterns unless
|
||||
strictly necessary.
|
||||
2. **Keep scope tight.** Implement only what is needed to satisfy the
|
||||
acceptance criteria and tests for *this* issue.
|
||||
acceptance criteria and tests for _this_ issue.
|
||||
3. **Avoid speculative abstractions.** Use the **Rule of Three**:
|
||||
don't extract helpers/patterns until the third concrete usage proves
|
||||
the shape.
|
||||
@@ -30,7 +29,7 @@ language: Match repository languages and conventions
|
||||
7. **Targeted tests only.** Add the smallest set of tests that prove
|
||||
the fix and guard against regression; don't rewrite suites.
|
||||
8. **Document the "why enough."** Include a one-paragraph note
|
||||
explaining why this minimal solution is sufficient *now*.
|
||||
explaining why this minimal solution is sufficient _now_.
|
||||
|
||||
## Future-Proofing Requires Evidence + Discussion
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ alwaysApply: false
|
||||
**Date**: 2025-08-19
|
||||
**Status**: 🎯 **ACTIVE** - Asset management guidelines
|
||||
|
||||
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build
|
||||
orchestration*
|
||||
_Scope: Assets Only (icons, splashes, image pipelines) — not overall build
|
||||
orchestration_
|
||||
|
||||
## Intent
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
globs: **/src/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
✅ use system date command to timestamp all documentation with accurate date and
|
||||
✅ use system date command to timestamp all interactions with accurate date and
|
||||
time
|
||||
✅ python script files must always have a blank line at their end
|
||||
✅ remove whitespace at the end of lines
|
||||
✅ use npm run lint-fix to check for warnings
|
||||
✅ do not use npm run dev let me handle running and supplying feedback
|
||||
@@ -21,10 +22,12 @@ alwaysApply: false
|
||||
|
||||
- [ ] **Timestamp Usage**: Include accurate timestamps in all interactions
|
||||
- [ ] **Code Quality**: Use npm run lint-fix to check for warnings
|
||||
- [ ] **File Standards**: Ensure Python files have blank line at end
|
||||
- [ ] **Whitespace**: Remove trailing whitespace from all lines
|
||||
|
||||
### After Development
|
||||
|
||||
- [ ] **Linting Check**: Run npm run lint-fix to verify code quality
|
||||
- [ ] **File Validation**: Confirm Python files end with blank line
|
||||
- [ ] **Whitespace Review**: Verify no trailing whitespace remains
|
||||
- [ ] **Documentation**: Update relevant documentation with changes
|
||||
|
||||
@@ -40,7 +40,7 @@ feature development, issue investigations, ADRs, and documentation**.
|
||||
|
||||
`2025-08-17`).
|
||||
|
||||
- Avoid ambiguous terms like *recently*, *last month*, or *soon*.
|
||||
- Avoid ambiguous terms like _recently_, _last month_, or _soon_.
|
||||
|
||||
- For time-based experiments (e.g., A/B tests), always include:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
- Optionally provide UTC alongside if context requires cross-team clarity.
|
||||
|
||||
- When interpreting relative terms like *now*, *today*, *last week*:
|
||||
- When interpreting relative terms like _now_, _today_, _last week_:
|
||||
|
||||
- Resolve them against the **developer's current time**.
|
||||
|
||||
|
||||
96
.cursor/rules/docs/documentation_references_model_agents.mdc
Normal file
96
.cursor/rules/docs/documentation_references_model_agents.mdc
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Documentation, References, and Model Agent Use
|
||||
version: 1.1
|
||||
alwaysApply: true
|
||||
scope: code, project-plans
|
||||
---
|
||||
|
||||
# Directive on Documentation, References, and Model Agent Use in Code and Project Plans
|
||||
|
||||
To ensure clarity, efficiency, and high-value documentation within code and project plans—and to leverage **model agents** (AI- or automation-based assistants) effectively—contributors must follow these rules:
|
||||
|
||||
---
|
||||
|
||||
## 1. Documentation and References Must Add Clear Value
|
||||
|
||||
- Only include documentation, comments, or reference links when they provide _new, meaningful information_ that assists understanding or decision-making.
|
||||
- Avoid duplicating content already obvious in the codebase, version history, or linked project documents.
|
||||
|
||||
---
|
||||
|
||||
## 2. Eliminate Redundant or Noisy References
|
||||
|
||||
- Remove references that serve no purpose beyond filling space.
|
||||
- Model agents may automatically flag and suggest removal of trivial references (e.g., links to unchanged boilerplate or self-evident context).
|
||||
|
||||
---
|
||||
|
||||
## 3. Explicit Role of Model Agents
|
||||
|
||||
Model agents are **active participants** in documentation quality control. Their tasks include:
|
||||
|
||||
- **Relevance Evaluation**: Automatically analyze references for their substantive contribution before inclusion.
|
||||
- **Redundancy Detection**: Flag duplicate or trivial references across commits, files, or tasks.
|
||||
- **Context Linking**: Suggest appropriate higher-level docs (designs, ADRs, meeting notes) when a code change touches multi-stage or cross-team items.
|
||||
- **Placement Optimization**: Recommend centralization of references (e.g., in plan overviews, ADRs, or merge commit messages) rather than scattered low-value inline references.
|
||||
- **Consistency Monitoring**: Ensure references align with team standards (e.g., ADR template, architecture repo, or external policy documents).
|
||||
|
||||
Contributors must treat agent recommendations as **first-pass reviews** but remain accountable for final human judgment.
|
||||
|
||||
---
|
||||
|
||||
## 4. Contextual References for Complex Items
|
||||
|
||||
- Use **centralized references** for multi-stage features (e.g., architectural docs, research threads).
|
||||
- Keep inline code comments light; push broader context into centralized documents.
|
||||
- Model agents may auto-summarize complex chains of discussion and attach them as a single reference point.
|
||||
|
||||
---
|
||||
|
||||
## 5. Centralization of Broader Context
|
||||
|
||||
- Store overarching context (design docs, proposals, workflows) in accessible, well-indexed places.
|
||||
- Model agents should assist by **generating reference maps** that track where docs are cited across the codebase.
|
||||
|
||||
---
|
||||
|
||||
## 6. Focused Documentation
|
||||
|
||||
- Documentation should explain **why** and **how** decisions are made, not just what was changed.
|
||||
- Model agents can auto-generate first-pass explanations from commit metadata, diffs, and linked issues—but humans must refine them for accuracy and intent.
|
||||
|
||||
---
|
||||
|
||||
## 7. Review and Accountability
|
||||
|
||||
- Reviewers and team leads must reject submissions containing unnecessary or low-quality documentation.
|
||||
- Model agent outputs are aids, not replacements—contributors remain responsible for **final clarity and relevance**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Continuous Improvement and Agent Feedback Loops
|
||||
|
||||
- Encourage iterative development of model agents so their evaluations become more precise over time.
|
||||
- Contributions should include **feedback on agent suggestions** (e.g., accepted, rejected, or corrected) to train better future outputs.
|
||||
- Agents should log patterns of “rejected” suggestions for refinement.
|
||||
|
||||
---
|
||||
|
||||
## 9. Workflow Overview (Mermaid Diagram)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Contributor] -->|Writes Code & Draft Docs| B[Model Agent]
|
||||
B -->|Evaluates References| C{Relevant?}
|
||||
C -->|Yes| D[Suggest Placement & Context Links]
|
||||
C -->|No| E[Flag Redundancy / Noise]
|
||||
D --> F[Contributor Refines Docs]
|
||||
E --> F
|
||||
F --> G[Reviewer]
|
||||
G -->|Approves / Requests Revisions| H[Final Documentation]
|
||||
G -->|Feedback on Agent Suggestions| B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
✅ **Outcome:** By integrating disciplined contributor standards with **model agent augmentation**, the team achieves documentation that is consistently _relevant, concise, centralized, and decision-focused_. AI ensures coverage and noise reduction, while humans ensure precision and judgment.
|
||||
@@ -192,6 +192,7 @@ Summary of key concepts and skills.
|
||||
|
||||
Where to apply this knowledge next.
|
||||
```
|
||||
|
||||
- [ ] Integration tests
|
||||
- [ ] E2E tests
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
alwaysApply: true
|
||||
inherits: base_context.mdc
|
||||
---
|
||||
```json
|
||||
{
|
||||
@@ -15,9 +16,10 @@ alwaysApply: false
|
||||
|
||||
**Author**: System/Shared
|
||||
**Date**: 2025-08-21 (UTC)
|
||||
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human Competence First*
|
||||
**Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human Competence First_
|
||||
|
||||
> **Alignment with Base Context**
|
||||
>
|
||||
> - **Purpose fit**: Prioritizes human competence and collaboration while delivering reproducible artifacts.
|
||||
> - **Output Contract**: This directive **adds universal constraints** for any technical topic while **inheriting** the Base Context contract sections.
|
||||
> - **Toggles honored**: Uses the same toggle semantics; defaults above can be overridden by the caller.
|
||||
@@ -25,9 +27,11 @@ alwaysApply: false
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Produce a **developer-grade, reproducible guide** for any technical topic that onboards a competent practitioner **without meta narration** and **with evidence-backed steps**.
|
||||
|
||||
## Scope & Constraints
|
||||
|
||||
- **One Markdown document** as the deliverable.
|
||||
- Use **absolute dates** in **UTC** (e.g., `2025-08-21T14:22Z`) — avoid “today/yesterday”.
|
||||
- Include at least **one diagram** (Mermaid preferred). Choose the most fitting type:
|
||||
@@ -36,10 +40,11 @@ Produce a **developer-grade, reproducible guide** for any technical topic that o
|
||||
- **APIs**: `curl` + one client library (e.g., `httpx` for Python).
|
||||
- **CLIs**: literal command blocks and expected output snippets.
|
||||
- **Code**: minimal, self-contained samples (language appropriate).
|
||||
- Cite **evidence** for *Works/Doesn’t* items (timestamps, filenames, line numbers, IDs/status codes, or logs).
|
||||
- Cite **evidence** for _Works/Doesn’t_ items (timestamps, filenames, line numbers, IDs/status codes, or logs).
|
||||
- If something is unknown, output `TODO:<missing>` — **never invent**.
|
||||
|
||||
## Required Sections (extends Base Output Contract)
|
||||
|
||||
Follow this exact order **after** the Base Contract’s **Objective → Result → Use/Run** headers:
|
||||
|
||||
1. **Context & Scope**
|
||||
@@ -51,9 +56,9 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
4. **Architecture / Process Overview**
|
||||
- Short prose + **one diagram** selected from the list above.
|
||||
5. **Interfaces & Contracts (choose one)**
|
||||
- **API-based**: Endpoint table (*Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref*).
|
||||
- **Data/Files**: I/O contract table (*Source, Format, Schema/Columns, Size, Validation rules*).
|
||||
- **Systems/Hardware**: Interfaces table (*Port/Bus, Protocol, Voltage/Timing, Constraints*).
|
||||
- **API-based**: Endpoint table (_Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref_).
|
||||
- **Data/Files**: I/O contract table (_Source, Format, Schema/Columns, Size, Validation rules_).
|
||||
- **Systems/Hardware**: Interfaces table (_Port/Bus, Protocol, Voltage/Timing, Constraints_).
|
||||
6. **Repro: End-to-End Procedure**
|
||||
- Minimal copy-paste steps with code/commands and **expected outputs**.
|
||||
7. **What Works (with Evidence)**
|
||||
@@ -68,16 +73,19 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
- Canonical docs, specs, tickets, prior analyses.
|
||||
|
||||
> **Competence Hooks (per Base Context; keep lightweight):**
|
||||
> - *Why this works* (≤3 bullets) — core invariants or guarantees.
|
||||
> - *Common pitfalls* (≤3 bullets) — the traps we saw in evidence.
|
||||
> - *Next skill unlock* (1 line) — the next capability to implement/learn.
|
||||
> - *Teach-back* (1 line) — prompt the reader to restate the flow/architecture.
|
||||
>
|
||||
> - _Why this works_ (≤3 bullets) — core invariants or guarantees.
|
||||
> - _Common pitfalls_ (≤3 bullets) — the traps we saw in evidence.
|
||||
> - _Next skill unlock_ (1 line) — the next capability to implement/learn.
|
||||
> - _Teach-back_ (1 line) — prompt the reader to restate the flow/architecture.
|
||||
|
||||
> **Collaboration Hooks (per Base Context):**
|
||||
>
|
||||
> - Name reviewers for **Interfaces & Contracts** and the **diagram**.
|
||||
> - Short **sign-off checklist** before merging/publishing the guide.
|
||||
|
||||
## Do / Don’t (Base-aligned)
|
||||
|
||||
- **Do** quantify progress only against a defined scope with acceptance criteria.
|
||||
- **Do** include minimal sample payloads/headers or I/O schemas; redact sensitive values.
|
||||
- **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**.
|
||||
@@ -85,6 +93,7 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
- **Don’t** include IDE-specific chatter or internal rules unrelated to the task.
|
||||
|
||||
## Validation Checklist (self-check before returning)
|
||||
|
||||
- [ ] All Required Sections present and ordered.
|
||||
- [ ] Diagram compiles (basic Mermaid syntax) and fits the problem.
|
||||
- [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint.
|
||||
@@ -95,6 +104,7 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
- [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References).
|
||||
|
||||
## Universal Template (fill-in)
|
||||
|
||||
```markdown
|
||||
# <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ)
|
||||
|
||||
@@ -131,37 +141,46 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
```
|
||||
|
||||
## Interfaces & Contracts
|
||||
|
||||
### If API-based
|
||||
|
||||
| Step | Method | Path/URL | Auth | Key Headers/Params | Sample |
|
||||
|---|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> | below |
|
||||
|
||||
### If Data/Files
|
||||
|
||||
| Source | Format | Schema/Columns | Size | Validation |
|
||||
|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> |
|
||||
|
||||
### If Systems/Hardware
|
||||
|
||||
| Interface | Protocol | Timing/Voltage | Constraints | Notes |
|
||||
|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> |
|
||||
|
||||
## Repro: End-to-End Procedure
|
||||
|
||||
```bash
|
||||
# commands / curl examples (redacted where necessary)
|
||||
```
|
||||
|
||||
```python
|
||||
# minimal client library example (language appropriate)
|
||||
```
|
||||
|
||||
> Expected output: <snippet/checks>
|
||||
|
||||
## What Works (Evidence)
|
||||
|
||||
- ✅ <short statement>
|
||||
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||
- **Evidence**: file/line/log or request id/status
|
||||
- **Verify at**: <where>
|
||||
|
||||
## What Doesn’t (Evidence & Hypotheses)
|
||||
|
||||
- ❌ <short failure> at `<component/endpoint/file>`
|
||||
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||
- **Evidence**: <snippet/id/status>
|
||||
@@ -169,38 +188,46 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
||||
- **Next probe**: <short>
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
|
||||
<bullets: limits, security boundaries, retries/backoff, idempotency, SLOs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| <name> | <action> | <measurable outcome> | <YYYY-MM-DD> |
|
||||
|
||||
## References
|
||||
|
||||
<links/titles>
|
||||
|
||||
## Competence Hooks
|
||||
- *Why this works*: <≤3 bullets>
|
||||
- *Common pitfalls*: <≤3 bullets>
|
||||
- *Next skill unlock*: <1 line>
|
||||
- *Teach-back*: <1 line>
|
||||
|
||||
- _Why this works_: <≤3 bullets>
|
||||
- _Common pitfalls_: <≤3 bullets>
|
||||
- _Next skill unlock_: <1 line>
|
||||
- _Teach-back_: <1 line>
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- Reviewers: <names/roles>
|
||||
- Sign-off checklist: <≤5 checks>
|
||||
|
||||
## Assumptions & Limits
|
||||
|
||||
<bullets>
|
||||
|
||||
## Deferred for depth
|
||||
|
||||
<park deeper material here to respect timeboxing>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
||||
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
|
||||
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
|
||||
|
||||
@@ -82,6 +82,7 @@ common investigation pitfalls.
|
||||
### **Safe Diagnosis Commands**
|
||||
|
||||
✅ **Safe to use during diagnosis:**
|
||||
|
||||
- `npm run lint-fix` - Syntax and style checking
|
||||
- `npm run type-check` - TypeScript validation (if available)
|
||||
- `git status` - Version control status
|
||||
@@ -90,6 +91,7 @@ common investigation pitfalls.
|
||||
- `grep_search` - Text pattern searching
|
||||
|
||||
❌ **Never use during diagnosis:**
|
||||
|
||||
- `npm run build:web` - Blocks chat
|
||||
- `npm run build:electron` - Blocks chat
|
||||
- `npm run build:capacitor` - Blocks chat
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Meta-Rule: Core Always-On Rules
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
@@ -35,6 +36,7 @@ that are essential for all AI interactions.
|
||||
**This meta-rule enforces current workflow mode constraints for all interactions:**
|
||||
|
||||
### **Current Workflow State**
|
||||
|
||||
```json
|
||||
{
|
||||
"workflowState": {
|
||||
@@ -61,26 +63,31 @@ that are essential for all AI interactions.
|
||||
### **Mode-Specific Enforcement**
|
||||
|
||||
**Diagnosis Mode (read_only):**
|
||||
|
||||
- ❌ **Forbidden**: File modification, code creation, build commands, git commits
|
||||
- ✅ **Allowed**: File reading, code analysis, investigation, documentation
|
||||
- **Response**: Guide user toward investigation and analysis, not implementation
|
||||
|
||||
**Fixing Mode (implementation):**
|
||||
|
||||
- ✅ **Allowed**: File modification, code creation, build commands, testing, git commits
|
||||
- ❌ **Forbidden**: None (full implementation mode)
|
||||
- **Response**: Proceed with implementation and testing
|
||||
|
||||
**Planning Mode (design_only):**
|
||||
|
||||
- ❌ **Forbidden**: Implementation, coding, building, deployment
|
||||
- ✅ **Allowed**: Analysis, design, estimation, documentation, architecture
|
||||
- **Response**: Focus on planning and design, not implementation
|
||||
|
||||
**Research Mode (investigation):**
|
||||
|
||||
- ❌ **Forbidden**: File modification, implementation, deployment
|
||||
- ✅ **Allowed**: Investigation, analysis, research, documentation
|
||||
- **Response**: Focus on investigation and analysis
|
||||
|
||||
**Documentation Mode (writing_only):**
|
||||
|
||||
- ❌ **Forbidden**: Implementation, coding, building, deployment
|
||||
- ✅ **Allowed**: Writing, editing, formatting, structuring, reviewing
|
||||
- **Response**: Focus on documentation creation and improvement
|
||||
@@ -293,6 +300,9 @@ or context. They form the foundation for all AI assistant behavior.
|
||||
**See also**:
|
||||
|
||||
- `.cursor/rules/meta_feature_planning.mdc` for workflow-specific rules
|
||||
- `.cursor/rules/meta_bug_diagnosis.mdc` for investigation workflows
|
||||
- `.cursor/rules/meta_bug_fixing.mdc` for fix implementation
|
||||
- `.cursor/rules/meta_feature_implementation.mdc` for feature development
|
||||
|
||||
**Status**: Active core always-on meta-rule
|
||||
**Priority**: Critical (applies to every prompt)
|
||||
|
||||
@@ -51,6 +51,7 @@ providing technical descriptions.
|
||||
## When to Use
|
||||
|
||||
**Use this meta-rule when**:
|
||||
|
||||
- Writing new documentation
|
||||
- Updating existing documentation
|
||||
- Creating technical guides
|
||||
@@ -107,6 +108,7 @@ providing technical descriptions.
|
||||
### **Document Structure**
|
||||
|
||||
**Mandatory Sections**:
|
||||
|
||||
- **Overview**: Clear purpose and scope with educational context
|
||||
- **Why This Matters**: Business value and user benefit explanation
|
||||
- **Core Concepts**: Fundamental understanding before implementation
|
||||
@@ -116,6 +118,7 @@ providing technical descriptions.
|
||||
- **Next Steps**: Where to go from here
|
||||
|
||||
**Optional Sections**:
|
||||
|
||||
- **Background**: Historical context and evolution
|
||||
- **Alternatives**: Other approaches and trade-offs
|
||||
- **Advanced Topics**: Deep dive into complex scenarios
|
||||
@@ -124,6 +127,7 @@ providing technical descriptions.
|
||||
### **Writing Style**
|
||||
|
||||
**Educational Approach**:
|
||||
|
||||
- **Conversational tone**: Write as if explaining to a colleague
|
||||
- **Progressive disclosure**: Start simple, add complexity gradually
|
||||
- **Active voice**: "You can do this" not "This can be done"
|
||||
@@ -131,6 +135,7 @@ providing technical descriptions.
|
||||
- **Analogies**: Use familiar concepts to explain complex ideas
|
||||
|
||||
**Technical Accuracy**:
|
||||
|
||||
- **Precise language**: Use exact technical terms consistently
|
||||
- **Code examples**: Working, tested code snippets
|
||||
- **Version information**: Specify applicable versions and platforms
|
||||
@@ -139,6 +144,7 @@ providing technical descriptions.
|
||||
### **Content Quality Standards**
|
||||
|
||||
**Educational Value**:
|
||||
|
||||
- [ ] **Concept clarity**: Reader understands the fundamental idea
|
||||
- [ ] **Context relevance**: Reader knows when to apply the knowledge
|
||||
- [ ] **Practical application**: Reader can implement the solution
|
||||
@@ -146,6 +152,7 @@ providing technical descriptions.
|
||||
- [ ] **Next steps**: Reader knows where to continue learning
|
||||
|
||||
**Technical Accuracy**:
|
||||
|
||||
- [ ] **Fact verification**: All technical details are correct
|
||||
- [ ] **Code validation**: Examples compile and run correctly
|
||||
- [ ] **Version compatibility**: Platform and version requirements clear
|
||||
@@ -183,6 +190,7 @@ providing technical descriptions.
|
||||
### **Review Checklist**
|
||||
|
||||
**Educational Quality**:
|
||||
|
||||
- [ ] **Clear learning objective**: What will the reader learn?
|
||||
- [ ] **Appropriate complexity**: Matches target audience knowledge
|
||||
- [ ] **Progressive disclosure**: Information builds logically
|
||||
@@ -190,6 +198,7 @@ providing technical descriptions.
|
||||
- [ ] **Common questions**: Anticipates and answers reader questions
|
||||
|
||||
**Technical Quality**:
|
||||
|
||||
- [ ] **Accuracy**: All technical details verified
|
||||
- [ ] **Completeness**: Covers all necessary information
|
||||
- [ ] **Consistency**: Terminology and formatting consistent
|
||||
|
||||
@@ -9,26 +9,31 @@ alwaysApply: false
|
||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||
|
||||
## Objective
|
||||
|
||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||
|
||||
## Context & Scope
|
||||
|
||||
- **Audience**: Developers debugging Playwright test failures
|
||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||
|
||||
## Artifacts & Links
|
||||
|
||||
- Test results: `test-results/` directory
|
||||
- Error context: `error-context.md` files with page snapshots
|
||||
- Trace files: `trace.zip` files for failed tests
|
||||
- HTML reports: Interactive test reports with screenshots
|
||||
|
||||
## Environment & Preconditions
|
||||
|
||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||
- Versions: Playwright test framework, browser drivers
|
||||
- Services: Local test server (localhost:8080), test data setup
|
||||
- Auth mode: None required for test investigation
|
||||
|
||||
## Architecture / Process Overview
|
||||
|
||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||
|
||||
```mermaid
|
||||
@@ -57,6 +62,7 @@ flowchart TD
|
||||
## Interfaces & Contracts
|
||||
|
||||
### Test Results Structure
|
||||
|
||||
| Component | Format | Content | Validation |
|
||||
|---|---|---|---|
|
||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||
@@ -65,6 +71,7 @@ flowchart TD
|
||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||
|
||||
### Investigation Commands
|
||||
|
||||
| Step | Command | Expected Output | Notes |
|
||||
|---|---|---|---|
|
||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||
@@ -74,6 +81,7 @@ flowchart TD
|
||||
## Repro: End-to-End Investigation Procedure
|
||||
|
||||
### 1. Locate Failed Test Results
|
||||
|
||||
```bash
|
||||
# Find all results for a specific test
|
||||
find test-results -name "*test-name*" -type d
|
||||
@@ -83,6 +91,7 @@ find test-results -name "error-context.md" | head -5
|
||||
```
|
||||
|
||||
### 2. Analyze Error Context
|
||||
|
||||
```bash
|
||||
# Read error context for specific test
|
||||
cat test-results/test-name-test-description-browser/error-context.md
|
||||
@@ -92,6 +101,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||
```
|
||||
|
||||
### 3. Check Trace Files
|
||||
|
||||
```bash
|
||||
# List available trace files
|
||||
find test-results -name "*.zip" | grep trace
|
||||
@@ -101,6 +111,7 @@ npx playwright show-trace test-results/test-name/trace.zip
|
||||
```
|
||||
|
||||
### 4. Investigate Selector Issues
|
||||
|
||||
```typescript
|
||||
// Check for multiple elements with same text
|
||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||
@@ -110,6 +121,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
```
|
||||
|
||||
## What Works (Evidence)
|
||||
|
||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||
@@ -126,6 +138,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
- **Verify at**: Error context markdown files
|
||||
|
||||
## What Doesn't (Evidence & Hypotheses)
|
||||
|
||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||
@@ -139,12 +152,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
|
||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||
- **Test isolation**: Shared state between tests may affect investigation results
|
||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||
@@ -152,21 +167,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||
|
||||
## References
|
||||
|
||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||
|
||||
## Competence Hooks
|
||||
|
||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: QA team, test automation engineers
|
||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||
|
||||
## Assumptions & Limits
|
||||
|
||||
- Test results directory structure follows Playwright conventions
|
||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||
- Error context files contain valid YAML page snapshots
|
||||
@@ -178,6 +197,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
**Priority**: High
|
||||
**Maintainer**: Development team
|
||||
**Next Review**: 2025-09-21
|
||||
|
||||
# Playwright Test Investigation — Harbor Pilot Directive
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
@@ -185,26 +205,31 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||
|
||||
## Objective
|
||||
|
||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||
|
||||
## Context & Scope
|
||||
|
||||
- **Audience**: Developers debugging Playwright test failures
|
||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||
|
||||
## Artifacts & Links
|
||||
|
||||
- Test results: `test-results/` directory
|
||||
- Error context: `error-context.md` files with page snapshots
|
||||
- Trace files: `trace.zip` files for failed tests
|
||||
- HTML reports: Interactive test reports with screenshots
|
||||
|
||||
## Environment & Preconditions
|
||||
|
||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||
- Versions: Playwright test framework, browser drivers
|
||||
- Services: Local test server (localhost:8080), test data setup
|
||||
- Auth mode: None required for test investigation
|
||||
|
||||
## Architecture / Process Overview
|
||||
|
||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||
|
||||
```mermaid
|
||||
@@ -233,6 +258,7 @@ flowchart TD
|
||||
## Interfaces & Contracts
|
||||
|
||||
### Test Results Structure
|
||||
|
||||
| Component | Format | Content | Validation |
|
||||
|---|---|---|---|
|
||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||
@@ -241,6 +267,7 @@ flowchart TD
|
||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||
|
||||
### Investigation Commands
|
||||
|
||||
| Step | Command | Expected Output | Notes |
|
||||
|---|---|---|---|
|
||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||
@@ -250,6 +277,7 @@ flowchart TD
|
||||
## Repro: End-to-End Investigation Procedure
|
||||
|
||||
### 1. Locate Failed Test Results
|
||||
|
||||
```bash
|
||||
# Find all results for a specific test
|
||||
find test-results -name "*test-name*" -type d
|
||||
@@ -259,6 +287,7 @@ find test-results -name "error-context.md" | head -5
|
||||
```
|
||||
|
||||
### 2. Analyze Error Context
|
||||
|
||||
```bash
|
||||
# Read error context for specific test
|
||||
cat test-results/test-name-test-description-browser/error-context.md
|
||||
@@ -268,6 +297,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||
```
|
||||
|
||||
### 3. Check Trace Files
|
||||
|
||||
```bash
|
||||
# List available trace files
|
||||
find test-results -name "*.zip" | grep trace
|
||||
@@ -277,6 +307,7 @@ npx playwright show-trace test-results/test-name/trace.zip
|
||||
```
|
||||
|
||||
### 4. Investigate Selector Issues
|
||||
|
||||
```typescript
|
||||
// Check for multiple elements with same text
|
||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||
@@ -286,6 +317,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
```
|
||||
|
||||
## What Works (Evidence)
|
||||
|
||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||
@@ -302,6 +334,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
- **Verify at**: Error context markdown files
|
||||
|
||||
## What Doesn't (Evidence & Hypotheses)
|
||||
|
||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||
@@ -315,12 +348,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
|
||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||
- **Test isolation**: Shared state between tests may affect investigation results
|
||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||
@@ -328,21 +363,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
|
||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||
|
||||
## References
|
||||
|
||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||
|
||||
## Competence Hooks
|
||||
|
||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: QA team, test automation engineers
|
||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||
|
||||
## Assumptions & Limits
|
||||
|
||||
- Test results directory structure follows Playwright conventions
|
||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||
- Error context files contain valid YAML page snapshots
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
### Avoid
|
||||
|
||||
- Vague: *improved, enhanced, better*
|
||||
- Vague: _improved, enhanced, better_
|
||||
|
||||
- Trivialities: tiny docs, one-liners, pure lint cleanups (separate,
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
**Status**: 🎯 **ACTIVE** - Version control guidelines
|
||||
|
||||
## Core Principles
|
||||
### 0) let the developer control git
|
||||
|
||||
### 1) Version-Control Ownership
|
||||
|
||||
- **MUST NOT** run `git add`, `git commit`, or any write action.
|
||||
|
||||
@@ -9,10 +9,6 @@ echo "🔍 Running pre-commit hooks..."
|
||||
|
||||
# Run lint-fix first
|
||||
echo "📝 Running lint-fix..."
|
||||
|
||||
# Capture git status before lint-fix to detect changes
|
||||
git_status_before=$(git status --porcelain)
|
||||
|
||||
npm run lint-fix || {
|
||||
echo
|
||||
echo "❌ Linting failed. Please fix the issues and try again."
|
||||
@@ -22,47 +18,16 @@ npm run lint-fix || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if lint-fix made any changes
|
||||
git_status_after=$(git status --porcelain)
|
||||
|
||||
if [ "$git_status_before" != "$git_status_after" ]; then
|
||||
echo
|
||||
echo "⚠️ lint-fix made changes to your files!"
|
||||
echo "📋 Changes detected:"
|
||||
git diff --name-only
|
||||
echo
|
||||
echo "❓ What would you like to do?"
|
||||
echo " [c] Continue commit without the new changes"
|
||||
echo " [a] Abort commit (recommended - review and stage the changes)"
|
||||
echo
|
||||
printf "Choose [c/a]: "
|
||||
# The `< /dev/tty` is necessary to make read work in git's non-interactive shell
|
||||
read choice < /dev/tty
|
||||
|
||||
case $choice in
|
||||
[Cc]* )
|
||||
echo "✅ Continuing commit without lint-fix changes..."
|
||||
sleep 3
|
||||
;;
|
||||
[Aa]* | * )
|
||||
echo "🛑 Commit aborted. Please review the changes made by lint-fix."
|
||||
echo "💡 You can stage the changes with 'git add .' and commit again."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Then run Build Architecture Guard
|
||||
|
||||
#echo "🏗️ Running Build Architecture Guard..."
|
||||
#bash ./scripts/build-arch-guard.sh --staged || {
|
||||
# echo
|
||||
# echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
|
||||
# echo "💡 To bypass this check for emergency commits, use:"
|
||||
# echo " git commit --no-verify"
|
||||
# echo
|
||||
# exit 1
|
||||
#}
|
||||
# Build Architecture Guard - DISABLED
|
||||
# echo "🏗️ Running Build Architecture Guard..."
|
||||
# bash ./scripts/build-arch-guard.sh --staged || {
|
||||
# echo
|
||||
# echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
|
||||
# echo "💡 To bypass this check for emergency commits, use:"
|
||||
# echo " git commit --no-verify"
|
||||
# echo
|
||||
# exit 1
|
||||
# }
|
||||
|
||||
echo "✅ All pre-commit checks passed!"
|
||||
|
||||
|
||||
@@ -5,23 +5,28 @@
|
||||
#
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
echo "🔍 Running Build Architecture Guard (pre-push)..."
|
||||
echo "🔍 Pre-push checks..."
|
||||
|
||||
# Get the remote branch we're pushing to
|
||||
REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)"
|
||||
# Build Architecture Guard - DISABLED
|
||||
# echo "🔍 Running Build Architecture Guard (pre-push)..."
|
||||
#
|
||||
# # Get the remote branch we're pushing to
|
||||
# REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)"
|
||||
#
|
||||
# # Check if remote branch exists
|
||||
# if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
|
||||
# RANGE="$REMOTE_BRANCH...HEAD"
|
||||
# else
|
||||
# # If remote branch doesn't exist, check last commit
|
||||
# RANGE="HEAD~1..HEAD"
|
||||
# fi
|
||||
#
|
||||
# bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
|
||||
# echo
|
||||
# echo "💡 To bypass this check for emergency pushes, use:"
|
||||
# echo " git push --no-verify"
|
||||
# echo
|
||||
# exit 1
|
||||
# }
|
||||
|
||||
# Check if remote branch exists
|
||||
if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
|
||||
RANGE="$REMOTE_BRANCH...HEAD"
|
||||
else
|
||||
# If remote branch doesn't exist, check last commit
|
||||
RANGE="HEAD~1..HEAD"
|
||||
fi
|
||||
|
||||
#bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
|
||||
# echo
|
||||
# echo "💡 To bypass this check for emergency pushes, use:"
|
||||
# echo " git push --no-verify"
|
||||
# echo
|
||||
# exit 1
|
||||
#}
|
||||
echo "✅ Pre-push checks passed!"
|
||||
|
||||
@@ -1,27 +1,56 @@
|
||||
{
|
||||
"MD013": {
|
||||
"line_length": 80,
|
||||
"code_blocks": false,
|
||||
"tables": false,
|
||||
"headings": false
|
||||
"MD013": false,
|
||||
"MD033": false,
|
||||
"MD041": false,
|
||||
"MD024": {
|
||||
"siblings_only": true
|
||||
},
|
||||
"MD029": {
|
||||
"style": "ordered"
|
||||
},
|
||||
"MD007": {
|
||||
"indent": 2
|
||||
},
|
||||
"MD012": {
|
||||
"maximum": 1
|
||||
},
|
||||
"MD012": true,
|
||||
"MD022": true,
|
||||
"MD025": true,
|
||||
"MD026": {
|
||||
"punctuation": ".,;:!"
|
||||
},
|
||||
"MD030": {
|
||||
"ul_single": 1,
|
||||
"ol_single": 1,
|
||||
"ul_multi": 1,
|
||||
"ol_multi": 1
|
||||
},
|
||||
"MD031": true,
|
||||
"MD032": true,
|
||||
"MD047": true,
|
||||
"MD009": true,
|
||||
"MD010": true,
|
||||
"MD004": { "style": "dash" },
|
||||
"MD029": { "style": "ordered" },
|
||||
"MD041": false,
|
||||
"MD025": false,
|
||||
"MD024": false,
|
||||
"MD034": true,
|
||||
"MD035": {
|
||||
"style": "---"
|
||||
},
|
||||
"MD036": false,
|
||||
"MD003": false,
|
||||
"MD040": false,
|
||||
"MD055": false,
|
||||
"MD056": false,
|
||||
"MD034": false,
|
||||
"MD023": false
|
||||
"MD037": true,
|
||||
"MD038": true,
|
||||
"MD039": true,
|
||||
"MD040": true,
|
||||
"MD042": true,
|
||||
"MD043": false,
|
||||
"MD044": false,
|
||||
"MD045": true,
|
||||
"MD046": {
|
||||
"style": "fenced"
|
||||
},
|
||||
"MD047": true,
|
||||
"MD048": {
|
||||
"style": "backtick"
|
||||
},
|
||||
"MD049": {
|
||||
"style": "underscore"
|
||||
},
|
||||
"MD050": {
|
||||
"style": "asterisk"
|
||||
}
|
||||
}
|
||||
88
BUILDING.md
88
BUILDING.md
@@ -93,6 +93,7 @@ The Build Architecture Guard protects your build system by enforcing documentati
|
||||
#### Protected File Patterns
|
||||
|
||||
The guard monitors these sensitive paths:
|
||||
|
||||
- `vite.config.*` - Build configuration
|
||||
- `scripts/**` - Build and utility scripts
|
||||
- `electron/**` - Desktop application code
|
||||
@@ -132,6 +133,7 @@ npm run guard:setup
|
||||
#### Troubleshooting
|
||||
|
||||
If you encounter `mapfile: command not found` errors:
|
||||
|
||||
```bash
|
||||
# Ensure script is executable
|
||||
chmod +x scripts/build-arch-guard.sh
|
||||
@@ -270,6 +272,7 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
|
||||
3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
|
||||
|
||||
**Why Use `serve`?**
|
||||
|
||||
- **Production Testing**: Test your optimized production build locally before deployment
|
||||
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
|
||||
- **Performance Testing**: Test the minified and optimized build locally
|
||||
@@ -335,15 +338,18 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
|
||||
The `serve` functionality provides a local HTTP server for testing production builds:
|
||||
|
||||
**What It Does:**
|
||||
|
||||
1. **Builds** the application using Vite
|
||||
2. **Serves** the built files from the `dist/` directory
|
||||
3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
|
||||
|
||||
**Server Options:**
|
||||
|
||||
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
|
||||
- **Fallback**: Python HTTP server (limited SPA routing support)
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
- Testing production builds before deployment
|
||||
- Validating SPA routing behavior
|
||||
- Performance testing of optimized builds
|
||||
@@ -365,8 +371,8 @@ current version to test DB migrations.
|
||||
- Put the commit hash in the changelog (which will help you remember to bump the
|
||||
version in the step later).
|
||||
|
||||
- Tag with the new version,
|
||||
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
|
||||
- Tag with the new version,
|
||||
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
|
||||
`git tag 1.0.2 && git push origin 1.0.2`.
|
||||
|
||||
- For test, build the app:
|
||||
@@ -1151,28 +1157,28 @@ If you need to build manually or want to understand the individual steps:
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx:
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
|
||||
##### 1. Bump the version in package.json, then here
|
||||
|
||||
```bash
|
||||
cd ios/App && xcrun agvtool new-version 47 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
```bash
|
||||
cd ios/App && xcrun agvtool new-version 40 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.7;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
|
||||
Here's prod. Also available: test, dev
|
||||
Here's prod. Also available: test, dev
|
||||
|
||||
```bash
|
||||
npm run build:ios:prod
|
||||
```
|
||||
```bash
|
||||
npm run build:ios:prod
|
||||
```
|
||||
|
||||
3.1. Use Xcode to build and run on simulator or device.
|
||||
|
||||
@@ -1185,14 +1191,14 @@ npm run build:ios:prod
|
||||
- Choose Product -> Archive
|
||||
- This will trigger a build and take time, needing user's "login" keychain
|
||||
password (user's login password), repeatedly.
|
||||
- If it fails with `building for 'iOS', but linking in dylib
|
||||
- If it fails with `building for 'iOS', but linking in dylib
|
||||
(.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run
|
||||
XCode outside that terminal (ie. not with `npx cap open ios`).
|
||||
- Click Distribute -> App Store Connect
|
||||
- In AppStoreConnect, add the build to the distribution. You may have to remove
|
||||
the current build with the "-" when you hover over it, then "Add Build" with the
|
||||
new build.
|
||||
- May have to go to App Review, click Submission, then hover over the build
|
||||
- May have to go to App Review, click Submission, then hover over the build
|
||||
and click "-".
|
||||
- It can take 15 minutes for the build to show up in the list of builds.
|
||||
- You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
@@ -1257,11 +1263,13 @@ npm run assets:validate
|
||||
##### What Gets Validated
|
||||
|
||||
**Source Assets (Required):**
|
||||
|
||||
- `resources/icon.png` - App icon source
|
||||
- `resources/splash.png` - Splash screen source
|
||||
- `resources/splash_dark.png` - Dark mode splash source
|
||||
|
||||
**Android Resources (Generated):**
|
||||
|
||||
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
|
||||
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
|
||||
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
|
||||
@@ -1315,26 +1323,26 @@ The recommended way to build for Android is using the automated build script:
|
||||
|
||||
#### Android Manual Build Process
|
||||
|
||||
##### 1. Bump the version in package.json, then update these versions & run:
|
||||
##### 1. Bump the version in package.json, then here: android/app/build.gradle
|
||||
|
||||
```bash
|
||||
perl -p -i -e 's/versionCode .*/versionCode 47/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.1.2"/g' android/app/build.gradle
|
||||
```
|
||||
```bash
|
||||
perl -p -i -e 's/versionCode .*/versionCode 40/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.0.7"/g' android/app/build.gradle
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
|
||||
Here's prod. Also available: test, dev
|
||||
|
||||
```bash
|
||||
npm run build:android:prod
|
||||
```
|
||||
```bash
|
||||
npm run build:android:prod
|
||||
```
|
||||
|
||||
##### 3. Open the project in Android Studio
|
||||
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
##### 4. Use Android Studio to build and run on emulator or device
|
||||
|
||||
@@ -2706,6 +2714,7 @@ configuration files in the repository.
|
||||
### 2025-08-21 - Cursor Rules Refactoring and Build System Updates
|
||||
|
||||
#### Package Dependencies Updated
|
||||
|
||||
- **Added**: `markdownlint-cli2` v0.18.1 - Modern markdown linting with improved performance
|
||||
- **Added**: `@commitlint/cli` v18.6.1 - Conventional commit message validation
|
||||
- **Added**: `@commitlint/config-conventional` v18.6.2 - Conventional commit standards
|
||||
@@ -2713,28 +2722,33 @@ configuration files in the repository.
|
||||
- **Updated**: `lint-staged` v15.2.2 - Pre-commit linting automation
|
||||
|
||||
#### Build Script Improvements
|
||||
|
||||
- **Markdown Linting**: Replaced custom markdown scripts with `markdownlint-cli2`
|
||||
- **Before**: `./scripts/fix-markdown.sh` and `./scripts/validate-markdown.sh`
|
||||
- **After**: `markdownlint-cli2 --fix` and `markdownlint-cli2`
|
||||
- **Benefits**: Faster execution, better error reporting, modern markdown standards
|
||||
|
||||
#### Lint-Staged Configuration Enhanced
|
||||
|
||||
- **Added**: Markdown file linting to pre-commit hooks
|
||||
- **Pattern**: `*.{md,markdown,mdc}` files now automatically formatted
|
||||
- **Command**: `markdownlint-cli2 --fix` runs before each commit
|
||||
- **Coverage**: All markdown files including `.mdc` cursor rules
|
||||
|
||||
#### Commit Message Standards
|
||||
|
||||
- **Added**: Conventional commit validation via commitlint
|
||||
- **Configuration**: Extends `@commitlint/config-conventional`
|
||||
- **Enforcement**: Ensures consistent commit message format across the project
|
||||
|
||||
#### Node.js Version Requirements
|
||||
|
||||
- **Updated**: Minimum Node.js version requirements for new dependencies
|
||||
- **markdownlint-cli2**: Requires Node.js >=20
|
||||
- **Various utilities**: Require Node.js >=18 for modern ES features
|
||||
|
||||
#### Build Process Impact
|
||||
|
||||
- **No Breaking Changes**: All existing build commands continue to work
|
||||
- **Improved Quality**: Better markdown formatting and commit message standards
|
||||
- **Enhanced Automation**: More comprehensive pre-commit validation
|
||||
@@ -2745,6 +2759,7 @@ configuration files in the repository.
|
||||
### 2025-08-21 - Commitlint Configuration Refinement
|
||||
|
||||
#### Commit Message Validation Improvements
|
||||
|
||||
- **Modified**: Commitlint configuration moved from `package.json` to dedicated `commitlint.config.js`
|
||||
- **Enhanced**: Strict validation rules downgraded from errors to warnings
|
||||
- **Before**: `subject-case` and `subject-full-stop` rules caused red error messages
|
||||
@@ -2752,16 +2767,18 @@ configuration files in the repository.
|
||||
- **Benefit**: Eliminates confusing red error messages while maintaining commit quality guidance
|
||||
|
||||
#### Configuration Structure
|
||||
|
||||
- **File**: `commitlint.config.js` - Dedicated commitlint configuration
|
||||
- **Extends**: `@commitlint/config-conventional` - Standard conventional commit rules
|
||||
- **Custom Rules**:
|
||||
- **Custom Rules**:
|
||||
- `subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]`
|
||||
- `subject-full-stop: [1, 'never', '.']`
|
||||
- **Levels**:
|
||||
- **Levels**:
|
||||
- `0` = Disabled, `1` = Warning, `2` = Error
|
||||
- Current: Problematic rules set to warning level (1)
|
||||
|
||||
#### User Experience Impact
|
||||
|
||||
- **Before**: Red error messages on every push with strict commit rules
|
||||
- **After**: Yellow warning messages that provide guidance without disruption
|
||||
- **Workflow**: Commits and pushes continue to work while maintaining quality standards
|
||||
@@ -2772,6 +2789,7 @@ configuration files in the repository.
|
||||
### 2025-08-26 - Capacitor Plugin Additions
|
||||
|
||||
#### New Capacitor Plugins Added
|
||||
|
||||
- **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms
|
||||
- **Purpose**: Enable copy/paste operations on mobile devices
|
||||
- **Platforms**: iOS and Android
|
||||
@@ -2785,23 +2803,27 @@ configuration files in the repository.
|
||||
- **Integration**: Automatically included in mobile builds
|
||||
|
||||
#### Android Build System Updates
|
||||
|
||||
- **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes
|
||||
- **Added**: `:capacitor-clipboard` project directory mapping
|
||||
- **Added**: `:capacitor-status-bar` project directory mapping
|
||||
- **Impact**: New plugins now properly integrated into Android build process
|
||||
|
||||
#### Package Dependencies
|
||||
|
||||
- **Updated**: `package.json` - Added new Capacitor plugin dependencies
|
||||
- **Updated**: `package-lock.json` - Locked dependency versions for consistency
|
||||
- **Version**: All new plugins use Capacitor 6.x compatible versions
|
||||
|
||||
#### Build Process Impact
|
||||
|
||||
- **No Breaking Changes**: Existing build commands continue to work unchanged
|
||||
- **Enhanced Mobile Features**: New clipboard and status bar capabilities available
|
||||
- **Automatic Integration**: Plugins automatically included in mobile builds
|
||||
- **Platform Support**: Both iOS and Android builds now include new functionality
|
||||
|
||||
#### Testing Requirements
|
||||
|
||||
- **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds
|
||||
- **Functionality**: Test clipboard operations and status bar management on devices
|
||||
- **Fallback**: Ensure graceful degradation when plugins are unavailable
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -5,21 +5,6 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [1.1.2] - 2025.11.06
|
||||
|
||||
### Fixed
|
||||
- Bad page when user follows prompt to backup seed
|
||||
|
||||
|
||||
## [1.1.1] - 2025.11.03
|
||||
|
||||
### Added
|
||||
- Meeting onboarding via prompts
|
||||
- Emojis on gift feed
|
||||
- Starred projects with notification
|
||||
|
||||
|
||||
## [1.0.7] - 2025.08.18
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,852 +0,0 @@
|
||||
# TimeSafari Code Quality: Comprehensive Deep Analysis
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: Tue Sep 16 05:22:10 AM UTC 2025
|
||||
**Status**: 🎯 **COMPREHENSIVE ANALYSIS** - Complete code quality assessment with actionable recommendations
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The TimeSafari codebase demonstrates **exceptional code quality** with mature patterns, minimal technical debt, and excellent separation of concerns. This comprehensive analysis covers **291 source files** totaling **104,527 lines** of code, including detailed examination of **94 Vue components and views**.
|
||||
|
||||
**Key Quality Metrics:**
|
||||
- **Technical Debt**: Extremely low (6 TODO/FIXME comments across entire codebase)
|
||||
- **Database Migration**: 99.5% complete (1 remaining legacy import)
|
||||
- **File Complexity**: High variance (largest file: 2,215 lines)
|
||||
- **Type Safety**: Mixed patterns (41 "as any" assertions in Vue files, 62 total)
|
||||
- **Error Handling**: Comprehensive (367 catch blocks with good coverage)
|
||||
- **Architecture**: Consistent Vue 3 Composition API with TypeScript
|
||||
|
||||
## Vue Components & Views Analysis (94 Files)
|
||||
|
||||
### Component Analysis (40 Components)
|
||||
|
||||
#### Component Size Distribution
|
||||
```
|
||||
Large Components (>500 lines): 5 components (12.5%)
|
||||
├── ImageMethodDialog.vue (947 lines) 🔴 CRITICAL
|
||||
├── GiftedDialog.vue (670 lines) ⚠️ HIGH PRIORITY
|
||||
├── PhotoDialog.vue (669 lines) ⚠️ HIGH PRIORITY
|
||||
├── PushNotificationPermission.vue (660 lines) ⚠️ HIGH PRIORITY
|
||||
└── MembersList.vue (550 lines) ⚠️ MODERATE PRIORITY
|
||||
|
||||
Medium Components (200-500 lines): 12 components (30%)
|
||||
├── GiftDetailsStep.vue (450 lines)
|
||||
├── EntityGrid.vue (348 lines)
|
||||
├── ActivityListItem.vue (334 lines)
|
||||
├── OfferDialog.vue (327 lines)
|
||||
├── OnboardingDialog.vue (314 lines)
|
||||
├── EntitySelectionStep.vue (313 lines)
|
||||
├── GiftedPrompts.vue (293 lines)
|
||||
├── ChoiceButtonDialog.vue (250 lines)
|
||||
├── DataExportSection.vue (251 lines)
|
||||
├── AmountInput.vue (224 lines)
|
||||
├── HiddenDidDialog.vue (220 lines)
|
||||
└── FeedFilters.vue (218 lines)
|
||||
|
||||
Small Components (<200 lines): 23 components (57.5%)
|
||||
├── ContactListItem.vue (217 lines)
|
||||
├── EntitySummaryButton.vue (202 lines)
|
||||
├── IdentitySection.vue (186 lines)
|
||||
├── ContactInputForm.vue (173 lines)
|
||||
├── SpecialEntityCard.vue (156 lines)
|
||||
├── RegistrationNotice.vue (154 lines)
|
||||
├── ContactNameDialog.vue (154 lines)
|
||||
├── PersonCard.vue (153 lines)
|
||||
├── UserNameDialog.vue (147 lines)
|
||||
├── InfiniteScroll.vue (132 lines)
|
||||
├── LocationSearchSection.vue (124 lines)
|
||||
├── UsageLimitsSection.vue (123 lines)
|
||||
├── QuickNav.vue (118 lines)
|
||||
├── ProjectCard.vue (104 lines)
|
||||
├── ContactListHeader.vue (101 lines)
|
||||
├── TopMessage.vue (98 lines)
|
||||
├── InviteDialog.vue (95 lines)
|
||||
├── ImageViewer.vue (94 lines)
|
||||
├── EntityIcon.vue (86 lines)
|
||||
├── ShowAllCard.vue (66 lines)
|
||||
├── ContactBulkActions.vue (53 lines)
|
||||
├── ProjectIcon.vue (47 lines)
|
||||
└── LargeIdenticonModal.vue (44 lines)
|
||||
```
|
||||
|
||||
#### Critical Component Analysis
|
||||
|
||||
**1. `ImageMethodDialog.vue` (947 lines) 🔴 CRITICAL REFACTORING NEEDED**
|
||||
|
||||
**Issues Identified:**
|
||||
- **Excessive Single Responsibility**: Handles camera preview, file upload, URL input, cropping, diagnostics, and error handling
|
||||
- **Complex State Management**: 20+ reactive properties with interdependencies
|
||||
- **Mixed Concerns**: Camera API, file handling, UI state, and business logic intertwined
|
||||
- **Template Complexity**: ~300 lines of template with deeply nested conditions
|
||||
|
||||
**Refactoring Strategy:**
|
||||
```typescript
|
||||
// Current monolithic structure
|
||||
ImageMethodDialog.vue (947 lines) {
|
||||
CameraPreview: ~200 lines
|
||||
FileUpload: ~150 lines
|
||||
URLInput: ~100 lines
|
||||
CroppingInterface: ~200 lines
|
||||
DiagnosticsPanel: ~150 lines
|
||||
ErrorHandling: ~100 lines
|
||||
StateManagement: ~47 lines
|
||||
}
|
||||
|
||||
// Proposed component decomposition
|
||||
ImageMethodDialog.vue (coordinator, ~200 lines)
|
||||
├── CameraPreviewComponent.vue (~250 lines)
|
||||
├── FileUploadComponent.vue (~150 lines)
|
||||
├── URLInputComponent.vue (~100 lines)
|
||||
├── ImageCropperComponent.vue (~200 lines)
|
||||
├── DiagnosticsPanelComponent.vue (~150 lines)
|
||||
└── ImageUploadErrorHandler.vue (~100 lines)
|
||||
```
|
||||
|
||||
**2. `GiftedDialog.vue` (670 lines) ⚠️ HIGH PRIORITY**
|
||||
|
||||
**Assessment**: **GOOD** - Already partially refactored with step components extracted.
|
||||
|
||||
**3. `PhotoDialog.vue` (669 lines) ⚠️ HIGH PRIORITY**
|
||||
|
||||
**Issues**: Similar to ImageMethodDialog with significant code duplication.
|
||||
|
||||
**4. `PushNotificationPermission.vue` (660 lines) ⚠️ HIGH PRIORITY**
|
||||
|
||||
**Issues**: Complex permission logic with platform-specific code mixed together.
|
||||
|
||||
### View Analysis (54 Views)
|
||||
|
||||
#### View Size Distribution
|
||||
```
|
||||
Large Views (>1000 lines): 9 views (16.7%)
|
||||
├── AccountViewView.vue (2,215 lines) 🔴 CRITICAL
|
||||
├── HomeView.vue (1,852 lines) ⚠️ HIGH PRIORITY
|
||||
├── ProjectViewView.vue (1,479 lines) ⚠️ HIGH PRIORITY
|
||||
├── DatabaseMigration.vue (1,438 lines) ⚠️ HIGH PRIORITY
|
||||
├── ContactsView.vue (1,382 lines) ⚠️ HIGH PRIORITY
|
||||
├── TestView.vue (1,259 lines) ⚠️ MODERATE PRIORITY
|
||||
├── ClaimView.vue (1,225 lines) ⚠️ MODERATE PRIORITY
|
||||
├── NewEditProjectView.vue (957 lines) ⚠️ MODERATE PRIORITY
|
||||
└── ContactQRScanShowView.vue (929 lines) ⚠️ MODERATE PRIORITY
|
||||
|
||||
Medium Views (500-1000 lines): 8 views (14.8%)
|
||||
├── ConfirmGiftView.vue (898 lines)
|
||||
├── DiscoverView.vue (888 lines)
|
||||
├── DIDView.vue (848 lines)
|
||||
├── GiftedDetailsView.vue (840 lines)
|
||||
├── OfferDetailsView.vue (781 lines)
|
||||
├── HelpView.vue (780 lines)
|
||||
├── ProjectsView.vue (742 lines)
|
||||
└── ContactQRScanFullView.vue (701 lines)
|
||||
|
||||
Small Views (<500 lines): 37 views (68.5%)
|
||||
├── OnboardMeetingSetupView.vue (687 lines)
|
||||
├── ContactImportView.vue (568 lines)
|
||||
├── HelpNotificationsView.vue (566 lines)
|
||||
├── OnboardMeetingListView.vue (507 lines)
|
||||
├── InviteOneView.vue (475 lines)
|
||||
├── QuickActionBvcEndView.vue (442 lines)
|
||||
├── ContactAmountsView.vue (416 lines)
|
||||
├── SearchAreaView.vue (384 lines)
|
||||
├── SharedPhotoView.vue (379 lines)
|
||||
├── ContactGiftingView.vue (373 lines)
|
||||
├── ContactEditView.vue (345 lines)
|
||||
├── IdentitySwitcherView.vue (324 lines)
|
||||
├── UserProfileView.vue (323 lines)
|
||||
├── NewActivityView.vue (323 lines)
|
||||
├── QuickActionBvcBeginView.vue (303 lines)
|
||||
├── SeedBackupView.vue (292 lines)
|
||||
├── InviteOneAcceptView.vue (292 lines)
|
||||
├── ClaimCertificateView.vue (279 lines)
|
||||
├── StartView.vue (271 lines)
|
||||
├── ImportAccountView.vue (265 lines)
|
||||
├── ClaimAddRawView.vue (249 lines)
|
||||
├── OnboardMeetingMembersView.vue (247 lines)
|
||||
├── DeepLinkErrorView.vue (239 lines)
|
||||
├── ClaimReportCertificateView.vue (236 lines)
|
||||
├── DeepLinkRedirectView.vue (219 lines)
|
||||
├── ImportDerivedAccountView.vue (207 lines)
|
||||
├── ShareMyContactInfoView.vue (196 lines)
|
||||
├── RecentOffersToUserProjectsView.vue (176 lines)
|
||||
├── RecentOffersToUserView.vue (166 lines)
|
||||
├── NewEditAccountView.vue (142 lines)
|
||||
├── StatisticsView.vue (133 lines)
|
||||
├── HelpOnboardingView.vue (118 lines)
|
||||
├── LogView.vue (104 lines)
|
||||
├── NewIdentifierView.vue (97 lines)
|
||||
├── HelpNotificationTypesView.vue (73 lines)
|
||||
├── ConfirmContactView.vue (57 lines)
|
||||
└── QuickActionBvcView.vue (54 lines)
|
||||
```
|
||||
|
||||
#### Critical View Analysis
|
||||
|
||||
**1. `AccountViewView.vue` (2,215 lines) 🔴 CRITICAL REFACTORING NEEDED**
|
||||
|
||||
**Issues Identified:**
|
||||
- **Monolithic Architecture**: Handles 7 distinct concerns in single file
|
||||
- **Template Complexity**: ~750 lines of template with deeply nested conditions
|
||||
- **Method Proliferation**: 50+ methods handling disparate concerns
|
||||
- **State Management**: 25+ reactive properties without clear organization
|
||||
|
||||
**Refactoring Strategy:**
|
||||
```typescript
|
||||
// Current monolithic structure
|
||||
AccountViewView.vue (2,215 lines) {
|
||||
ProfileSection: ~400 lines
|
||||
SettingsSection: ~300 lines
|
||||
NotificationSection: ~200 lines
|
||||
ServerConfigSection: ~250 lines
|
||||
ExportImportSection: ~300 lines
|
||||
LimitsSection: ~150 lines
|
||||
MapSection: ~200 lines
|
||||
StateManagement: ~415 lines
|
||||
}
|
||||
|
||||
// Proposed component extraction
|
||||
AccountViewView.vue (coordinator, ~400 lines)
|
||||
├── ProfileManagementSection.vue (~300 lines)
|
||||
├── ServerConfigurationSection.vue (~250 lines)
|
||||
├── NotificationSettingsSection.vue (~200 lines)
|
||||
├── DataExportImportSection.vue (~300 lines)
|
||||
├── UsageLimitsDisplay.vue (~150 lines)
|
||||
├── LocationProfileSection.vue (~200 lines)
|
||||
└── AccountViewStateManager.ts (~200 lines)
|
||||
```
|
||||
|
||||
**2. `HomeView.vue` (1,852 lines) ⚠️ HIGH PRIORITY**
|
||||
|
||||
**Issues Identified:**
|
||||
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
|
||||
- **Complex State Management**: 20+ reactive properties with interdependencies
|
||||
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
|
||||
|
||||
**3. `ProjectViewView.vue` (1,479 lines) ⚠️ HIGH PRIORITY**
|
||||
|
||||
**Issues Identified:**
|
||||
- **Project Management Complexity**: Handles project details, members, offers, and activities
|
||||
- **Mixed Concerns**: Project data, member management, and activity feed in single view
|
||||
|
||||
### Vue Component Quality Patterns
|
||||
|
||||
#### Excellent Patterns Found:
|
||||
|
||||
**1. EntityIcon.vue (86 lines) ✅ EXCELLENT**
|
||||
```typescript
|
||||
// Clean, focused responsibility
|
||||
@Component({ name: "EntityIcon" })
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop() contact?: Contact;
|
||||
@Prop({ default: "" }) entityId!: string;
|
||||
@Prop({ default: 0 }) iconSize!: number;
|
||||
|
||||
generateIcon(): string {
|
||||
// Clear priority order: profile image → avatar → fallback
|
||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
||||
if (imageUrl) return `<img src="${imageUrl}" ... />`;
|
||||
|
||||
const identifier = this.contact?.did || this.entityId;
|
||||
if (!identifier) return `<img src="${blankSquareSvg}" ... />`;
|
||||
|
||||
return createAvatar(avataaars, { seed: identifier, size: this.iconSize }).toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. QuickNav.vue (118 lines) ✅ EXCELLENT**
|
||||
```typescript
|
||||
// Simple, focused navigation component
|
||||
@Component({ name: "QuickNav" })
|
||||
export default class QuickNav extends Vue {
|
||||
@Prop selected = "";
|
||||
|
||||
// Clean template with consistent patterns
|
||||
// Proper accessibility attributes
|
||||
// Responsive design with safe area handling
|
||||
}
|
||||
```
|
||||
|
||||
**3. Small Focused Views ✅ EXCELLENT**
|
||||
```typescript
|
||||
// QuickActionBvcView.vue (54 lines) - Perfect size
|
||||
// ConfirmContactView.vue (57 lines) - Focused responsibility
|
||||
// HelpNotificationTypesView.vue (73 lines) - Clear purpose
|
||||
// LogView.vue (104 lines) - Simple utility view
|
||||
```
|
||||
|
||||
#### Problematic Patterns Found:
|
||||
|
||||
**1. Excessive Props in Dialog Components**
|
||||
```typescript
|
||||
// GiftedDialog.vue - Too many props
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop() isFromProjectView = false;
|
||||
@Prop() hideShowAll = false;
|
||||
@Prop({ default: "person" }) giverEntityType = "person";
|
||||
@Prop({ default: "person" }) recipientEntityType = "person";
|
||||
// ... 10+ more props
|
||||
```
|
||||
|
||||
**2. Complex State Machines**
|
||||
```typescript
|
||||
// ImageMethodDialog.vue - Complex state management
|
||||
cameraState: "off" | "initializing" | "active" | "error" | "retrying" | "stopped" = "off";
|
||||
showCameraPreview = false;
|
||||
isRetrying = false;
|
||||
showDiagnostics = false;
|
||||
// ... 15+ more state properties
|
||||
```
|
||||
|
||||
**3. Excessive Reactive Properties**
|
||||
```typescript
|
||||
// AccountViewView.vue - Too many reactive properties
|
||||
downloadUrl: string = "";
|
||||
loadingLimits: boolean = false;
|
||||
loadingProfile: boolean = true;
|
||||
showAdvanced: boolean = false;
|
||||
showB64Copy: boolean = false;
|
||||
showContactGives: boolean = false;
|
||||
showDidCopy: boolean = false;
|
||||
showDerCopy: boolean = false;
|
||||
showGeneralAdvanced: boolean = false;
|
||||
showLargeIdenticonId?: string;
|
||||
showLargeIdenticonUrl?: string;
|
||||
showPubCopy: boolean = false;
|
||||
showShortcutBvc: boolean = false;
|
||||
warnIfProdServer: boolean = false;
|
||||
warnIfTestServer: boolean = false;
|
||||
zoom: number = 2;
|
||||
isMapReady: boolean = false;
|
||||
// ... 10+ more properties
|
||||
```
|
||||
|
||||
## File Size and Complexity Analysis (All Files)
|
||||
|
||||
### Problematic Large Files
|
||||
|
||||
#### 1. `AccountViewView.vue` (2,215 lines) 🔴 **CRITICAL**
|
||||
**Issues Identified:**
|
||||
- **Excessive Single File Responsibility**: Handles profile, settings, notifications, server configuration, export/import, limits checking
|
||||
- **Template Complexity**: ~750 lines of template with deeply nested conditions
|
||||
- **Method Proliferation**: 50+ methods handling disparate concerns
|
||||
- **State Management**: 25+ reactive properties without clear organization
|
||||
|
||||
#### 2. `PlatformServiceMixin.ts` (2,091 lines) ⚠️ **HIGH PRIORITY**
|
||||
**Issues Identified:**
|
||||
- **God Object Pattern**: Single file handling 80+ methods across multiple concerns
|
||||
- **Mixed Abstraction Levels**: Low-level SQL utilities mixed with high-level business logic
|
||||
- **Method Length Variance**: Some methods 100+ lines, others single-line wrappers
|
||||
|
||||
**Refactoring Strategy:**
|
||||
```typescript
|
||||
// Current monolithic mixin
|
||||
PlatformServiceMixin.ts (2,091 lines)
|
||||
|
||||
// Proposed separation of concerns
|
||||
├── CoreDatabaseMixin.ts // $db, $exec, $query, $first (200 lines)
|
||||
├── SettingsManagementMixin.ts // $settings, $saveSettings (400 lines)
|
||||
├── ContactManagementMixin.ts // $contacts, $insertContact (300 lines)
|
||||
├── EntityOperationsMixin.ts // $insertEntity, $updateEntity (400 lines)
|
||||
├── CachingMixin.ts // Cache management (150 lines)
|
||||
├── ActiveIdentityMixin.ts // Active DID management (200 lines)
|
||||
├── UtilityMixin.ts // Mapping, JSON parsing (200 lines)
|
||||
└── LoggingMixin.ts // $log, $logError (100 lines)
|
||||
```
|
||||
|
||||
#### 3. `HomeView.vue` (1,852 lines) ⚠️ **MODERATE PRIORITY**
|
||||
**Issues Identified:**
|
||||
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
|
||||
- **Complex State Management**: 20+ reactive properties with interdependencies
|
||||
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
|
||||
|
||||
### File Size Distribution Analysis
|
||||
```
|
||||
Files > 1000 lines: 9 files (4.6% of codebase)
|
||||
Files 500-1000 lines: 23 files (11.7% of codebase)
|
||||
Files 200-500 lines: 45 files (22.8% of codebase)
|
||||
Files < 200 lines: 120 files (60.9% of codebase)
|
||||
```
|
||||
|
||||
**Assessment**: Good distribution with most files reasonably sized, but critical outliers need attention.
|
||||
|
||||
## Type Safety Analysis
|
||||
|
||||
### Type Assertion Patterns
|
||||
|
||||
#### "as any" Usage (62 total instances) ⚠️
|
||||
|
||||
**Vue Components & Views (41 instances):**
|
||||
```typescript
|
||||
// ImageMethodDialog.vue:504
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
|
||||
// GiftedDialog.vue:228
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
|
||||
// AccountViewView.vue: Multiple instances for:
|
||||
// - PlatformServiceMixin method access
|
||||
// - Vue refs with complex typing
|
||||
// - External library integration (Leaflet)
|
||||
```
|
||||
|
||||
**Other Files (21 instances):**
|
||||
- **Vue Component References** (23 instances): `(this.$refs.dialog as any)`
|
||||
- **Platform Detection** (12 instances): `(navigator as any).standalone`
|
||||
- **External Library Integration** (15 instances): Leaflet, Axios extensions
|
||||
- **Legacy Code Compatibility** (8 instances): Temporary migration code
|
||||
- **Event Handler Workarounds** (4 instances): Vue event typing issues
|
||||
|
||||
**Example Problematic Pattern:**
|
||||
```typescript
|
||||
// src/views/AccountViewView.vue:934
|
||||
const iconDefault = L.Icon.Default.prototype as unknown as Record<string, unknown>;
|
||||
|
||||
// Better approach:
|
||||
interface LeafletIconPrototype {
|
||||
_getIconUrl?: unknown;
|
||||
}
|
||||
const iconDefault = L.Icon.Default.prototype as LeafletIconPrototype;
|
||||
```
|
||||
|
||||
#### "unknown" Type Usage (755 instances)
|
||||
**Analysis**: Generally good practice showing defensive programming, but some areas could benefit from more specific typing.
|
||||
|
||||
### Recommended Type Safety Improvements
|
||||
|
||||
1. **Create Interface Extensions**:
|
||||
```typescript
|
||||
// src/types/platform-service-mixin.ts
|
||||
interface VueWithPlatformServiceMixin extends Vue {
|
||||
$getActiveIdentity(): Promise<{ activeDid: string }>;
|
||||
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
// ... other methods
|
||||
}
|
||||
|
||||
// src/types/external.ts
|
||||
declare global {
|
||||
interface Navigator {
|
||||
standalone?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface VueRefWithOpen {
|
||||
open: (callback: (result?: unknown) => void) => void;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Component Ref Typing**:
|
||||
```typescript
|
||||
// Instead of: (this.$refs.dialog as any).open()
|
||||
// Use: (this.$refs.dialog as VueRefWithOpen).open()
|
||||
```
|
||||
|
||||
## Error Handling Consistency Analysis
|
||||
|
||||
### Error Handling Patterns (367 catch blocks)
|
||||
|
||||
#### Pattern Distribution:
|
||||
1. **Structured Logging** (85%): Uses logger.error with context
|
||||
2. **User Notification** (78%): Shows user-friendly error messages
|
||||
3. **Graceful Degradation** (92%): Provides fallback behavior
|
||||
4. **Error Propagation** (45%): Re-throws when appropriate
|
||||
|
||||
#### Excellent Pattern Example:
|
||||
```typescript
|
||||
// src/views/AccountViewView.vue:1617
|
||||
try {
|
||||
const response = await this.axios.delete(url, { headers });
|
||||
if (response.status === 204) {
|
||||
this.profileImageUrl = "";
|
||||
this.notify.success("Image deleted successfully.");
|
||||
}
|
||||
} catch (error) {
|
||||
if (isApiError(error) && error.response?.status === 404) {
|
||||
// Graceful handling - image already gone
|
||||
this.profileImageUrl = "";
|
||||
} else {
|
||||
this.notify.error("Failed to delete image", TIMEOUTS.STANDARD);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Areas for Improvement:
|
||||
1. **Inconsistent Error Typing**: Some catch(error: any), others catch(error: unknown)
|
||||
2. **Missing Error Boundaries**: No Vue error boundary components
|
||||
3. **Silent Failures**: 15% of catch blocks don't notify users
|
||||
|
||||
## Code Duplication Analysis
|
||||
|
||||
### Significant Duplication Patterns
|
||||
|
||||
#### 1. **Toggle Component Pattern** (12 occurrences)
|
||||
```html
|
||||
<!-- Repeated across multiple files -->
|
||||
<div class="relative ml-2 cursor-pointer" @click="toggleMethod()">
|
||||
<input v-model="property" type="checkbox" class="sr-only" />
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<div class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Solution**: Create `ToggleSwitch.vue` component with props for value, label, and change handler.
|
||||
|
||||
#### 2. **API Error Handling Pattern** (25 occurrences)
|
||||
```typescript
|
||||
try {
|
||||
const response = await this.axios.post(url, data, { headers });
|
||||
if (response.status === 200) {
|
||||
this.notify.success("Operation successful");
|
||||
}
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
this.notify.error(`Failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Create `ApiRequestMixin.ts` with standardized request/response handling.
|
||||
|
||||
#### 3. **Settings Update Pattern** (40+ occurrences)
|
||||
```typescript
|
||||
async methodName() {
|
||||
await this.$saveSettings({ property: this.newValue });
|
||||
this.property = this.newValue;
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Enhanced PlatformServiceMixin already provides `$saveSettings()` - migrate remaining manual patterns.
|
||||
|
||||
## Dependency and Coupling Analysis
|
||||
|
||||
### Import Dependency Patterns
|
||||
|
||||
#### Legacy Database Coupling (EXCELLENT)
|
||||
- **Status**: 99.5% resolved (1 remaining databaseUtil import)
|
||||
- **Remaining**: `src/views/DeepLinkErrorView.vue:import { logConsoleAndDb }`
|
||||
- **Resolution**: Replace with PlatformServiceMixin `$logAndConsole()`
|
||||
|
||||
#### Circular Dependency Status (EXCELLENT)
|
||||
- **Status**: 100% resolved, no active circular dependencies
|
||||
- **Previous Issues**: All resolved through PlatformServiceMixin architecture
|
||||
|
||||
#### Component Coupling Analysis
|
||||
```typescript
|
||||
// High coupling components (>10 imports)
|
||||
AccountViewView.vue: 15 imports (understandable given scope)
|
||||
HomeView.vue: 12 imports
|
||||
ProjectViewView.vue: 11 imports
|
||||
|
||||
// Well-isolated components (<5 imports)
|
||||
QuickActionViews: 3-4 imports each
|
||||
Component utilities: 2-3 imports each
|
||||
```
|
||||
|
||||
**Assessment**: Reasonable coupling levels with clear architectural boundaries.
|
||||
|
||||
## Console Logging Analysis (129 instances)
|
||||
|
||||
### Logging Pattern Distribution:
|
||||
1. **console.log**: 89 instances (69%)
|
||||
2. **console.warn**: 24 instances (19%)
|
||||
3. **console.error**: 16 instances (12%)
|
||||
|
||||
### Vue Components & Views Logging (3 instances):
|
||||
- **Components**: 1 console.* call
|
||||
- **Views**: 2 console.* calls
|
||||
|
||||
### Inconsistent Logging Approach:
|
||||
```typescript
|
||||
// Mixed patterns found:
|
||||
console.log("Direct console logging"); // 89 instances
|
||||
logger.debug("Structured logging"); // Preferred pattern
|
||||
this.$logAndConsole("Mixin logging"); // PlatformServiceMixin
|
||||
```
|
||||
|
||||
### Recommended Standardization:
|
||||
1. **Migration Strategy**: Replace all console.* with logger.* calls
|
||||
2. **Structured Context**: Add consistent metadata to log entries
|
||||
3. **Log Levels**: Standardize debug/info/warn/error usage
|
||||
|
||||
## Technical Debt Analysis (6 total)
|
||||
|
||||
### Components (1 TODO):
|
||||
```typescript
|
||||
// PushNotificationPermission.vue
|
||||
// TODO: secretDB functionality needs to be migrated to PlatformServiceMixin
|
||||
```
|
||||
|
||||
### Views (2 TODOs):
|
||||
```typescript
|
||||
// AccountViewView.vue
|
||||
// TODO: Implement this for SQLite
|
||||
// TODO: implement this for SQLite
|
||||
```
|
||||
|
||||
### Other Files (3 TODOs):
|
||||
```typescript
|
||||
// src/db/tables/accounts.ts
|
||||
// TODO: When finished with migration, move these fields to Account and move identity and mnemonic here.
|
||||
|
||||
// src/util.d.ts
|
||||
// TODO: , inspect: inspect
|
||||
|
||||
// src/libs/crypto/vc/passkeyHelpers.ts
|
||||
// TODO: If it's after February 2025 when you read this then consider whether it still makes sense
|
||||
```
|
||||
|
||||
**Assessment**: **EXCELLENT** - Only 6 TODO comments across 291 files.
|
||||
|
||||
## Performance Anti-Patterns
|
||||
|
||||
### Identified Issues:
|
||||
|
||||
#### 1. **Excessive Reactive Properties**
|
||||
```typescript
|
||||
// AccountViewView.vue has 25+ reactive properties
|
||||
// Many could be computed or moved to component state
|
||||
```
|
||||
|
||||
#### 2. **Inline Method Calls in Templates**
|
||||
```html
|
||||
<!-- Anti-pattern: -->
|
||||
<span>{{ readableDate(timeStr) }}</span>
|
||||
|
||||
<!-- Better: -->
|
||||
<span>{{ readableTime }}</span>
|
||||
<!-- With computed property -->
|
||||
```
|
||||
|
||||
#### 3. **Missing Key Attributes in Lists**
|
||||
```html
|
||||
<!-- Several v-for loops missing :key attributes -->
|
||||
<li v-for="item in items">
|
||||
```
|
||||
|
||||
#### 4. **Complex Template Logic**
|
||||
```html
|
||||
<!-- AccountViewView.vue - Complex nested conditions -->
|
||||
<div v-if="!activeDid" id="noticeBeforeShare" class="bg-amber-200...">
|
||||
<p class="mb-4">
|
||||
<b>Note:</b> Before you can share with others or take any action, you need an identifier.
|
||||
</p>
|
||||
<router-link :to="{ name: 'new-identifier' }" class="inline-block...">
|
||||
Create An Identifier
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Identity Details -->
|
||||
<IdentitySection
|
||||
:given-name="givenName"
|
||||
:profile-image-url="profileImageUrl"
|
||||
:active-did="activeDid"
|
||||
:is-registered="isRegistered"
|
||||
:show-large-identicon-id="showLargeIdenticonId"
|
||||
:show-large-identicon-url="showLargeIdenticonUrl"
|
||||
:show-did-copy="showDidCopy"
|
||||
@edit-name="onEditName"
|
||||
@show-qr-code="onShowQrCode"
|
||||
@add-image="onAddImage"
|
||||
@delete-image="onDeleteImage"
|
||||
@show-large-identicon-id="onShowLargeIdenticonId"
|
||||
@show-large-identicon-url="onShowLargeIdenticonUrl"
|
||||
/>
|
||||
```
|
||||
|
||||
## Specific Actionable Recommendations
|
||||
|
||||
### Priority 1: Critical File Refactoring
|
||||
|
||||
1. **Split AccountViewView.vue**:
|
||||
- **Timeline**: 2-3 sprints
|
||||
- **Strategy**: Extract 6 major sections into focused components
|
||||
- **Risk**: Medium (requires careful state management coordination)
|
||||
- **Benefit**: Massive maintainability improvement, easier testing
|
||||
|
||||
2. **Decompose ImageMethodDialog.vue**:
|
||||
- **Timeline**: 2-3 sprints
|
||||
- **Strategy**: Extract 6 focused components (camera, file upload, cropping, etc.)
|
||||
- **Risk**: Medium (complex camera state management)
|
||||
- **Benefit**: Massive maintainability improvement
|
||||
|
||||
3. **Decompose PlatformServiceMixin.ts**:
|
||||
- **Timeline**: 1-2 sprints
|
||||
- **Strategy**: Create focused mixins by concern area
|
||||
- **Risk**: Low (well-defined interfaces already exist)
|
||||
- **Benefit**: Better code organization, reduced cognitive load
|
||||
|
||||
### Priority 2: Component Extraction
|
||||
|
||||
1. **HomeView.vue** → 4 focused sections
|
||||
- **Timeline**: 1-2 sprints
|
||||
- **Risk**: Low (clear separation of concerns)
|
||||
- **Benefit**: Better code organization
|
||||
|
||||
2. **ProjectViewView.vue** → 4 focused sections
|
||||
- **Timeline**: 1-2 sprints
|
||||
- **Risk**: Low (well-defined boundaries)
|
||||
- **Benefit**: Improved maintainability
|
||||
|
||||
### Priority 3: Shared Component Creation
|
||||
|
||||
1. **CameraPreviewComponent.vue**
|
||||
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
|
||||
- **Benefit**: Eliminate code duplication
|
||||
|
||||
2. **FileUploadComponent.vue**
|
||||
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
|
||||
- **Benefit**: Consistent file handling
|
||||
|
||||
3. **ToggleSwitch.vue**
|
||||
- Replace 12 duplicate toggle patterns
|
||||
- **Benefit**: Consistent UI components
|
||||
|
||||
4. **DiagnosticsPanelComponent.vue**
|
||||
- Extract from ImageMethodDialog.vue
|
||||
- **Benefit**: Reusable debugging component
|
||||
|
||||
### Priority 4: Type Safety Enhancement
|
||||
|
||||
1. **Eliminate "as any" Assertions**:
|
||||
- **Timeline**: 1 sprint
|
||||
- **Strategy**: Create proper interface extensions
|
||||
- **Risk**: Low
|
||||
- **Benefit**: Better compile-time error detection
|
||||
|
||||
2. **Standardize Error Typing**:
|
||||
- **Timeline**: 0.5 sprint
|
||||
- **Strategy**: Use consistent `catch (error: unknown)` pattern
|
||||
- **Risk**: None
|
||||
- **Benefit**: Better error handling consistency
|
||||
|
||||
### Priority 5: State Management Optimization
|
||||
|
||||
1. **Create Composables for Complex State**:
|
||||
```typescript
|
||||
// src/composables/useCameraState.ts
|
||||
export function useCameraState() {
|
||||
const cameraState = ref<CameraState>("off");
|
||||
const showPreview = ref(false);
|
||||
const isRetrying = ref(false);
|
||||
|
||||
const startCamera = async () => { /* ... */ };
|
||||
const stopCamera = () => { /* ... */ };
|
||||
|
||||
return { cameraState, showPreview, isRetrying, startCamera, stopCamera };
|
||||
}
|
||||
```
|
||||
|
||||
2. **Group Related Reactive Properties**:
|
||||
```typescript
|
||||
// Instead of:
|
||||
showB64Copy: boolean = false;
|
||||
showDidCopy: boolean = false;
|
||||
showDerCopy: boolean = false;
|
||||
showPubCopy: boolean = false;
|
||||
|
||||
// Use:
|
||||
copyStates = {
|
||||
b64: false,
|
||||
did: false,
|
||||
der: false,
|
||||
pub: false
|
||||
};
|
||||
```
|
||||
|
||||
### Priority 6: Code Standardization
|
||||
|
||||
1. **Logging Standardization**:
|
||||
- **Timeline**: 1 sprint
|
||||
- **Strategy**: Replace all console.* with logger.*
|
||||
- **Risk**: None
|
||||
- **Benefit**: Consistent logging, better debugging
|
||||
|
||||
2. **Template Optimization**:
|
||||
- Add missing `:key` attributes
|
||||
- Convert inline method calls to computed properties
|
||||
- Implement virtual scrolling for large lists
|
||||
|
||||
## Quality Metrics Summary
|
||||
|
||||
### Vue Component Quality Distribution:
|
||||
| Size Category | Count | Percentage | Quality Assessment |
|
||||
|---------------|-------|------------|-------------------|
|
||||
| Large (>500 lines) | 5 | 12.5% | 🔴 Needs Refactoring |
|
||||
| Medium (200-500 lines) | 12 | 30% | 🟡 Good with Minor Issues |
|
||||
| Small (<200 lines) | 23 | 57.5% | 🟢 Excellent |
|
||||
|
||||
### Vue View Quality Distribution:
|
||||
| Size Category | Count | Percentage | Quality Assessment |
|
||||
|---------------|-------|------------|-------------------|
|
||||
| Large (>1000 lines) | 9 | 16.7% | 🔴 Needs Refactoring |
|
||||
| Medium (500-1000 lines) | 8 | 14.8% | 🟡 Good with Minor Issues |
|
||||
| Small (<500 lines) | 37 | 68.5% | 🟢 Excellent |
|
||||
|
||||
### Overall Quality Metrics:
|
||||
| Metric | Components | Views | Overall Assessment |
|
||||
|--------|------------|-------|-------------------|
|
||||
| Technical Debt | 1 TODO | 2 TODOs | 🟢 Excellent |
|
||||
| Type Safety | 6 "as any" | 35 "as any" | 🟡 Good |
|
||||
| Console Logging | 1 instance | 2 instances | 🟢 Excellent |
|
||||
| Architecture Consistency | 100% | 100% | 🟢 Excellent |
|
||||
| Component Reuse | High | High | 🟢 Excellent |
|
||||
|
||||
### Before vs. Target State:
|
||||
| Metric | Current | Target | Status |
|
||||
|--------|---------|---------|---------|
|
||||
| Files >1000 lines | 9 files | 3 files | 🟡 Needs Work |
|
||||
| "as any" assertions | 62 | 15 | 🟡 Moderate |
|
||||
| Console.* calls | 129 | 0 | 🔴 Needs Work |
|
||||
| Component reuse | 40% | 75% | 🟡 Moderate |
|
||||
| Error consistency | 85% | 95% | 🟢 Good |
|
||||
| Type coverage | 88% | 95% | 🟢 Good |
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Low Risk Improvements (High Impact):
|
||||
- Logging standardization
|
||||
- Type assertion cleanup
|
||||
- Missing key attributes
|
||||
- Component extraction from AccountViewView.vue
|
||||
- Shared component creation (ToggleSwitch, CameraPreview)
|
||||
|
||||
### Medium Risk Improvements:
|
||||
- PlatformServiceMixin decomposition
|
||||
- State management optimization
|
||||
- ImageMethodDialog decomposition
|
||||
|
||||
### High Risk Items:
|
||||
- None identified - project demonstrates excellent architectural discipline
|
||||
|
||||
## Conclusion
|
||||
|
||||
The TimeSafari codebase demonstrates **exceptional code quality** with:
|
||||
|
||||
**Key Strengths:**
|
||||
- **Consistent Architecture**: 100% Vue 3 Composition API with TypeScript
|
||||
- **Minimal Technical Debt**: Only 6 TODO comments across 291 files
|
||||
- **Excellent Small Components**: 68.5% of views and 57.5% of components are well-sized
|
||||
- **Strong Type Safety**: Minimal "as any" usage, mostly justified
|
||||
- **Clean Logging**: Minimal console.* usage, structured logging preferred
|
||||
- **Excellent Database Migration**: 99.5% complete
|
||||
- **Comprehensive Error Handling**: 367 catch blocks with good coverage
|
||||
- **No Circular Dependencies**: 100% resolved
|
||||
|
||||
**Primary Focus Areas:**
|
||||
1. **Decompose Large Files**: 5 components and 9 views need refactoring
|
||||
2. **Extract Shared Components**: Camera, file upload, and diagnostics components
|
||||
3. **Optimize State Management**: Group related properties and create composables
|
||||
4. **Improve Type Safety**: Create proper interface extensions for mixin methods
|
||||
5. **Logging Standardization**: Replace 129 console.* calls with structured logger.*
|
||||
|
||||
**The component architecture is production-ready** with these improvements representing **strategic optimization** rather than critical fixes. The codebase demonstrates **mature Vue.js development practices** with excellent separation of concerns and consistent patterns.
|
||||
|
||||
---
|
||||
|
||||
**Investigation Methodology:**
|
||||
- Static analysis of 291 source files (197 general + 94 Vue components/views)
|
||||
- Pattern recognition across 104,527 lines of code
|
||||
- Manual review of large files and complexity patterns
|
||||
- Dependency analysis and coupling assessment
|
||||
- Performance anti-pattern identification
|
||||
- Architecture consistency evaluation
|
||||
@@ -66,14 +66,17 @@ test-image.tar a1b2c3d4e5f6...
|
||||
```
|
||||
|
||||
### Docs
|
||||
|
||||
- [x] **BUILDING.md** updated (sections): Docker deployment
|
||||
- [x] Troubleshooting updated: Added Docker troubleshooting section
|
||||
|
||||
### Rollback
|
||||
|
||||
- [x] Verified steps to restore previous behavior:
|
||||
1. `git revert HEAD`
|
||||
2. `docker rmi test-image`
|
||||
3. Restore previous BUILDING.md
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
74
README.md
74
README.md
@@ -11,7 +11,7 @@ See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e
|
||||
|
||||
Quick start:
|
||||
|
||||
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
|
||||
- For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
@@ -68,16 +68,16 @@ TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environ
|
||||
|
||||
```bash
|
||||
# Show only errors
|
||||
VITE_LOG_LEVEL=error npm run build:web:dev
|
||||
VITE_LOG_LEVEL=error npm run dev
|
||||
|
||||
# Show warnings and errors
|
||||
VITE_LOG_LEVEL=warn npm run build:web:dev
|
||||
VITE_LOG_LEVEL=warn npm run dev
|
||||
|
||||
# Show info, warnings, and errors (default)
|
||||
VITE_LOG_LEVEL=info npm run build:web:dev
|
||||
VITE_LOG_LEVEL=info npm run dev
|
||||
|
||||
# Show all log levels including debug
|
||||
VITE_LOG_LEVEL=debug npm run build:web:dev
|
||||
VITE_LOG_LEVEL=debug npm run dev
|
||||
```
|
||||
|
||||
### Available Levels
|
||||
@@ -90,6 +90,7 @@ VITE_LOG_LEVEL=debug npm run build:web:dev
|
||||
See [Logging Configuration Guide](doc/logging-configuration.md) for complete details.
|
||||
|
||||
### Quick Usage
|
||||
|
||||
```bash
|
||||
# Run the database clearing script
|
||||
./scripts/clear-database.sh
|
||||
@@ -102,16 +103,19 @@ npm run build:web:dev # For Web
|
||||
### What It Does
|
||||
|
||||
#### **Electron (Desktop App)**
|
||||
|
||||
- Automatically finds and clears the SQLite database files
|
||||
- Works on Linux, macOS, and Windows
|
||||
- Clears all data and forces fresh migrations on next startup
|
||||
|
||||
#### **Web Browser**
|
||||
|
||||
- Provides instructions for using custom browser data directories
|
||||
- Shows manual clearing via browser DevTools
|
||||
- Ensures reliable database clearing without browser complications
|
||||
|
||||
### Safety Features
|
||||
|
||||
- ✅ **Interactive Script**: Guides you through the process
|
||||
- ✅ **Platform Detection**: Automatically detects your OS
|
||||
- ✅ **Clear Instructions**: Step-by-step guidance for each platform
|
||||
@@ -120,6 +124,7 @@ npm run build:web:dev # For Web
|
||||
### Manual Commands (if needed)
|
||||
|
||||
#### **Electron Database Location**
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
rm -rf ~/.config/TimeSafari/*
|
||||
@@ -132,6 +137,7 @@ rmdir /s /q %APPDATA%\TimeSafari
|
||||
```
|
||||
|
||||
#### **Web Browser (Custom Data Directory)**
|
||||
|
||||
```bash
|
||||
# Create isolated browser profile
|
||||
mkdir ~/timesafari-dev-data
|
||||
@@ -144,6 +150,7 @@ URL generation across all environments. This prevents localhost URLs from
|
||||
appearing in shared links during development.
|
||||
|
||||
### Key Features
|
||||
|
||||
- ✅ **Production URLs for Sharing**: All copy link buttons use production domain
|
||||
- ✅ **Environment-Specific Internal URLs**: Internal operations use appropriate
|
||||
environment URLs
|
||||
@@ -227,6 +234,7 @@ npm run test:prerequisites
|
||||
- **Build failures**: Run `npm run check:dependencies` to diagnose environment issues
|
||||
|
||||
**Required Versions**:
|
||||
|
||||
- Node.js: 18+ (LTS recommended)
|
||||
- npm: 8+ (comes with Node.js)
|
||||
- Platform-specific tools: Android Studio, Xcode (for mobile builds)
|
||||
@@ -246,25 +254,26 @@ To add a Font Awesome icon, add to `fontawesome.ts` and reference with
|
||||
|
||||
### Reference Material
|
||||
|
||||
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||
- Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
||||
|
||||
* [Customize Vue configuration](https://cli.vuejs.org/config/).
|
||||
- [Customize Vue configuration](https://cli.vuejs.org/config/).
|
||||
|
||||
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||
- If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||
|
||||
### Code Organization
|
||||
|
||||
The project uses a centralized approach to type definitions and interfaces:
|
||||
|
||||
* `src/interfaces/` - Contains all TypeScript interfaces and type definitions
|
||||
* `deepLinks.ts` - Deep linking type system and Zod validation schemas
|
||||
* `give.ts` - Give-related interfaces and type definitions
|
||||
* `claims.ts` - Claim-related interfaces and verifiable credentials
|
||||
* `common.ts` - Shared interfaces and utility types
|
||||
* Other domain-specific interface files
|
||||
- `src/interfaces/` - Contains all TypeScript interfaces and type definitions
|
||||
- `deepLinks.ts` - Deep linking type system and Zod validation schemas
|
||||
- `give.ts` - Give-related interfaces and type definitions
|
||||
- `claims.ts` - Claim-related interfaces and verifiable credentials
|
||||
- `common.ts` - Shared interfaces and utility types
|
||||
- Other domain-specific interface files
|
||||
|
||||
Key principles:
|
||||
|
||||
- All interfaces and types are defined in the interfaces folder
|
||||
- Zod schemas are used for runtime validation and type generation
|
||||
- Domain-specific interfaces are separated into their own files
|
||||
@@ -275,11 +284,11 @@ Key principles:
|
||||
|
||||
The application uses a platform-agnostic database layer with Vue mixins for service access:
|
||||
|
||||
* `src/services/PlatformService.ts` - Database interface definition
|
||||
* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
|
||||
* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
|
||||
* `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching
|
||||
* `src/db/` - Legacy Dexie database (migration in progress)
|
||||
- `src/services/PlatformService.ts` - Database interface definition
|
||||
- `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
|
||||
- `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
|
||||
- `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching
|
||||
- `src/db/` - Legacy Dexie database (migration in progress)
|
||||
|
||||
**Development Guidelines**:
|
||||
|
||||
@@ -305,17 +314,6 @@ timesafari/
|
||||
└── 📄 doc/README-BUILD-GUARD.md # Guard system documentation
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Critical Vue Reactivity Bug
|
||||
A critical Vue reactivity bug was discovered during ActiveDid migration testing where component properties fail to trigger template updates correctly.
|
||||
|
||||
**Impact**: The `newDirectOffersActivityNumber` element in HomeView.vue requires a watcher workaround to render correctly.
|
||||
|
||||
**Status**: Workaround implemented, investigation ongoing.
|
||||
|
||||
**Documentation**: See [Vue Reactivity Bug Report](doc/vue-reactivity-bug-report.md) for details.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. **Follow the Build Architecture Guard** - Update BUILDING.md when modifying build files
|
||||
@@ -327,11 +325,11 @@ A critical Vue reactivity bug was discovered during ActiveDid migration testing
|
||||
|
||||
Gifts make the world go 'round!
|
||||
|
||||
* [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
|
||||
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
||||
* [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
||||
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
||||
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
||||
* [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
|
||||
* Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)
|
||||
- [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
|
||||
- [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
||||
- [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
||||
- [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
||||
- [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||
- Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
||||
- [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
|
||||
- Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)
|
||||
|
||||
@@ -5,33 +5,33 @@
|
||||
|
||||
We can't trust iOS IndexedDB to persist. I want to start delivering an app to people now, in preparation for presentations mid-June: Rotary on June 12 and Porcfest on June 17.
|
||||
|
||||
* Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
|
||||
- Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
|
||||
|
||||
* The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
|
||||
- The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
|
||||
|
||||
* Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
|
||||
- Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
|
||||
|
||||
Also, with sensitive data, the accounts info should be encrypted.
|
||||
|
||||
# Options
|
||||
|
||||
* There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
|
||||
- There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
|
||||
|
||||
* [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
|
||||
- [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
|
||||
|
||||
* Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
|
||||
- Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
|
||||
|
||||
* There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
|
||||
- There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
|
||||
|
||||
* [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
|
||||
- [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
|
||||
|
||||
* Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
|
||||
- Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
|
||||
|
||||
# Current Plan
|
||||
|
||||
* Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
|
||||
- Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
|
||||
|
||||
* After that is delivered, write a migration for current web users from IndexedDB to SQLite.
|
||||
- After that is delivered, write a migration for current web users from IndexedDB to SQLite.
|
||||
|
||||
# Current method calls
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 47
|
||||
versionName "1.1.2"
|
||||
versionCode 40
|
||||
versionName "1.0.7"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
21
doc/GLOSSARY.md
Normal file
21
doc/GLOSSARY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Glossary
|
||||
|
||||
**T (slot time)** — The local wall-clock time a notification should fire (e.g., 08:00).
|
||||
|
||||
**T–lead** — The moment **`prefetchLeadMinutes`** before **T** when the system *attempts* a **single** background prefetch. T–lead **controls prefetch attempts, not arming**; locals are pre-armed earlier to guarantee closed-app delivery.
|
||||
|
||||
**Rolling window** — Always keep **today's remaining** (and tomorrow if iOS pending caps allow) locals **armed** so the OS can deliver while the app is closed.
|
||||
|
||||
**TTL (time-to-live)** — Maximum allowed payload age **at fire time**. If `T − fetchedAt > ttlSeconds`, we **skip** arming for that T.
|
||||
|
||||
**Shared DB (default)** — The app and plugin open the **same SQLite file**; the app owns schema/migrations, the plugin performs short writes with WAL.
|
||||
|
||||
**WAL (Write-Ahead Logging)** — SQLite journaling mode that permits concurrent reads during writes; recommended for foreground-read + background-write.
|
||||
|
||||
**`PRAGMA user_version`** — An integer the app increments on each migration; the plugin **checks** (does not migrate) to ensure compatibility.
|
||||
|
||||
**Exact alarm (Android)** — Minute-precise alarm via `AlarmManager.setExactAndAllowWhileIdle`, subject to policy and permission.
|
||||
|
||||
**Windowed alarm (Android)** — Batched/inexact alarm via `setWindow(start,len)`; we target **±10 minutes** when exact alarms are unavailable.
|
||||
|
||||
**Start-on-Login** — Electron feature that automatically launches the application when the user logs into their system, enabling background notification scheduling and delivery after system reboot.
|
||||
@@ -330,6 +330,7 @@ Track the effectiveness of your Build Architecture Guard:
|
||||
## 📝 **Changelog**
|
||||
|
||||
### 2025-08-22 - Shell Compatibility Fix
|
||||
|
||||
- **Fixed**: Replaced `mapfile` command with portable alternative for cross-shell compatibility
|
||||
- **Impact**: Resolves "mapfile: command not found" errors in pre-commit hooks
|
||||
- **Files**: `scripts/build-arch-guard.sh`
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
# TimeSafari Docs
|
||||
# TimeSafari — Native-First Notification System (Clean Pack) — 2025-09-07
|
||||
|
||||
## Generating PDF from Markdown on OSx
|
||||
This pack contains a single-version **Native-First** documentation set with a clear definition of **T–lead** and aligned terminology.
|
||||
|
||||
This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew.
|
||||
**Native-First =** OS-scheduled **background prefetch at T–lead** + **pre-armed one-shot local notifications**. Web-push is retired.
|
||||
|
||||
### Set Up
|
||||
**Included files**
|
||||
|
||||
```bash
|
||||
brew install pandoc
|
||||
|
||||
brew install basictex
|
||||
|
||||
# Setting up LaTex packages
|
||||
|
||||
# First update tlmgr
|
||||
sudo tlmgr update --self
|
||||
|
||||
# Then install LaTex packages
|
||||
sudo tlmgr install bbding
|
||||
sudo tlmgr install enumitem
|
||||
sudo tlmgr install environ
|
||||
sudo tlmgr install fancyhdr
|
||||
sudo tlmgr install framed
|
||||
sudo tlmgr install import
|
||||
sudo tlmgr install lastpage # Enables Page X of Y
|
||||
sudo tlmgr install mdframed
|
||||
sudo tlmgr install multirow
|
||||
sudo tlmgr install needspace
|
||||
sudo tlmgr install ntheorem
|
||||
sudo tlmgr install tabu
|
||||
sudo tlmgr install tcolorbox
|
||||
sudo tlmgr install textpos
|
||||
sudo tlmgr install titlesec
|
||||
sudo tlmgr install titling # Required for the fancy headers used
|
||||
sudo tlmgr install threeparttable
|
||||
sudo tlmgr install trimspaces
|
||||
sudo tlmgr install tocloft # Required for \tableofcontents generation
|
||||
sudo tlmgr install varwidth
|
||||
sudo tlmgr install wrapfig
|
||||
|
||||
# Install fonts
|
||||
sudo tlmgr install cmbright
|
||||
sudo tlmgr install collection-fontsrecommended # And set up fonts
|
||||
sudo tlmgr install fira
|
||||
sudo tlmgr install fontaxes
|
||||
sudo tlmgr install libertine # The main font the doc uses
|
||||
sudo tlmgr install opensans
|
||||
sudo tlmgr install sourceserifpro
|
||||
|
||||
```
|
||||
|
||||
#### References
|
||||
|
||||
The following guide was adapted to this project except that we install with Brew and have a few more packages.
|
||||
|
||||
Guide: <https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x>
|
||||
|
||||
### Usage
|
||||
|
||||
Use the `pandoc` command to generate a PDF.
|
||||
|
||||
```bash
|
||||
pandoc usage-guide.md -o usage-guide.pdf
|
||||
```
|
||||
|
||||
And you can open the PDF with the `open` command.
|
||||
|
||||
```bash
|
||||
open usage-guide.pdf
|
||||
```
|
||||
|
||||
Or use this one-liner
|
||||
|
||||
```bash
|
||||
pandoc usage-guide.md -o usage-guide.pdf && open usage-guide.pdf
|
||||
```
|
||||
- `notification-system.md` (merged comprehensive guide)
|
||||
- `web-push-cleanup-guide.md` (cleanup instructions)
|
||||
- `GLOSSARY.md` (definitions incl. **T** and **T–lead**)
|
||||
|
||||
@@ -117,25 +117,25 @@ async function getDatabaseService() {
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **src/interfaces/worker-messages.ts** *(NEW)*
|
||||
1. **src/interfaces/worker-messages.ts** _(NEW)_
|
||||
- Type definitions for worker communication
|
||||
- Request and response message interfaces
|
||||
|
||||
2. **src/registerSQLWorker.js** *(MAJOR REWRITE)*
|
||||
2. **src/registerSQLWorker.js** _(MAJOR REWRITE)_
|
||||
- Message-based operation handling
|
||||
- **Fixed circular dependency with lazy loading**
|
||||
- Proper error handling and response formatting
|
||||
|
||||
3. **src/services/platforms/WebPlatformService.ts** *(MAJOR REWRITE)*
|
||||
3. **src/services/platforms/WebPlatformService.ts** _(MAJOR REWRITE)_
|
||||
- Worker-only database access
|
||||
- Message sending and response handling
|
||||
- Timeout and error management
|
||||
|
||||
4. **src/main.web.ts** *(SIMPLIFIED)*
|
||||
4. **src/main.web.ts** _(SIMPLIFIED)_
|
||||
- Removed duplicate worker creation
|
||||
- Simplified initialization flow
|
||||
|
||||
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** *(NEW)*
|
||||
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** _(NEW)_
|
||||
- Complete documentation of changes
|
||||
|
||||
## Benefits
|
||||
|
||||
@@ -11,12 +11,14 @@ The Android Asset Validation System automatically detects and fixes missing Andr
|
||||
## Problem Solved
|
||||
|
||||
Previously, Android builds would fail with errors like:
|
||||
|
||||
```
|
||||
error: resource drawable/splash (aka app.timesafari.app:drawable/splash) not found.
|
||||
error: resource mipmap/ic_launcher (aka app.timesafari.app:mipmap/ic_launcher) not found.
|
||||
```
|
||||
|
||||
This happened when:
|
||||
|
||||
- Source assets existed but weren't generated into Android resources
|
||||
- Android resource directories were missing
|
||||
- Asset generation tools weren't run before building
|
||||
@@ -45,16 +47,19 @@ npm run build:android:studio
|
||||
### What Gets Validated
|
||||
|
||||
#### Source Assets (Required)
|
||||
|
||||
- `resources/icon.png` - App icon source
|
||||
- `resources/splash.png` - Splash screen source
|
||||
- `resources/splash_dark.png` - Dark mode splash source
|
||||
|
||||
#### Android Resources (Generated)
|
||||
|
||||
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
|
||||
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
|
||||
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
|
||||
|
||||
### Density Levels Checked
|
||||
|
||||
- `mipmap-mdpi` (1x)
|
||||
- `mipmap-hdpi` (1.5x)
|
||||
- `mipmap-xhdpi` (2x)
|
||||
@@ -64,6 +69,7 @@ npm run build:android:studio
|
||||
## Usage
|
||||
|
||||
### Automatic Validation (Recommended)
|
||||
|
||||
The validation runs automatically during all Android builds:
|
||||
|
||||
```bash
|
||||
@@ -78,6 +84,7 @@ npm run build:android:debug
|
||||
```
|
||||
|
||||
### Manual Validation
|
||||
|
||||
Run validation only to check/fix assets:
|
||||
|
||||
```bash
|
||||
@@ -89,6 +96,7 @@ npm run assets:validate:android
|
||||
```
|
||||
|
||||
### Validation Only (No Regeneration)
|
||||
|
||||
Check configuration without fixing:
|
||||
|
||||
```bash
|
||||
@@ -98,6 +106,7 @@ npm run assets:validate
|
||||
## Error Handling
|
||||
|
||||
### Missing Source Assets
|
||||
|
||||
If source assets are missing, the build fails with clear error messages:
|
||||
|
||||
```
|
||||
@@ -108,6 +117,7 @@ If source assets are missing, the build fails with clear error messages:
|
||||
```
|
||||
|
||||
### Missing Generated Resources
|
||||
|
||||
If generated resources are missing, they're automatically regenerated:
|
||||
|
||||
```
|
||||
@@ -119,6 +129,7 @@ If generated resources are missing, they're automatically regenerated:
|
||||
```
|
||||
|
||||
### Generation Failure
|
||||
|
||||
If regeneration fails, helpful guidance is provided:
|
||||
|
||||
```
|
||||
@@ -131,6 +142,7 @@ If regeneration fails, helpful guidance is provided:
|
||||
## Integration Points
|
||||
|
||||
### Build Script Integration
|
||||
|
||||
The validation is integrated into the main build process:
|
||||
|
||||
```bash
|
||||
@@ -143,6 +155,7 @@ validate_android_assets || {
|
||||
```
|
||||
|
||||
### NPM Scripts
|
||||
|
||||
New npm scripts for asset management:
|
||||
|
||||
```json
|
||||
@@ -156,17 +169,20 @@ New npm scripts for asset management:
|
||||
## Benefits
|
||||
|
||||
### For Developers
|
||||
|
||||
- **No More Build Failures**: Automatic detection and fixing of missing resources
|
||||
- **Faster Development**: No need to manually run asset generation tools
|
||||
- **Clear Error Messages**: Helpful guidance when issues occur
|
||||
- **Consistent Results**: Same validation on all development machines
|
||||
|
||||
### For CI/CD
|
||||
|
||||
- **Reliable Builds**: Consistent asset validation across environments
|
||||
- **Early Detection**: Catches issues before they reach production
|
||||
- **Automated Fixes**: Self-healing builds when possible
|
||||
|
||||
### For Project Maintenance
|
||||
|
||||
- **Reduced Support**: Fewer "build doesn't work" issues
|
||||
- **Documentation**: Clear requirements for required assets
|
||||
- **Standardization**: Consistent asset structure across the project
|
||||
@@ -176,21 +192,27 @@ New npm scripts for asset management:
|
||||
### Common Issues
|
||||
|
||||
#### "No assets found in the asset path"
|
||||
|
||||
This occurs when the `assets/` directory is empty. The validation system automatically copies source assets and regenerates them.
|
||||
|
||||
#### "Failed to generate Android assets"
|
||||
|
||||
Check that:
|
||||
|
||||
- Source assets exist in `resources/`
|
||||
- `@capacitor/assets` is installed
|
||||
- You have write permissions to the Android directories
|
||||
|
||||
#### "Asset generation completed but some resources are still missing"
|
||||
|
||||
This indicates a problem with the asset generation tool. Try:
|
||||
|
||||
1. Running `npm install` to ensure dependencies are up to date
|
||||
2. Manually running `npx @capacitor/assets generate`
|
||||
3. Checking the asset generation logs for specific errors
|
||||
|
||||
### Manual Recovery
|
||||
|
||||
If automatic regeneration fails, you can manually create the missing resources:
|
||||
|
||||
```bash
|
||||
@@ -213,12 +235,14 @@ rm assets/icon.png assets/splash.png assets/splash_dark.png
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Improvements
|
||||
|
||||
- **iOS Asset Validation**: Extend validation to iOS assets
|
||||
- **Asset Quality Checks**: Validate image dimensions and formats
|
||||
- **Performance Optimization**: Cache validation results
|
||||
- **CI/CD Integration**: Add validation to GitHub Actions
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- **Custom Asset Paths**: Support for different asset directory structures
|
||||
- **Validation Rules**: Configurable validation requirements
|
||||
- **Skip Options**: Ability to skip validation for specific scenarios
|
||||
|
||||
@@ -1,655 +0,0 @@
|
||||
# Android Emulator Deployment Guide (No Android Studio)
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-01-27
|
||||
**Status**: 🎯 **ACTIVE** - Complete guide for deploying TimeSafari to Android emulator using command-line tools
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides comprehensive instructions for building and deploying TimeSafari to Android emulators using only command-line tools, without requiring Android Studio. It leverages the existing build system and adds emulator-specific deployment workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Tools
|
||||
|
||||
1. **Android SDK Command Line Tools**
|
||||
```bash
|
||||
# Install via package manager (Arch Linux)
|
||||
sudo pacman -S android-sdk-cmdline-tools-latest
|
||||
|
||||
# Or download from Google
|
||||
# https://developer.android.com/studio/command-line
|
||||
```
|
||||
|
||||
2. **Android SDK Platform Tools**
|
||||
```bash
|
||||
# Install via package manager
|
||||
sudo pacman -S android-sdk-platform-tools
|
||||
|
||||
# Or via Android SDK Manager
|
||||
sdkmanager "platform-tools"
|
||||
```
|
||||
|
||||
3. **Android SDK Build Tools**
|
||||
```bash
|
||||
sdkmanager "build-tools;34.0.0"
|
||||
```
|
||||
|
||||
4. **Android Platform**
|
||||
```bash
|
||||
sdkmanager "platforms;android-34"
|
||||
```
|
||||
|
||||
5. **Android Emulator**
|
||||
```bash
|
||||
sdkmanager "emulator"
|
||||
```
|
||||
|
||||
6. **System Images**
|
||||
```bash
|
||||
# For API 34 (Android 14)
|
||||
sdkmanager "system-images;android-34;google_apis;x86_64"
|
||||
|
||||
# For API 33 (Android 13) - alternative
|
||||
sdkmanager "system-images;android-33;google_apis;x86_64"
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
# Add to ~/.bashrc or ~/.zshrc
|
||||
export ANDROID_HOME=$HOME/Android/Sdk
|
||||
export ANDROID_AVD_HOME=$HOME/.android/avd # Important for AVD location
|
||||
export PATH=$PATH:$ANDROID_HOME/emulator
|
||||
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
|
||||
export PATH=$PATH:$ANDROID_HOME/build-tools/34.0.0
|
||||
|
||||
# Reload shell
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
# Check all tools are available
|
||||
adb version
|
||||
emulator -version
|
||||
avdmanager list
|
||||
```
|
||||
|
||||
## Resource-Aware Emulator Setup
|
||||
|
||||
### ⚡ **Quick Start Recommendation**
|
||||
|
||||
**For best results, always start with resource analysis:**
|
||||
|
||||
```bash
|
||||
# 1. Check your system capabilities
|
||||
./scripts/avd-resource-checker.sh
|
||||
|
||||
# 2. Use the generated optimal startup script
|
||||
/tmp/start-avd-TimeSafari_Emulator.sh
|
||||
|
||||
# 3. Deploy your app
|
||||
npm run build:android:dev
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
This prevents system lockups and ensures optimal performance.
|
||||
|
||||
### AVD Resource Checker Script
|
||||
|
||||
**New Feature**: TimeSafari includes an intelligent resource checker that automatically detects your system capabilities and recommends optimal AVD configurations.
|
||||
|
||||
```bash
|
||||
# Check system resources and get recommendations
|
||||
./scripts/avd-resource-checker.sh
|
||||
|
||||
# Check resources for specific AVD
|
||||
./scripts/avd-resource-checker.sh TimeSafari_Emulator
|
||||
|
||||
# Test AVD startup performance
|
||||
./scripts/avd-resource-checker.sh TimeSafari_Emulator --test
|
||||
|
||||
# Create optimized AVD with recommended settings
|
||||
./scripts/avd-resource-checker.sh TimeSafari_Emulator --create
|
||||
```
|
||||
|
||||
**What the script analyzes:**
|
||||
- **System Memory**: Total and available RAM
|
||||
- **CPU Cores**: Available processing power
|
||||
- **GPU Capabilities**: NVIDIA, AMD, Intel, or software rendering
|
||||
- **Hardware Acceleration**: Optimal graphics settings
|
||||
|
||||
**What it generates:**
|
||||
- **Optimal configuration**: Memory, cores, and GPU settings
|
||||
- **Startup command**: Ready-to-use emulator command
|
||||
- **Startup script**: Saved to `/tmp/start-avd-{name}.sh` for reuse
|
||||
|
||||
## Emulator Management
|
||||
|
||||
### Create Android Virtual Device (AVD)
|
||||
|
||||
```bash
|
||||
# List available system images
|
||||
avdmanager list target
|
||||
|
||||
# Create AVD for API 34
|
||||
avdmanager create avd \
|
||||
--name "TimeSafari_Emulator" \
|
||||
--package "system-images;android-34;google_apis;x86_64" \
|
||||
--device "pixel_7"
|
||||
|
||||
# List created AVDs
|
||||
avdmanager list avd
|
||||
```
|
||||
|
||||
### Start Emulator
|
||||
|
||||
```bash
|
||||
# Start emulator with hardware acceleration (recommended)
|
||||
emulator -avd TimeSafari_Emulator -gpu host -no-audio &
|
||||
|
||||
# Start with reduced resources (if system has limited RAM)
|
||||
emulator -avd TimeSafari_Emulator \
|
||||
-no-audio \
|
||||
-memory 2048 \
|
||||
-cores 2 \
|
||||
-gpu swiftshader_indirect &
|
||||
|
||||
# Start with minimal resources (safest for low-end systems)
|
||||
emulator -avd TimeSafari_Emulator \
|
||||
-no-audio \
|
||||
-memory 1536 \
|
||||
-cores 1 \
|
||||
-gpu swiftshader_indirect &
|
||||
|
||||
# Check if emulator is running
|
||||
adb devices
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
|
||||
**Important**: Android emulators can consume significant system resources. Choose the appropriate configuration based on your system:
|
||||
|
||||
- **High-end systems** (16GB+ RAM, dedicated GPU): Use `-gpu host`
|
||||
- **Mid-range systems** (8-16GB RAM): Use `-memory 2048 -cores 2`
|
||||
- **Low-end systems** (4-8GB RAM): Use `-memory 1536 -cores 1 -gpu swiftshader_indirect`
|
||||
|
||||
### Emulator Control
|
||||
|
||||
```bash
|
||||
# Stop emulator
|
||||
adb emu kill
|
||||
|
||||
# Restart emulator
|
||||
adb reboot
|
||||
|
||||
# Check emulator status
|
||||
adb get-state
|
||||
```
|
||||
|
||||
## Build and Deploy Workflow
|
||||
|
||||
### Method 1: Using Existing Build Scripts
|
||||
|
||||
The TimeSafari project already has comprehensive Android build scripts that can be adapted for emulator deployment:
|
||||
|
||||
```bash
|
||||
# Development build with auto-run
|
||||
npm run build:android:dev:run
|
||||
|
||||
# Test build with auto-run
|
||||
npm run build:android:test:run
|
||||
|
||||
# Production build with auto-run
|
||||
npm run build:android:prod:run
|
||||
```
|
||||
|
||||
### Method 2: Custom Emulator Deployment Script
|
||||
|
||||
Create a new script specifically for emulator deployment:
|
||||
|
||||
```bash
|
||||
# Create emulator deployment script
|
||||
cat > scripts/deploy-android-emulator.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# deploy-android-emulator.sh
|
||||
# Author: Matthew Raymer
|
||||
# Date: 2025-01-27
|
||||
# Description: Deploy TimeSafari to Android emulator without Android Studio
|
||||
|
||||
set -e
|
||||
|
||||
# Source common utilities
|
||||
source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Default values
|
||||
BUILD_MODE="development"
|
||||
AVD_NAME="TimeSafari_Emulator"
|
||||
START_EMULATOR=true
|
||||
CLEAN_BUILD=true
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dev|--development)
|
||||
BUILD_MODE="development"
|
||||
shift
|
||||
;;
|
||||
--test)
|
||||
BUILD_MODE="test"
|
||||
shift
|
||||
;;
|
||||
--prod|--production)
|
||||
BUILD_MODE="production"
|
||||
shift
|
||||
;;
|
||||
--avd)
|
||||
AVD_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-start-emulator)
|
||||
START_EMULATOR=false
|
||||
shift
|
||||
;;
|
||||
--no-clean)
|
||||
CLEAN_BUILD=false
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " --dev, --development Build for development"
|
||||
echo " --test Build for testing"
|
||||
echo " --prod, --production Build for production"
|
||||
echo " --avd NAME Use specific AVD name"
|
||||
echo " --no-start-emulator Don't start emulator"
|
||||
echo " --no-clean Skip clean build"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Function to check if emulator is running
|
||||
check_emulator_running() {
|
||||
if adb devices | grep -q "emulator.*device"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start emulator
|
||||
start_emulator() {
|
||||
log_info "Starting Android emulator: $AVD_NAME"
|
||||
|
||||
# Check if AVD exists
|
||||
if ! avdmanager list avd | grep -q "$AVD_NAME"; then
|
||||
log_error "AVD '$AVD_NAME' not found. Please create it first."
|
||||
log_info "Create AVD with: avdmanager create avd --name $AVD_NAME --package system-images;android-34;google_apis;x86_64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start emulator in background
|
||||
emulator -avd "$AVD_NAME" -no-audio -no-snapshot &
|
||||
EMULATOR_PID=$!
|
||||
|
||||
# Wait for emulator to boot
|
||||
log_info "Waiting for emulator to boot..."
|
||||
adb wait-for-device
|
||||
|
||||
# Wait for boot to complete
|
||||
log_info "Waiting for boot to complete..."
|
||||
while [ "$(adb shell getprop sys.boot_completed)" != "1" ]; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log_success "Emulator is ready!"
|
||||
}
|
||||
|
||||
# Function to build and deploy
|
||||
build_and_deploy() {
|
||||
log_info "Building TimeSafari for $BUILD_MODE mode..."
|
||||
|
||||
# Clean build if requested
|
||||
if [ "$CLEAN_BUILD" = true ]; then
|
||||
log_info "Cleaning previous build..."
|
||||
npm run clean:android
|
||||
fi
|
||||
|
||||
# Build based on mode
|
||||
case $BUILD_MODE in
|
||||
"development")
|
||||
npm run build:android:dev
|
||||
;;
|
||||
"test")
|
||||
npm run build:android:test
|
||||
;;
|
||||
"production")
|
||||
npm run build:android:prod
|
||||
;;
|
||||
esac
|
||||
|
||||
# Deploy to emulator
|
||||
log_info "Deploying to emulator..."
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
# Launch app
|
||||
log_info "Launching TimeSafari..."
|
||||
adb shell am start -n app.timesafari/.MainActivity
|
||||
|
||||
log_success "TimeSafari deployed and launched successfully!"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log_info "TimeSafari Android Emulator Deployment"
|
||||
log_info "Build Mode: $BUILD_MODE"
|
||||
log_info "AVD Name: $AVD_NAME"
|
||||
|
||||
# Start emulator if requested and not running
|
||||
if [ "$START_EMULATOR" = true ]; then
|
||||
if ! check_emulator_running; then
|
||||
start_emulator
|
||||
else
|
||||
log_info "Emulator already running"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build and deploy
|
||||
build_and_deploy
|
||||
|
||||
log_success "Deployment completed successfully!"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
EOF
|
||||
|
||||
# Make script executable
|
||||
chmod +x scripts/deploy-android-emulator.sh
|
||||
```
|
||||
|
||||
### Method 3: Direct Command Line Deployment
|
||||
|
||||
For quick deployments without scripts:
|
||||
|
||||
```bash
|
||||
# 1. Ensure emulator is running
|
||||
adb devices
|
||||
|
||||
# 2. Build the app
|
||||
npm run build:android:dev
|
||||
|
||||
# 3. Install APK
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
# 4. Launch app
|
||||
adb shell am start -n app.timesafari/.MainActivity
|
||||
|
||||
# 5. View logs
|
||||
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)"
|
||||
```
|
||||
|
||||
## Advanced Deployment Options
|
||||
|
||||
### Custom API Server Configuration
|
||||
|
||||
For development with custom API endpoints:
|
||||
|
||||
```bash
|
||||
# Build with custom API IP
|
||||
npm run build:android:dev:custom
|
||||
|
||||
# Or modify capacitor.config.ts for specific IP
|
||||
# Then build normally
|
||||
npm run build:android:dev
|
||||
```
|
||||
|
||||
### Debug vs Release Builds
|
||||
|
||||
```bash
|
||||
# Debug build (default)
|
||||
npm run build:android:debug
|
||||
|
||||
# Release build
|
||||
npm run build:android:release
|
||||
|
||||
# Install specific build
|
||||
adb install -r android/app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
### Asset Management
|
||||
|
||||
```bash
|
||||
# Validate Android assets
|
||||
npm run assets:validate:android
|
||||
|
||||
# Generate assets only
|
||||
npm run build:android:assets
|
||||
|
||||
# Clean assets
|
||||
npm run assets:clean
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Emulator Not Starting / AVD Not Found**
|
||||
```bash
|
||||
# Check available AVDs
|
||||
avdmanager list avd
|
||||
|
||||
# If AVD exists but emulator can't find it, check AVD location
|
||||
echo $ANDROID_AVD_HOME
|
||||
ls -la ~/.android/avd/
|
||||
|
||||
# Fix AVD path issue (common on Arch Linux)
|
||||
export ANDROID_AVD_HOME=/home/$USER/.config/.android/avd
|
||||
|
||||
# Or create symlinks if AVDs are in different location
|
||||
mkdir -p ~/.android/avd
|
||||
ln -s /home/$USER/.config/.android/avd/* ~/.android/avd/
|
||||
|
||||
# Create new AVD if needed
|
||||
avdmanager create avd --name "TimeSafari_Emulator" --package "system-images;android-34;google_apis;x86_64"
|
||||
|
||||
# Check emulator logs
|
||||
emulator -avd TimeSafari_Emulator -verbose
|
||||
```
|
||||
|
||||
2. **System Lockup / High Resource Usage**
|
||||
```bash
|
||||
# Kill any stuck emulator processes
|
||||
pkill -f emulator
|
||||
|
||||
# Check system resources
|
||||
free -h
|
||||
nvidia-smi # if using NVIDIA GPU
|
||||
|
||||
# Start with minimal resources
|
||||
emulator -avd TimeSafari_Emulator \
|
||||
-no-audio \
|
||||
-memory 1536 \
|
||||
-cores 1 \
|
||||
-gpu swiftshader_indirect &
|
||||
|
||||
# Monitor resource usage
|
||||
htop
|
||||
|
||||
# If still having issues, try software rendering only
|
||||
emulator -avd TimeSafari_Emulator \
|
||||
-no-audio \
|
||||
-no-snapshot \
|
||||
-memory 1024 \
|
||||
-cores 1 \
|
||||
-gpu off &
|
||||
```
|
||||
|
||||
3. **ADB Device Not Found**
|
||||
```bash
|
||||
# Restart ADB server
|
||||
adb kill-server
|
||||
adb start-server
|
||||
|
||||
# Check devices
|
||||
adb devices
|
||||
|
||||
# Check emulator status
|
||||
adb get-state
|
||||
```
|
||||
|
||||
3. **Build Failures**
|
||||
```bash
|
||||
# Clean everything
|
||||
npm run clean:android
|
||||
|
||||
# Rebuild
|
||||
npm run build:android:dev
|
||||
|
||||
# Check Gradle logs
|
||||
cd android && ./gradlew clean --stacktrace
|
||||
```
|
||||
|
||||
4. **Installation Failures**
|
||||
```bash
|
||||
# Uninstall existing app
|
||||
adb uninstall app.timesafari
|
||||
|
||||
# Reinstall
|
||||
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
# Check package info
|
||||
adb shell pm list packages | grep timesafari
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
1. **Emulator Performance**
|
||||
```bash
|
||||
# Start with hardware acceleration
|
||||
emulator -avd TimeSafari_Emulator -gpu host
|
||||
|
||||
# Use snapshot for faster startup
|
||||
emulator -avd TimeSafari_Emulator -snapshot default
|
||||
|
||||
# Allocate more RAM
|
||||
emulator -avd TimeSafari_Emulator -memory 4096
|
||||
```
|
||||
|
||||
2. **Build Performance**
|
||||
```bash
|
||||
# Use Gradle daemon
|
||||
echo "org.gradle.daemon=true" >> android/gradle.properties
|
||||
|
||||
# Increase heap size
|
||||
echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties
|
||||
|
||||
# Enable parallel builds
|
||||
echo "org.gradle.parallel=true" >> android/gradle.properties
|
||||
```
|
||||
|
||||
## Integration with Existing Build System
|
||||
|
||||
### NPM Scripts Integration
|
||||
|
||||
Add emulator-specific scripts to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"emulator:check": "./scripts/avd-resource-checker.sh",
|
||||
"emulator:check:test": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --test",
|
||||
"emulator:check:create": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --create",
|
||||
"emulator:start": "emulator -avd TimeSafari_Emulator -no-audio &",
|
||||
"emulator:start:optimized": "/tmp/start-avd-TimeSafari_Emulator.sh",
|
||||
"emulator:stop": "adb emu kill",
|
||||
"emulator:deploy": "./scripts/deploy-android-emulator.sh",
|
||||
"emulator:deploy:dev": "./scripts/deploy-android-emulator.sh --dev",
|
||||
"emulator:deploy:test": "./scripts/deploy-android-emulator.sh --test",
|
||||
"emulator:deploy:prod": "./scripts/deploy-android-emulator.sh --prod",
|
||||
"emulator:logs": "adb logcat | grep -E '(TimeSafari|Capacitor|MainActivity)'",
|
||||
"emulator:shell": "adb shell"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
For automated testing and deployment:
|
||||
|
||||
```bash
|
||||
# GitHub Actions example
|
||||
- name: Start Android Emulator
|
||||
run: |
|
||||
emulator -avd TimeSafari_Emulator -no-audio -no-snapshot &
|
||||
adb wait-for-device
|
||||
adb shell getprop sys.boot_completed
|
||||
|
||||
- name: Build and Deploy
|
||||
run: |
|
||||
npm run build:android:test
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
adb shell am start -n app.timesafari/.MainActivity
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
npm run test:android
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Start emulator once per session**
|
||||
```bash
|
||||
emulator -avd TimeSafari_Emulator -no-audio &
|
||||
```
|
||||
|
||||
2. **Use incremental builds**
|
||||
```bash
|
||||
# For rapid iteration
|
||||
npm run build:android:sync
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
3. **Monitor logs continuously**
|
||||
```bash
|
||||
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)" --color=always
|
||||
```
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Use snapshots for faster startup**
|
||||
2. **Enable hardware acceleration**
|
||||
3. **Allocate sufficient RAM (4GB+)**
|
||||
4. **Use SSD storage for AVDs**
|
||||
5. **Close unnecessary applications**
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **Use debug builds for development only**
|
||||
2. **Never commit debug keystores**
|
||||
3. **Use release builds for testing**
|
||||
4. **Validate API endpoints in production builds**
|
||||
|
||||
## Conclusion
|
||||
|
||||
This guide provides a complete solution for deploying TimeSafari to Android emulators without Android Studio. The approach leverages the existing build system while adding emulator-specific deployment capabilities.
|
||||
|
||||
The key benefits:
|
||||
- ✅ **No Android Studio required**
|
||||
- ✅ **Command-line only workflow**
|
||||
- ✅ **Integration with existing build scripts**
|
||||
- ✅ **Automated deployment options**
|
||||
- ✅ **Comprehensive troubleshooting guide**
|
||||
|
||||
For questions or issues, refer to the troubleshooting section or check the existing build documentation in `BUILDING.md`.
|
||||
@@ -122,4 +122,4 @@ export default class HomeView extends Vue {
|
||||
|
||||
---
|
||||
|
||||
*This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application.*
|
||||
_This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application._
|
||||
|
||||
@@ -92,5 +92,5 @@ Multiple stack traces showing Vue router navigation and component mounting cycle
|
||||
3. Address API/server issues in separate debugging session
|
||||
|
||||
---
|
||||
*Log Entry by: Migration Assistant*
|
||||
*Session: ProjectsView.vue Triple Migration Pattern*
|
||||
_Log Entry by: Migration Assistant_
|
||||
_Session: ProjectsView.vue Triple Migration Pattern_
|
||||
|
||||
@@ -32,6 +32,7 @@ you apply 1-3 meta-rules that automatically include everything you need.
|
||||
### **Step 1: Always Start with Core Always-On**
|
||||
|
||||
**Every single interaction** starts with:
|
||||
|
||||
```
|
||||
meta_core_always_on.mdc
|
||||
```
|
||||
@@ -65,12 +66,14 @@ meta_core_always_on + meta_research + meta_bug_diagnosis
|
||||
**Important**: Meta-rules represent **workflow phases**, not a rigid sequence. You can:
|
||||
|
||||
### **Jump Between Phases Freely**
|
||||
|
||||
- **Start with diagnosis** if you already know the problem
|
||||
- **Go back to research** if your fix reveals new issues
|
||||
- **Switch to planning** mid-implementation if scope changes
|
||||
- **Document at any phase** - not just at the end
|
||||
|
||||
### **Mode Switching by Invoking Meta-Rules**
|
||||
|
||||
Each meta-rule invocation **automatically switches your workflow mode**:
|
||||
|
||||
```
|
||||
@@ -80,11 +83,13 @@ Planning Mode → Invoke @meta_feature_implementation → Implementation Mode
|
||||
```
|
||||
|
||||
### **Phase Constraints, Not Sequence Constraints**
|
||||
|
||||
- **Within each phase**: Clear constraints on what you can/cannot do
|
||||
- **Between phases**: Complete freedom to move as needed
|
||||
- **No forced order**: Choose the phase that matches your current need
|
||||
|
||||
### **Example of Flexible Workflow**
|
||||
|
||||
```
|
||||
1. Start with @meta_research (investigation mode)
|
||||
2. Jump to @meta_bug_diagnosis (diagnosis mode)
|
||||
@@ -103,16 +108,19 @@ Planning Mode → Invoke @meta_feature_implementation → Implementation Mode
|
||||
**Scenario**: User reports that the contact list isn't loading properly
|
||||
|
||||
**Initial Meta-Rule Selection**:
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_research + meta_bug_diagnosis
|
||||
```
|
||||
|
||||
**What This Gives You**:
|
||||
|
||||
- **Core Always-On**: Human competence focus, time standards, context
|
||||
- **Research**: Systematic investigation methodology, evidence collection
|
||||
- **Bug Diagnosis**: Defect analysis framework, root cause identification
|
||||
|
||||
**Flexible Workflow**:
|
||||
|
||||
1. Apply core always-on for foundation
|
||||
2. Use research meta-rule for systematic investigation
|
||||
3. Switch to bug diagnosis when you have enough evidence
|
||||
@@ -125,16 +133,19 @@ meta_core_always_on + meta_research + meta_bug_diagnosis
|
||||
**Scenario**: Building a new contact search feature
|
||||
|
||||
**Meta-Rule Selection**:
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_feature_planning + meta_feature_implementation
|
||||
```
|
||||
|
||||
**What This Gives You**:
|
||||
|
||||
- **Core Always-On**: Foundation principles and context
|
||||
- **Feature Planning**: Requirements analysis, architecture planning
|
||||
- **Feature Implementation**: Development workflow, testing strategy
|
||||
|
||||
**Iterative Workflow**:
|
||||
|
||||
1. Start with core always-on
|
||||
2. Use feature planning for design and requirements
|
||||
3. Switch to feature implementation for coding and testing
|
||||
@@ -147,15 +158,18 @@ meta_core_always_on + meta_feature_planning + meta_feature_implementation
|
||||
**Scenario**: Writing a migration guide for the new database system
|
||||
|
||||
**Meta-Rule Selection**:
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_documentation
|
||||
```
|
||||
|
||||
**What This Gives You**:
|
||||
|
||||
- **Core Always-On**: Foundation and context
|
||||
- **Documentation**: Educational focus, templates, quality standards
|
||||
|
||||
**Parallel Workflow**:
|
||||
|
||||
1. Apply core always-on for foundation
|
||||
2. Use documentation meta-rule for educational content creation
|
||||
3. **Can research** while documenting if you need more information
|
||||
@@ -198,27 +212,35 @@ Each meta-rule includes success criteria. Use these to validate your work:
|
||||
## Common Meta-Rule Combinations
|
||||
|
||||
### **Research + Diagnosis**
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_research + meta_bug_diagnosis
|
||||
```
|
||||
|
||||
**Use for**: Complex bug investigations requiring systematic analysis
|
||||
|
||||
### **Planning + Implementation**
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_feature_planning + meta_feature_implementation
|
||||
```
|
||||
|
||||
**Use for**: End-to-end feature development from concept to deployment
|
||||
|
||||
### **Research + Planning**
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_research + meta_feature_planning
|
||||
```
|
||||
|
||||
**Use for**: Feasibility research and solution design
|
||||
|
||||
### **Documentation + Context**
|
||||
|
||||
```
|
||||
meta_core_always_on + meta_documentation + [context-specific]
|
||||
```
|
||||
|
||||
**Use for**: Creating comprehensive, educational documentation
|
||||
|
||||
## Best Practices
|
||||
|
||||
231
doc/notification-system.md
Normal file
231
doc/notification-system.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# TimeSafari — Native-First Notification System
|
||||
|
||||
**Status:** Ready for implementation
|
||||
**Date:** 2025-09-07
|
||||
**Author:** Matthew Raymer
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Ship a **single, Native-First** notification system: OS-scheduled **background prefetch at T–lead** + **pre-armed** local notifications. Web-push is retired.
|
||||
|
||||
### What we deliver
|
||||
|
||||
- **Closed-app delivery:** Pre-armed locals fire even if the app is closed.
|
||||
- **Freshness:** One prefetch attempt per slot at **T–lead**; ETag/TTL controls; skip when stale.
|
||||
- **Android precision:** Exact alarms with permission; windowed fallback (±10m) otherwise.
|
||||
- **Resilience:** Re-arm after reboot/time-change (Android receivers; iOS on next wake/silent push).
|
||||
- **Cross-platform:** Same TS API (iOS/Android/Electron). Electron is best-effort while running.
|
||||
|
||||
### Success signals
|
||||
|
||||
- High delivery reliability, minute-precision on Android with permission.
|
||||
- Prefetch budget hit rate at **T–lead**; zero stale deliveries beyond TTL.
|
||||
|
||||
---
|
||||
|
||||
## Strategic Plan
|
||||
|
||||
### Goal
|
||||
|
||||
Deliver 1..M daily notifications with **OS background prefetch at T–lead** and **rolling-window safety** so messages display with fresh content even when the app is closed.
|
||||
|
||||
### Tenets
|
||||
|
||||
- **Reliability first:** OS delivers once scheduled; no JS at delivery time.
|
||||
- **Freshness with guardrails:** Prefetch at **T–lead**; enforce **TTL-at-fire**; ETag-aware.
|
||||
- **Single system:** One TS API; native adapters swap under the hood.
|
||||
- **Platform honesty:** Android exactness via permission; iOS best-effort budget.
|
||||
|
||||
### Architecture (high level)
|
||||
|
||||
App (Vue/TS) → Orchestrator (policy) → Native Adapters:
|
||||
|
||||
- **SchedulerNative** — AlarmManager (Android) / UNUserNotificationCenter (iOS)
|
||||
- **BackgroundPrefetchNative** — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
|
||||
- **DataStore** — SQLite
|
||||
|
||||
**Storage (single shared DB):** The app and the native plugin will use **the same SQLite database file**. The app owns schema/migrations; the plugin opens the same file with WAL enabled and performs short, serialized writes. This keeps one source of truth for payloads, delivery logs, and config.
|
||||
|
||||
### SQLite Ownership & Concurrency
|
||||
|
||||
* **One DB file:** The plugin opens the **same path** the app uses (no second DB).
|
||||
* **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version`. The plugin **never** migrates; it **asserts** the expected version.
|
||||
* **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL allows foreground reads while a background job commits quickly.
|
||||
* **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return.
|
||||
* **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings.
|
||||
|
||||
### Scheduling & T–lead
|
||||
|
||||
- **Arm** a rolling window (today + tomorrow within iOS cap).
|
||||
- **Attempt** a single **online-first** fetch per slot at **T–lead = T − prefetchLeadMinutes**.
|
||||
- If prefetch is skipped, the armed local **still fires** using cached content.
|
||||
|
||||
### Policies
|
||||
|
||||
- **TTL-at-fire:** If (T − fetchedAt) > `ttlSeconds` → **skip** arming.
|
||||
- **Android exactness:** Request `SCHEDULE_EXACT_ALARM`; fallback **±10m** window.
|
||||
- **Reboot/time change:** Android receivers re-arm next 24h; iOS on next wake/silent push.
|
||||
- **No delivery-time mutation:** iOS locals cannot be mutated by NSE; render before scheduling.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### 1) Interfaces (TS stable)
|
||||
|
||||
- **SchedulerNative**: `scheduleExact({slotId, whenMs, title, body, extra})`, `scheduleWindow(..., windowLenMs)`, `cancelBySlot`, `rescheduleAll`, `capabilities()`
|
||||
- **BackgroundPrefetchNative**: `schedulePrefetch(slotId, atMs)`, `cancelPrefetch(slotId)`
|
||||
- **DataStore**: SQLite adapters (notif_contents, notif_deliveries, notif_config)
|
||||
- **Public API**: `configure`, `requestPermissions`, `runFullPipelineNow`, `reschedule`, `getState`
|
||||
|
||||
### DB Path & Adapter Configuration
|
||||
|
||||
* **Configure option:** `dbPath: string` (absolute path or platform alias) is passed from JS to the plugin during `configure()`.
|
||||
* **Shared tables:**
|
||||
|
||||
* `notif_contents(slot_id, payload_json, fetched_at, etag, …)`
|
||||
* `notif_deliveries(slot_id, fire_at, delivered_at, status, error_code, …)`
|
||||
* `notif_config(k, v)`
|
||||
* **Open settings:**
|
||||
|
||||
* `journal_mode=WAL`
|
||||
* `synchronous=NORMAL`
|
||||
* `busy_timeout=5000`
|
||||
* `foreign_keys=ON`
|
||||
|
||||
**Type (TS) extension**
|
||||
|
||||
```ts
|
||||
export type ConfigureOptions = {
|
||||
// …existing fields…
|
||||
dbPath: string; // shared DB file the plugin will open
|
||||
storage: 'shared'; // canonical value; plugin-owned DB is not used
|
||||
};
|
||||
```
|
||||
|
||||
**Plugin side (pseudo)**
|
||||
|
||||
```kotlin
|
||||
// Android open
|
||||
val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE)
|
||||
db.execSQL("PRAGMA journal_mode=WAL")
|
||||
db.execSQL("PRAGMA synchronous=NORMAL")
|
||||
db.execSQL("PRAGMA foreign_keys=ON")
|
||||
db.execSQL("PRAGMA busy_timeout=5000")
|
||||
// Verify schema version
|
||||
val uv = rawQuery("PRAGMA user_version").use { it.moveToFirst(); it.getInt(0) }
|
||||
require(uv >= MIN_EXPECTED_VERSION) { "Schema version too old" }
|
||||
```
|
||||
|
||||
```swift
|
||||
// iOS open (FMDB / SQLite3)
|
||||
// Set WAL via PRAGMA after open; check user_version the same way.
|
||||
```
|
||||
|
||||
### 2) Templating & Arming
|
||||
|
||||
- Render `title/body` **before** scheduling; pass via **SchedulerNative**.
|
||||
- Route all arming through **SchedulerNative** to centralize Android exact/window semantics.
|
||||
|
||||
### 3) T–lead (single attempt)
|
||||
|
||||
**T–lead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **T–lead = T − prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content.
|
||||
|
||||
- Compute T–lead = `whenMs - prefetchLeadMinutes*60_000`.
|
||||
- `BackgroundPrefetchNative.schedulePrefetch(slotId, atMs=T–lead)`.
|
||||
- On wake: **ETag** fetch (timeout **12s**), persist, optionally cancel & re-arm if within TTL.
|
||||
- Never fetch at delivery time.
|
||||
|
||||
### 4) TTL-at-fire
|
||||
|
||||
**TTL-at-fire:** Before arming for time **T**, compute `T − fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time.
|
||||
|
||||
`if (whenMs - fetchedAt) > ttlSeconds*1000 → skip`
|
||||
|
||||
### 5) Android specifics
|
||||
|
||||
- Request `SCHEDULE_EXACT_ALARM`; deep-link if denied; fallback to `setWindow(start,len)` (±10m).
|
||||
- Receivers: `BOOT_COMPLETED`, `TIMEZONE_CHANGED`, `TIME_SET` → recompute & re-arm for next 24h and schedule T–lead prefetch.
|
||||
|
||||
### 6) iOS specifics
|
||||
|
||||
- `BGTaskScheduler` for T–lead prefetch (best-effort). Optional silent push nudge.
|
||||
- Locals: `UNCalendarNotificationTrigger` (one-shots); no NSE mutation for locals.
|
||||
|
||||
### 7) Network & Timeouts
|
||||
|
||||
- Content fetch: **12s** timeout; single attempt at T–lead; ETag/304 respected.
|
||||
- ACK/Error: **8s** timeout, fire-and-forget.
|
||||
|
||||
### 8) Electron
|
||||
|
||||
- Notifications while app is running; recommend **Start-on-Login**. No true background scheduling when fully closed.
|
||||
|
||||
### 9) Telemetry
|
||||
|
||||
- Record `scheduled|shown|error`; ACK deliveries (8s timeout); include slot/times/TZ/app version.
|
||||
|
||||
---
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
| Capability | Android (Native) | iOS (Native) | Electron | Web |
|
||||
|---|---|---|---|---|
|
||||
| Multi-daily locals (closed app) | ✅ | ✅ | ✅ (app running) | — |
|
||||
| Prefetch at T–lead (app closed) | ✅ WorkManager | ⚠️ BGTask (best-effort) | ✅ (app running) | — |
|
||||
| Re-arm after reboot/time-change | ✅ Receivers | ⚠️ On next wake/silent push | ✅ Start-on-Login | — |
|
||||
| Minute-precision alarms | ✅ with exact permission | ❌ not guaranteed | ✅ timer best-effort | — |
|
||||
| Delivery-time mutation for locals | ❌ | ❌ | — | — |
|
||||
| ETag/TTL enforcement | ✅ | ✅ | ✅ | — |
|
||||
| Rolling-window safety | ✅ | ✅ | ✅ | — |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Core
|
||||
|
||||
- **Closed-app delivery:** Armed locals fire at T with last rendered content. No delivery-time network.
|
||||
- **T–lead prefetch:** Single background attempt at **T–lead**; if skipped, delivery still occurs from cache.
|
||||
- **TTL-at-fire:** No armed local violates TTL at T.
|
||||
|
||||
### Android
|
||||
|
||||
- **Exact permission path:** With `SCHEDULE_EXACT_ALARM` → within ±1m; else **±10m** window.
|
||||
- **Reboot recovery:** After reboot, receivers re-arm next 24h and schedule T–lead prefetch.
|
||||
- **TZ/DST change:** Recompute & re-arm; future slots align to new wall-clock.
|
||||
|
||||
### iOS
|
||||
|
||||
- **BGTask budget respected:** Prefetch often runs but may be skipped; delivery still occurs via rolling window.
|
||||
- **Force-quit caveat:** No background execution after user terminate; delivery still occurs if pre-armed.
|
||||
|
||||
### Electron
|
||||
|
||||
- **Running-app rule:** Delivery only while app runs; with Start-on-Login, after reboot the orchestrator re-arms and subsequent slots deliver.
|
||||
|
||||
### Network
|
||||
|
||||
- Content fetch timeout **12s**; ACK/Error **8s**; no retries inside lead; ETag honored.
|
||||
|
||||
### Observability
|
||||
|
||||
- Log/telemetry for `scheduled|shown|error`; ACK payload includes slot, times, device TZ, app version.
|
||||
|
||||
### DB Sharing
|
||||
|
||||
* **Shared DB visibility:** A background prefetch writes `notif_contents`; the foreground UI **immediately** reads the same row.
|
||||
* **WAL overlap:** With the app reading while the plugin commits, no user-visible blocking occurs.
|
||||
* **Version safety:** If `user_version` is behind, the plugin emits an error and does not write (protects against partial installs).
|
||||
|
||||
---
|
||||
|
||||
## Web-Push Cleanup
|
||||
|
||||
Web-push functionality has been retired due to unreliability. All web-push related code paths and documentation sections should be removed or marked as deprecated. See `web-push-cleanup-guide.md` for detailed cleanup steps.
|
||||
|
||||
---
|
||||
|
||||
_This document consolidates the Native-First notification system strategy, implementation details, capabilities, and acceptance criteria into a single comprehensive reference._
|
||||
@@ -1,181 +0,0 @@
|
||||
# Seed Phrase Backup Reminder Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive.
|
||||
|
||||
## Features
|
||||
|
||||
- **Modal Dialog**: Uses the existing notification group modal system from `App.vue`
|
||||
- **Smart Timing**: Only shows when `hasBackedUpSeed = false`
|
||||
- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day
|
||||
- **Action-Based Triggers**: Shows after specific user actions
|
||||
- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Core Utility (`src/utils/seedPhraseReminder.ts`)
|
||||
|
||||
The main utility provides:
|
||||
|
||||
- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown
|
||||
- `markSeedReminderShown()`: Updates localStorage timestamp
|
||||
- `createSeedReminderNotification()`: Creates the modal configuration
|
||||
- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder
|
||||
|
||||
### Trigger Points
|
||||
|
||||
The reminder is shown after these user actions:
|
||||
|
||||
**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims.
|
||||
|
||||
1. **Profile Saving** (`AccountViewView.vue`)
|
||||
- After clicking "Save Profile" button
|
||||
- Only when profile save is successful
|
||||
|
||||
2. **Claim Creation** (Multiple views)
|
||||
- `ClaimAddRawView.vue`: After submitting raw claims
|
||||
- `GiftedDialog.vue`: After creating gifts/claims
|
||||
- `GiftedDetailsView.vue`: After recording gifts/claims
|
||||
- `OfferDialog.vue`: After creating offers
|
||||
|
||||
3. **QR Code Views Exit**
|
||||
- `ContactQRScanFullView.vue`: When exiting via back button
|
||||
- `ContactQRScanShowView.vue`: When exiting via back button
|
||||
|
||||
### Modal Configuration
|
||||
|
||||
```typescript
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Backup Your Identifier Seed?",
|
||||
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
|
||||
yesText: "Backup Identifier Seed",
|
||||
noText: "Remind me Later",
|
||||
onYes: () => navigate to /seed-backup,
|
||||
onNo: () => mark as shown for 24 hours,
|
||||
onCancel: () => mark as shown for 24 hours
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically.
|
||||
|
||||
### Cooldown Mechanism
|
||||
|
||||
- **Storage Key**: `seedPhraseReminderLastShown`
|
||||
- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds)
|
||||
- **Implementation**: localStorage with timestamp comparison
|
||||
- **Fallback**: Shows reminder if timestamp is invalid or missing
|
||||
|
||||
## User Experience
|
||||
|
||||
### When Reminder Appears
|
||||
|
||||
- User has not backed up their seed phrase (`hasBackedUpSeed = false`)
|
||||
- At least 24 hours have passed since last reminder
|
||||
- User performs one of the trigger actions
|
||||
- **1-second delay** after the success message to allow users to see the confirmation
|
||||
|
||||
### User Options
|
||||
|
||||
1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page
|
||||
2. **"Remind me Later"**: Dismisses and won't show again for 24 hours
|
||||
3. **Cancel/Close**: Same behavior as "Remind me Later"
|
||||
|
||||
### Frequency Control
|
||||
|
||||
- **First Time**: Always shows if user hasn't backed up
|
||||
- **Subsequent**: Only shows after 24-hour cooldown
|
||||
- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`)
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Graceful fallback if localStorage operations fail
|
||||
- Logging of errors for debugging
|
||||
- Non-blocking implementation (doesn't affect main functionality)
|
||||
|
||||
### Integration Points
|
||||
|
||||
- **Platform Service**: Uses `$accountSettings()` to check backup status
|
||||
- **Notification System**: Integrates with existing `$notify` system
|
||||
- **Router**: Uses `window.location.href` for navigation
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Minimal localStorage operations
|
||||
- No blocking operations
|
||||
- Efficient timestamp comparisons
|
||||
- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Scenarios
|
||||
|
||||
1. **First Time User**
|
||||
- Create new account
|
||||
- Perform trigger action (save profile, create claim, exit QR view)
|
||||
- Verify reminder appears
|
||||
|
||||
2. **Repeat User (Within 24h)**
|
||||
- Perform trigger action
|
||||
- Verify reminder does NOT appear
|
||||
|
||||
3. **Repeat User (After 24h)**
|
||||
- Wait 24+ hours
|
||||
- Perform trigger action
|
||||
- Verify reminder appears again
|
||||
|
||||
4. **User Who Has Backed Up**
|
||||
- Complete seed backup
|
||||
- Perform trigger action
|
||||
- Verify reminder does NOT appear
|
||||
|
||||
5. **QR Code View Exit**
|
||||
- Navigate to QR code view (full or show)
|
||||
- Exit via back button
|
||||
- Verify reminder appears (if conditions are met)
|
||||
|
||||
### Browser Testing
|
||||
|
||||
- Test localStorage functionality
|
||||
- Verify timestamp handling
|
||||
- Check navigation to seed backup page
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **Customizable Cooldown**: Allow users to set reminder frequency
|
||||
2. **Progressive Urgency**: Increase reminder frequency over time
|
||||
3. **Analytics**: Track reminder effectiveness and user response
|
||||
4. **A/B Testing**: Test different reminder messages and timing
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- Reminder frequency settings
|
||||
- Custom reminder messages
|
||||
- Different trigger conditions
|
||||
- Integration with other notification systems
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Monitoring
|
||||
|
||||
- Check localStorage usage in browser dev tools
|
||||
- Monitor user feedback about reminder frequency
|
||||
- Track navigation success to seed backup page
|
||||
|
||||
### Updates
|
||||
|
||||
- Modify reminder text in `createSeedReminderNotification()`
|
||||
- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant
|
||||
- Add new trigger points as needed
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder.
|
||||
|
||||
The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction.
|
||||
551
doc/web-push-cleanup-guide.md
Normal file
551
doc/web-push-cleanup-guide.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# TimeSafari Web-Push Cleanup Guide
|
||||
|
||||
**Status:** 🚀 Native-First Implementation
|
||||
**Date:** 2025-01-27T14:30Z (UTC)
|
||||
**Author:** Matthew Raymer
|
||||
**Scope:** Web-push code cleanup and deprecation
|
||||
**Goal:** Remove or quarantine all web-push code paths and mark as deprecated.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document provides a comprehensive cleanup guide for removing web-push code
|
||||
paths from TimeSafari. Web-push has been retired for unreliability, and the
|
||||
system now focuses on native mobile reliability with Electron best-effort support.
|
||||
|
||||
---
|
||||
|
||||
## Cleanup Strategy
|
||||
|
||||
### Phase 1: Identify Web-Push Code Paths
|
||||
|
||||
#### Service Worker Files
|
||||
|
||||
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
|
||||
- [ ] `sw_scripts/` directory - Review for web-push dependencies
|
||||
- [ ] Service worker registration code - Remove or quarantine
|
||||
|
||||
#### Web-Specific Code
|
||||
|
||||
- [ ] Web push notification handlers
|
||||
- [ ] Service worker event listeners
|
||||
- [ ] Web notification API usage
|
||||
- [ ] Push subscription management
|
||||
|
||||
#### Configuration Files
|
||||
|
||||
- [ ] VitePWA plugin configuration
|
||||
- [ ] Service worker build configuration
|
||||
- [ ] Web push manifest files
|
||||
|
||||
### Phase 2: Mark as Deprecated
|
||||
|
||||
#### Code Comments
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Web-push notification handling
|
||||
// This code is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
```
|
||||
|
||||
#### Documentation Updates
|
||||
|
||||
- [ ] Mark web-push sections as deprecated
|
||||
- [ ] Add deprecation notices
|
||||
- [ ] Update README files
|
||||
- [ ] Update API documentation
|
||||
|
||||
### Phase 3: Remove or Quarantine
|
||||
|
||||
#### Complete Removal
|
||||
|
||||
- [ ] Web push subscription code
|
||||
- [ ] Service worker notification handlers
|
||||
- [ ] Web-specific notification APIs
|
||||
- [ ] Push message handling
|
||||
|
||||
#### Quarantine (Keep for Reference)
|
||||
|
||||
- [ ] Service worker registration code
|
||||
- [ ] Web push configuration
|
||||
- [ ] Historical web-push tests
|
||||
|
||||
---
|
||||
|
||||
## Detailed Cleanup Tasks
|
||||
|
||||
### 1. Service Worker Cleanup
|
||||
|
||||
#### Files to Deprecate
|
||||
|
||||
**`sw_scripts/notification-click.js`**
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Service worker notification handling
|
||||
// This code is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
// Original web-push notification click handler
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
// DEPRECATED: Web-push only
|
||||
event.notification.close();
|
||||
|
||||
const slotId = event.notification.data?.slotId;
|
||||
const route = slotId ? '/#/daily' : '/#/notifications';
|
||||
|
||||
event.waitUntil(
|
||||
clients.openWindow(route).catch(() => {
|
||||
return clients.openWindow('/');
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
**Service Worker Registration**
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Service worker registration
|
||||
// This code is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
if ('serviceWorker' in navigator && process.env.VITE_PLATFORM === 'web') {
|
||||
// DEPRECATED: Web-push only
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered:', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Web Push API Cleanup
|
||||
|
||||
#### Push Subscription Management
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Web push subscription management
|
||||
// This code is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
class WebPushManager {
|
||||
// DEPRECATED: Web-push only
|
||||
async subscribeToPush() {
|
||||
// Implementation kept for reference
|
||||
}
|
||||
|
||||
// DEPRECATED: Web-push only
|
||||
async unsubscribeFromPush() {
|
||||
// Implementation kept for reference
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Push Message Handling
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Push message handling
|
||||
// This code is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
// DEPRECATED: Web-push only
|
||||
const data = event.data ? event.data.json() : {};
|
||||
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: '/icon-192x192.png',
|
||||
badge: '/badge-72x72.png',
|
||||
data: data
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title, options)
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Configuration Cleanup
|
||||
|
||||
#### VitePWA Plugin Configuration
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: VitePWA plugin configuration
|
||||
// This configuration is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VitePWA({
|
||||
// DEPRECATED: Web-push only
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||
},
|
||||
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
|
||||
manifest: {
|
||||
name: 'TimeSafari',
|
||||
short_name: 'TimeSafari',
|
||||
description: 'TimeSafari App',
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: 'pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### Service Worker Build Configuration
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Service worker build configuration
|
||||
// This configuration is kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// DEPRECATED: Web-push only
|
||||
sw: 'sw_scripts/notification-click.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Test Cleanup
|
||||
|
||||
#### Web Push Tests
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Web push tests
|
||||
// These tests are kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
describe('Web Push Notifications (DEPRECATED)', () => {
|
||||
// DEPRECATED: Web-push only
|
||||
it('should handle push notifications', async () => {
|
||||
// Test implementation kept for reference
|
||||
});
|
||||
|
||||
// DEPRECATED: Web-push only
|
||||
it('should handle notification clicks', async () => {
|
||||
// Test implementation kept for reference
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Service Worker Tests
|
||||
|
||||
```javascript
|
||||
// DEPRECATED: Service worker tests
|
||||
// These tests are kept for reference but not used in production
|
||||
// Replaced by Native-First notification system
|
||||
|
||||
describe('Service Worker (DEPRECATED)', () => {
|
||||
// DEPRECATED: Web-push only
|
||||
it('should register service worker', async () => {
|
||||
// Test implementation kept for reference
|
||||
});
|
||||
|
||||
// DEPRECATED: Web-push only
|
||||
it('should handle push events', async () => {
|
||||
// Test implementation kept for reference
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Documentation Cleanup
|
||||
|
||||
#### README Updates
|
||||
|
||||
```markdown
|
||||
# TimeSafari Native-First Notification System
|
||||
|
||||
## Web-Push Status: DEPRECATED
|
||||
|
||||
Web-push has been retired for unreliability. The system now focuses on native mobile reliability with Electron best-effort support.
|
||||
|
||||
### Deprecated Features
|
||||
- ❌ Web push notifications
|
||||
- ❌ Service worker notification handling
|
||||
- ❌ Web notification API
|
||||
|
||||
### Active Features
|
||||
- ✅ Native mobile notifications (Android/iOS)
|
||||
- ✅ Electron notifications (best-effort)
|
||||
- ✅ OS-scheduled background prefetch
|
||||
- ✅ Rolling window safety
|
||||
```
|
||||
|
||||
#### API Documentation Updates
|
||||
|
||||
```markdown
|
||||
## Notification API (Native-First)
|
||||
|
||||
### Deprecated Methods
|
||||
- `subscribeToPush()` - DEPRECATED: Web-push only
|
||||
- `unsubscribeFromPush()` - DEPRECATED: Web-push only
|
||||
- `handlePushMessage()` - DEPRECATED: Web-push only
|
||||
|
||||
### Active Methods
|
||||
- `scheduleExact()` - Native exact scheduling
|
||||
- `scheduleWindow()` - Native windowed scheduling
|
||||
- `schedulePrefetch()` - Native background prefetch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File-by-File Cleanup Checklist
|
||||
|
||||
### Service Worker Files
|
||||
|
||||
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
|
||||
- [ ] `sw_scripts/` directory - Review for web-push dependencies
|
||||
- [ ] Service worker build configuration - Remove or quarantine
|
||||
|
||||
### Web-Specific Code
|
||||
|
||||
- [ ] `src/main.web.ts` - Remove service worker registration
|
||||
- [ ] `src/services/webPush.ts` - Mark as deprecated
|
||||
- [ ] `src/utils/serviceWorker.ts` - Mark as deprecated
|
||||
- [ ] Web notification API usage - Remove or quarantine
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- [ ] `vite.config.web.mts` - Remove VitePWA plugin
|
||||
- [ ] `package.json` - Remove web-push dependencies
|
||||
- [ ] `public/manifest.json` - Mark as deprecated
|
||||
- [ ] Service worker build scripts - Remove or quarantine
|
||||
|
||||
### Test Files
|
||||
|
||||
- [ ] `test-playwright/web-push.spec.ts` - Mark as deprecated
|
||||
- [ ] `test/services/webPush.test.ts` - Mark as deprecated
|
||||
- [ ] Service worker tests - Mark as deprecated
|
||||
|
||||
### Documentation Files
|
||||
|
||||
- [ ] `README.md` - Update to reflect native-first approach
|
||||
- [ ] `doc/web-push.md` - Mark as deprecated
|
||||
- [ ] API documentation - Remove web-push references
|
||||
|
||||
---
|
||||
|
||||
## Dependencies to Remove
|
||||
|
||||
### NPM Packages
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
// DEPRECATED: Web-push only
|
||||
"web-push": "^7.4.0",
|
||||
"vite-plugin-pwa": "^0.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
// DEPRECATED: Web-push only
|
||||
"workbox-webpack-plugin": "^6.5.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
// DEPRECATED: Web-push only
|
||||
"build:sw": "workbox generateSW",
|
||||
"test:sw": "jest --testPathPattern=serviceWorker"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Web-Push to Native-First
|
||||
|
||||
#### Step 1: Remove Web-Push Dependencies
|
||||
|
||||
```bash
|
||||
# Remove web-push packages
|
||||
npm uninstall web-push vite-plugin-pwa workbox-webpack-plugin
|
||||
|
||||
# Remove service worker files
|
||||
rm -rf sw_scripts/
|
||||
rm -f public/sw.js
|
||||
rm -f public/workbox-*.js
|
||||
```
|
||||
|
||||
#### Step 2: Update Configuration
|
||||
|
||||
```javascript
|
||||
// Remove VitePWA plugin from vite.config.web.mts
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// Remove VitePWA plugin
|
||||
// VitePWA({ ... })
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### Step 3: Update Service Registration
|
||||
|
||||
```javascript
|
||||
// Remove service worker registration from main.web.ts
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/sw.js')
|
||||
// }
|
||||
```
|
||||
|
||||
#### Step 4: Update Tests
|
||||
|
||||
```javascript
|
||||
// Remove web-push tests
|
||||
// describe('Web Push Notifications', () => { ... })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### Code Removal Verification
|
||||
|
||||
- [ ] No web-push imports remain
|
||||
- [ ] No service worker registration code
|
||||
- [ ] No push subscription management
|
||||
- [ ] No web notification API usage
|
||||
- [ ] No VitePWA plugin configuration
|
||||
|
||||
### Documentation Verification
|
||||
|
||||
- [ ] All web-push references marked as deprecated
|
||||
- [ ] README updated to reflect native-first approach
|
||||
- [ ] API documentation updated
|
||||
- [ ] Test documentation updated
|
||||
|
||||
### Build Verification
|
||||
|
||||
- [ ] Web build succeeds without service worker
|
||||
- [ ] No service worker files generated
|
||||
- [ ] No web-push dependencies in bundle
|
||||
- [ ] Native builds work correctly
|
||||
|
||||
### Test Verification
|
||||
|
||||
- [ ] Web-push tests are marked as deprecated
|
||||
- [ ] Native notification tests pass
|
||||
- [ ] No web-push test failures
|
||||
- [ ] Test suite runs successfully
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Emergency Rollback
|
||||
|
||||
If native-first implementation fails, web-push code can be restored:
|
||||
|
||||
#### 1. **Restore Dependencies**
|
||||
|
||||
```bash
|
||||
npm install web-push vite-plugin-pwa workbox-webpack-plugin
|
||||
```
|
||||
|
||||
#### 2. **Restore Service Worker Files**
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 -- sw_scripts/
|
||||
git checkout HEAD~1 -- public/sw.js
|
||||
```
|
||||
|
||||
#### 3. **Restore Configuration**
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 -- vite.config.web.mts
|
||||
git checkout HEAD~1 -- package.json
|
||||
```
|
||||
|
||||
#### 4. **Restore Tests**
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 -- test-playwright/web-push.spec.ts
|
||||
git checkout HEAD~1 -- test/services/webPush.test.ts
|
||||
```
|
||||
|
||||
### Rollback Verification
|
||||
|
||||
- [ ] Web-push functionality restored
|
||||
- [ ] Service worker registration works
|
||||
- [ ] Push notifications work
|
||||
- [ ] Tests pass
|
||||
|
||||
---
|
||||
|
||||
## Post-Cleanup Tasks
|
||||
|
||||
### Code Review
|
||||
|
||||
- [ ] Review all changes for completeness
|
||||
- [ ] Verify no web-push code remains
|
||||
- [ ] Check for orphaned references
|
||||
- [ ] Validate native-first implementation
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Run full test suite
|
||||
- [ ] Verify native notifications work
|
||||
- [ ] Check Electron functionality
|
||||
- [ ] Validate mobile builds
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] Update all documentation
|
||||
- [ ] Remove web-push references
|
||||
- [ ] Update API documentation
|
||||
- [ ] Update user guides
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Complete Web-Push Removal
|
||||
|
||||
- [ ] All web-push code marked as deprecated
|
||||
- [ ] Service worker files quarantined
|
||||
- [ ] Dependencies removed
|
||||
- [ ] Configuration updated
|
||||
|
||||
### Native-First Implementation
|
||||
|
||||
- [ ] Native notifications work on Android
|
||||
- [ ] Native notifications work on iOS
|
||||
- [ ] Electron notifications work
|
||||
- [ ] Background prefetch works
|
||||
|
||||
### Documentation Updated
|
||||
|
||||
- [ ] All docs reflect native-first approach
|
||||
- [ ] Web-push marked as deprecated
|
||||
- [ ] Migration guide provided
|
||||
- [ ] Rollback plan documented
|
||||
|
||||
---
|
||||
|
||||
_This cleanup guide provides comprehensive instructions for removing web-push
|
||||
code paths from TimeSafari. Web-push has been retired for unreliability, and the
|
||||
system now focuses on native mobile reliability with Electron best-effort support._
|
||||
@@ -5,12 +5,15 @@
|
||||
**Status**: 🎯 **ACTIVE** - Z-index layering standards
|
||||
|
||||
## Objective
|
||||
|
||||
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements.
|
||||
|
||||
## Result
|
||||
|
||||
This document defines the z-index hierarchy for all UI components.
|
||||
|
||||
## Use/Run
|
||||
|
||||
Reference these values when implementing new components or modifying existing ones to maintain consistent layering.
|
||||
|
||||
## Z-Index Hierarchy
|
||||
@@ -51,7 +54,7 @@ Reference these values when implementing new components or modifying existing on
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: Frontend team, UI/UX designers
|
||||
- **Sign-off checklist**:
|
||||
- **Sign-off checklist**:
|
||||
- [ ] All new components follow z-index guidelines
|
||||
- [ ] Existing components updated to use defined values
|
||||
- [ ] Cross-browser testing completed
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no, interactive-widget=overlays-content" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
|
||||
<!-- CORS headers removed to allow images from any domain -->
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -413,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.2;
|
||||
MARKETING_VERSION = 1.0.7;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -430,7 +430,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
CURRENT_PROJECT_VERSION = 40;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -440,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.2;
|
||||
MARKETING_VERSION = 1.0.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
95
package-lock.json
generated
95
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.0-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.0-beta",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
@@ -27,7 +27,6 @@
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@ethersproject/wallet": "^5.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||
"@jlongster/sql.js": "^1.6.7",
|
||||
@@ -91,7 +90,6 @@
|
||||
"vue": "3.5.13",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "3.0.4",
|
||||
"vue-markdown-render": "^2.2.1",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-qrcode-reader": "^5.5.3",
|
||||
"vue-router": "^4.5.0",
|
||||
@@ -108,7 +106,6 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/ramda": "^0.29.11",
|
||||
@@ -6789,17 +6786,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
@@ -10161,12 +10147,6 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||
@@ -10174,22 +10154,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
|
||||
@@ -32919,61 +32883,6 @@
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
|
||||
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
|
||||
"dependencies": {
|
||||
"markdown-it": "^13.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render/node_modules/entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render/node_modules/linkify-it": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render/node_modules/markdown-it": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
|
||||
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-markdown-render/node_modules/mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||
},
|
||||
"node_modules/vue-markdown-render/node_modules/uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"node_modules/vue-picture-cropper": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.0-beta",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
@@ -106,7 +106,7 @@
|
||||
"guard": "bash ./scripts/build-arch-guard.sh",
|
||||
"guard:test": "bash ./scripts/build-arch-guard.sh --staged",
|
||||
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
|
||||
"clean:android": "./scripts/uninstall-android.sh",
|
||||
"clean:android": "./scripts/clean-android.sh",
|
||||
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
|
||||
"clean:electron": "./scripts/build-electron.sh --clean",
|
||||
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
|
||||
@@ -156,7 +156,6 @@
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@ethersproject/wallet": "^5.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||
"@jlongster/sql.js": "^1.6.7",
|
||||
@@ -220,7 +219,6 @@
|
||||
"vue": "3.5.13",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "3.0.4",
|
||||
"vue-markdown-render": "^2.2.1",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-qrcode-reader": "^5.5.3",
|
||||
"vue-router": "^4.5.0",
|
||||
@@ -237,7 +235,6 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/ramda": "^0.29.11",
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 4,
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
['list'],
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"src": "../icons/icon-48.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "48x48",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-72.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "72x72",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-96.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-128.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-192.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-256.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-512.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
#!/bin/bash
|
||||
# avd-resource-checker.sh
|
||||
# Author: Matthew Raymer
|
||||
# Date: 2025-01-27
|
||||
# Description: Check system resources and recommend optimal AVD configuration
|
||||
|
||||
set -e
|
||||
|
||||
# Source common utilities
|
||||
source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Colors for output
|
||||
RED_COLOR='\033[0;31m'
|
||||
GREEN_COLOR='\033[0;32m'
|
||||
YELLOW_COLOR='\033[1;33m'
|
||||
BLUE_COLOR='\033[0;34m'
|
||||
NC_COLOR='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local color=$1
|
||||
local message=$2
|
||||
echo -e "${color}${message}${NC_COLOR}"
|
||||
}
|
||||
|
||||
# Function to get system memory in MB
|
||||
get_system_memory() {
|
||||
if command -v free >/dev/null 2>&1; then
|
||||
free -m | awk 'NR==2{print $2}'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get available memory in MB
|
||||
get_available_memory() {
|
||||
if command -v free >/dev/null 2>&1; then
|
||||
free -m | awk 'NR==2{print $7}'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get CPU core count
|
||||
get_cpu_cores() {
|
||||
if command -v nproc >/dev/null 2>&1; then
|
||||
nproc
|
||||
elif [ -f /proc/cpuinfo ]; then
|
||||
grep -c ^processor /proc/cpuinfo
|
||||
else
|
||||
echo "1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check GPU capabilities
|
||||
check_gpu_capabilities() {
|
||||
local gpu_type="unknown"
|
||||
local gpu_memory="0"
|
||||
|
||||
# Check for NVIDIA GPU
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
gpu_type="nvidia"
|
||||
gpu_memory=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 || echo "0")
|
||||
print_status $GREEN_COLOR "✓ NVIDIA GPU detected (${gpu_memory}MB VRAM)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for AMD GPU
|
||||
if command -v rocm-smi >/dev/null 2>&1; then
|
||||
gpu_type="amd"
|
||||
print_status $GREEN_COLOR "✓ AMD GPU detected"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for Intel GPU
|
||||
if lspci 2>/dev/null | grep -i "vga.*intel" >/dev/null; then
|
||||
gpu_type="intel"
|
||||
print_status $YELLOW_COLOR "✓ Intel integrated GPU detected"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for generic GPU
|
||||
if lspci 2>/dev/null | grep -i "vga" >/dev/null; then
|
||||
gpu_type="generic"
|
||||
print_status $YELLOW_COLOR "✓ Generic GPU detected"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_status $RED_COLOR "✗ No GPU detected"
|
||||
return 2
|
||||
}
|
||||
|
||||
# Function to check if hardware acceleration is available
|
||||
check_hardware_acceleration() {
|
||||
local gpu_capable=$1
|
||||
|
||||
if [ $gpu_capable -eq 0 ]; then
|
||||
print_status $GREEN_COLOR "✓ Hardware acceleration recommended"
|
||||
return 0
|
||||
elif [ $gpu_capable -eq 1 ]; then
|
||||
print_status $YELLOW_COLOR "⚠ Limited hardware acceleration"
|
||||
return 1
|
||||
else
|
||||
print_status $RED_COLOR "✗ No hardware acceleration available"
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to recommend AVD configuration
|
||||
recommend_avd_config() {
|
||||
local total_memory=$1
|
||||
local available_memory=$2
|
||||
local cpu_cores=$3
|
||||
local gpu_capable=$4
|
||||
|
||||
print_status $BLUE_COLOR "\n=== AVD Configuration Recommendation ==="
|
||||
|
||||
# Calculate recommended memory (leave 2GB for system)
|
||||
local system_reserve=2048
|
||||
local recommended_memory=$((available_memory - system_reserve))
|
||||
|
||||
# Cap memory at reasonable limits
|
||||
if [ $recommended_memory -gt 4096 ]; then
|
||||
recommended_memory=4096
|
||||
elif [ $recommended_memory -lt 1024 ]; then
|
||||
recommended_memory=1024
|
||||
fi
|
||||
|
||||
# Calculate recommended cores (leave 2 cores for system)
|
||||
local recommended_cores=$((cpu_cores - 2))
|
||||
if [ $recommended_cores -lt 1 ]; then
|
||||
recommended_cores=1
|
||||
elif [ $recommended_cores -gt 4 ]; then
|
||||
recommended_cores=4
|
||||
fi
|
||||
|
||||
# Determine GPU setting
|
||||
local gpu_setting=""
|
||||
case $gpu_capable in
|
||||
0) gpu_setting="-gpu host" ;;
|
||||
1) gpu_setting="-gpu swiftshader_indirect" ;;
|
||||
2) gpu_setting="-gpu swiftshader_indirect" ;;
|
||||
esac
|
||||
|
||||
# Generate recommendation
|
||||
print_status $GREEN_COLOR "Recommended AVD Configuration:"
|
||||
echo " Memory: ${recommended_memory}MB"
|
||||
echo " Cores: ${recommended_cores}"
|
||||
echo " GPU: ${gpu_setting}"
|
||||
|
||||
# Get AVD name from function parameter (passed from main)
|
||||
local avd_name=$5
|
||||
local command="emulator -avd ${avd_name} -no-audio -memory ${recommended_memory} -cores ${recommended_cores} ${gpu_setting} &"
|
||||
|
||||
print_status $BLUE_COLOR "\nGenerated Command:"
|
||||
echo " ${command}"
|
||||
|
||||
# Save to file for easy execution
|
||||
local script_file="/tmp/start-avd-${avd_name}.sh"
|
||||
cat > "$script_file" << EOF
|
||||
#!/bin/bash
|
||||
# Auto-generated AVD startup script
|
||||
# Generated by avd-resource-checker.sh on $(date)
|
||||
|
||||
echo "Starting AVD: ${avd_name}"
|
||||
echo "Memory: ${recommended_memory}MB"
|
||||
echo "Cores: ${recommended_cores}"
|
||||
echo "GPU: ${gpu_setting}"
|
||||
|
||||
${command}
|
||||
|
||||
echo "AVD started in background"
|
||||
echo "Check status with: adb devices"
|
||||
echo "View logs with: adb logcat"
|
||||
EOF
|
||||
|
||||
chmod +x "$script_file"
|
||||
print_status $GREEN_COLOR "\n✓ Startup script saved to: ${script_file}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to test AVD startup
|
||||
test_avd_startup() {
|
||||
local avd_name=$1
|
||||
local test_duration=${2:-30}
|
||||
|
||||
print_status $BLUE_COLOR "\n=== Testing AVD Startup ==="
|
||||
|
||||
# Check if AVD exists
|
||||
if ! avdmanager list avd | grep -q "$avd_name"; then
|
||||
print_status $RED_COLOR "✗ AVD '$avd_name' not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_status $YELLOW_COLOR "Testing AVD startup for ${test_duration} seconds..."
|
||||
|
||||
# Start emulator in test mode
|
||||
emulator -avd "$avd_name" -no-audio -no-window -no-snapshot -memory 1024 -cores 1 -gpu swiftshader_indirect &
|
||||
local emulator_pid=$!
|
||||
|
||||
# Wait for boot
|
||||
local boot_time=0
|
||||
local max_wait=$test_duration
|
||||
|
||||
while [ $boot_time -lt $max_wait ]; do
|
||||
if adb devices | grep -q "emulator.*device"; then
|
||||
print_status $GREEN_COLOR "✓ AVD booted successfully in ${boot_time} seconds"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
boot_time=$((boot_time + 2))
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
kill $emulator_pid 2>/dev/null || true
|
||||
adb emu kill 2>/dev/null || true
|
||||
|
||||
if [ $boot_time -ge $max_wait ]; then
|
||||
print_status $RED_COLOR "✗ AVD failed to boot within ${test_duration} seconds"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to list available AVDs
|
||||
list_available_avds() {
|
||||
print_status $BLUE_COLOR "\n=== Available AVDs ==="
|
||||
|
||||
if ! command -v avdmanager >/dev/null 2>&1; then
|
||||
print_status $RED_COLOR "✗ avdmanager not found. Please install Android SDK command line tools."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local avd_list=$(avdmanager list avd 2>/dev/null)
|
||||
if [ -z "$avd_list" ]; then
|
||||
print_status $YELLOW_COLOR "⚠ No AVDs found. Create one with:"
|
||||
echo " avdmanager create avd --name TimeSafari_Emulator --package system-images;android-34;google_apis;x86_64"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$avd_list"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to create optimized AVD
|
||||
create_optimized_avd() {
|
||||
local avd_name=$1
|
||||
local memory=$2
|
||||
local cores=$3
|
||||
|
||||
print_status $BLUE_COLOR "\n=== Creating Optimized AVD ==="
|
||||
|
||||
# Check if system image is available
|
||||
local system_image="system-images;android-34;google_apis;x86_64"
|
||||
if ! sdkmanager --list | grep -q "$system_image"; then
|
||||
print_status $YELLOW_COLOR "Installing system image: $system_image"
|
||||
sdkmanager "$system_image"
|
||||
fi
|
||||
|
||||
# Create AVD
|
||||
print_status $YELLOW_COLOR "Creating AVD: $avd_name"
|
||||
avdmanager create avd \
|
||||
--name "$avd_name" \
|
||||
--package "$system_image" \
|
||||
--device "pixel_7" \
|
||||
--force
|
||||
|
||||
# Configure AVD
|
||||
local avd_config_file="$HOME/.android/avd/${avd_name}.avd/config.ini"
|
||||
if [ -f "$avd_config_file" ]; then
|
||||
print_status $YELLOW_COLOR "Configuring AVD settings..."
|
||||
|
||||
# Set memory
|
||||
sed -i "s/vm.heapSize=.*/vm.heapSize=${memory}/" "$avd_config_file"
|
||||
|
||||
# Set cores
|
||||
sed -i "s/hw.cpu.ncore=.*/hw.cpu.ncore=${cores}/" "$avd_config_file"
|
||||
|
||||
# Disable unnecessary features
|
||||
echo "hw.audioInput=no" >> "$avd_config_file"
|
||||
echo "hw.audioOutput=no" >> "$avd_config_file"
|
||||
echo "hw.camera.back=none" >> "$avd_config_file"
|
||||
echo "hw.camera.front=none" >> "$avd_config_file"
|
||||
echo "hw.gps=no" >> "$avd_config_file"
|
||||
echo "hw.sensors.orientation=no" >> "$avd_config_file"
|
||||
echo "hw.sensors.proximity=no" >> "$avd_config_file"
|
||||
|
||||
print_status $GREEN_COLOR "✓ AVD configured successfully"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
print_status $BLUE_COLOR "=== TimeSafari AVD Resource Checker ==="
|
||||
print_status $BLUE_COLOR "Checking system resources and recommending optimal AVD configuration\n"
|
||||
|
||||
# Get system information
|
||||
local total_memory=$(get_system_memory)
|
||||
local available_memory=$(get_available_memory)
|
||||
local cpu_cores=$(get_cpu_cores)
|
||||
|
||||
print_status $BLUE_COLOR "=== System Information ==="
|
||||
echo "Total Memory: ${total_memory}MB"
|
||||
echo "Available Memory: ${available_memory}MB"
|
||||
echo "CPU Cores: ${cpu_cores}"
|
||||
|
||||
# Check GPU capabilities
|
||||
print_status $BLUE_COLOR "\n=== GPU Analysis ==="
|
||||
check_gpu_capabilities
|
||||
local gpu_capable=$?
|
||||
|
||||
# Check hardware acceleration
|
||||
check_hardware_acceleration $gpu_capable
|
||||
local hw_accel=$?
|
||||
|
||||
# List available AVDs
|
||||
list_available_avds
|
||||
|
||||
# Get AVD name from user or use default
|
||||
local avd_name="TimeSafari_Emulator"
|
||||
if [ $# -gt 0 ]; then
|
||||
avd_name="$1"
|
||||
fi
|
||||
|
||||
# Recommend configuration
|
||||
recommend_avd_config $total_memory $available_memory $cpu_cores $gpu_capable "$avd_name"
|
||||
|
||||
# Test AVD if requested
|
||||
if [ "$2" = "--test" ]; then
|
||||
test_avd_startup "$avd_name"
|
||||
fi
|
||||
|
||||
# Create optimized AVD if requested
|
||||
if [ "$2" = "--create" ]; then
|
||||
local recommended_memory=$((available_memory - 2048))
|
||||
if [ $recommended_memory -gt 4096 ]; then
|
||||
recommended_memory=4096
|
||||
elif [ $recommended_memory -lt 1024 ]; then
|
||||
recommended_memory=1024
|
||||
fi
|
||||
|
||||
local recommended_cores=$((cpu_cores - 2))
|
||||
if [ $recommended_cores -lt 1 ]; then
|
||||
recommended_cores=1
|
||||
elif [ $recommended_cores -gt 4 ]; then
|
||||
recommended_cores=4
|
||||
fi
|
||||
|
||||
create_optimized_avd "$avd_name" $recommended_memory $recommended_cores
|
||||
fi
|
||||
|
||||
print_status $GREEN_COLOR "\n=== Resource Check Complete ==="
|
||||
print_status $YELLOW_COLOR "Tip: Use the generated startup script for consistent AVD launches"
|
||||
}
|
||||
|
||||
# Show help
|
||||
show_help() {
|
||||
echo "Usage: $0 [AVD_NAME] [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --test Test AVD startup (30 second test)"
|
||||
echo " --create Create optimized AVD with recommended settings"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Check resources and recommend config"
|
||||
echo " $0 TimeSafari_Emulator # Check resources for specific AVD"
|
||||
echo " $0 TimeSafari_Emulator --test # Test AVD startup"
|
||||
echo " $0 TimeSafari_Emulator --create # Create optimized AVD"
|
||||
echo ""
|
||||
echo "The script will:"
|
||||
echo " - Analyze system resources (RAM, CPU, GPU)"
|
||||
echo " - Recommend optimal AVD configuration"
|
||||
echo " - Generate startup command and script"
|
||||
echo " - Optionally test or create AVD"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -22,7 +22,6 @@
|
||||
# --sync Sync Capacitor only
|
||||
# --assets Generate assets only
|
||||
# --deploy Deploy APK to connected device
|
||||
# --uninstall Uninstall app from connected device
|
||||
# -h, --help Show this help message
|
||||
# -v, --verbose Enable verbose logging
|
||||
#
|
||||
@@ -197,7 +196,6 @@ SYNC_ONLY=false
|
||||
ASSETS_ONLY=false
|
||||
DEPLOY_APP=false
|
||||
AUTO_RUN=false
|
||||
UNINSTALL=false
|
||||
CUSTOM_API_IP=""
|
||||
|
||||
# Function to parse Android-specific arguments
|
||||
@@ -248,9 +246,6 @@ parse_android_args() {
|
||||
--auto-run)
|
||||
AUTO_RUN=true
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL=true
|
||||
;;
|
||||
--api-ip)
|
||||
if [ $((i + 1)) -lt ${#args[@]} ]; then
|
||||
CUSTOM_API_IP="${args[$((i + 1))]}"
|
||||
@@ -296,7 +291,6 @@ print_android_usage() {
|
||||
echo " --assets Generate assets only"
|
||||
echo " --deploy Deploy APK to connected device"
|
||||
echo " --auto-run Auto-run app after build"
|
||||
echo " --uninstall Uninstall app from connected device"
|
||||
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
|
||||
echo ""
|
||||
echo "Common Options:"
|
||||
@@ -311,7 +305,6 @@ print_android_usage() {
|
||||
echo " $0 --clean # Clean only"
|
||||
echo " $0 --sync # Sync only"
|
||||
echo " $0 --deploy # Build and deploy to device"
|
||||
echo " $0 --uninstall # Uninstall app from device"
|
||||
echo " $0 --dev # Dev build with default 10.0.2.2"
|
||||
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
|
||||
echo ""
|
||||
@@ -358,18 +351,8 @@ fi
|
||||
# Setup application directories
|
||||
setup_app_directories
|
||||
|
||||
# Load environment-specific .env file if it exists
|
||||
env_file=".env.$BUILD_MODE"
|
||||
if [ -f "$env_file" ]; then
|
||||
load_env_file "$env_file"
|
||||
else
|
||||
log_debug "No $env_file file found, using default environment"
|
||||
fi
|
||||
|
||||
# Load .env file if it exists (fallback)
|
||||
if [ -f ".env" ]; then
|
||||
load_env_file ".env"
|
||||
fi
|
||||
# Load environment from .env file if it exists
|
||||
load_env_file ".env"
|
||||
|
||||
# Handle clean-only mode
|
||||
if [ "$CLEAN_ONLY" = true ]; then
|
||||
@@ -424,13 +407,8 @@ safe_execute "Validating asset configuration" "npm run assets:validate" || {
|
||||
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
|
||||
}
|
||||
|
||||
# Step 2: Uninstall Android app
|
||||
if [ "$UNINSTALL" = true ]; then
|
||||
log_info "Uninstall: uninstalling app from device"
|
||||
safe_execute "Uninstalling Android app" "./scripts/uninstall-android.sh" || exit 1
|
||||
log_success "Uninstall completed successfully!"
|
||||
exit 0
|
||||
fi
|
||||
# Step 2: Clean Android app
|
||||
safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
|
||||
|
||||
# Step 3: Clean dist directory
|
||||
log_info "Cleaning dist directory..."
|
||||
|
||||
@@ -341,19 +341,7 @@ main_electron_build() {
|
||||
# Setup environment
|
||||
setup_build_env "electron" "$BUILD_MODE"
|
||||
setup_app_directories
|
||||
|
||||
# Load environment-specific .env file if it exists
|
||||
env_file=".env.$BUILD_MODE"
|
||||
if [ -f "$env_file" ]; then
|
||||
load_env_file "$env_file"
|
||||
else
|
||||
log_debug "No $env_file file found, using default environment"
|
||||
fi
|
||||
|
||||
# Load .env file if it exists (fallback)
|
||||
if [ -f ".env" ]; then
|
||||
load_env_file ".env"
|
||||
fi
|
||||
load_env_file ".env"
|
||||
|
||||
# Step 1: Clean Electron build artifacts
|
||||
clean_electron_artifacts
|
||||
|
||||
@@ -324,18 +324,8 @@ fi
|
||||
# Setup application directories
|
||||
setup_app_directories
|
||||
|
||||
# Load environment-specific .env file if it exists
|
||||
env_file=".env.$BUILD_MODE"
|
||||
if [ -f "$env_file" ]; then
|
||||
load_env_file "$env_file"
|
||||
else
|
||||
log_debug "No $env_file file found, using default environment"
|
||||
fi
|
||||
|
||||
# Load .env file if it exists (fallback)
|
||||
if [ -f ".env" ]; then
|
||||
load_env_file ".env"
|
||||
fi
|
||||
# Load environment from .env file if it exists
|
||||
load_env_file ".env"
|
||||
|
||||
# Validate iOS environment
|
||||
validate_ios_environment
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
# uninstall-android.sh
|
||||
# clean-android.sh
|
||||
# Author: Matthew Raymer
|
||||
# Date: 2025-08-19
|
||||
# Description: Uninstall Android app with timeout protection to prevent hanging
|
||||
# Description: Clean Android app with timeout protection to prevent hanging
|
||||
# This script safely uninstalls the TimeSafari app from connected Android devices
|
||||
# with a 30-second timeout to prevent indefinite hanging.
|
||||
|
||||
@@ -386,7 +386,7 @@ export default class App extends Vue {
|
||||
let allGoingOff = false;
|
||||
|
||||
try {
|
||||
const settings: Settings = await this.$accountSettings();
|
||||
const settings: Settings = await this.$settings();
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
const notifyingReminder = !!settings?.notifyingReminderTime;
|
||||
|
||||
@@ -7,24 +7,6 @@
|
||||
html {
|
||||
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Fix iOS viewport height changes when keyboard appears/disappears */
|
||||
html, body {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Dynamic viewport height for better mobile support */
|
||||
overflow: hidden; /* Disable all scrolling on html and body */
|
||||
position: fixed; /* Force fixed positioning to prevent viewport changes */
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@@ -38,26 +20,6 @@
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@apply bg-white px-4 py-6 rounded-lg w-full max-w-lg max-h-[calc(100vh-3rem)] overflow-y-auto;
|
||||
}
|
||||
|
||||
/* Markdown content styling to restore list elements */
|
||||
.markdown-content ul {
|
||||
@apply list-disc list-inside ml-4;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
@apply list-decimal list-inside ml-4;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
.markdown-content ul ul,
|
||||
.markdown-content ol ol,
|
||||
.markdown-content ul ol,
|
||||
.markdown-content ol ul {
|
||||
@apply ml-4 mt-1;
|
||||
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||
}
|
||||
}
|
||||
@@ -77,95 +77,15 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Emoji Section -->
|
||||
<div
|
||||
v-if="hasEmojis || isRegistered"
|
||||
class="float-right ml-3 mb-1 bg-white rounded border border-slate-300 px-1.5 py-0.5 max-w-[240px]"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<!-- Existing Emojis Display -->
|
||||
<div v-if="hasEmojis" class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="(count, emoji) in record.emojiCount"
|
||||
:key="emoji"
|
||||
class="inline-flex items-center gap-0.5 px-1 py-0.5 text-xs bg-slate-50 hover:bg-slate-100 rounded border border-slate-200 transition-colors cursor-pointer"
|
||||
:class="{
|
||||
'bg-blue-50 border-blue-200': isUserEmojiWithoutLoading(emoji),
|
||||
'opacity-75 cursor-wait': loadingEmojis,
|
||||
}"
|
||||
:title="
|
||||
loadingEmojis
|
||||
? 'Loading...'
|
||||
: !emojisOnActivity?.isResolved
|
||||
? 'Click to load your emojis'
|
||||
: isUserEmojiWithoutLoading(emoji)
|
||||
? 'Click to remove your emoji'
|
||||
: 'Click to add this emoji'
|
||||
"
|
||||
:disabled="!isRegistered"
|
||||
@click="toggleThisEmoji(emoji)"
|
||||
>
|
||||
<!-- Show spinner when loading -->
|
||||
<div v-if="loadingEmojis" class="animate-spin text-xs">
|
||||
<font-awesome icon="spinner" class="fa-spin" />
|
||||
</div>
|
||||
<span v-else class="text-sm leading-none">{{ emoji }}</span>
|
||||
<span class="text-xs text-slate-600 font-medium leading-none">{{
|
||||
count
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Emoji Button -->
|
||||
<button
|
||||
v-if="isRegistered"
|
||||
class="inline-flex px-1 py-0.5 text-xs bg-slate-100 hover:bg-slate-200 rounded border border-slate-300 transition-colors items-center justify-center ml-2 ml-auto"
|
||||
:title="showEmojiPicker ? 'Close emoji picker' : 'Add emoji'"
|
||||
@click="toggleEmojiPicker"
|
||||
>
|
||||
<span class="px-2 text-sm leading-none">{{
|
||||
showEmojiPicker ? "x" : "😊"
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Emoji Picker (placeholder for now) -->
|
||||
<div
|
||||
v-if="showEmojiPicker"
|
||||
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300"
|
||||
>
|
||||
<!-- Temporary emoji buttons for testing -->
|
||||
<div class="flex flex-wrap gap-3 mt-1">
|
||||
<button
|
||||
v-for="emoji in QUICK_EMOJIS"
|
||||
:key="emoji"
|
||||
class="p-0.5 hover:bg-slate-200 rounded text-base transition-opacity"
|
||||
:class="{
|
||||
'opacity-75 cursor-wait': loadingEmojis,
|
||||
}"
|
||||
:disabled="loadingEmojis"
|
||||
@click="toggleThisEmoji(emoji)"
|
||||
>
|
||||
<!-- Show spinner when loading -->
|
||||
<div v-if="loadingEmojis" class="animate-spin text-sm">⟳</div>
|
||||
<span v-else>{{ emoji }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="block cursor-pointer" @click="emitLoadClaim(record.jwtId)">
|
||||
<vue-markdown
|
||||
:source="truncatedDescription"
|
||||
class="markdown-content"
|
||||
/>
|
||||
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="clear-right relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
||||
>
|
||||
<!-- Source -->
|
||||
<div
|
||||
@@ -328,51 +248,33 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import VueMarkdown from "vue-markdown-render";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import {
|
||||
createAndSubmitClaim,
|
||||
getHeaders,
|
||||
isHiddenDid,
|
||||
} from "../libs/endorserServer";
|
||||
import { GiveRecordWithContactInfo } from "@/interfaces/give";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { createNotifyHelpers, NotifyFunction, TIMEOUTS } from "@/utils/notify";
|
||||
import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_PERSON_HIDDEN,
|
||||
NOTIFY_UNKNOWN_PERSON,
|
||||
} from "@/constants/notifications";
|
||||
import { EmojiSummaryRecord, GenericVerifiableCredential } from "@/interfaces";
|
||||
import { GiveRecordWithContactInfo } from "@/interfaces/give";
|
||||
import { PromiseTracker } from "@/libs/util";
|
||||
import { TIMEOUTS } from "@/utils/notify";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
ProjectIcon,
|
||||
VueMarkdown,
|
||||
},
|
||||
})
|
||||
export default class ActivityListItem extends Vue {
|
||||
readonly QUICK_EMOJIS = ["👍", "👏", "❤️", "🎉", "😊", "😆", "🔥"];
|
||||
|
||||
@Prop() record!: GiveRecordWithContactInfo;
|
||||
@Prop() lastViewedClaimId?: string;
|
||||
@Prop() isRegistered!: boolean;
|
||||
@Prop() activeDid!: string;
|
||||
@Prop() apiServer!: string;
|
||||
|
||||
isHiddenDid = isHiddenDid;
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
$notify!: NotifyFunction;
|
||||
|
||||
// Emoji-related data
|
||||
showEmojiPicker = false;
|
||||
loadingEmojis = false; // Track if emojis are currently loading
|
||||
|
||||
emojisOnActivity: PromiseTracker<EmojiSummaryRecord[]> | null = null; // load this only when needed
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
@@ -401,14 +303,6 @@ export default class ActivityListItem extends Vue {
|
||||
return `${claim?.description || ""}`;
|
||||
}
|
||||
|
||||
get truncatedDescription(): string {
|
||||
const desc = this.description;
|
||||
if (desc.length <= 300) {
|
||||
return desc;
|
||||
}
|
||||
return desc.substring(0, 300) + "...";
|
||||
}
|
||||
|
||||
private displayAmount(code: string, amt: number) {
|
||||
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
|
||||
}
|
||||
@@ -436,186 +330,5 @@ export default class ActivityListItem extends Vue {
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
// Emoji-related computed properties and methods
|
||||
get hasEmojis(): boolean {
|
||||
return Object.keys(this.record.emojiCount).length > 0;
|
||||
}
|
||||
|
||||
triggerUserEmojiLoad(): PromiseTracker<EmojiSummaryRecord[]> {
|
||||
if (!this.emojisOnActivity) {
|
||||
const promise = new Promise<EmojiSummaryRecord[]>((resolve) => {
|
||||
(async () => {
|
||||
this.axios
|
||||
.get(
|
||||
`${this.apiServer}/api/v2/report/emoji?parentHandleId=${encodeURIComponent(this.record.handleId)}`,
|
||||
{ headers: await getHeaders(this.activeDid) },
|
||||
)
|
||||
.then((response) => {
|
||||
const userEmojiRecords = response.data.data.filter(
|
||||
(e: EmojiSummaryRecord) => e.issuerDid === this.activeDid,
|
||||
);
|
||||
resolve(userEmojiRecords);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error loading user emojis:", error);
|
||||
resolve([]);
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
this.emojisOnActivity = new PromiseTracker(promise);
|
||||
}
|
||||
return this.emojisOnActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param emoji - The emoji to check.
|
||||
* @returns True if the emoji is in the user's emojis, false otherwise.
|
||||
*
|
||||
* @note This method is quick and synchronous, and can check resolved emojis
|
||||
* without triggering a server request. Returns false if emojis haven't been loaded yet.
|
||||
*/
|
||||
isUserEmojiWithoutLoading(emoji: string): boolean {
|
||||
if (this.emojisOnActivity?.isResolved && this.emojisOnActivity.value) {
|
||||
return this.emojisOnActivity.value.some(
|
||||
(record) => record.text === emoji,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async toggleEmojiPicker() {
|
||||
this.triggerUserEmojiLoad(); // trigger it, but don't wait for it to complete
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
}
|
||||
|
||||
async toggleThisEmoji(emoji: string) {
|
||||
// Start loading indicator
|
||||
this.loadingEmojis = true;
|
||||
this.showEmojiPicker = false; // always close the picker when an emoji is clicked
|
||||
|
||||
try {
|
||||
this.triggerUserEmojiLoad(); // trigger just in case
|
||||
|
||||
const userEmojiList = await this.emojisOnActivity!.promise; // must wait now that they've chosen
|
||||
|
||||
const userHasEmoji: boolean = userEmojiList.some(
|
||||
(record) => record.text === emoji,
|
||||
);
|
||||
|
||||
if (userHasEmoji) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Remove Emoji",
|
||||
text: `Do you want to remove your ${emoji} ?`,
|
||||
yesText: "Remove",
|
||||
onYes: async () => {
|
||||
await this.removeEmoji(emoji);
|
||||
},
|
||||
},
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
} else {
|
||||
// User doesn't have this emoji, add it
|
||||
await this.submitEmoji(emoji);
|
||||
}
|
||||
} finally {
|
||||
// Remove loading indicator
|
||||
this.loadingEmojis = false;
|
||||
}
|
||||
}
|
||||
|
||||
async submitEmoji(emoji: string) {
|
||||
try {
|
||||
// Create an Emoji claim and send to the server
|
||||
const emojiClaim: GenericVerifiableCredential = {
|
||||
"@context": "https://endorser.ch",
|
||||
"@type": "Emoji",
|
||||
text: emoji,
|
||||
parentItem: { lastClaimId: this.record.jwtId },
|
||||
};
|
||||
const claim = await createAndSubmitClaim(
|
||||
emojiClaim,
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (claim.success && !claim.embeddedRecordError) {
|
||||
// Update emoji count
|
||||
this.record.emojiCount[emoji] =
|
||||
(this.record.emojiCount[emoji] || 0) + 1;
|
||||
|
||||
// Create a new emoji record (we'll get the actual jwtId from the server response later)
|
||||
const newEmojiRecord: EmojiSummaryRecord = {
|
||||
issuerDid: this.activeDid,
|
||||
jwtId: claim.claimId || "",
|
||||
text: emoji,
|
||||
parentHandleId: this.record.jwtId,
|
||||
};
|
||||
|
||||
// Update user emojis list by creating a new promise with the updated data
|
||||
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
|
||||
this.triggerUserEmojiLoad();
|
||||
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
|
||||
this.emojisOnActivity = new PromiseTracker(
|
||||
Promise.resolve([...currentEmojis, newEmojiRecord]),
|
||||
);
|
||||
} else {
|
||||
this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error submitting emoji:", error);
|
||||
this.notify.error("Got error adding emoji.", TIMEOUTS.STANDARD);
|
||||
}
|
||||
}
|
||||
|
||||
async removeEmoji(emoji: string) {
|
||||
try {
|
||||
// Create an Emoji claim and send to the server
|
||||
const emojiClaim: GenericVerifiableCredential = {
|
||||
"@context": "https://endorser.ch",
|
||||
"@type": "Emoji",
|
||||
text: emoji,
|
||||
parentItem: { lastClaimId: this.record.jwtId },
|
||||
};
|
||||
const claim = await createAndSubmitClaim(
|
||||
emojiClaim,
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (claim.success && !claim.embeddedRecordError) {
|
||||
// Update emoji count
|
||||
const newCount = Math.max(0, (this.record.emojiCount[emoji] || 0) - 1);
|
||||
if (newCount === 0) {
|
||||
delete this.record.emojiCount[emoji];
|
||||
} else {
|
||||
this.record.emojiCount[emoji] = newCount;
|
||||
}
|
||||
|
||||
// Update user emojis list by creating a new promise with the updated data
|
||||
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
|
||||
this.triggerUserEmojiLoad();
|
||||
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
|
||||
this.emojisOnActivity = new PromiseTracker(
|
||||
Promise.resolve(
|
||||
currentEmojis.filter(
|
||||
(record) =>
|
||||
record.issuerDid === this.activeDid && record.text !== emoji,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error removing emoji:", error);
|
||||
this.notify.error("Got error removing emoji.", TIMEOUTS.STANDARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<div class="text-slate-900 text-center">
|
||||
<h3 class="text-lg font-semibold leading-[1.25] mb-2">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="text-sm mb-4">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- Member Selection Table -->
|
||||
<div class="mb-4">
|
||||
<table
|
||||
class="w-full border-collapse border border-slate-300 text-sm text-start"
|
||||
>
|
||||
<!-- Select All Header -->
|
||||
<thead v-if="membersData && membersData.length > 0">
|
||||
<tr class="bg-slate-100 font-medium">
|
||||
<th class="border border-slate-300 px-3 py-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Empty State -->
|
||||
<tr v-if="!membersData || membersData.length === 0">
|
||||
<td
|
||||
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
|
||||
>
|
||||
{{ emptyStateText }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Member Rows -->
|
||||
<tr
|
||||
v-for="member in membersData || []"
|
||||
:key="member.member.memberId"
|
||||
>
|
||||
<td class="border border-slate-300 px-3 py-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isMemberSelected(member.did)"
|
||||
@change="toggleMemberSelection(member.did)"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="text-sm font-semibold">
|
||||
{{ member.name || SOMEONE_UNNAMED }}
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-0.5 text-xs text-slate-500"
|
||||
>
|
||||
<span class="font-semibold sm:hidden">DID:</span>
|
||||
<span
|
||||
class="w-[35vw] sm:w-auto truncate text-left"
|
||||
style="direction: rtl"
|
||||
>{{ member.did }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Contact indicator - only show if they are already a contact -->
|
||||
<font-awesome
|
||||
v-if="member.isContact"
|
||||
icon="user-circle"
|
||||
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
||||
@click="showContactInfo"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Select All Footer -->
|
||||
<tfoot v-if="membersData && membersData.length > 0">
|
||||
<tr class="bg-slate-100 font-medium">
|
||||
<th class="border border-slate-300 px-3 py-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-2">
|
||||
<!-- Main Action Button -->
|
||||
<button
|
||||
v-if="membersData && membersData.length > 0"
|
||||
:disabled="!hasSelectedMembers"
|
||||
:class="[
|
||||
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
|
||||
hasSelectedMembers
|
||||
? 'bg-blue-600 text-white cursor-pointer'
|
||||
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
|
||||
]"
|
||||
@click="processSelectedMembers"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
<!-- Cancel Button -->
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
@click="cancel"
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
import { MemberData } from "@/interfaces";
|
||||
import { setVisibilityUtil, getHeaders, register } from "@/libs/endorserServer";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
emits: ["close"],
|
||||
})
|
||||
export default class BulkMembersDialog extends Vue {
|
||||
@Prop({ default: "" }) activeDid!: string;
|
||||
@Prop({ default: "" }) apiServer!: string;
|
||||
// isOrganizer: true = organizer mode (admit members), false = member mode (set visibility)
|
||||
@Prop({ required: true }) isOrganizer!: boolean;
|
||||
|
||||
// Vue notification system
|
||||
$notify!: (
|
||||
notification: { group: string; type: string; title: string; text: string },
|
||||
timeout?: number,
|
||||
) => void;
|
||||
|
||||
// Notification system
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
// Component state
|
||||
membersData: MemberData[] = [];
|
||||
selectedMembers: string[] = [];
|
||||
visible = false;
|
||||
|
||||
// Constants
|
||||
// In Vue templates, imported constants need to be explicitly made available to the template
|
||||
readonly SOMEONE_UNNAMED = SOMEONE_UNNAMED;
|
||||
|
||||
get hasSelectedMembers() {
|
||||
return this.selectedMembers.length > 0;
|
||||
}
|
||||
|
||||
get isAllSelected() {
|
||||
if (!this.membersData || this.membersData.length === 0) return false;
|
||||
return this.membersData.every((member) =>
|
||||
this.selectedMembers.includes(member.did),
|
||||
);
|
||||
}
|
||||
|
||||
get isIndeterminate() {
|
||||
if (!this.membersData || this.membersData.length === 0) return false;
|
||||
const selectedCount = this.membersData.filter((member) =>
|
||||
this.selectedMembers.includes(member.did),
|
||||
).length;
|
||||
return selectedCount > 0 && selectedCount < this.membersData.length;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.isOrganizer
|
||||
? "Admit Pending Members"
|
||||
: "Add Members to Contacts";
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.isOrganizer
|
||||
? "Would you like to admit these members to the meeting and add them to your contacts?"
|
||||
: "Would you like to add these members to your contacts?";
|
||||
}
|
||||
|
||||
get buttonText() {
|
||||
return this.isOrganizer ? "Admit + Add to Contacts" : "Add to Contacts";
|
||||
}
|
||||
|
||||
get emptyStateText() {
|
||||
return this.isOrganizer
|
||||
? "No pending members to admit"
|
||||
: "No members are not in your contacts";
|
||||
}
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
|
||||
open(members: MemberData[]) {
|
||||
this.visible = true;
|
||||
this.membersData = members;
|
||||
// Select all by default
|
||||
this.selectedMembers = this.membersData.map((member) => member.did);
|
||||
}
|
||||
|
||||
close(notSelectedMemberDids: string[]) {
|
||||
this.visible = false;
|
||||
this.$emit("close", { notSelectedMemberDids: notSelectedMemberDids });
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.close(this.membersData.map((member) => member.did));
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
if (!this.membersData || this.membersData.length === 0) return;
|
||||
|
||||
if (this.isAllSelected) {
|
||||
// Deselect all
|
||||
this.selectedMembers = [];
|
||||
} else {
|
||||
// Select all
|
||||
this.selectedMembers = this.membersData.map((member) => member.did);
|
||||
}
|
||||
}
|
||||
|
||||
toggleMemberSelection(memberDid: string) {
|
||||
const index = this.selectedMembers.indexOf(memberDid);
|
||||
if (index > -1) {
|
||||
this.selectedMembers.splice(index, 1);
|
||||
} else {
|
||||
this.selectedMembers.push(memberDid);
|
||||
}
|
||||
}
|
||||
|
||||
isMemberSelected(memberDid: string) {
|
||||
return this.selectedMembers.includes(memberDid);
|
||||
}
|
||||
|
||||
async processSelectedMembers() {
|
||||
try {
|
||||
const selectedMembers: MemberData[] = this.membersData.filter((member) =>
|
||||
this.selectedMembers.includes(member.did),
|
||||
);
|
||||
const notSelectedMembers: MemberData[] = this.membersData.filter(
|
||||
(member) => !this.selectedMembers.includes(member.did),
|
||||
);
|
||||
|
||||
let admittedCount = 0;
|
||||
let contactAddedCount = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const member of selectedMembers) {
|
||||
try {
|
||||
// Organizer mode: admit and register the member first
|
||||
if (this.isOrganizer) {
|
||||
await this.admitMember(member);
|
||||
await this.registerMember(member);
|
||||
admittedCount++;
|
||||
}
|
||||
|
||||
// If they're not a contact yet, add them as a contact
|
||||
if (!member.isContact) {
|
||||
// Organizer mode: set isRegistered to true, member mode: undefined
|
||||
await this.addAsContact(
|
||||
member,
|
||||
this.isOrganizer ? true : undefined,
|
||||
);
|
||||
contactAddedCount++;
|
||||
}
|
||||
|
||||
// Set their seesMe to true
|
||||
await this.updateContactVisibility(member.did, true);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error processing member ${member.did}:`, error);
|
||||
// Continue with other members even if one fails
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
if (this.isOrganizer) {
|
||||
if (admittedCount > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Members Admitted Successfully",
|
||||
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted and registered${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
if (errors > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to fully admit some members. Work with them individually below.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Member mode: show contacts added notification
|
||||
if (contactAddedCount > 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contacts Added Successfully",
|
||||
text: `${contactAddedCount} member${contactAddedCount === 1 ? "" : "s"} added as contact${contactAddedCount === 1 ? "" : "s"}.`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.close(notSelectedMembers.map((member) => member.did));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error ${this.isOrganizer ? "admitting members" : "adding contacts"}:`,
|
||||
error,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Some errors occurred. Work with members individually below.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async admitMember(member: {
|
||||
did: string;
|
||||
name: string;
|
||||
member: { memberId: string };
|
||||
}) {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
await this.axios.put(
|
||||
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`,
|
||||
{ admitted: true },
|
||||
{ headers },
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error admitting member:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async registerMember(member: MemberData) {
|
||||
try {
|
||||
const contact: Contact = { did: member.did };
|
||||
const result = await register(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
);
|
||||
if (result.success) {
|
||||
if (result.embeddedRecordError) {
|
||||
throw new Error(result.embeddedRecordError);
|
||||
}
|
||||
await this.$updateContact(member.did, { registered: true });
|
||||
} else {
|
||||
throw result;
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error registering member:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async addAsContact(
|
||||
member: { did: string; name: string },
|
||||
isRegistered?: boolean,
|
||||
) {
|
||||
try {
|
||||
const newContact: Contact = {
|
||||
did: member.did,
|
||||
name: member.name,
|
||||
registered: isRegistered,
|
||||
};
|
||||
|
||||
await this.$insertContact(newContact);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error adding contact:", err);
|
||||
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
|
||||
// Contact already exists, continue
|
||||
} else {
|
||||
throw err; // Re-throw if it's not a duplicate error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateContactVisibility(did: string, seesMe: boolean) {
|
||||
try {
|
||||
// Get the contact object
|
||||
const contact = await this.$getContact(did);
|
||||
if (!contact) {
|
||||
throw new Error(`Contact not found for DID: ${did}`);
|
||||
}
|
||||
|
||||
// Use the proper API to set visibility on the server
|
||||
const result = await setVisibilityUtil(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
seesMe,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to set visibility");
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error updating contact visibility:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
showContactInfo() {
|
||||
// isOrganizer: true = admit mode, false = visibility mode
|
||||
const message = this.isOrganizer
|
||||
? "This user is already your contact, but they are not yet admitted to the meeting."
|
||||
: "This user is already your contact, but your activities are not visible to them yet.";
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Contact Info",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<span class="text-xs truncate">{{ contact.did }}</span>
|
||||
</div>
|
||||
<div class="text-sm truncate">
|
||||
<div class="text-sm">
|
||||
{{ contact.notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,12 +16,6 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
:to="{ name: 'seed-backup' }"
|
||||
:class="backupButtonClasses"
|
||||
>
|
||||
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
|
||||
<font-awesome
|
||||
v-if="showRedNotificationDot"
|
||||
icon="circle"
|
||||
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
|
||||
></font-awesome>
|
||||
Backup Identifier Seed
|
||||
</router-link>
|
||||
|
||||
@@ -104,12 +98,6 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
isExporting = false;
|
||||
|
||||
/**
|
||||
* Flag indicating if the user has backed up their seed phrase
|
||||
* Used to control the visibility of the notification dot
|
||||
*/
|
||||
showRedNotificationDot = false;
|
||||
|
||||
/**
|
||||
* Notification helper for consistent notification patterns
|
||||
* Created as a getter to ensure $notify is available when called
|
||||
@@ -141,7 +129,7 @@ export default class DataExportSection extends Vue {
|
||||
* CSS classes for the backup button (router link)
|
||||
*/
|
||||
get backupButtonClasses(): string {
|
||||
return "block relative w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
|
||||
return "block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,23 +218,6 @@ export default class DataExportSection extends Vue {
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
this.loadSeedBackupStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the seed backup status from account settings
|
||||
* Updates the hasBackedUpSeed flag to control notification dot visibility
|
||||
*/
|
||||
private async loadSeedBackupStatus(): Promise<void> {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.showRedNotificationDot =
|
||||
!!settings.isRegistered && !settings.hasBackedUpSeed;
|
||||
} catch (err: unknown) {
|
||||
logger.error("Failed to load seed backup status:", err);
|
||||
// Default to false (show notification dot) if we can't load the setting
|
||||
this.showRedNotificationDot = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,55 +2,12 @@
|
||||
GiftedDialog.vue to provide a reusable grid layout * for displaying people,
|
||||
projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
<template>
|
||||
<!-- Quick Search -->
|
||||
<div id="QuickSearch" class="mb-4 flex items-center text-sm">
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
type="text"
|
||||
placeholder="Search…"
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-1.5 placeholder:italic placeholder:text-slate-400 focus:outline-none"
|
||||
@input="handleSearchInput"
|
||||
@keydown.enter="performSearch"
|
||||
/>
|
||||
<div
|
||||
v-show="isSearching && searchTerm"
|
||||
class="border-y border-slate-400 ps-2 py-1.5 text-center text-slate-400"
|
||||
>
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin-pulse leading-[1.1]"
|
||||
></font-awesome>
|
||||
</div>
|
||||
<button
|
||||
:disabled="!searchTerm"
|
||||
class="px-2 py-1.5 rounded-r bg-white border border-l-0 border-slate-400 text-slate-400 disabled:cursor-not-allowed"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="searchTerm ? 'times' : 'magnifying-glass'"
|
||||
class="fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="searchTerm && !isSearching && filteredEntities.length === 0"
|
||||
class="mb-4 text-sm italic text-slate-500 text-center"
|
||||
>
|
||||
“{{ searchTerm }}” doesn't match any
|
||||
{{ entityType === "people" ? "people" : "projects" }}. Try a different
|
||||
search.
|
||||
</div>
|
||||
|
||||
<ul
|
||||
ref="scrollContainer"
|
||||
class="border-t border-slate-300 mb-4 max-h-[60vh] overflow-y-auto"
|
||||
>
|
||||
<ul :class="gridClasses">
|
||||
<!-- Special entities (You, Unnamed) for people grids -->
|
||||
<template v-if="entityType === 'people'">
|
||||
<!-- "You" entity -->
|
||||
<SpecialEntityCard
|
||||
v-if="showYouEntity && !searchTerm.trim()"
|
||||
v-if="showYouEntity"
|
||||
entity-type="you"
|
||||
label="You"
|
||||
icon="hand"
|
||||
@@ -64,7 +21,6 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
|
||||
<!-- "Unnamed" entity -->
|
||||
<SpecialEntityCard
|
||||
v-if="showUnnamedEntity && !searchTerm.trim()"
|
||||
entity-type="unnamed"
|
||||
:label="unnamedEntityName"
|
||||
icon="circle-question"
|
||||
@@ -82,60 +38,16 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
|
||||
<!-- Entity cards (people or projects) -->
|
||||
<template v-if="entityType === 'people'">
|
||||
<!-- When showing contacts without search: split into recent and alphabetical -->
|
||||
<template v-if="!searchTerm.trim()">
|
||||
<!-- Recently Added Section -->
|
||||
<template v-if="recentContacts.length > 0">
|
||||
<li
|
||||
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
|
||||
>
|
||||
Recently Added
|
||||
</li>
|
||||
<PersonCard
|
||||
v-for="person in recentContacts"
|
||||
:key="person.did"
|
||||
:person="person"
|
||||
:conflicted="isPersonConflicted(person.did)"
|
||||
:show-time-icon="true"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
@person-selected="handlePersonSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Alphabetical Section -->
|
||||
<template v-if="alphabeticalContacts.length > 0">
|
||||
<li
|
||||
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
|
||||
>
|
||||
Everyone Else
|
||||
</li>
|
||||
<PersonCard
|
||||
v-for="person in alphabeticalContacts"
|
||||
:key="person.did"
|
||||
:person="person"
|
||||
:conflicted="isPersonConflicted(person.did)"
|
||||
:show-time-icon="true"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
@person-selected="handlePersonSelected"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- When searching: show filtered results normally -->
|
||||
<template v-else>
|
||||
<PersonCard
|
||||
v-for="person in displayedEntities as Contact[]"
|
||||
:key="person.did"
|
||||
:person="person"
|
||||
:conflicted="isPersonConflicted(person.did)"
|
||||
:show-time-icon="true"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
@person-selected="handlePersonSelected"
|
||||
/>
|
||||
</template>
|
||||
<PersonCard
|
||||
v-for="person in displayedEntities as Contact[]"
|
||||
:key="person.did"
|
||||
:person="person"
|
||||
:conflicted="isPersonConflicted(person.did)"
|
||||
:show-time-icon="true"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
@person-selected="handlePersonSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="entityType === 'projects'">
|
||||
@@ -151,27 +63,28 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
@project-selected="handleProjectSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Show All navigation -->
|
||||
<ShowAllCard
|
||||
v-if="shouldShowAll"
|
||||
:entity-type="entityType"
|
||||
:route-name="showAllRoute"
|
||||
:query-params="showAllQueryParams"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Emit, Watch } from "vue-facing-decorator";
|
||||
import { useInfiniteScroll } from "@vueuse/core";
|
||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import PersonCard from "./PersonCard.vue";
|
||||
import ProjectCard from "./ProjectCard.vue";
|
||||
import SpecialEntityCard from "./SpecialEntityCard.vue";
|
||||
import ShowAllCard from "./ShowAllCard.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* Constants for infinite scroll configuration
|
||||
*/
|
||||
const INITIAL_BATCH_SIZE = 20;
|
||||
const INCREMENT_SIZE = 20;
|
||||
const RECENT_CONTACTS_COUNT = 3;
|
||||
|
||||
/**
|
||||
* EntityGrid - Unified grid layout for displaying people or projects
|
||||
*
|
||||
@@ -180,6 +93,7 @@ const RECENT_CONTACTS_COUNT = 3;
|
||||
* - Special entity integration (You, Unnamed)
|
||||
* - Conflict detection integration
|
||||
* - Empty state messaging
|
||||
* - Show All navigation
|
||||
* - Event delegation for entity selection
|
||||
* - Warning notifications for conflicted entities
|
||||
* - Template streamlined with computed CSS properties
|
||||
@@ -190,6 +104,7 @@ const RECENT_CONTACTS_COUNT = 3;
|
||||
PersonCard,
|
||||
ProjectCard,
|
||||
SpecialEntityCard,
|
||||
ShowAllCard,
|
||||
},
|
||||
})
|
||||
export default class EntityGrid extends Vue {
|
||||
@@ -197,21 +112,14 @@ export default class EntityGrid extends Vue {
|
||||
@Prop({ required: true })
|
||||
entityType!: "people" | "projects";
|
||||
|
||||
// Search state
|
||||
searchTerm = "";
|
||||
isSearching = false;
|
||||
searchTimeout: NodeJS.Timeout | null = null;
|
||||
filteredEntities: Contact[] | PlanData[] = [];
|
||||
|
||||
// Infinite scroll state
|
||||
displayedCount = INITIAL_BATCH_SIZE;
|
||||
infiniteScrollReset?: () => void;
|
||||
scrollContainer?: HTMLElement;
|
||||
|
||||
/** Array of entities to display */
|
||||
@Prop({ required: true })
|
||||
entities!: Contact[] | PlanData[];
|
||||
|
||||
/** Maximum number of entities to display */
|
||||
@Prop({ default: 10 })
|
||||
maxItems!: number;
|
||||
|
||||
/** Active user's DID */
|
||||
@Prop({ required: true })
|
||||
activeDid!: string;
|
||||
@@ -232,14 +140,18 @@ export default class EntityGrid extends Vue {
|
||||
@Prop({ default: true })
|
||||
showYouEntity!: boolean;
|
||||
|
||||
/** Whether to show the "Unnamed" entity for people grids */
|
||||
@Prop({ default: true })
|
||||
showUnnamedEntity!: boolean;
|
||||
|
||||
/** Whether the "You" entity is selectable */
|
||||
@Prop({ default: true })
|
||||
youSelectable!: boolean;
|
||||
|
||||
/** Route name for "Show All" navigation */
|
||||
@Prop({ default: "" })
|
||||
showAllRoute!: string;
|
||||
|
||||
/** Query parameters for "Show All" navigation */
|
||||
@Prop({ default: () => ({}) })
|
||||
showAllQueryParams!: Record<string, string>;
|
||||
|
||||
/** Notification function from parent component */
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -248,31 +160,42 @@ export default class EntityGrid extends Vue {
|
||||
@Prop({ default: "other party" })
|
||||
conflictContext!: string;
|
||||
|
||||
/** Whether to hide the "Show All" navigation */
|
||||
@Prop({ default: false })
|
||||
hideShowAll!: boolean;
|
||||
|
||||
/**
|
||||
* Function to determine which entities to display (allows parent control)
|
||||
*
|
||||
* This function prop allows parent components to customize which entities
|
||||
* are displayed in the grid, enabling advanced filtering and sorting.
|
||||
* Note: Infinite scroll is disabled when this prop is provided.
|
||||
* are displayed in the grid, enabling advanced filtering, sorting, and
|
||||
* display logic beyond the default simple slice behavior.
|
||||
*
|
||||
* @param entities - The full array of entities (Contact[] or PlanData[])
|
||||
* @param entityType - The type of entities being displayed ("people" or "projects")
|
||||
* @param maxItems - The maximum number of items to display (from maxItems prop)
|
||||
* @returns Filtered/sorted array of entities to display
|
||||
*
|
||||
* @example
|
||||
* // Custom filtering: only show contacts with profile images
|
||||
* :display-entities-function="(entities, type) =>
|
||||
* entities.filter(e => e.profileImageUrl)"
|
||||
* :display-entities-function="(entities, type, max) =>
|
||||
* entities.filter(e => e.profileImageUrl).slice(0, max)"
|
||||
*
|
||||
* @example
|
||||
* // Custom sorting: sort projects by name
|
||||
* :display-entities-function="(entities, type) =>
|
||||
* entities.sort((a, b) => a.name.localeCompare(b.name))"
|
||||
* :display-entities-function="(entities, type, max) =>
|
||||
* entities.sort((a, b) => a.name.localeCompare(b.name)).slice(0, max)"
|
||||
*
|
||||
* @example
|
||||
* // Advanced logic: different limits for different entity types
|
||||
* :display-entities-function="(entities, type, max) =>
|
||||
* type === 'projects' ? entities.slice(0, 5) : entities.slice(0, max)"
|
||||
*/
|
||||
@Prop({ default: null })
|
||||
displayEntitiesFunction?: (
|
||||
entities: Contact[] | PlanData[],
|
||||
entityType: "people" | "projects",
|
||||
maxItems: number,
|
||||
) => Contact[] | PlanData[];
|
||||
|
||||
/**
|
||||
@@ -283,60 +206,33 @@ export default class EntityGrid extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed entities to display - uses function prop if provided, otherwise uses infinite scroll
|
||||
* When searching, returns filtered results with infinite scroll applied
|
||||
* Computed CSS classes for the grid layout
|
||||
*/
|
||||
get gridClasses(): string {
|
||||
const baseClasses = "grid gap-x-2 gap-y-4 text-center mb-4";
|
||||
|
||||
if (this.entityType === "projects") {
|
||||
return `${baseClasses} grid-cols-3 md:grid-cols-4`;
|
||||
} else {
|
||||
return `${baseClasses} grid-cols-4 sm:grid-cols-5 md:grid-cols-6`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed entities to display - uses function prop if provided, otherwise defaults
|
||||
*/
|
||||
get displayedEntities(): Contact[] | PlanData[] {
|
||||
// If searching, return filtered results with infinite scroll
|
||||
if (this.searchTerm.trim()) {
|
||||
return this.filteredEntities.slice(0, this.displayedCount);
|
||||
}
|
||||
|
||||
// If custom function provided, use it (disables infinite scroll)
|
||||
if (this.displayEntitiesFunction) {
|
||||
return this.displayEntitiesFunction(this.entities, this.entityType);
|
||||
return this.displayEntitiesFunction(
|
||||
this.entities,
|
||||
this.entityType,
|
||||
this.maxItems,
|
||||
);
|
||||
}
|
||||
|
||||
// Default: projects use infinite scroll
|
||||
if (this.entityType === "projects") {
|
||||
return (this.entities as PlanData[]).slice(0, this.displayedCount);
|
||||
}
|
||||
|
||||
// People: handled by recentContacts + alphabeticalContacts (both use displayedCount)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 3 most recently added contacts (when showing contacts and not searching)
|
||||
*/
|
||||
get recentContacts(): Contact[] {
|
||||
if (this.entityType !== "people" || this.searchTerm.trim()) {
|
||||
return [];
|
||||
}
|
||||
// Entities are already sorted by date added (newest first)
|
||||
return (this.entities as Contact[]).slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the remaining contacts sorted alphabetically (when showing contacts and not searching)
|
||||
* Uses infinite scroll to control how many are displayed
|
||||
*/
|
||||
get alphabeticalContacts(): Contact[] {
|
||||
if (this.entityType !== "people" || this.searchTerm.trim()) {
|
||||
return [];
|
||||
}
|
||||
// Skip the first 3 (recent contacts) and sort the rest alphabetically
|
||||
// Create a copy to avoid mutating the original array
|
||||
const remaining = (this.entities as Contact[]).slice(RECENT_CONTACTS_COUNT);
|
||||
const sorted = [...remaining].sort((a: Contact, b: Contact) => {
|
||||
// Sort alphabetically by name, falling back to DID if name is missing
|
||||
const nameA = (a.name || a.did).toLowerCase();
|
||||
const nameB = (b.name || b.did).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
// Apply infinite scroll: show based on displayedCount (minus the 3 recent)
|
||||
const toShow = Math.max(0, this.displayedCount - RECENT_CONTACTS_COUNT);
|
||||
return sorted.slice(0, toShow);
|
||||
// Default implementation for backward compatibility
|
||||
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems;
|
||||
return this.entities.slice(0, maxDisplay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +246,15 @@ export default class EntityGrid extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show the "Show All" navigation
|
||||
*/
|
||||
get shouldShowAll(): boolean {
|
||||
return (
|
||||
!this.hideShowAll && this.entities.length > 0 && this.showAllRoute !== ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the "You" entity is conflicted
|
||||
*/
|
||||
@@ -423,144 +328,6 @@ export default class EntityGrid extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle search input with debouncing
|
||||
*/
|
||||
handleSearchInput(): void {
|
||||
// Show spinner immediately when user types
|
||||
this.isSearching = true;
|
||||
|
||||
// Clear existing timeout
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
// Set new timeout for 500ms delay
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual search
|
||||
*/
|
||||
async performSearch(): Promise<void> {
|
||||
if (!this.searchTerm.trim()) {
|
||||
this.filteredEntities = [];
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
|
||||
try {
|
||||
// Simulate async search (in case we need to add API calls later)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const searchLower = this.searchTerm.toLowerCase().trim();
|
||||
|
||||
if (this.entityType === "people") {
|
||||
this.filteredEntities = (this.entities as Contact[])
|
||||
.filter((contact: Contact) => {
|
||||
const name = contact.name?.toLowerCase() || "";
|
||||
const did = contact.did.toLowerCase();
|
||||
return name.includes(searchLower) || did.includes(searchLower);
|
||||
})
|
||||
.sort((a: Contact, b: Contact) => {
|
||||
// Sort alphabetically by name, falling back to DID if name is missing
|
||||
const nameA = (a.name || a.did).toLowerCase();
|
||||
const nameB = (b.name || b.did).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
} else {
|
||||
this.filteredEntities = (this.entities as PlanData[])
|
||||
.filter((project: PlanData) => {
|
||||
const name = project.name?.toLowerCase() || "";
|
||||
const handleId = project.handleId.toLowerCase();
|
||||
return name.includes(searchLower) || handleId.includes(searchLower);
|
||||
})
|
||||
.sort((a: PlanData, b: PlanData) => {
|
||||
// Sort alphabetically by name
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// Reset displayed count when search completes
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search
|
||||
*/
|
||||
clearSearch(): void {
|
||||
this.searchTerm = "";
|
||||
this.filteredEntities = [];
|
||||
this.isSearching = false;
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
|
||||
// Clear any pending timeout
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if more entities can be loaded
|
||||
*/
|
||||
canLoadMore(): boolean {
|
||||
if (this.displayEntitiesFunction) {
|
||||
// Custom function disables infinite scroll
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.searchTerm.trim()) {
|
||||
// Search mode: check filtered entities
|
||||
return this.displayedCount < this.filteredEntities.length;
|
||||
}
|
||||
|
||||
if (this.entityType === "projects") {
|
||||
// Projects: check if more available
|
||||
return this.displayedCount < this.entities.length;
|
||||
}
|
||||
|
||||
// People: check if more alphabetical contacts available
|
||||
// Total available = 3 recent + all alphabetical
|
||||
const remaining = (this.entities as Contact[]).slice(RECENT_CONTACTS_COUNT);
|
||||
const totalAvailable = RECENT_CONTACTS_COUNT + remaining.length;
|
||||
return this.displayedCount < totalAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize infinite scroll on mount
|
||||
*/
|
||||
mounted(): void {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.scrollContainer as HTMLElement;
|
||||
|
||||
if (container) {
|
||||
const { reset } = useInfiniteScroll(
|
||||
container,
|
||||
() => {
|
||||
// Load more: increment displayedCount
|
||||
this.displayedCount += INCREMENT_SIZE;
|
||||
},
|
||||
{
|
||||
distance: 50, // pixels from bottom
|
||||
canLoadMore: () => this.canLoadMore(),
|
||||
},
|
||||
);
|
||||
this.infiniteScrollReset = reset;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
|
||||
@Emit("entity-selected")
|
||||
@@ -573,33 +340,6 @@ export default class EntityGrid extends Vue {
|
||||
} {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes in search term to reset displayed count
|
||||
*/
|
||||
@Watch("searchTerm")
|
||||
onSearchTermChange(): void {
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes in entities prop to reset displayed count
|
||||
*/
|
||||
@Watch("entities")
|
||||
onEntitiesChange(): void {
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup timeouts when component is destroyed
|
||||
*/
|
||||
beforeUnmount(): void {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ from GiftedDialog.vue to handle the complete step 1 * entity selection interface
|
||||
with dynamic labeling and grid display. * * Features: * - Dynamic step labeling
|
||||
based on context * - EntityGrid integration for unified entity display * -
|
||||
Conflict detection and prevention * - Special entity handling (You, Unnamed) * -
|
||||
Cancel functionality * - Event delegation for entity selection * - Warning
|
||||
notifications for conflicted entities * - Template streamlined with computed CSS
|
||||
properties * * @author Matthew Raymer */
|
||||
Show All navigation with context preservation * - Cancel functionality * - Event
|
||||
delegation for entity selection * - Warning notifications for conflicted
|
||||
entities * - Template streamlined with computed CSS properties * * @author
|
||||
Matthew Raymer */
|
||||
<template>
|
||||
<div id="sectionGiftedGiver">
|
||||
<label class="block font-bold mb-4">
|
||||
@@ -15,14 +16,18 @@ properties * * @author Matthew Raymer */
|
||||
<EntityGrid
|
||||
:entity-type="shouldShowProjects ? 'projects' : 'people'"
|
||||
:entities="shouldShowProjects ? projects : allContacts"
|
||||
:max-items="10"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="allContacts"
|
||||
:conflict-checker="conflictChecker"
|
||||
:show-you-entity="shouldShowYouEntity"
|
||||
:you-selectable="youSelectable"
|
||||
:show-all-route="showAllRoute"
|
||||
:show-all-query-params="showAllQueryParams"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
:hide-show-all="hideShowAll"
|
||||
@entity-selected="handleEntitySelected"
|
||||
/>
|
||||
|
||||
@@ -63,6 +68,7 @@ interface EntitySelectionEvent {
|
||||
* - EntityGrid integration for unified entity display
|
||||
* - Conflict detection and prevention
|
||||
* - Special entity handling (You, Unnamed)
|
||||
* - Show All navigation with context preservation
|
||||
* - Cancel functionality
|
||||
* - Event delegation for entity selection
|
||||
* - Warning notifications for conflicted entities
|
||||
@@ -148,6 +154,10 @@ export default class EntitySelectionStep extends Vue {
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/** Whether to hide the "Show All" navigation */
|
||||
@Prop({ default: false })
|
||||
hideShowAll!: boolean;
|
||||
|
||||
/**
|
||||
* CSS classes for the cancel button
|
||||
*/
|
||||
@@ -212,6 +222,59 @@ export default class EntitySelectionStep extends Vue {
|
||||
return !this.conflictChecker(this.activeDid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route name for "Show All" navigation
|
||||
*/
|
||||
get showAllRoute(): string {
|
||||
if (this.shouldShowProjects) {
|
||||
return "discover";
|
||||
} else if (this.allContacts.length > 0) {
|
||||
return "contact-gift";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Query parameters for "Show All" navigation
|
||||
*/
|
||||
get showAllQueryParams(): Record<string, string> {
|
||||
const baseParams = {
|
||||
stepType: this.stepType,
|
||||
giverEntityType: this.giverEntityType,
|
||||
recipientEntityType: this.recipientEntityType,
|
||||
// Form field values to preserve
|
||||
description: this.description,
|
||||
amountInput: this.amountInput,
|
||||
unitCode: this.unitCode,
|
||||
offerId: this.offerId,
|
||||
fromProjectId: this.fromProjectId,
|
||||
toProjectId: this.toProjectId,
|
||||
showProjects: this.showProjects.toString(),
|
||||
isFromProjectView: this.isFromProjectView.toString(),
|
||||
};
|
||||
|
||||
if (this.shouldShowProjects) {
|
||||
// For project contexts, still pass entity type information
|
||||
return baseParams;
|
||||
}
|
||||
|
||||
return {
|
||||
...baseParams,
|
||||
// Always pass both giver and recipient info for context preservation
|
||||
giverProjectId: this.fromProjectId || "",
|
||||
giverProjectName: this.giver?.name || "",
|
||||
giverProjectImage: this.giver?.image || "",
|
||||
giverProjectHandleId: this.giver?.handleId || "",
|
||||
giverDid: this.giverEntityType === "person" ? this.giver?.did || "" : "",
|
||||
recipientProjectId: this.toProjectId || "",
|
||||
recipientProjectName: this.receiver?.name || "",
|
||||
recipientProjectImage: this.receiver?.image || "",
|
||||
recipientProjectHandleId: this.receiver?.handleId || "",
|
||||
recipientDid:
|
||||
this.recipientEntityType === "person" ? this.receiver?.did || "" : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle entity selection from EntityGrid
|
||||
*/
|
||||
|
||||
@@ -211,6 +211,8 @@ export default class FeedFilters extends Vue {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles if needed */
|
||||
<style>
|
||||
#dialogFeedFilters.dialog-overlay {
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
:unit-code="unitCode"
|
||||
:offer-id="offerId"
|
||||
:notify="$notify"
|
||||
:hide-show-all="hideShowAll"
|
||||
@entity-selected="handleEntitySelected"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
@@ -81,7 +82,6 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||
@@ -116,6 +116,7 @@ export default class GiftedDialog extends Vue {
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop() isFromProjectView = false;
|
||||
@Prop() hideShowAll = false;
|
||||
@Prop({ default: "person" }) giverEntityType = "person" as
|
||||
| "person"
|
||||
| "project";
|
||||
@@ -218,20 +219,11 @@ export default class GiftedDialog extends Vue {
|
||||
this.stepType = "giver";
|
||||
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
const settings = await this.$settings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
|
||||
logger.debug("[GiftedDialog] Settings received:", {
|
||||
activeDid: this.activeDid,
|
||||
apiServer: this.apiServer,
|
||||
});
|
||||
|
||||
this.allContacts = await this.$contactsByDateAdded();
|
||||
this.allContacts = await this.$contacts();
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
@@ -419,15 +411,6 @@ export default class GiftedDialog extends Vue {
|
||||
);
|
||||
} else {
|
||||
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
|
||||
if (this.callbackOnSuccess) {
|
||||
this.callbackOnSuccess(amount);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
class="text-blue-500"
|
||||
@click="copyTextToClipboard('A link to this page', deepLinkUrl)"
|
||||
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||
>click here to copy this page, paste it into a message, and ask if
|
||||
they'll tell you more about the {{ roleName }}.</a
|
||||
>
|
||||
@@ -110,7 +110,7 @@
|
||||
* @since 2024-12-19
|
||||
*/
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { copyToClipboard } from "../services/ClipboardService";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import * as R from "ramda";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
@@ -197,24 +197,19 @@ export default class HiddenDidDialog extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
async copyTextToClipboard(name: string, text: string) {
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
this.notify.success(
|
||||
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
`Error copying ${name || "content"} to clipboard: ${error}`,
|
||||
true,
|
||||
);
|
||||
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
|
||||
}
|
||||
copyToClipboard(name: string, text: string) {
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
.then(() => {
|
||||
this.notify.success(
|
||||
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
this.copyTextToClipboard("A link to this page", this.deepLinkUrl);
|
||||
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||
window.navigator.share({
|
||||
title: "Help Connect Me",
|
||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
v-if="shouldMirrorVideo"
|
||||
class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs"
|
||||
>
|
||||
<font-awesome icon="circle-user" class="w-[1em] mr-1" />
|
||||
<font-awesome icon="mirror" class="w-[1em] mr-1" />
|
||||
Mirrored
|
||||
</div>
|
||||
<div :class="cameraControlsClasses">
|
||||
@@ -293,7 +293,7 @@ const inputImageFileNameRef = ref<Blob>();
|
||||
export default class ImageMethodDialog extends Vue {
|
||||
$notify!: NotifyFunction;
|
||||
$router!: Router;
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
/** Active DID for user authentication */
|
||||
activeDid = "";
|
||||
@@ -498,14 +498,9 @@ export default class ImageMethodDialog extends Vue {
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
// Initialize notification helpers
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
try {
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
} catch (error) {
|
||||
logger.error("Error retrieving settings from database:", error);
|
||||
this.notify.error(
|
||||
|
||||
@@ -1,255 +1,183 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="space-y-4">
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
|
||||
>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
|
||||
<!-- Members List -->
|
||||
|
||||
<div v-else>
|
||||
<div class="text-center text-red-600 my-4">
|
||||
{{ decryptionErrorMessage() }}
|
||||
</div>
|
||||
|
||||
<div v-if="missingMyself" class="py-4 text-red-600">
|
||||
You are not currently admitted by the organizer.
|
||||
</div>
|
||||
<div v-if="!firstName" class="py-4 text-red-600">
|
||||
Your name is not set, so others may not recognize you. Reload this
|
||||
page to set it.
|
||||
</div>
|
||||
|
||||
<ul class="list-disc text-sm ps-4 space-y-2 mb-4">
|
||||
<li
|
||||
v-if="
|
||||
membersToShow().length > 0 && showOrganizerTools && isOrganizer
|
||||
"
|
||||
>
|
||||
Click
|
||||
<font-awesome icon="circle-plus" class="text-blue-500 text-sm" />
|
||||
/
|
||||
<font-awesome icon="circle-minus" class="text-rose-500 text-sm" />
|
||||
to add/remove them to/from the meeting.
|
||||
</li>
|
||||
<li
|
||||
v-if="
|
||||
membersToShow().length > 0 && getNonContactMembers().length > 0
|
||||
"
|
||||
>
|
||||
Click
|
||||
<font-awesome icon="circle-user" class="text-green-600 text-sm" />
|
||||
to add them to your contacts.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
title="Refresh members list now"
|
||||
@click="refreshData(false)"
|
||||
>
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
Refresh
|
||||
<span class="text-xs">({{ countdownTimer }}s)</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
v-if="membersToShow().length > 0"
|
||||
class="border-t border-slate-300 my-2"
|
||||
>
|
||||
<li
|
||||
v-for="member in membersToShow()"
|
||||
:key="member.member.memberId"
|
||||
:class="[
|
||||
'border-b px-2 sm:px-3 py-1.5',
|
||||
{
|
||||
'bg-blue-50 border-t border-blue-300 -mt-[1px]':
|
||||
!member.member.admitted &&
|
||||
(isOrganizer || member.did === activeDid),
|
||||
},
|
||||
{ 'border-slate-300': member.member.admitted },
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center gap-1 overflow-hidden">
|
||||
<h3
|
||||
:class="[
|
||||
'font-semibold truncate',
|
||||
{
|
||||
'text-slate-500':
|
||||
!member.member.admitted &&
|
||||
(isOrganizer || member.did === activeDid),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<font-awesome
|
||||
v-if="member.member.memberId === members[0]?.memberId"
|
||||
icon="crown"
|
||||
class="fa-fw text-amber-400"
|
||||
/>
|
||||
<font-awesome
|
||||
v-if="member.did === activeDid"
|
||||
icon="hand"
|
||||
class="fa-fw text-slate-500"
|
||||
/>
|
||||
<font-awesome
|
||||
v-if="
|
||||
!member.member.admitted &&
|
||||
(isOrganizer || member.did === activeDid)
|
||||
"
|
||||
icon="hourglass-half"
|
||||
class="fa-fw text-slate-400"
|
||||
/>
|
||||
{{ member.name || unnamedMember }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex items-center gap-1.5 ml-2 ms-1"
|
||||
>
|
||||
<button
|
||||
class="btn-add-contact ml-2"
|
||||
title="Add as contact"
|
||||
@click="addAsContact(member)"
|
||||
>
|
||||
<font-awesome icon="circle-user" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-info-contact ml-2"
|
||||
title="Contact Info"
|
||||
@click="
|
||||
informAboutAddingContact(
|
||||
getContactFor(member.did) !== undefined,
|
||||
)
|
||||
"
|
||||
>
|
||||
<font-awesome icon="circle-info" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex items-center gap-1.5 ms-1"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'contact-edit', params: { did: member.did } }"
|
||||
>
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="text-sm text-blue-500 ml-2 mb-1"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'did', params: { did: member.did } }"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="text-sm text-blue-500 ml-2 mb-1"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
showOrganizerTools && isOrganizer && member.did !== activeDid
|
||||
"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<button
|
||||
:class="
|
||||
member.member.admitted
|
||||
? 'btn-admission-remove'
|
||||
: 'btn-admission-add'
|
||||
"
|
||||
:title="
|
||||
member.member.admitted ? 'Remove member' : 'Admit member'
|
||||
"
|
||||
@click="checkWhetherContactBeforeAdmitting(member)"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="
|
||||
member.member.admitted ? 'circle-minus' : 'circle-plus'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-info-admission"
|
||||
title="Admission Info"
|
||||
@click="informAboutAdmission()"
|
||||
>
|
||||
<font-awesome icon="circle-info" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 truncate">
|
||||
{{ member.did }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="membersToShow().length > 0" class="flex justify-between">
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
title="Refresh members list now"
|
||||
@click="refreshData(false)"
|
||||
>
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
Refresh
|
||||
<span class="text-xs">({{ countdownTimer }}s)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="members.length === 0" class="text-gray-500 py-4">
|
||||
No members have joined this meeting yet
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
|
||||
>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
|
||||
<!-- Bulk Members Dialog for both admitting and setting visibility -->
|
||||
<BulkMembersDialog
|
||||
ref="bulkMembersDialog"
|
||||
:active-did="activeDid"
|
||||
:api-server="apiServer"
|
||||
:is-organizer="isOrganizer"
|
||||
@close="closeBulkMembersDialogCallback"
|
||||
/>
|
||||
<!-- Members List -->
|
||||
|
||||
<div v-else>
|
||||
<div class="text-center text-red-600 py-4">
|
||||
{{ decryptionErrorMessage() }}
|
||||
</div>
|
||||
|
||||
<div v-if="missingMyself" class="py-4 text-red-600">
|
||||
You are not currently admitted by the organizer.
|
||||
</div>
|
||||
<div v-if="!firstName" class="py-4 text-red-600">
|
||||
Your name is not set, so others may not recognize you. Reload this page
|
||||
to set it.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span
|
||||
v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
|
||||
class="inline-flex items-center flex-wrap"
|
||||
>
|
||||
<span class="inline-flex items-center">
|
||||
• Click
|
||||
<span
|
||||
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
|
||||
>
|
||||
<font-awesome icon="plus" class="text-sm" />
|
||||
</span>
|
||||
/
|
||||
<span
|
||||
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
|
||||
>
|
||||
<font-awesome icon="minus" class="text-sm" />
|
||||
</span>
|
||||
to add/remove them to/from the meeting.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
v-if="membersToShow().length > 0"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
• Click
|
||||
<span
|
||||
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
|
||||
>
|
||||
<font-awesome icon="circle-user" class="text-xl" />
|
||||
</span>
|
||||
to add them to your contacts.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
class="btn-action-refresh"
|
||||
title="Refresh members list"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="member in membersToShow()"
|
||||
:key="member.member.memberId"
|
||||
class="mt-2 p-4 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ member.name || unnamedMember }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex justify-end"
|
||||
>
|
||||
<button
|
||||
class="btn-add-contact"
|
||||
title="Add as contact"
|
||||
@click="addAsContact(member)"
|
||||
>
|
||||
<font-awesome icon="circle-user" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="member.did !== activeDid"
|
||||
class="btn-info-contact"
|
||||
title="Contact info"
|
||||
@click="
|
||||
informAboutAddingContact(
|
||||
getContactFor(member.did) !== undefined,
|
||||
)
|
||||
"
|
||||
>
|
||||
<font-awesome icon="circle-info" class="text-base" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="
|
||||
showOrganizerTools && isOrganizer && member.did !== activeDid
|
||||
"
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
class="btn-admission"
|
||||
:title="
|
||||
member.member.admitted ? 'Remove member' : 'Admit member'
|
||||
"
|
||||
@click="checkWhetherContactBeforeAdmitting(member)"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="member.member.admitted ? 'minus' : 'plus'"
|
||||
class="text-sm"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn-info-admission"
|
||||
title="Admission info"
|
||||
@click="informAboutAdmission()"
|
||||
>
|
||||
<font-awesome icon="circle-info" class="text-base" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 truncate">
|
||||
{{ member.did }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
|
||||
<button
|
||||
class="btn-action-refresh"
|
||||
title="Refresh members list"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="members.length === 0" class="text-gray-500 py-4">
|
||||
No members have joined this meeting yet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import {
|
||||
NOTIFY_ADD_CONTACT_FIRST,
|
||||
NOTIFY_CONTINUE_WITHOUT_ADDING,
|
||||
} from "@/constants/notifications";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
import {
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
register,
|
||||
serverMessageForUser,
|
||||
} from "@/libs/endorserServer";
|
||||
import { decryptMessage } from "@/libs/crypto";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MemberData } from "@/interfaces";
|
||||
} from "../libs/endorserServer";
|
||||
import { decryptMessage } from "../libs/crypto";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import BulkMembersDialog from "./BulkMembersDialog.vue";
|
||||
import {
|
||||
NOTIFY_ADD_CONTACT_FIRST,
|
||||
NOTIFY_CONTINUE_WITHOUT_ADDING,
|
||||
} from "@/constants/notifications";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
@@ -265,15 +193,13 @@ interface DecryptedMember {
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
BulkMembersDialog,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class MembersList extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
libsUtil = libsUtil;
|
||||
|
||||
@Prop({ required: true }) password!: string;
|
||||
@Prop({ default: false }) showOrganizerTools!: boolean;
|
||||
@@ -284,7 +210,6 @@ export default class MembersList extends Vue {
|
||||
return message;
|
||||
}
|
||||
|
||||
contacts: Array<Contact> = [];
|
||||
decryptedMembers: DecryptedMember[] = [];
|
||||
firstName = "";
|
||||
isLoading = true;
|
||||
@@ -294,12 +219,7 @@ export default class MembersList extends Vue {
|
||||
missingMyself = false;
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
// Auto-refresh functionality
|
||||
countdownTimer = 10;
|
||||
autoRefreshInterval: NodeJS.Timeout | null = null;
|
||||
lastRefreshTime = 0;
|
||||
previousMemberDidsIgnored: string[] = [];
|
||||
contacts: Array<Contact> = [];
|
||||
|
||||
/**
|
||||
* Get the unnamed member constant
|
||||
@@ -312,16 +232,11 @@ export default class MembersList extends Vue {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.firstName = settings.firstName || "";
|
||||
|
||||
this.refreshData();
|
||||
await this.fetchMembers();
|
||||
await this.loadContacts();
|
||||
}
|
||||
|
||||
async fetchMembers() {
|
||||
@@ -367,10 +282,7 @@ export default class MembersList extends Vue {
|
||||
const content = JSON.parse(decryptedContent);
|
||||
|
||||
this.decryptedMembers.push({
|
||||
member: {
|
||||
...member,
|
||||
admitted: member.admitted !== undefined ? member.admitted : true, // Default to true for non-organizers
|
||||
},
|
||||
member: member,
|
||||
name: content.name,
|
||||
did: content.did,
|
||||
isRegistered: !!content.isRegistered,
|
||||
@@ -412,81 +324,22 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
|
||||
membersToShow(): DecryptedMember[] {
|
||||
let members: DecryptedMember[] = [];
|
||||
|
||||
if (this.isOrganizer) {
|
||||
if (this.showOrganizerTools) {
|
||||
members = this.decryptedMembers;
|
||||
return this.decryptedMembers;
|
||||
} else {
|
||||
members = this.decryptedMembers.filter(
|
||||
return this.decryptedMembers.filter(
|
||||
(member: DecryptedMember) => member.member.admitted,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// non-organizers only get visible members from server, plus themselves
|
||||
|
||||
// Check if current user is already in the decrypted members list
|
||||
if (
|
||||
!this.decryptedMembers.find((member) => member.did === this.activeDid)
|
||||
) {
|
||||
// this is a stub for this user just in case they are waiting to get in
|
||||
// which is especially useful so they can see their own DID
|
||||
const currentUser: DecryptedMember = {
|
||||
member: {
|
||||
admitted: false,
|
||||
content: "{}",
|
||||
memberId: -1,
|
||||
},
|
||||
name: this.firstName,
|
||||
did: this.activeDid,
|
||||
isRegistered: false,
|
||||
};
|
||||
members = [currentUser, ...this.decryptedMembers];
|
||||
} else {
|
||||
members = this.decryptedMembers;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort members according to priority:
|
||||
// 1. Organizer at the top
|
||||
// 2. Current user next
|
||||
// 3. Non-admitted members next
|
||||
// 4. Everyone else after
|
||||
return members.sort((a, b) => {
|
||||
// Check if either member is the organizer (first member in original list)
|
||||
const aIsOrganizer = a.member.memberId === this.members[0]?.memberId;
|
||||
const bIsOrganizer = b.member.memberId === this.members[0]?.memberId;
|
||||
|
||||
// Check if either member is the current user
|
||||
const aIsCurrentUser = a.did === this.activeDid;
|
||||
const bIsCurrentUser = b.did === this.activeDid;
|
||||
|
||||
// Organizer always comes first
|
||||
if (aIsOrganizer && !bIsOrganizer) return -1;
|
||||
if (!aIsOrganizer && bIsOrganizer) return 1;
|
||||
|
||||
// If both are organizers, maintain original order
|
||||
if (aIsOrganizer && bIsOrganizer) return 0;
|
||||
|
||||
// Current user comes second (after organizer)
|
||||
if (aIsCurrentUser && !bIsCurrentUser && !bIsOrganizer) return -1;
|
||||
if (!aIsCurrentUser && bIsCurrentUser && !aIsOrganizer) return 1;
|
||||
|
||||
// If both are current users, maintain original order
|
||||
if (aIsCurrentUser && bIsCurrentUser) return 0;
|
||||
|
||||
// Non-admitted members come before admitted members
|
||||
if (!a.member.admitted && b.member.admitted) return -1;
|
||||
if (a.member.admitted && !b.member.admitted) return 1;
|
||||
|
||||
// If admission status is the same, maintain original order
|
||||
return 0;
|
||||
});
|
||||
// non-organizers only get visible members from server
|
||||
return this.decryptedMembers;
|
||||
}
|
||||
|
||||
informAboutAdmission() {
|
||||
this.notify.info(
|
||||
"This is to register people in Time Safari and to admit them to the meeting. A (+) symbol means they are not yet admitted and you can register and admit them. A (-) symbol means you can remove them, but they will stay registered.",
|
||||
"This is to register people in Time Safari and to admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.",
|
||||
TIMEOUTS.VERY_LONG,
|
||||
);
|
||||
}
|
||||
@@ -505,85 +358,18 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async loadContacts() {
|
||||
this.contacts = await this.$getAllContacts();
|
||||
}
|
||||
|
||||
getContactFor(did: string): Contact | undefined {
|
||||
return this.contacts.find((contact) => contact.did === did);
|
||||
}
|
||||
|
||||
getPendingMembersToAdmit(): MemberData[] {
|
||||
return this.decryptedMembers
|
||||
.filter(
|
||||
(member) => member.did !== this.activeDid && !member.member.admitted,
|
||||
)
|
||||
.map(this.convertDecryptedMemberToMemberData);
|
||||
}
|
||||
|
||||
getNonContactMembers(): MemberData[] {
|
||||
return this.decryptedMembers
|
||||
.filter(
|
||||
(member) =>
|
||||
member.did !== this.activeDid && !this.getContactFor(member.did),
|
||||
)
|
||||
.map(this.convertDecryptedMemberToMemberData);
|
||||
}
|
||||
|
||||
convertDecryptedMemberToMemberData(
|
||||
decryptedMember: DecryptedMember,
|
||||
): MemberData {
|
||||
return {
|
||||
did: decryptedMember.did,
|
||||
name: decryptedMember.name,
|
||||
isContact: !!this.getContactFor(decryptedMember.did),
|
||||
member: {
|
||||
memberId: decryptedMember.member.memberId.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the bulk members dialog if conditions are met
|
||||
* (admit pending members for organizers, add to contacts for non-organizers)
|
||||
*/
|
||||
async refreshData(bypassPromptIfAllWereIgnored = true) {
|
||||
// Force refresh both contacts and members
|
||||
this.contacts = await this.$getAllContacts();
|
||||
await this.fetchMembers();
|
||||
|
||||
const pendingMembers = this.isOrganizer
|
||||
? this.getPendingMembersToAdmit()
|
||||
: this.getNonContactMembers();
|
||||
if (pendingMembers.length === 0) {
|
||||
this.startAutoRefresh();
|
||||
return;
|
||||
}
|
||||
if (bypassPromptIfAllWereIgnored) {
|
||||
// only show if there are members that have not been ignored
|
||||
const pendingMembersNotIgnored = pendingMembers.filter(
|
||||
(member) => !this.previousMemberDidsIgnored.includes(member.did),
|
||||
);
|
||||
if (pendingMembersNotIgnored.length === 0) {
|
||||
this.startAutoRefresh();
|
||||
// everyone waiting has been ignored
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.stopAutoRefresh();
|
||||
(this.$refs.bulkMembersDialog as BulkMembersDialog).open(pendingMembers);
|
||||
}
|
||||
|
||||
// Bulk Members Dialog methods
|
||||
async closeBulkMembersDialogCallback(
|
||||
result: { notSelectedMemberDids: string[] } | undefined,
|
||||
) {
|
||||
this.previousMemberDidsIgnored = result?.notSelectedMemberDids || [];
|
||||
|
||||
await this.refreshData();
|
||||
}
|
||||
|
||||
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||
const contact = this.getContactFor(decrMember.did);
|
||||
if (!decrMember.member.admitted && !contact) {
|
||||
// If not a contact, stop auto-refresh and show confirmation dialog
|
||||
this.stopAutoRefresh();
|
||||
// If not a contact, show confirmation dialog
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
@@ -596,7 +382,6 @@ export default class MembersList extends Vue {
|
||||
await this.addAsContact(decrMember);
|
||||
// After adding as contact, proceed with admission
|
||||
await this.toggleAdmission(decrMember);
|
||||
this.startAutoRefresh();
|
||||
},
|
||||
onNo: async () => {
|
||||
// If they choose not to add as contact, show second confirmation
|
||||
@@ -609,19 +394,14 @@ export default class MembersList extends Vue {
|
||||
yesText: NOTIFY_CONTINUE_WITHOUT_ADDING.yesText,
|
||||
onYes: async () => {
|
||||
await this.toggleAdmission(decrMember);
|
||||
this.startAutoRefresh();
|
||||
},
|
||||
onCancel: async () => {
|
||||
// Do nothing, effectively canceling the operation
|
||||
this.startAutoRefresh();
|
||||
},
|
||||
},
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
},
|
||||
onCancel: async () => {
|
||||
this.startAutoRefresh();
|
||||
},
|
||||
},
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
@@ -723,41 +503,6 @@ export default class MembersList extends Vue {
|
||||
this.notify.error(message, TIMEOUTS.LONG);
|
||||
}
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
this.stopAutoRefresh();
|
||||
this.lastRefreshTime = Date.now();
|
||||
this.countdownTimer = 10;
|
||||
|
||||
this.autoRefreshInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastRefresh = (now - this.lastRefreshTime) / 1000;
|
||||
|
||||
if (timeSinceLastRefresh >= 10) {
|
||||
// Time to refresh
|
||||
this.refreshData();
|
||||
this.lastRefreshTime = now;
|
||||
this.countdownTimer = 10;
|
||||
} else {
|
||||
// Update countdown
|
||||
this.countdownTimer = Math.max(
|
||||
0,
|
||||
Math.round(10 - timeSinceLastRefresh),
|
||||
);
|
||||
}
|
||||
}, 1000); // Update every second
|
||||
}
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval);
|
||||
this.autoRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -772,26 +517,29 @@ export default class MembersList extends Vue {
|
||||
|
||||
.btn-add-contact {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply text-lg text-green-600 hover:text-green-800
|
||||
@apply ml-2 w-8 h-8 flex items-center justify-center rounded-full
|
||||
bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800
|
||||
transition-colors;
|
||||
}
|
||||
|
||||
.btn-info-contact {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full
|
||||
bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800
|
||||
transition-colors;
|
||||
}
|
||||
|
||||
.btn-admission {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply mr-2 w-6 h-6 flex items-center justify-center rounded-full
|
||||
bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800
|
||||
transition-colors;
|
||||
}
|
||||
|
||||
.btn-info-contact,
|
||||
.btn-info-admission {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply text-slate-400 hover:text-slate-600
|
||||
transition-colors;
|
||||
}
|
||||
|
||||
.btn-admission-add {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply text-lg text-blue-500 hover:text-blue-700
|
||||
transition-colors;
|
||||
}
|
||||
|
||||
.btn-admission-remove {
|
||||
/* stylelint-disable-next-line at-rule-no-unknown */
|
||||
@apply text-lg text-rose-500 hover:text-rose-700
|
||||
@apply mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full
|
||||
bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800
|
||||
transition-colors;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,7 +64,6 @@ import * as libsUtil from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_OFFER_SETTINGS_ERROR,
|
||||
NOTIFY_OFFER_RECORDING,
|
||||
@@ -176,11 +175,7 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
@@ -304,14 +299,6 @@ export default class OfferDialog extends Vue {
|
||||
);
|
||||
} else {
|
||||
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -270,12 +270,7 @@ export default class OnboardingDialog extends Vue {
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
const contacts = await this.$getAllContacts();
|
||||
|
||||
@@ -3,25 +3,30 @@ GiftedDialog.vue to handle person entity display * with selection states and
|
||||
conflict detection. * * @author Matthew Raymer */
|
||||
<template>
|
||||
<li :class="cardClasses" @click="handleClick">
|
||||
<div>
|
||||
<div class="relative w-fit mx-auto">
|
||||
<EntityIcon
|
||||
v-if="person.did"
|
||||
:contact="person"
|
||||
class="!size-[2rem] shrink-0 border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-5xl mb-1 shrink-0"
|
||||
class="text-slate-400 text-5xl mb-1"
|
||||
/>
|
||||
|
||||
<!-- Time icon overlay for contacts -->
|
||||
<div
|
||||
v-if="person.did && showTimeIcon"
|
||||
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
|
||||
>
|
||||
<font-awesome icon="clock" class="block text-white text-xs w-[1em]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<h3 :class="nameClasses">
|
||||
{{ displayName }}
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 truncate">{{ person.did }}</p>
|
||||
</div>
|
||||
<h3 :class="nameClasses">
|
||||
{{ displayName }}
|
||||
</h3>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -76,32 +81,29 @@ export default class PersonCard extends Vue {
|
||||
* Computed CSS classes for the card
|
||||
*/
|
||||
get cardClasses(): string {
|
||||
const baseCardClasses =
|
||||
"flex items-center gap-2 px-2 py-1.5 border-b border-slate-300";
|
||||
|
||||
if (!this.selectable || this.conflicted) {
|
||||
return `${baseCardClasses} *:opacity-50 cursor-not-allowed`;
|
||||
return "opacity-50 cursor-not-allowed";
|
||||
}
|
||||
|
||||
return `${baseCardClasses} cursor-pointer hover:bg-slate-50`;
|
||||
return "cursor-pointer hover:bg-slate-50";
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed CSS classes for the person name
|
||||
*/
|
||||
get nameClasses(): string {
|
||||
const baseNameClasses = "text-sm font-semibold truncate";
|
||||
const baseClasses =
|
||||
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
|
||||
|
||||
if (this.conflicted) {
|
||||
return `${baseNameClasses} text-slate-500`;
|
||||
return `${baseClasses} text-slate-400`;
|
||||
}
|
||||
|
||||
// Add italic styling for entities without set names
|
||||
if (!this.person.name) {
|
||||
return `${baseNameClasses} italic text-slate-500`;
|
||||
return `${baseClasses} italic text-slate-500`;
|
||||
}
|
||||
|
||||
return baseNameClasses;
|
||||
return baseClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -268,12 +268,7 @@ export default class PhotoDialog extends Vue {
|
||||
// logger.log("PhotoDialog mounted");
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
logger.log("isRegistered:", this.isRegistered);
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -2,26 +2,25 @@
|
||||
GiftedDialog.vue to handle project entity display * with selection states and
|
||||
issuer information. * * @author Matthew Raymer */
|
||||
<template>
|
||||
<li
|
||||
class="flex items-center gap-2 px-2 py-1.5 border-b border-slate-300 hover:bg-slate-50 cursor-pointer"
|
||||
@click="handleClick"
|
||||
>
|
||||
<ProjectIcon
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:image-url="project.image"
|
||||
class="!size-[2rem] shrink-0 border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
/>
|
||||
<li class="cursor-pointer" @click="handleClick">
|
||||
<div class="relative w-fit mx-auto">
|
||||
<ProjectIcon
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:image-url="project.image"
|
||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<h3 class="text-sm font-semibold truncate">
|
||||
{{ project.name || unnamedProject }}
|
||||
</h3>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ project.name || unnamedProject }}
|
||||
</h3>
|
||||
|
||||
<div class="text-xs text-slate-500 truncate">
|
||||
<font-awesome icon="user" class="text-slate-400" />
|
||||
{{ issuerDisplayName }}
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 truncate">
|
||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
||||
{{ issuerDisplayName }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
66
src/components/ShowAllCard.vue
Normal file
66
src/components/ShowAllCard.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
/** * ShowAllCard.vue - Show All navigation card component * * Extracted from
|
||||
GiftedDialog.vue to handle "Show All" navigation * for both people and projects
|
||||
entity types. * * @author Matthew Raymer */
|
||||
<template>
|
||||
<li class="cursor-pointer">
|
||||
<router-link :to="navigationRoute" class="block text-center">
|
||||
<font-awesome icon="circle-right" class="text-blue-500 text-5xl mb-1" />
|
||||
<h3
|
||||
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Show All
|
||||
</h3>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationRaw } from "vue-router";
|
||||
|
||||
/**
|
||||
* ShowAllCard - Displays "Show All" navigation for entity grids
|
||||
*
|
||||
* Features:
|
||||
* - Provides navigation to full entity listings
|
||||
* - Supports different routes based on entity type
|
||||
* - Maintains context through query parameters
|
||||
* - Consistent visual styling with other cards
|
||||
*/
|
||||
@Component({ name: "ShowAllCard" })
|
||||
export default class ShowAllCard extends Vue {
|
||||
/** Type of entities being shown */
|
||||
@Prop({ required: true })
|
||||
entityType!: "people" | "projects";
|
||||
|
||||
/** Route name to navigate to */
|
||||
@Prop({ required: true })
|
||||
routeName!: string;
|
||||
|
||||
/** Query parameters to pass to the route */
|
||||
@Prop({ default: () => ({}) })
|
||||
queryParams!: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Computed navigation route with query parameters
|
||||
*/
|
||||
get navigationRoute(): RouteLocationRaw {
|
||||
return {
|
||||
name: this.routeName,
|
||||
query: this.queryParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Ensure router-link styling is consistent */
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover .fa-circle-right {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -63,24 +63,23 @@ export default class SpecialEntityCard extends Vue {
|
||||
conflictContext!: string;
|
||||
|
||||
/**
|
||||
* Computed CSS classes for the card
|
||||
* Computed CSS classes for the card container
|
||||
*/
|
||||
get cardClasses(): string {
|
||||
const baseCardClasses =
|
||||
"flex items-center gap-2 px-2 py-1.5 border-b border-slate-300";
|
||||
const baseClasses = "block";
|
||||
|
||||
if (!this.selectable || this.conflicted) {
|
||||
return `${baseCardClasses} *:opacity-50 cursor-not-allowed`;
|
||||
return `${baseClasses} cursor-not-allowed opacity-50`;
|
||||
}
|
||||
|
||||
return `${baseCardClasses} cursor-pointer hover:bg-slate-50`;
|
||||
return `${baseClasses} cursor-pointer`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed CSS classes for the icon
|
||||
*/
|
||||
get iconClasses(): string {
|
||||
const baseClasses = "text-[2rem]";
|
||||
const baseClasses = "text-5xl mb-1";
|
||||
|
||||
if (this.conflicted) {
|
||||
return `${baseClasses} text-slate-400`;
|
||||
@@ -102,7 +101,7 @@ export default class SpecialEntityCard extends Vue {
|
||||
*/
|
||||
get nameClasses(): string {
|
||||
const baseClasses =
|
||||
"text-sm font-semibold text-ellipsis whitespace-nowrap overflow-hidden";
|
||||
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
|
||||
|
||||
if (this.conflicted) {
|
||||
return `${baseClasses} text-slate-400`;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="message"
|
||||
class="-mt-6 bg-rose-100 border border-t-0 border-dashed border-rose-600 text-rose-900 text-sm text-center font-semibold rounded-b-md px-3 py-2 mb-3"
|
||||
class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))]"
|
||||
>
|
||||
{{ message }}
|
||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||
<span class="ml-2">
|
||||
<router-link
|
||||
:to="{ name: 'help' }"
|
||||
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
|
||||
>
|
||||
Help
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,8 +27,8 @@ import { logger } from "../utils/logger";
|
||||
})
|
||||
export default class TopMessage extends Vue {
|
||||
// Enhanced PlatformServiceMixin v4.0 provides:
|
||||
// - Cached database operations: this.$contacts(), this.$accountSettings()
|
||||
// - Settings shortcuts: this.$saveSettings()
|
||||
// - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings()
|
||||
// - Settings shortcuts: this.$saveSettings(), this.$saveMySettings()
|
||||
// - Cache management: this.$refreshSettings(), this.$clearAllCaches()
|
||||
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()
|
||||
// - All methods use smart caching with TTL for massive performance gains
|
||||
@@ -42,11 +49,8 @@ export default class TopMessage extends Vue {
|
||||
logger.debug("[TopMessage] 📥 Loading settings without overrides...");
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Get activeDid from new active_identity table (ActiveDid migration)
|
||||
const activeIdentity = await this.$getActiveIdentity();
|
||||
|
||||
logger.debug("[TopMessage] 📊 Settings loaded:", {
|
||||
activeDid: activeIdentity.activeDid,
|
||||
activeDid: settings.activeDid,
|
||||
apiServer: settings.apiServer,
|
||||
warnIfTestServer: settings.warnIfTestServer,
|
||||
warnIfProdServer: settings.warnIfProdServer,
|
||||
@@ -60,7 +64,7 @@ export default class TopMessage extends Vue {
|
||||
settings.apiServer &&
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = activeIdentity.activeDid?.slice(11, 15);
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You're not using prod, user " + didPrefix;
|
||||
logger.debug("[TopMessage] ⚠️ Test server warning displayed:", {
|
||||
apiServer: settings.apiServer,
|
||||
@@ -71,7 +75,7 @@ export default class TopMessage extends Vue {
|
||||
settings.apiServer &&
|
||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = activeIdentity.activeDid?.slice(11, 15);
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You are using prod, user " + didPrefix;
|
||||
logger.debug("[TopMessage] ⚠️ Production server warning displayed:", {
|
||||
apiServer: settings.apiServer,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<!-- show spinner if loading limits -->
|
||||
<div
|
||||
v-if="loadingLimits"
|
||||
class="text-slate-500 text-center italic mb-4"
|
||||
class="text-center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
@@ -19,10 +19,7 @@
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
</div>
|
||||
<div
|
||||
v-if="limitsMessage"
|
||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
||||
>
|
||||
<div class="mb-4 text-center">
|
||||
{{ limitsMessage }}
|
||||
</div>
|
||||
<div v-if="endorserLimits">
|
||||
|
||||
@@ -84,6 +84,7 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async open(aCallback?: (name?: string) => void) {
|
||||
this.callback = aCallback || this.callback;
|
||||
// Load from account-specific settings instead of master settings
|
||||
const settings = await this.$accountSettings();
|
||||
this.givenName = settings.firstName || "";
|
||||
this.visible = true;
|
||||
@@ -95,9 +96,9 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
try {
|
||||
// Get activeDid from new active_identity table (ActiveDid migration)
|
||||
const activeIdentity = await this.$getActiveIdentity();
|
||||
const activeDid = activeIdentity.activeDid;
|
||||
// Get the current active DID to save to user-specific settings
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid;
|
||||
|
||||
if (activeDid) {
|
||||
// Save to user-specific settings for the current identity
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ACCOUNT_VIEW_CONSTANTS = {
|
||||
CANNOT_UPLOAD_IMAGES: "You cannot upload images.",
|
||||
BAD_SERVER_RESPONSE: "Bad server response.",
|
||||
ERROR_RETRIEVING_LIMITS:
|
||||
"No limits were found, so no actions are allowed. You need to get registered.",
|
||||
"No limits were found, so no actions are allowed. You will need to get registered.",
|
||||
},
|
||||
|
||||
// Project assignment errors
|
||||
|
||||
@@ -59,7 +59,7 @@ export const PASSKEYS_ENABLED =
|
||||
export interface NotificationIface {
|
||||
group: string; // "alert" | "modal"
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title?: string;
|
||||
title: string;
|
||||
text?: string;
|
||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
||||
noText?: string;
|
||||
@@ -68,11 +68,4 @@ export interface NotificationIface {
|
||||
onYes?: () => Promise<void>;
|
||||
promptToStopAsking?: boolean;
|
||||
yesText?: string;
|
||||
membersData?: Array<{
|
||||
member: { admitted: boolean; content: string; memberId: number };
|
||||
name: string;
|
||||
did: string;
|
||||
isContact: boolean;
|
||||
contact?: { did: string; name?: string; seesMe?: boolean };
|
||||
}>; // For passing member data to visibility dialog
|
||||
}
|
||||
|
||||
@@ -510,6 +510,14 @@ export const NOTIFY_REGISTER_CONTACT = {
|
||||
text: "Do you want to register them?",
|
||||
};
|
||||
|
||||
// Used in: ContactsView.vue (showOnboardMeetingDialog method - complex modal for onboarding meeting)
|
||||
export const NOTIFY_ONBOARDING_MEETING = {
|
||||
title: "Onboarding Meeting",
|
||||
text: "Would you like to start a new meeting?",
|
||||
yesText: "Start New Meeting",
|
||||
noText: "Join Existing Meeting",
|
||||
};
|
||||
|
||||
// TestView.vue specific constants
|
||||
// Used in: TestView.vue (executeSql method - SQL error handling)
|
||||
export const NOTIFY_SQL_ERROR = {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from "../services/migrationService";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { arrayBufferToBase64 } from "@/libs/crypto";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
// Generate a random secret for the secret table
|
||||
|
||||
@@ -29,61 +28,7 @@ import { logger } from "@/utils/logger";
|
||||
// where they couldn't take action because they couldn't unlock that identity.)
|
||||
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
const secretBase64 = arrayBufferToBase64(randomBytes.buffer);
|
||||
|
||||
// Single source of truth for migration 004 SQL
|
||||
const MIG_004_SQL = `
|
||||
-- Migration 004: active_identity_management (CONSOLIDATED)
|
||||
-- Combines original migrations 004, 005, and 006 into single atomic operation
|
||||
-- CRITICAL SECURITY: Uses ON DELETE RESTRICT constraint from the start
|
||||
-- Assumes master code deployed with migration 003 (hasBackedUpSeed)
|
||||
|
||||
-- Enable foreign key constraints for data integrity
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Add UNIQUE constraint to accounts.did for foreign key support
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did);
|
||||
|
||||
-- Create active_identity table with SECURE constraint (ON DELETE RESTRICT)
|
||||
-- This prevents accidental account deletion - critical security feature
|
||||
CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
|
||||
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- Add performance indexes
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id);
|
||||
|
||||
-- Seed singleton row (only if not already exists)
|
||||
INSERT INTO active_identity (id, activeDid, lastUpdated)
|
||||
SELECT 1, NULL, datetime('now')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1);
|
||||
|
||||
-- MIGRATE EXISTING DATA: Copy activeDid from settings to active_identity
|
||||
-- This prevents data loss when migration runs on existing databases
|
||||
UPDATE active_identity
|
||||
SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
|
||||
lastUpdated = datetime('now')
|
||||
WHERE id = 1
|
||||
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '');
|
||||
|
||||
-- Copy important settings that were set in the MASTER_SETTINGS_KEY to the main identity.
|
||||
-- (We're not doing them all because some were already identity-specific and others aren't as critical.)
|
||||
UPDATE settings
|
||||
SET lastViewedClaimId = (SELECT lastViewedClaimId FROM settings WHERE id = 1),
|
||||
profileImageUrl = (SELECT profileImageUrl FROM settings WHERE id = 1),
|
||||
showShortcutBvc = (SELECT showShortcutBvc FROM settings WHERE id = 1),
|
||||
warnIfProdServer = (SELECT warnIfProdServer FROM settings WHERE id = 1),
|
||||
warnIfTestServer = (SELECT warnIfTestServer FROM settings WHERE id = 1)
|
||||
WHERE id = 2;
|
||||
|
||||
-- CLEANUP: Remove orphaned settings records and clear legacy activeDid values
|
||||
-- which usually simply deletes the MASTER_SETTINGS_KEY record.
|
||||
-- This completes the migration from settings-based to table-based active identity
|
||||
DELETE FROM settings WHERE accountDid IS NULL;
|
||||
UPDATE settings SET activeDid = NULL;
|
||||
`;
|
||||
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||
|
||||
// Each migration can include multiple SQL statements (with semicolons)
|
||||
const MIGRATIONS = [
|
||||
@@ -179,52 +124,8 @@ const MIGRATIONS = [
|
||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "003_add_hasBackedUpSeed_to_settings",
|
||||
sql: `
|
||||
-- Add hasBackedUpSeed field to settings
|
||||
-- This migration assumes master code has been deployed
|
||||
-- The error handling will catch this if column already exists and mark migration as applied
|
||||
ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "004_active_identity_management",
|
||||
sql: MIG_004_SQL,
|
||||
},
|
||||
{
|
||||
name: "005_add_starredPlanHandleIds_to_settings",
|
||||
sql: `
|
||||
ALTER TABLE settings ADD COLUMN starredPlanHandleIds TEXT DEFAULT '[]'; -- JSON string
|
||||
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract single value from database query result
|
||||
* Works with different database service result formats
|
||||
*/
|
||||
function extractSingleValue<T>(result: T): string | number | null {
|
||||
if (!result) return null;
|
||||
|
||||
// Handle AbsurdSQL format: QueryExecResult[]
|
||||
if (Array.isArray(result) && result.length > 0 && result[0]?.values) {
|
||||
const values = result[0].values;
|
||||
return values.length > 0 ? values[0][0] : null;
|
||||
}
|
||||
|
||||
// Handle Capacitor SQLite format: { values: unknown[][] }
|
||||
if (typeof result === "object" && result !== null && "values" in result) {
|
||||
const values = (result as { values: unknown[][] }).values;
|
||||
return values && values.length > 0
|
||||
? (values[0][0] as string | number)
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sqlExec - A function that executes a SQL statement and returns the result
|
||||
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
|
||||
@@ -234,57 +135,8 @@ export async function runMigrations<T>(
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
logger.debug("[Migration] Starting database migrations");
|
||||
|
||||
for (const migration of MIGRATIONS) {
|
||||
logger.debug("[Migration] Registering migration:", migration.name);
|
||||
registerMigration(migration);
|
||||
}
|
||||
|
||||
logger.debug("[Migration] Running migration service");
|
||||
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
|
||||
|
||||
logger.debug("[Migration] Database migrations completed");
|
||||
|
||||
// Bootstrapping: Ensure active account is selected after migrations
|
||||
logger.debug("[Migration] Running bootstrapping hooks");
|
||||
try {
|
||||
// Check if we have accounts but no active selection
|
||||
const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
|
||||
const accountsCount = (extractSingleValue(accountsResult) as number) || 0;
|
||||
|
||||
// Check if active_identity table exists, and if not, try to recover
|
||||
let activeDid: string | null = null;
|
||||
try {
|
||||
const activeResult = await sqlQuery(
|
||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||
);
|
||||
activeDid = (extractSingleValue(activeResult) as string) || null;
|
||||
} catch (error) {
|
||||
// Table doesn't exist - migration 004 may not have run yet
|
||||
logger.debug(
|
||||
"[Migration] active_identity table not found - migration may not have run",
|
||||
);
|
||||
activeDid = null;
|
||||
}
|
||||
|
||||
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
|
||||
logger.debug("[Migration] Auto-selecting first account as active");
|
||||
const firstAccountResult = await sqlQuery(
|
||||
"SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
|
||||
);
|
||||
const firstAccountDid =
|
||||
(extractSingleValue(firstAccountResult) as string) || null;
|
||||
|
||||
if (firstAccountDid) {
|
||||
await sqlExec(
|
||||
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
|
||||
[firstAccountDid],
|
||||
);
|
||||
logger.info(`[Migration] Set active account to: ${firstAccountDid}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("[Migration] Bootstrapping hook failed (non-critical):", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,34 @@ import { logger } from "@/utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
|
||||
export async function updateDefaultSettings(
|
||||
settingsChanges: Settings,
|
||||
): Promise<boolean> {
|
||||
delete settingsChanges.accountDid; // just in case
|
||||
// ensure there is no "id" that would override the key
|
||||
delete settingsChanges.id;
|
||||
try {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const { sql, params } = generateUpdateStatement(
|
||||
settingsChanges,
|
||||
"settings",
|
||||
"id = ?",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
const result = await platformService.dbExec(sql, params);
|
||||
return result.changes === 1;
|
||||
} catch (error) {
|
||||
logger.error("Error updating default settings:", error);
|
||||
if (error instanceof Error) {
|
||||
throw error; // Re-throw if it's already an Error with a message
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to update settings. We recommend you try again or restart the app.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertDidSpecificSettings(
|
||||
did: string,
|
||||
settings: Partial<Settings> = {},
|
||||
@@ -63,7 +91,6 @@ export async function updateDidSpecificSettings(
|
||||
? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0]
|
||||
: null;
|
||||
|
||||
// Note that we want to eliminate this check (and fix the above if it doesn't work).
|
||||
// Check if any of the target fields were actually changed
|
||||
let actuallyUpdated = false;
|
||||
if (currentRecord && updatedRecord) {
|
||||
@@ -130,11 +157,10 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||
result.columns,
|
||||
result.values,
|
||||
)[0] as Settings;
|
||||
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||
settings.starredPlanHandleIds = parseJsonField(
|
||||
settings.starredPlanHandleIds,
|
||||
[],
|
||||
);
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -200,11 +226,10 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
);
|
||||
}
|
||||
|
||||
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||
settings.starredPlanHandleIds = parseJsonField(
|
||||
settings.starredPlanHandleIds,
|
||||
[],
|
||||
);
|
||||
// Handle searchBoxes parsing
|
||||
if (settings.searchBoxes) {
|
||||
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
@@ -542,8 +567,6 @@ export async function debugSettingsData(did?: string): Promise<void> {
|
||||
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
|
||||
* - Capacitor SQLite: Returns raw strings that need manual parsing
|
||||
*
|
||||
* Maybe consolidate with PlatformServiceMixin._parseJsonField
|
||||
*
|
||||
* @param value The value to parse (could be string or already parsed object)
|
||||
* @param defaultValue Default value if parsing fails
|
||||
* @returns Parsed object or default value
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* ActiveIdentity type describes the active identity selection.
|
||||
* This replaces the activeDid field in the settings table for better
|
||||
* database architecture and data integrity.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @since 2025-08-29
|
||||
*/
|
||||
|
||||
export interface ActiveIdentity {
|
||||
id: number;
|
||||
activeDid: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
@@ -9,8 +9,6 @@ export type Contact = {
|
||||
// When adding a property:
|
||||
// - Consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
|
||||
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
|
||||
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
|
||||
//
|
||||
|
||||
did: string;
|
||||
contactMethods?: Array<ContactMethod>;
|
||||
|
||||
@@ -14,12 +14,6 @@ export type BoundingBox = {
|
||||
* New entries that are boolean should also be added to PlatformServiceMixin._mapColumnsToValues
|
||||
*/
|
||||
export type Settings = {
|
||||
//
|
||||
// When adding a property:
|
||||
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
|
||||
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
|
||||
//
|
||||
|
||||
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
|
||||
id?: string | number; // this is erased for all those entries that are keyed with accountDid
|
||||
|
||||
@@ -35,7 +29,6 @@ export type Settings = {
|
||||
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
||||
|
||||
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
||||
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
|
||||
hideRegisterPromptOnNewContact?: boolean;
|
||||
isRegistered?: boolean;
|
||||
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
||||
@@ -43,7 +36,6 @@ export type Settings = {
|
||||
|
||||
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
||||
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
||||
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred plan changes that they've acknowledged seeing
|
||||
|
||||
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
||||
lastNotifiedClaimId?: string;
|
||||
@@ -68,18 +60,15 @@ export type Settings = {
|
||||
showContactGivesInline?: boolean; // Display contact inline or not
|
||||
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
|
||||
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
|
||||
|
||||
starredPlanHandleIds?: string[]; // Array of starred plan handle IDs
|
||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||
warnIfProdServer?: boolean; // Warn if using a production server
|
||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||
webPushServer?: string; // Web Push server URL
|
||||
};
|
||||
|
||||
// type of settings where the values are JSON strings instead of objects
|
||||
// type of settings where the searchBoxes are JSON strings instead of objects
|
||||
export type SettingsWithJsonStrings = Settings & {
|
||||
searchBoxes: string;
|
||||
starredPlanHandleIds: string;
|
||||
};
|
||||
|
||||
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
||||
@@ -96,11 +85,6 @@ export const SettingsSchema = {
|
||||
/**
|
||||
* Constants.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is deprecated.
|
||||
* It only remains for those with a PWA who have not migrated, but we'll soon remove it.
|
||||
*/
|
||||
export const MASTER_SETTINGS_KEY = "1";
|
||||
|
||||
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;
|
||||
|
||||
@@ -14,13 +14,6 @@ export interface AgreeActionClaim extends ClaimObject {
|
||||
object: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EmojiClaim extends ClaimObject {
|
||||
// default context is "https://endorser.ch"
|
||||
"@type": "Emoji";
|
||||
text: string;
|
||||
parentItem: { lastClaimId: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveActionClaim extends ClaimObject {
|
||||
@@ -79,15 +72,11 @@ export interface PlanActionClaim extends ClaimObject {
|
||||
name: string;
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
endTime?: string;
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
lastClaimId?: string;
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
|
||||
@@ -70,11 +70,18 @@ export interface AxiosErrorResponse {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
export interface CreateAndSubmitClaimResult {
|
||||
success: boolean;
|
||||
embeddedRecordError?: string;
|
||||
error?: string;
|
||||
claimId?: string;
|
||||
handleId?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
export * from "./claims";
|
||||
export * from "./claims-result";
|
||||
export * from "./common";
|
||||
export * from "./deepLinks";
|
||||
export type {
|
||||
// From common.ts
|
||||
CreateAndSubmitClaimResult,
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
KeyMeta,
|
||||
// Exclude types that are also exported from other files
|
||||
// GiveVerifiableCredential,
|
||||
// OfferVerifiableCredential,
|
||||
// RegisterVerifiableCredential,
|
||||
// PlanSummaryRecord,
|
||||
// UserInfo,
|
||||
} from "./common";
|
||||
|
||||
export type {
|
||||
// From claims.ts
|
||||
GiveActionClaim,
|
||||
OfferClaim,
|
||||
RegisterActionClaim,
|
||||
} from "./claims";
|
||||
|
||||
export type {
|
||||
// From records.ts
|
||||
PlanSummaryRecord,
|
||||
} from "./records";
|
||||
|
||||
export type {
|
||||
// From user.ts
|
||||
UserInfo,
|
||||
} from "./user";
|
||||
|
||||
export * from "./limits";
|
||||
export * from "./deepLinks";
|
||||
export * from "./common";
|
||||
export * from "./claims-result";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims";
|
||||
import { GenericCredWrapper } from "./common";
|
||||
|
||||
export interface EmojiSummaryRecord {
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
text: string;
|
||||
parentHandleId: string;
|
||||
}
|
||||
import { GiveActionClaim, OfferClaim } from "./claims";
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface GiveSummaryRecord {
|
||||
[x: string]:
|
||||
| PropertyKey
|
||||
| undefined
|
||||
| GiveActionClaim
|
||||
| Record<string, number>;
|
||||
[x: string]: PropertyKey | undefined | GiveActionClaim;
|
||||
type?: string;
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
emojiCount: Record<string, number>; // Map of emoji character to count
|
||||
fullClaim: GiveActionClaim;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
@@ -74,11 +61,6 @@ export interface PlanSummaryRecord {
|
||||
jwtId?: string;
|
||||
}
|
||||
|
||||
export interface PlanSummaryAndPreviousClaim {
|
||||
plan: PlanSummaryRecord;
|
||||
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data about a project
|
||||
*
|
||||
@@ -105,10 +87,7 @@ export interface PlanData {
|
||||
name: string;
|
||||
/**
|
||||
* The identifier of the project record -- different from jwtId
|
||||
*
|
||||
* This has been used to iterate through plan records, because jwtId ordering doesn't match
|
||||
* chronological create ordering, though it does match most recent edit order (in reverse order).
|
||||
* (It may be worthwhile to order by jwtId instead. It is an indexed field.)
|
||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||
**/
|
||||
rowId?: string;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,3 @@ export interface UserInfo {
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
export interface MemberData {
|
||||
did: string;
|
||||
name: string;
|
||||
isContact: boolean;
|
||||
member: {
|
||||
memberId: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* @module endorserServer
|
||||
*/
|
||||
|
||||
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { Axios, AxiosRequestConfig } from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import { sha256 } from "ethereum-cryptography/sha256";
|
||||
import { LRUCache } from "lru-cache";
|
||||
@@ -42,6 +42,9 @@ import {
|
||||
PlanActionClaim,
|
||||
RegisterActionClaim,
|
||||
TenureClaim,
|
||||
} from "../interfaces/claims";
|
||||
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
AxiosErrorResponse,
|
||||
@@ -52,12 +55,9 @@ import {
|
||||
QuantitativeValue,
|
||||
KeyMetaWithPrivate,
|
||||
KeyMetaMaybeWithPrivate,
|
||||
OfferSummaryRecord,
|
||||
OfferToPlanSummaryRecord,
|
||||
PlanSummaryAndPreviousClaim,
|
||||
PlanSummaryRecord,
|
||||
} from "../interfaces";
|
||||
import { logger, safeStringify } from "../utils/logger";
|
||||
} from "../interfaces/common";
|
||||
import { PlanSummaryRecord } from "../interfaces/records";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
@@ -315,7 +315,7 @@ export function didInfoForContact(
|
||||
return { displayName: "You", known: true };
|
||||
} else if (contact) {
|
||||
return {
|
||||
displayName: contact.name || "Contact Without a Name",
|
||||
displayName: contact.name || "Contact With No Name",
|
||||
known: true,
|
||||
profileImageUrl: contact.profileImageUrl,
|
||||
};
|
||||
@@ -362,22 +362,6 @@ export function didInfo(
|
||||
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* In some contexts (eg. agent), a blank really is nobody.
|
||||
*/
|
||||
export function didInfoOrNobody(
|
||||
did: string | undefined,
|
||||
activeDid: string | undefined,
|
||||
allMyDids: string[],
|
||||
contacts: Contact[],
|
||||
): string {
|
||||
if (did == null) {
|
||||
return "Nobody";
|
||||
} else {
|
||||
return didInfo(did, activeDid, allMyDids, contacts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return text description without any references to "you" as user
|
||||
*/
|
||||
@@ -625,7 +609,11 @@ async function performPlanRequest(
|
||||
|
||||
return cred;
|
||||
} else {
|
||||
logger.debug(
|
||||
// Use debug level for development to reduce console noise
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
const log = isDevelopment ? logger.debug : logger.log;
|
||||
|
||||
log(
|
||||
"[Plan Loading] ⚠️ Plan cache is empty for handle",
|
||||
handleId,
|
||||
" Got data:",
|
||||
@@ -697,7 +685,7 @@ export function serverMessageForUser(error: unknown): string | undefined {
|
||||
export function errorStringForLog(error: unknown) {
|
||||
let stringifiedError = "" + error;
|
||||
try {
|
||||
stringifiedError = safeStringify(error);
|
||||
stringifiedError = JSON.stringify(error);
|
||||
} catch (e) {
|
||||
// can happen with Dexie, eg:
|
||||
// TypeError: Converting circular structure to JSON
|
||||
@@ -709,7 +697,7 @@ export function errorStringForLog(error: unknown) {
|
||||
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorResponseText = safeStringify(err.response);
|
||||
const errorResponseText = JSON.stringify(err.response);
|
||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||
// add error.response stuff
|
||||
@@ -719,7 +707,7 @@ export function errorStringForLog(error: unknown) {
|
||||
R.equals(err.config, err.response.config)
|
||||
) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = safeStringify(
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], err.response),
|
||||
);
|
||||
fullError +=
|
||||
@@ -742,7 +730,7 @@ export async function getNewOffersToUser(
|
||||
activeDid: string,
|
||||
afterOfferJwtId?: string,
|
||||
beforeOfferJwtId?: string,
|
||||
): Promise<{ data: Array<OfferSummaryRecord>; hitLimit: boolean }> {
|
||||
) {
|
||||
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
|
||||
if (afterOfferJwtId) {
|
||||
url += "&afterId=" + afterOfferJwtId;
|
||||
@@ -764,7 +752,7 @@ export async function getNewOffersToUserProjects(
|
||||
activeDid: string,
|
||||
afterOfferJwtId?: string,
|
||||
beforeOfferJwtId?: string,
|
||||
): Promise<{ data: Array<OfferToPlanSummaryRecord>; hitLimit: boolean }> {
|
||||
) {
|
||||
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
|
||||
if (afterOfferJwtId) {
|
||||
url += "?afterId=" + afterOfferJwtId;
|
||||
@@ -778,46 +766,6 @@ export async function getNewOffersToUserProjects(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starred projects that have been updated since the last check
|
||||
*
|
||||
* @param axios - axios instance
|
||||
* @param apiServer - endorser API server URL
|
||||
* @param activeDid - user's DID for authentication
|
||||
* @param starredPlanHandleIds - array of starred project handle IDs
|
||||
* @param afterId - JWT ID to check for changes after (from lastAckedStarredPlanChangesJwtId)
|
||||
* @returns { data: Array<PlanSummaryAndPreviousClaim>, hitLimit: boolean }
|
||||
*/
|
||||
export async function getStarredProjectsWithChanges(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
activeDid: string,
|
||||
starredPlanHandleIds: string[],
|
||||
afterId?: string,
|
||||
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> {
|
||||
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
|
||||
return { data: [], hitLimit: false };
|
||||
}
|
||||
|
||||
if (!afterId) {
|
||||
// This doesn't make sense: there should always be some previous one they've seen.
|
||||
// We'll just return blank.
|
||||
return { data: [], hitLimit: false };
|
||||
}
|
||||
|
||||
// Use POST method for larger lists of project IDs
|
||||
const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`;
|
||||
const headers = await getHeaders(activeDid);
|
||||
|
||||
const requestBody = {
|
||||
planIds: starredPlanHandleIds,
|
||||
afterId: afterId,
|
||||
};
|
||||
|
||||
const response = await axios.post(url, requestBody, { headers });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct GiveAction VC for submission to server
|
||||
*
|
||||
@@ -1183,7 +1131,7 @@ export async function createAndSubmitClaim(
|
||||
// Enhanced diagnostic logging for claim submission
|
||||
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.debug("[Claim Submission] 🚀 Starting claim submission:", {
|
||||
logger.info("[Claim Submission] 🚀 Starting claim submission:", {
|
||||
requestId,
|
||||
apiServer,
|
||||
requesterDid: issuerDid,
|
||||
@@ -1209,7 +1157,7 @@ export async function createAndSubmitClaim(
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("[Claim Submission] ✅ Claim submitted successfully:", {
|
||||
logger.info("[Claim Submission] ✅ Claim submitted successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
handleId: response.data?.handleId,
|
||||
@@ -1217,12 +1165,7 @@ export async function createAndSubmitClaim(
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
claimId: response.data?.claimId,
|
||||
handleId: response.data?.handleId,
|
||||
embeddedRecordError: response.data?.embeddedRecordError,
|
||||
};
|
||||
return { success: true, handleId: response.data?.handleId };
|
||||
} catch (error: unknown) {
|
||||
// Enhanced error logging with comprehensive context
|
||||
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
@@ -1370,28 +1313,6 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats type string for display by adding spaces before capitals
|
||||
* and optionally adds an appropriate article prefix (a/an)
|
||||
*
|
||||
* @param text - Text to format
|
||||
* @returns Formatted string with article prefix
|
||||
*/
|
||||
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
|
||||
text: string,
|
||||
): string => {
|
||||
const word = capitalizeAndInsertSpacesBeforeCaps(text);
|
||||
if (word) {
|
||||
// if the word starts with a vowel, use "an" instead of "a"
|
||||
const firstLetter = word[0].toLowerCase();
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||
return particle + " " + word;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
return readable summary of claim, or something generic
|
||||
|
||||
@@ -1657,35 +1578,30 @@ export async function register(
|
||||
message?: string;
|
||||
}>(url, { jwtEncoded: vcJwt });
|
||||
|
||||
if (resp.data?.success?.embeddedRecordError) {
|
||||
if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else if (resp.data?.success?.embeddedRecordError) {
|
||||
let message =
|
||||
"There was some problem with the registration and so it may not be complete.";
|
||||
if (typeof resp.data.success.embeddedRecordError === "string") {
|
||||
message += " " + resp.data.success.embeddedRecordError;
|
||||
}
|
||||
return { error: message };
|
||||
} else if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else {
|
||||
logger.error("Registration non-thrown error:", JSON.stringify(resp.data));
|
||||
return {
|
||||
error:
|
||||
(resp.data?.error as { message?: string })?.message ||
|
||||
(resp.data?.error as string) ||
|
||||
"Got a server error when registering.",
|
||||
};
|
||||
logger.error("Registration error:", JSON.stringify(resp.data));
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object") {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorMessage =
|
||||
err.response?.data?.error?.message ||
|
||||
err.response?.data?.error ||
|
||||
err.message;
|
||||
logger.error(
|
||||
"Registration thrown error:",
|
||||
errorMessage || JSON.stringify(err),
|
||||
);
|
||||
err.message ||
|
||||
(err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"message" in err.response.data
|
||||
? (err.response.data as { message: string }).message
|
||||
: undefined);
|
||||
logger.error("Registration error:", errorMessage || JSON.stringify(err));
|
||||
return { error: errorMessage || "Got a server error when registering." };
|
||||
}
|
||||
return { error: "Got a server error when registering." };
|
||||
@@ -1759,19 +1675,49 @@ export async function fetchEndorserRateLimits(
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// not wrapped in a 'try' because the error returned is self-explanatory
|
||||
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
||||
try {
|
||||
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
||||
|
||||
// Log successful registration check
|
||||
logger.debug("[User Registration] User registration check successful:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
status: response.status,
|
||||
isRegistered: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// Log successful registration check
|
||||
logger.debug("[User Registration] User registration check successful:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
status: response.status,
|
||||
isRegistered: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response;
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Enhanced error logging with user registration context
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
const errorCode = axiosError.response?.data?.error?.code;
|
||||
const errorMessage = axiosError.response?.data?.error?.message;
|
||||
const httpStatus = axiosError.response?.status;
|
||||
|
||||
logger.warn("[User Registration] User not registered on server:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
errorCode: errorCode,
|
||||
errorMessage: errorMessage,
|
||||
httpStatus: httpStatus,
|
||||
needsRegistration: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Log the original error for debugging
|
||||
logger.error(
|
||||
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
|
||||
errorStringForLog(error),
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1786,7 +1732,7 @@ export async function fetchImageRateLimits(
|
||||
axios: Axios,
|
||||
issuerDid: string,
|
||||
imageServer?: string,
|
||||
): Promise<AxiosResponse | null> {
|
||||
) {
|
||||
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
const url = server + "/image-limits";
|
||||
const headers = await getHeaders(issuerDid);
|
||||
@@ -1820,17 +1766,15 @@ export async function fetchImageRateLimits(
|
||||
};
|
||||
};
|
||||
|
||||
logger.warn(
|
||||
"[Image Server] Image rate limits check failed, which is expected for users not registered on test server (eg. when only registered on local server).",
|
||||
{
|
||||
did: issuerDid,
|
||||
server: server,
|
||||
errorCode: axiosError.response?.data?.error?.code,
|
||||
errorMessage: axiosError.response?.data?.error?.message,
|
||||
httpStatus: axiosError.response?.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
logger.warn("[Image Server] Image rate limits check failed:", {
|
||||
did: issuerDid,
|
||||
server: server,
|
||||
errorCode: axiosError.response?.data?.error?.code,
|
||||
errorMessage: axiosError.response?.data?.error?.message,
|
||||
httpStatus: axiosError.response?.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleMinus,
|
||||
faCirclePlus,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
@@ -38,7 +37,6 @@ import {
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faCrown,
|
||||
faDollar,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
@@ -60,7 +58,6 @@ import {
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHourglassHalf,
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
@@ -89,7 +86,6 @@ import {
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faStar,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
@@ -98,9 +94,6 @@ import {
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
// Initialize Font Awesome library with all required icons
|
||||
library.add(
|
||||
faArrowDown,
|
||||
@@ -126,7 +119,6 @@ library.add(
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleMinus,
|
||||
faCirclePlus,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
@@ -135,7 +127,6 @@ library.add(
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faCrown,
|
||||
faDollar,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
@@ -157,7 +148,6 @@ library.add(
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHourglassHalf,
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
@@ -178,16 +168,14 @@ library.add(
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRightFromBracket,
|
||||
faRotate,
|
||||
faRightFromBracket,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faStar,
|
||||
faStarRegular,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
|
||||
182
src/libs/util.ts
182
src/libs/util.ts
@@ -3,7 +3,7 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import * as R from "ramda";
|
||||
import { copyToClipboard } from "../services/ClipboardService";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
@@ -160,49 +160,6 @@ export const isGiveAction = (
|
||||
return isGiveClaimType(veriClaim.claimType);
|
||||
};
|
||||
|
||||
export interface OfferFulfillment {
|
||||
offerHandleId: string;
|
||||
offerType: string;
|
||||
}
|
||||
|
||||
interface FulfillmentItem {
|
||||
"@type": string;
|
||||
identifier?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract offer fulfillment information from the fulfills field
|
||||
* Handles both array and single object cases
|
||||
*/
|
||||
export const extractOfferFulfillment = (
|
||||
fulfills: FulfillmentItem | FulfillmentItem[] | null | undefined,
|
||||
): OfferFulfillment | null => {
|
||||
if (!fulfills) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle both array and single object cases
|
||||
let offerFulfill = null;
|
||||
|
||||
if (Array.isArray(fulfills)) {
|
||||
// Find the Offer in the fulfills array
|
||||
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
|
||||
} else if (fulfills["@type"] === "Offer") {
|
||||
// fulfills is a single Offer object
|
||||
offerFulfill = fulfills;
|
||||
}
|
||||
|
||||
if (offerFulfill) {
|
||||
return {
|
||||
offerHandleId: offerFulfill.identifier || "",
|
||||
offerType: offerFulfill["@type"],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const shortDid = (did: string) => {
|
||||
if (did.startsWith("did:peer:")) {
|
||||
return (
|
||||
@@ -240,19 +197,11 @@ export const nameForContact = (
|
||||
);
|
||||
};
|
||||
|
||||
export const doCopyTwoSecRedo = async (
|
||||
text: string,
|
||||
fn: () => void,
|
||||
): Promise<void> => {
|
||||
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||
fn();
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
setTimeout(fn, 2000);
|
||||
} catch (error) {
|
||||
// Note: This utility function doesn't have access to notification system
|
||||
// The calling component should handle error notifications
|
||||
// Error is silently caught to avoid breaking the 2-second redo pattern
|
||||
}
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
.then(() => setTimeout(fn, 2000));
|
||||
};
|
||||
|
||||
export interface ConfirmerData {
|
||||
@@ -720,8 +669,7 @@ export async function saveNewIdentity(
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
|
||||
// Update active identity in the active_identity table instead of settings
|
||||
await platformService.updateActiveDid(identity.did);
|
||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||
|
||||
await platformService.insertNewDidIntoSettings(identity.did);
|
||||
}
|
||||
@@ -774,8 +722,7 @@ export const registerSaveAndActivatePasskey = async (
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
const platformService = await getPlatformService();
|
||||
// Update active identity in the active_identity table instead of settings
|
||||
await platformService.updateActiveDid(account.did);
|
||||
await platformService.updateDefaultSettings({ activeDid: account.did });
|
||||
await platformService.updateDidSpecificSettings(account.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
@@ -988,6 +935,11 @@ export async function importFromMnemonic(
|
||||
): Promise<void> {
|
||||
const mne: string = mnemonic.trim().toLowerCase();
|
||||
|
||||
// Check if this is Test User #0
|
||||
const TEST_USER_0_MNEMONIC =
|
||||
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
|
||||
const isTestUser0 = mne === TEST_USER_0_MNEMONIC;
|
||||
|
||||
// Derive address and keys from mnemonic
|
||||
const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath);
|
||||
|
||||
@@ -1002,6 +954,90 @@ export async function importFromMnemonic(
|
||||
|
||||
// Save the new identity
|
||||
await saveNewIdentity(newId, mne, derivationPath);
|
||||
|
||||
// Set up Test User #0 specific settings
|
||||
if (isTestUser0) {
|
||||
// Set up Test User #0 specific settings with enhanced error handling
|
||||
const platformService = await getPlatformService();
|
||||
|
||||
try {
|
||||
// First, ensure the DID-specific settings record exists
|
||||
await platformService.insertNewDidIntoSettings(newId.did);
|
||||
|
||||
// Then update with Test User #0 specific settings
|
||||
await platformService.updateDidSpecificSettings(newId.did, {
|
||||
firstName: "User Zero",
|
||||
isRegistered: true,
|
||||
});
|
||||
|
||||
// Verify the settings were saved correctly
|
||||
const verificationResult = await platformService.dbQuery(
|
||||
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
|
||||
[newId.did],
|
||||
);
|
||||
|
||||
if (verificationResult?.values?.length) {
|
||||
const settings = verificationResult.values[0];
|
||||
const firstName = settings[0];
|
||||
const isRegistered = settings[1];
|
||||
|
||||
logger.debug(
|
||||
"[importFromMnemonic] Test User #0 settings verification",
|
||||
{
|
||||
did: newId.did,
|
||||
firstName,
|
||||
isRegistered,
|
||||
expectedFirstName: "User Zero",
|
||||
expectedIsRegistered: true,
|
||||
},
|
||||
);
|
||||
|
||||
// If settings weren't saved correctly, try individual updates
|
||||
if (firstName !== "User Zero" || isRegistered !== 1) {
|
||||
logger.warn(
|
||||
"[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates",
|
||||
);
|
||||
|
||||
await platformService.dbExec(
|
||||
"UPDATE settings SET firstName = ? WHERE accountDid = ?",
|
||||
["User Zero", newId.did],
|
||||
);
|
||||
|
||||
await platformService.dbExec(
|
||||
"UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
|
||||
[1, newId.did],
|
||||
);
|
||||
|
||||
// Verify again
|
||||
const retryResult = await platformService.dbQuery(
|
||||
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
|
||||
[newId.did],
|
||||
);
|
||||
|
||||
if (retryResult?.values?.length) {
|
||||
const retrySettings = retryResult.values[0];
|
||||
logger.debug(
|
||||
"[importFromMnemonic] Test User #0 settings after retry",
|
||||
{
|
||||
firstName: retrySettings[0],
|
||||
isRegistered: retrySettings[1],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
"[importFromMnemonic] Failed to verify Test User #0 settings - no record found",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[importFromMnemonic] Error setting up Test User #0 settings:",
|
||||
error,
|
||||
);
|
||||
// Don't throw - allow the import to continue even if settings fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1058,29 +1094,3 @@ export async function checkForDuplicateAccount(
|
||||
|
||||
return (existingAccount?.values?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
export class PromiseTracker<T> {
|
||||
private _promise: Promise<T>;
|
||||
private _resolved = false;
|
||||
private _value: T | undefined;
|
||||
|
||||
constructor(promise: Promise<T>) {
|
||||
this._promise = promise.then((value) => {
|
||||
this._resolved = true;
|
||||
this._value = value;
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
get isResolved(): boolean {
|
||||
return this._resolved;
|
||||
}
|
||||
|
||||
get value(): T | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
get promise(): Promise<T> {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,18 +69,18 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
||||
*/
|
||||
const handleDeepLink = async (data: { url: string }) => {
|
||||
const { url } = data;
|
||||
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
||||
logger.info(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
||||
|
||||
try {
|
||||
// Wait for router to be ready
|
||||
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
|
||||
logger.info(`[Main] ⏳ Waiting for router to be ready...`);
|
||||
await router.isReady();
|
||||
logger.debug(`[Main] ✅ Router is ready, processing deeplink`);
|
||||
logger.info(`[Main] ✅ Router is ready, processing deeplink`);
|
||||
|
||||
// Process the deeplink
|
||||
logger.debug(`[Main] 🚀 Starting deeplink processing`);
|
||||
logger.info(`[Main] 🚀 Starting deeplink processing`);
|
||||
await deepLinkHandler.handleDeepLink(url);
|
||||
logger.debug(`[Main] ✅ Deeplink processed successfully`);
|
||||
logger.info(`[Main] ✅ Deeplink processed successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`[Main] ❌ Deeplink processing failed:`, {
|
||||
url,
|
||||
@@ -115,25 +115,25 @@ const registerDeepLinkListener = async () => {
|
||||
);
|
||||
|
||||
// Check if Capacitor App plugin is available
|
||||
logger.debug(`[Main] 🔍 Checking Capacitor App plugin availability...`);
|
||||
logger.info(`[Main] 🔍 Checking Capacitor App plugin availability...`);
|
||||
if (!CapacitorApp) {
|
||||
throw new Error("Capacitor App plugin not available");
|
||||
}
|
||||
logger.info(`[Main] ✅ Capacitor App plugin is available`);
|
||||
|
||||
// Check available methods on CapacitorApp
|
||||
logger.debug(
|
||||
logger.info(
|
||||
`[Main] 🔍 Capacitor App plugin methods:`,
|
||||
Object.getOwnPropertyNames(CapacitorApp),
|
||||
);
|
||||
logger.debug(
|
||||
logger.info(
|
||||
`[Main] 🔍 Capacitor App plugin addListener method:`,
|
||||
typeof CapacitorApp.addListener,
|
||||
);
|
||||
|
||||
// Wait for router to be ready first
|
||||
await router.isReady();
|
||||
logger.debug(
|
||||
logger.info(
|
||||
`[Main] ✅ Router is ready, proceeding with listener registration`,
|
||||
);
|
||||
|
||||
@@ -148,6 +148,9 @@ const registerDeepLinkListener = async () => {
|
||||
listenerHandle,
|
||||
);
|
||||
|
||||
// Test the listener registration by checking if it's actually registered
|
||||
logger.info(`[Main] 🧪 Verifying listener registration...`);
|
||||
|
||||
return listenerHandle;
|
||||
} catch (error) {
|
||||
logger.error(`[Main] ❌ Failed to register deeplink listener:`, {
|
||||
|
||||
@@ -24,12 +24,12 @@ logger.info("[Main] 🌍 Boot-time environment configuration:", {
|
||||
|
||||
// Dynamically import the appropriate main entry point
|
||||
if (platform === "capacitor") {
|
||||
logger.debug(`[Main] 📱 Loading Capacitor-specific entry point`);
|
||||
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);
|
||||
import("./main.capacitor");
|
||||
} else if (platform === "electron") {
|
||||
logger.debug(`[Main] 💻 Loading Electron-specific entry point`);
|
||||
logger.info(`[Main] 💻 Loading Electron-specific entry point`);
|
||||
import("./main.electron");
|
||||
} else {
|
||||
logger.debug(`[Main] 🌐 Loading Web-specific entry point`);
|
||||
logger.info(`[Main] 🌐 Loading Web-specific entry point`);
|
||||
import("./main.web");
|
||||
}
|
||||
|
||||
@@ -285,16 +285,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "user-profile",
|
||||
component: () => import("../views/UserProfileView.vue"),
|
||||
},
|
||||
// Catch-all route for 404 errors - must be last
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "not-found",
|
||||
component: () => import("../views/NotFoundView.vue"),
|
||||
meta: {
|
||||
title: "Page Not Found",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const isElectron = window.location.protocol === "file:";
|
||||
@@ -397,7 +387,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`[Router] ✅ Navigation guard passed for: ${to.path}`);
|
||||
logger.info(`[Router] ✅ Navigation guard passed for: ${to.path}`);
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error("[Router] ❌ Identity creation failed in navigation guard:", {
|
||||
|
||||
@@ -155,16 +155,6 @@ export interface PlatformService {
|
||||
*/
|
||||
dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||
|
||||
/**
|
||||
* Not recommended except for debugging.
|
||||
* Return the raw result of a SQL query.
|
||||
*
|
||||
* @param sql - The SQL query to execute
|
||||
* @param params - The parameters to pass to the query
|
||||
* @returns Promise resolving to the raw query result, or undefined if no results
|
||||
*/
|
||||
dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
|
||||
|
||||
// Database utility methods
|
||||
/**
|
||||
* Generates an INSERT SQL statement for a given model and table.
|
||||
@@ -183,7 +173,6 @@ export interface PlatformService {
|
||||
* @returns Promise that resolves when the update is complete
|
||||
*/
|
||||
updateDefaultSettings(settings: Record<string, unknown>): Promise<void>;
|
||||
updateActiveDid(did: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Inserts a new DID into the settings table.
|
||||
|
||||
@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
// Generate a short random ID for this scanner instance
|
||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
this.options = options ?? {};
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||
{
|
||||
...this.options,
|
||||
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
||||
this.video = document.createElement("video");
|
||||
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.cameraStateListeners.forEach((listener) => {
|
||||
try {
|
||||
listener.onStateChange(state, message);
|
||||
logger.debug(
|
||||
logger.info(
|
||||
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
||||
{
|
||||
state,
|
||||
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera" as PermissionName,
|
||||
});
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||
permissions.state,
|
||||
);
|
||||
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
"initializing",
|
||||
"Requesting camera permissions...",
|
||||
);
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||
);
|
||||
|
||||
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
(device) => device.kind === "videoinput",
|
||||
);
|
||||
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||
count: videoDevices.length,
|
||||
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
||||
userAgent: navigator.userAgent,
|
||||
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
// Try to get a stream with specific constraints
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
||||
{
|
||||
facingMode: "environment",
|
||||
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
async isSupported(): Promise<boolean> {
|
||||
try {
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
||||
);
|
||||
// Check for secure context first
|
||||
if (!window.isSecureContext) {
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
||||
);
|
||||
return false;
|
||||
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
(device) => device.kind === "videoinput",
|
||||
);
|
||||
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||
hasSecureContext: window.isSecureContext,
|
||||
hasMediaDevices: !!navigator.mediaDevices,
|
||||
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
||||
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Log scan attempt every 100 frames or 1 second
|
||||
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||
attempt: this.scanAttempts,
|
||||
dimensions: {
|
||||
width: this.canvas.width,
|
||||
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
!code.data ||
|
||||
code.data.length === 0;
|
||||
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||
data: code.data,
|
||||
location: code.location,
|
||||
attempts: this.scanAttempts,
|
||||
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.scanAttempts = 0;
|
||||
this.lastScanTime = Date.now();
|
||||
this.updateCameraState("initializing", "Starting camera...");
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||
this.options,
|
||||
);
|
||||
|
||||
// Get camera stream with options
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||
);
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
this.updateCameraState("active", "Camera is active");
|
||||
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||
tracks: this.stream.getTracks().map((t) => ({
|
||||
kind: t.kind,
|
||||
label: t.label,
|
||||
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.video.style.display = "none";
|
||||
}
|
||||
await this.video.play();
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||
);
|
||||
}
|
||||
|
||||
// Emit stream to component
|
||||
this.events.emit("stream", this.stream);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||
|
||||
// Start QR code scanning
|
||||
this.scanQRCode();
|
||||
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||
scanAttempts: this.scanAttempts,
|
||||
duration: Date.now() - this.lastScanTime,
|
||||
});
|
||||
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
||||
);
|
||||
}
|
||||
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
if (this.video) {
|
||||
this.video.pause();
|
||||
this.video.srcObject = null;
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||
}
|
||||
|
||||
// Stop all tracks in the stream
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach((track) => {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Emit stream stopped event
|
||||
this.events.emit("stream", null);
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
throw error;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
addListener(listener: ScanListener): void {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
||||
);
|
||||
this.events.on("stream", callback);
|
||||
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
try {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||
await this.stopScan();
|
||||
this.events.removeAllListeners();
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||
|
||||
// Clean up DOM elements
|
||||
if (this.video) {
|
||||
this.video.remove();
|
||||
this.video = null;
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||
}
|
||||
if (this.canvas) {
|
||||
this.canvas.remove();
|
||||
this.canvas = null;
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||
}
|
||||
this.context = null;
|
||||
logger.debug(
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { logger, safeStringify } from "../utils/logger";
|
||||
* @remarks
|
||||
* Special handling includes:
|
||||
* - Enhanced logging for Capacitor platform
|
||||
* - Rate limit detection and handling
|
||||
* - Detailed error information logging including:
|
||||
* - Error message
|
||||
* - HTTP status
|
||||
@@ -49,5 +50,11 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Specific handling for rate limits
|
||||
if (error.response?.status === 400) {
|
||||
logger.warn(`[Rate Limit] ${endpoint}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
@@ -73,8 +73,6 @@ interface Migration {
|
||||
name: string;
|
||||
/** SQL statement(s) to execute for this migration */
|
||||
sql: string;
|
||||
/** Optional array of individual SQL statements for better error handling */
|
||||
statements?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,104 +225,6 @@ export function registerMigration(migration: Migration): void {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
/**
|
||||
* Helper function to check if a SQLite result indicates a table exists
|
||||
* @param result - The result from a sqlite_master query
|
||||
* @returns true if the table exists
|
||||
*/
|
||||
function checkSqliteTableResult(result: unknown): boolean {
|
||||
return (
|
||||
(result as unknown as { values: unknown[][] })?.values?.length > 0 ||
|
||||
(Array.isArray(result) && result.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate that a table exists in the database
|
||||
* @param tableName - Name of the table to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to true if table exists
|
||||
*/
|
||||
async function validateTableExists<T>(
|
||||
tableName: string,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
|
||||
);
|
||||
return checkSqliteTableResult(result);
|
||||
} catch (error) {
|
||||
logger.error(`❌ [Validation] Error checking table ${tableName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate that a column exists in a table
|
||||
* @param tableName - Name of the table
|
||||
* @param columnName - Name of the column to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to true if column exists
|
||||
*/
|
||||
async function validateColumnExists<T>(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await sqlQuery(`SELECT ${columnName} FROM ${tableName} LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`❌ [Validation] Error checking column ${columnName} in ${tableName}:`,
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate multiple tables exist
|
||||
* @param tableNames - Array of table names to check
|
||||
* @param sqlQuery - Function to execute SQL queries
|
||||
* @returns Promise resolving to array of validation results
|
||||
*/
|
||||
async function validateMultipleTables<T>(
|
||||
tableNames: string[],
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<{ exists: boolean; missing: string[] }> {
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const exists = await validateTableExists(tableName, sqlQuery);
|
||||
if (!exists) {
|
||||
missing.push(tableName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exists: missing.length === 0,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to add validation error with consistent logging
|
||||
* @param validation - The validation object to update
|
||||
* @param message - Error message to add
|
||||
* @param error - The error object for logging
|
||||
*/
|
||||
function addValidationError(
|
||||
validation: MigrationValidation,
|
||||
message: string,
|
||||
error: unknown,
|
||||
): void {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(message);
|
||||
logger.error(`❌ [Migration-Validation] ${message}:`, error);
|
||||
}
|
||||
|
||||
async function validateMigrationApplication<T>(
|
||||
migration: Migration,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
@@ -348,82 +248,36 @@ async function validateMigrationApplication<T>(
|
||||
"temp",
|
||||
];
|
||||
|
||||
const tableValidation = await validateMultipleTables(tables, sqlQuery);
|
||||
if (!tableValidation.exists) {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(
|
||||
`Missing tables: ${tableValidation.missing.join(", ")}`,
|
||||
);
|
||||
logger.error(
|
||||
`❌ [Migration-Validation] Missing tables:`,
|
||||
tableValidation.missing,
|
||||
);
|
||||
}
|
||||
validation.tableExists = tableValidation.exists;
|
||||
} else if (migration.name === "002_add_iViewContent_to_contacts") {
|
||||
// Validate iViewContent column exists in contacts table
|
||||
const columnExists = await validateColumnExists(
|
||||
"contacts",
|
||||
"iViewContent",
|
||||
sqlQuery,
|
||||
);
|
||||
if (!columnExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Column iViewContent missing from contacts table",
|
||||
new Error("Column not found"),
|
||||
);
|
||||
} else {
|
||||
validation.hasExpectedColumns = true;
|
||||
}
|
||||
} else if (migration.name === "004_active_identity_management") {
|
||||
// Validate active_identity table exists and has correct structure
|
||||
const activeIdentityExists = await validateTableExists(
|
||||
"active_identity",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!activeIdentityExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Table active_identity missing",
|
||||
new Error("Table not found"),
|
||||
);
|
||||
} else {
|
||||
validation.tableExists = true;
|
||||
|
||||
// Check that active_identity has the expected structure
|
||||
const hasExpectedColumns = await validateColumnExists(
|
||||
"active_identity",
|
||||
"id, activeDid, lastUpdated",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!hasExpectedColumns) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"active_identity table missing expected columns",
|
||||
new Error("Columns not found"),
|
||||
for (const tableName of tables) {
|
||||
try {
|
||||
await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
|
||||
);
|
||||
// Reduced logging - only log on error
|
||||
} catch (error) {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(`Table ${tableName} missing`);
|
||||
logger.error(
|
||||
`❌ [Migration-Validation] Table ${tableName} missing:`,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
validation.hasExpectedColumns = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that hasBackedUpSeed column exists in settings table
|
||||
// Note: This validation is included here because migration 004 is consolidated
|
||||
// and includes the functionality from the original migration 003
|
||||
const hasBackedUpSeedExists = await validateColumnExists(
|
||||
"settings",
|
||||
"hasBackedUpSeed",
|
||||
sqlQuery,
|
||||
);
|
||||
|
||||
if (!hasBackedUpSeedExists) {
|
||||
addValidationError(
|
||||
validation,
|
||||
"Column hasBackedUpSeed missing from settings table",
|
||||
new Error("Column not found"),
|
||||
validation.tableExists = validation.errors.length === 0;
|
||||
} else if (migration.name === "002_add_iViewContent_to_contacts") {
|
||||
// Validate iViewContent column exists in contacts table
|
||||
try {
|
||||
await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`);
|
||||
validation.hasExpectedColumns = true;
|
||||
// Reduced logging - only log on error
|
||||
} catch (error) {
|
||||
validation.isValid = false;
|
||||
validation.errors.push(
|
||||
`Column iViewContent missing from contacts table`,
|
||||
);
|
||||
logger.error(
|
||||
`❌ [Migration-Validation] Column iViewContent missing:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -489,55 +343,6 @@ async function isSchemaAlreadyPresent<T>(
|
||||
// Reduced logging - only log on error
|
||||
return false;
|
||||
}
|
||||
} else if (migration.name === "003_add_hasBackedUpSeed_to_settings") {
|
||||
// Check if hasBackedUpSeed column exists in settings table
|
||||
try {
|
||||
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} else if (migration.name === "004_active_identity_management") {
|
||||
// Check if active_identity table exists and has correct structure
|
||||
try {
|
||||
// Check that active_identity table exists
|
||||
const activeIdentityResult = await sqlQuery(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`,
|
||||
);
|
||||
const hasActiveIdentityTable =
|
||||
(activeIdentityResult as unknown as { values: unknown[][] })?.values
|
||||
?.length > 0 ||
|
||||
(Array.isArray(activeIdentityResult) &&
|
||||
activeIdentityResult.length > 0);
|
||||
|
||||
if (!hasActiveIdentityTable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that active_identity has the expected structure
|
||||
try {
|
||||
await sqlQuery(
|
||||
`SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`,
|
||||
);
|
||||
|
||||
// Also check that hasBackedUpSeed column exists in settings
|
||||
// This is included because migration 004 is consolidated
|
||||
try {
|
||||
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add schema checks for future migrations here
|
||||
@@ -599,10 +404,15 @@ export async function runMigrations<T>(
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug("📋 [Migration] Starting migration process...");
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
// Use debug level for routine migration messages in development
|
||||
const migrationLog = isDevelopment ? logger.debug : logger.log;
|
||||
|
||||
try {
|
||||
migrationLog("📋 [Migration] Starting migration process...");
|
||||
|
||||
// Step 1: Create migrations table if it doesn't exist
|
||||
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
|
||||
await sqlExec(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
@@ -626,8 +436,7 @@ export async function runMigrations<T>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Only log migration counts in development
|
||||
logger.debug(
|
||||
migrationLog(
|
||||
`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
|
||||
);
|
||||
|
||||
@@ -639,22 +448,22 @@ export async function runMigrations<T>(
|
||||
// Check 1: Is it recorded as applied in migrations table?
|
||||
const isRecordedAsApplied = appliedMigrations.has(migration.name);
|
||||
|
||||
// Skip if already recorded as applied (name-only check)
|
||||
// Check 2: Does the schema already exist in the database?
|
||||
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
|
||||
|
||||
// Skip if already recorded as applied
|
||||
if (isRecordedAsApplied) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 2: Does the schema already exist in the database?
|
||||
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
|
||||
|
||||
// Handle case where schema exists but isn't recorded
|
||||
if (isSchemaPresent) {
|
||||
try {
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.debug(
|
||||
migrationLog(
|
||||
`✅ [Migration] Marked existing schema as applied: ${migration.name}`,
|
||||
);
|
||||
skippedCount++;
|
||||
@@ -669,20 +478,11 @@ export async function runMigrations<T>(
|
||||
}
|
||||
|
||||
// Apply the migration
|
||||
logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);
|
||||
migrationLog(`🔄 [Migration] Applying migration: ${migration.name}`);
|
||||
|
||||
try {
|
||||
// Execute the migration SQL as single atomic operation
|
||||
logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
|
||||
logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);
|
||||
|
||||
// Execute the migration SQL directly - it should be atomic
|
||||
// The SQL itself should handle any necessary transactions
|
||||
const execResult = await sqlExec(migration.sql);
|
||||
|
||||
logger.debug(
|
||||
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
|
||||
);
|
||||
// Execute the migration SQL
|
||||
await sqlExec(migration.sql);
|
||||
|
||||
// Validate the migration was applied correctly
|
||||
const validation = await validateMigrationApplication(
|
||||
@@ -701,33 +501,11 @@ export async function runMigrations<T>(
|
||||
migration.name,
|
||||
]);
|
||||
|
||||
logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
|
||||
migrationLog(`🎉 [Migration] Successfully applied: ${migration.name}`);
|
||||
appliedCount++;
|
||||
} catch (error) {
|
||||
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
|
||||
|
||||
// Provide explicit rollback instructions for migration failures
|
||||
logger.error(
|
||||
`🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
|
||||
);
|
||||
logger.error(` 1. Stop the application immediately`);
|
||||
logger.error(
|
||||
` 2. Restore database from pre-migration backup/snapshot`,
|
||||
);
|
||||
logger.error(
|
||||
` 3. Remove migration entry: DELETE FROM migrations WHERE name = '${migration.name}'`,
|
||||
);
|
||||
logger.error(
|
||||
` 4. Verify database state matches pre-migration condition`,
|
||||
);
|
||||
logger.error(` 5. Restart application and investigate root cause`);
|
||||
logger.error(
|
||||
` FAILURE CAUSE: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.error(
|
||||
` REQUIRED OPERATOR ACTION: Manual database restoration required`,
|
||||
);
|
||||
|
||||
// Handle specific cases where the migration might be partially applied
|
||||
const errorMessage = String(error).toLowerCase();
|
||||
|
||||
@@ -739,7 +517,7 @@ export async function runMigrations<T>(
|
||||
(errorMessage.includes("table") &&
|
||||
errorMessage.includes("already exists"))
|
||||
) {
|
||||
logger.debug(
|
||||
migrationLog(
|
||||
`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`,
|
||||
);
|
||||
|
||||
@@ -753,8 +531,6 @@ export async function runMigrations<T>(
|
||||
`⚠️ [Migration] Schema validation failed for ${migration.name}:`,
|
||||
validation.errors,
|
||||
);
|
||||
// Don't mark as applied if validation fails
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark the migration as applied since the schema change already exists
|
||||
@@ -762,7 +538,7 @@ export async function runMigrations<T>(
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.debug(`✅ [Migration] Marked as applied: ${migration.name}`);
|
||||
migrationLog(`✅ [Migration] Marked as applied: ${migration.name}`);
|
||||
appliedCount++;
|
||||
} catch (insertError) {
|
||||
// If we can't insert the migration record, log it but don't fail
|
||||
@@ -782,7 +558,7 @@ export async function runMigrations<T>(
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Final validation - verify all migrations are properly recorded
|
||||
// Step 5: Final validation - verify all migrations are properly recorded
|
||||
const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations");
|
||||
const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult);
|
||||
|
||||
@@ -798,7 +574,7 @@ export async function runMigrations<T>(
|
||||
);
|
||||
}
|
||||
|
||||
// Only show completion message in development
|
||||
// Always show completion message
|
||||
logger.log(
|
||||
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
|
||||
);
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Base Database Service for Platform Services
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This abstract base class provides common database operations that are
|
||||
* identical across all platform implementations. It eliminates code
|
||||
* duplication and ensures consistency in database operations.
|
||||
*
|
||||
* Key Features:
|
||||
* - Common database utility methods
|
||||
* - Consistent settings management
|
||||
* - Active identity management
|
||||
* - Abstract methods for platform-specific database operations
|
||||
*
|
||||
* Architecture:
|
||||
* - Abstract base class with common implementations
|
||||
* - Platform services extend this class
|
||||
* - Platform-specific database operations remain abstract
|
||||
*
|
||||
* @since 1.1.1-beta
|
||||
*/
|
||||
|
||||
import { logger } from "../../utils/logger";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
|
||||
/**
|
||||
* Abstract base class for platform-specific database services.
|
||||
*
|
||||
* This class provides common database operations that are identical
|
||||
* across all platform implementations (Web, Capacitor, Electron).
|
||||
* Platform-specific services extend this class and implement the
|
||||
* abstract database operation methods.
|
||||
*
|
||||
* Common Operations:
|
||||
* - Settings management (update, retrieve, insert)
|
||||
* - Active identity management
|
||||
* - Database utility methods
|
||||
*
|
||||
* @abstract
|
||||
* @example
|
||||
* ```typescript
|
||||
* export class WebPlatformService extends BaseDatabaseService {
|
||||
* async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
||||
* // Web-specific implementation
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class BaseDatabaseService {
|
||||
/**
|
||||
* Generate an INSERT statement for a model object.
|
||||
*
|
||||
* Creates a parameterized INSERT statement with placeholders for
|
||||
* all properties in the model object. This ensures safe SQL
|
||||
* execution and prevents SQL injection.
|
||||
*
|
||||
* @param model - Object containing the data to insert
|
||||
* @param tableName - Name of the target table
|
||||
* @returns Object containing the SQL statement and parameters
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { sql, params } = this.generateInsertStatement(
|
||||
* { name: 'John', age: 30 },
|
||||
* 'users'
|
||||
* );
|
||||
* // sql: "INSERT INTO users (name, age) VALUES (?, ?)"
|
||||
* // params: ['John', 30]
|
||||
* ```
|
||||
*/
|
||||
generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const keys = Object.keys(model);
|
||||
const placeholders = keys.map(() => "?").join(", ");
|
||||
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
|
||||
const params = keys.map((key) => model[key]);
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update default settings for the currently active account.
|
||||
*
|
||||
* Retrieves the active DID from the active_identity table and updates
|
||||
* the corresponding settings record. This ensures settings are always
|
||||
* updated for the correct account.
|
||||
*
|
||||
* @param settings - Object containing the settings to update
|
||||
* @returns Promise that resolves when settings are updated
|
||||
*
|
||||
* @throws {Error} If no active DID is found or database operation fails
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await this.updateDefaultSettings({
|
||||
* theme: 'dark',
|
||||
* notifications: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async updateDefaultSettings(
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
// Get current active DID and update that identity's settings
|
||||
const activeIdentity = await this.getActiveIdentity();
|
||||
const activeDid = activeIdentity.activeDid;
|
||||
|
||||
if (!activeDid) {
|
||||
logger.warn(
|
||||
"[BaseDatabaseService] No active DID found, cannot update default settings",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
|
||||
const params = [...keys.map((key) => settings[key]), activeDid];
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the active DID in the active_identity table.
|
||||
*
|
||||
* Sets the active DID and updates the lastUpdated timestamp.
|
||||
* This is used when switching between different accounts/identities.
|
||||
*
|
||||
* @param did - The DID to set as active
|
||||
* @returns Promise that resolves when the update is complete
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await this.updateActiveDid('did:example:123');
|
||||
* ```
|
||||
*/
|
||||
async updateActiveDid(did: string): Promise<void> {
|
||||
await this.dbExec(
|
||||
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
|
||||
[did],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active DID from the active_identity table.
|
||||
*
|
||||
* Retrieves the active DID that represents the currently selected
|
||||
* account/identity. This is used throughout the application to
|
||||
* ensure operations are performed on the correct account.
|
||||
*
|
||||
* @returns Promise resolving to object containing the active DID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { activeDid } = await this.getActiveIdentity();
|
||||
* console.log('Current active DID:', activeDid);
|
||||
* ```
|
||||
*/
|
||||
async getActiveIdentity(): Promise<{ activeDid: string }> {
|
||||
const result = (await this.dbQuery(
|
||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||
)) as QueryExecResult;
|
||||
return {
|
||||
activeDid: (result?.values?.[0]?.[0] as string) || "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new DID into the settings table with default values.
|
||||
*
|
||||
* Creates a new settings record for a DID with default configuration
|
||||
* values. Uses INSERT OR REPLACE to handle cases where settings
|
||||
* already exist for the DID.
|
||||
*
|
||||
* @param did - The DID to create settings for
|
||||
* @returns Promise that resolves when settings are created
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await this.insertNewDidIntoSettings('did:example:123');
|
||||
* ```
|
||||
*/
|
||||
async insertNewDidIntoSettings(did: string): Promise<void> {
|
||||
// Import constants dynamically to avoid circular dependencies
|
||||
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
|
||||
await import("@/constants/app");
|
||||
|
||||
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
|
||||
// This prevents duplicate accountDid entries and ensures data integrity
|
||||
await this.dbExec(
|
||||
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
|
||||
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings for a specific DID.
|
||||
*
|
||||
* Updates settings for a particular DID rather than the active one.
|
||||
* This is useful for bulk operations or when managing multiple accounts.
|
||||
*
|
||||
* @param did - The DID to update settings for
|
||||
* @param settings - Object containing the settings to update
|
||||
* @returns Promise that resolves when settings are updated
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await this.updateDidSpecificSettings('did:example:123', {
|
||||
* theme: 'light',
|
||||
* notifications: false
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async updateDidSpecificSettings(
|
||||
did: string,
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
|
||||
const params = [...keys.map((key) => settings[key]), did];
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve settings for the currently active account.
|
||||
*
|
||||
* Gets the active DID and retrieves all settings for that account.
|
||||
* Excludes the 'id' column from the returned settings object.
|
||||
*
|
||||
* @returns Promise resolving to settings object or null if no active DID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const settings = await this.retrieveSettingsForActiveAccount();
|
||||
* if (settings) {
|
||||
* console.log('Theme:', settings.theme);
|
||||
* console.log('Notifications:', settings.notifications);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async retrieveSettingsForActiveAccount(): Promise<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null> {
|
||||
// Get current active DID from active_identity table
|
||||
const activeIdentity = await this.getActiveIdentity();
|
||||
const activeDid = activeIdentity.activeDid;
|
||||
|
||||
if (!activeDid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = (await this.dbQuery(
|
||||
"SELECT * FROM settings WHERE accountDid = ?",
|
||||
[activeDid],
|
||||
)) as QueryExecResult;
|
||||
if (result?.values?.[0]) {
|
||||
// Convert the row to an object
|
||||
const row = result.values[0];
|
||||
const columns = result.columns || [];
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
columns.forEach((column: string, index: number) => {
|
||||
if (column !== "id") {
|
||||
// Exclude the id column
|
||||
settings[column] = row[index];
|
||||
}
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by platform-specific services
|
||||
|
||||
/**
|
||||
* Execute a database query (SELECT operations).
|
||||
*
|
||||
* @abstract
|
||||
* @param sql - SQL query string
|
||||
* @param params - Optional parameters for prepared statements
|
||||
* @returns Promise resolving to query results
|
||||
*/
|
||||
abstract dbQuery(sql: string, params?: unknown[]): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Execute a database statement (INSERT, UPDATE, DELETE operations).
|
||||
*
|
||||
* @abstract
|
||||
* @param sql - SQL statement string
|
||||
* @param params - Optional parameters for prepared statements
|
||||
* @returns Promise resolving to execution results
|
||||
*/
|
||||
abstract dbExec(sql: string, params?: unknown[]): Promise<unknown>;
|
||||
}
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { BaseDatabaseService } from "./BaseDatabaseService";
|
||||
|
||||
interface QueuedOperation {
|
||||
type: "run" | "query" | "rawQuery";
|
||||
type: "run" | "query";
|
||||
sql: string;
|
||||
params: unknown[];
|
||||
resolve: (value: unknown) => void;
|
||||
@@ -40,10 +39,7 @@ interface QueuedOperation {
|
||||
* - Platform-specific features
|
||||
* - SQLite database operations
|
||||
*/
|
||||
export class CapacitorPlatformService
|
||||
extends BaseDatabaseService
|
||||
implements PlatformService
|
||||
{
|
||||
export class CapacitorPlatformService implements PlatformService {
|
||||
/** Current camera direction */
|
||||
private currentDirection: CameraDirection = CameraDirection.Rear;
|
||||
|
||||
@@ -56,7 +52,6 @@ export class CapacitorPlatformService
|
||||
private isProcessingQueue: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
||||
}
|
||||
|
||||
@@ -71,13 +66,13 @@ export class CapacitorPlatformService
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
this.initializationPromise = this._initialize();
|
||||
try {
|
||||
// Start initialization
|
||||
this.initializationPromise = this._initialize();
|
||||
await this.initializationPromise;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Initialize database method failed:",
|
||||
"[CapacitorPlatformService] Initialize method failed:",
|
||||
error,
|
||||
);
|
||||
this.initializationPromise = null; // Reset on failure
|
||||
@@ -91,92 +86,16 @@ export class CapacitorPlatformService
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to create/Open database connection
|
||||
try {
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
1,
|
||||
false,
|
||||
);
|
||||
} catch (createError: unknown) {
|
||||
// If connection already exists, try to retrieve it or handle gracefully
|
||||
const errorMessage =
|
||||
createError instanceof Error
|
||||
? createError.message
|
||||
: String(createError);
|
||||
const errorObj =
|
||||
typeof createError === "object" && createError !== null
|
||||
? (createError as { errorMessage?: string; message?: string })
|
||||
: {};
|
||||
// Create/Open database
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
1,
|
||||
false,
|
||||
);
|
||||
|
||||
const fullErrorMessage =
|
||||
errorObj.errorMessage || errorObj.message || errorMessage;
|
||||
|
||||
if (fullErrorMessage.includes("already exists")) {
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Connection already exists on native side, attempting to retrieve",
|
||||
);
|
||||
// Check if connection exists in JavaScript Map
|
||||
const isConnResult = await this.sqlite.isConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
);
|
||||
if (isConnResult.result) {
|
||||
// Connection exists in Map, retrieve it
|
||||
this.db = await this.sqlite.retrieveConnection(this.dbName, false);
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Successfully retrieved existing connection from Map",
|
||||
);
|
||||
} else {
|
||||
// Connection exists on native side but not in JavaScript Map
|
||||
// This can happen when the app is restarted but native connections persist
|
||||
// Try to close the native connection first, then create a new one
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Connection exists natively but not in Map, closing and recreating",
|
||||
);
|
||||
try {
|
||||
await this.sqlite.closeConnection(this.dbName, false);
|
||||
} catch (closeError) {
|
||||
// Ignore close errors - connection might not be properly tracked
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Error closing connection (may be expected):",
|
||||
closeError,
|
||||
);
|
||||
}
|
||||
// Now try to create the connection again
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
1,
|
||||
false,
|
||||
);
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Successfully created connection after cleanup",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Re-throw if it's a different error
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the connection if it's not already open
|
||||
try {
|
||||
await this.db.open();
|
||||
} catch (openError: unknown) {
|
||||
const openErrorMessage =
|
||||
openError instanceof Error ? openError.message : String(openError);
|
||||
// If already open, that's fine - continue
|
||||
if (!openErrorMessage.includes("already open")) {
|
||||
throw openError;
|
||||
}
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Database connection already open",
|
||||
);
|
||||
}
|
||||
await this.db.open();
|
||||
|
||||
// Set journal mode to WAL for better performance
|
||||
// await this.db.execute("PRAGMA journal_mode=WAL;");
|
||||
@@ -240,14 +159,6 @@ export class CapacitorPlatformService
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "rawQuery": {
|
||||
const queryResult = await this.db.query(
|
||||
operation.sql,
|
||||
operation.params,
|
||||
);
|
||||
result = queryResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
operation.resolve(result);
|
||||
} catch (error) {
|
||||
@@ -589,24 +500,9 @@ export class CapacitorPlatformService
|
||||
// This is essential for proper parameter binding and SQL injection prevention
|
||||
await this.db!.run(sql, params);
|
||||
} else {
|
||||
// For multi-statement SQL (like migrations), use executeSet method
|
||||
// This handles multiple statements properly
|
||||
if (
|
||||
sql.includes(";") &&
|
||||
sql.split(";").filter((s) => s.trim()).length > 1
|
||||
) {
|
||||
// Multi-statement SQL - use executeSet for proper handling
|
||||
const statements = sql.split(";").filter((s) => s.trim());
|
||||
await this.db!.executeSet(
|
||||
statements.map((stmt) => ({
|
||||
statement: stmt.trim(),
|
||||
values: [], // Empty values array for non-parameterized statements
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
// Single statement - use execute method
|
||||
await this.db!.execute(sql);
|
||||
}
|
||||
// Use execute method for non-parameterized queries
|
||||
// This is more efficient for simple DDL statements
|
||||
await this.db!.execute(sql);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1374,14 +1270,6 @@ export class CapacitorPlatformService
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbRawQuery
|
||||
*/
|
||||
async dbRawQuery(sql: string, params?: unknown[]): Promise<unknown> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation("rawQuery", sql, params || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Capacitor platform.
|
||||
* @returns true, as this is the Capacitor implementation
|
||||
@@ -1409,8 +1297,63 @@ export class CapacitorPlatformService
|
||||
// --- PWA/Web-only methods (no-op for Capacitor) ---
|
||||
public registerServiceWorker(): void {}
|
||||
|
||||
// Database utility methods - inherited from BaseDatabaseService
|
||||
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
|
||||
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
|
||||
// retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
|
||||
// Database utility methods
|
||||
generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const keys = Object.keys(model);
|
||||
const placeholders = keys.map(() => "?").join(", ");
|
||||
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
|
||||
const params = keys.map((key) => model[key]);
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
async updateDefaultSettings(
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE id = 1`;
|
||||
const params = keys.map((key) => settings[key]);
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async insertNewDidIntoSettings(did: string): Promise<void> {
|
||||
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
|
||||
}
|
||||
|
||||
async updateDidSpecificSettings(
|
||||
did: string,
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
|
||||
const params = [...keys.map((key) => settings[key]), did];
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async retrieveSettingsForActiveAccount(): Promise<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null> {
|
||||
const result = await this.dbQuery("SELECT * FROM settings WHERE id = 1");
|
||||
if (result?.values?.[0]) {
|
||||
// Convert the row to an object
|
||||
const row = result.values[0];
|
||||
const columns = result.columns || [];
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
columns.forEach((column, index) => {
|
||||
if (column !== "id") {
|
||||
// Exclude the id column
|
||||
settings[column] = row[index];
|
||||
}
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
import { BaseDatabaseService } from "./BaseDatabaseService";
|
||||
// Dynamic import of initBackend to prevent worker context errors
|
||||
import type {
|
||||
WorkerRequest,
|
||||
@@ -30,10 +29,7 @@ import type {
|
||||
* Note: File system operations are not available in the web platform
|
||||
* due to browser security restrictions. These methods throw appropriate errors.
|
||||
*/
|
||||
export class WebPlatformService
|
||||
extends BaseDatabaseService
|
||||
implements PlatformService
|
||||
{
|
||||
export class WebPlatformService implements PlatformService {
|
||||
private static instanceCount = 0; // Debug counter
|
||||
private worker: Worker | null = null;
|
||||
private workerReady = false;
|
||||
@@ -50,16 +46,17 @@ export class WebPlatformService
|
||||
private readonly messageTimeout = 30000; // 30 seconds
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
WebPlatformService.instanceCount++;
|
||||
|
||||
logger.debug("[WebPlatformService] Initializing web platform service");
|
||||
// Use debug level logging for development mode to reduce console noise
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
const log = isDevelopment ? logger.debug : logger.log;
|
||||
|
||||
log("[WebPlatformService] Initializing web platform service");
|
||||
|
||||
// Only initialize SharedArrayBuffer setup for web platforms
|
||||
if (this.isWorker()) {
|
||||
logger.debug(
|
||||
"[WebPlatformService] Skipping initBackend call in worker context",
|
||||
);
|
||||
log("[WebPlatformService] Skipping initBackend call in worker context");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -639,17 +636,6 @@ export class WebPlatformService
|
||||
} as GetOneRowRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbRawQuery
|
||||
*/
|
||||
async dbRawQuery(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<unknown | undefined> {
|
||||
// This class doesn't post-process the result, so we can just use it.
|
||||
return this.dbQuery(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the camera between front and back cameras.
|
||||
* @returns Promise that resolves when the camera is rotated
|
||||
@@ -673,8 +659,69 @@ export class WebPlatformService
|
||||
// SharedArrayBuffer initialization is handled by initBackend call in initializeWorker
|
||||
}
|
||||
|
||||
// Database utility methods - inherited from BaseDatabaseService
|
||||
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
|
||||
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
|
||||
// retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
|
||||
// Database utility methods
|
||||
generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const keys = Object.keys(model);
|
||||
const placeholders = keys.map(() => "?").join(", ");
|
||||
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
|
||||
const params = keys.map((key) => model[key]);
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
async updateDefaultSettings(
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE id = 1`;
|
||||
const params = keys.map((key) => settings[key]);
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async insertNewDidIntoSettings(did: string): Promise<void> {
|
||||
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
|
||||
}
|
||||
|
||||
async updateDidSpecificSettings(
|
||||
did: string,
|
||||
settings: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const keys = Object.keys(settings);
|
||||
const setClause = keys.map((key) => `${key} = ?`).join(", ");
|
||||
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
|
||||
const params = [...keys.map((key) => settings[key]), did];
|
||||
// Log update operation for debugging
|
||||
logger.debug(
|
||||
"[WebPlatformService] updateDidSpecificSettings",
|
||||
sql,
|
||||
JSON.stringify(params, null, 2),
|
||||
);
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async retrieveSettingsForActiveAccount(): Promise<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null> {
|
||||
const result = await this.dbQuery("SELECT * FROM settings WHERE id = 1");
|
||||
if (result?.values?.[0]) {
|
||||
// Convert the row to an object
|
||||
const row = result.values[0];
|
||||
const columns = result.columns || [];
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
columns.forEach((column, index) => {
|
||||
if (column !== "id") {
|
||||
// Exclude the id column
|
||||
settings[column] = row[index];
|
||||
}
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<EntityGrid
|
||||
entity-type="people"
|
||||
:entities="people"
|
||||
:max-items="5"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="people"
|
||||
@@ -38,6 +39,7 @@
|
||||
<EntityGrid
|
||||
entity-type="projects"
|
||||
:entities="projects"
|
||||
:max-items="3"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="people"
|
||||
@@ -150,8 +152,11 @@ export default class EntityGridFunctionPropTest extends Vue {
|
||||
customPeopleFunction = (
|
||||
entities: Contact[],
|
||||
_entityType: string,
|
||||
maxItems: number,
|
||||
): Contact[] => {
|
||||
return entities.filter((person) => person.profileImageUrl);
|
||||
return entities
|
||||
.filter((person) => person.profileImageUrl)
|
||||
.slice(0, maxItems);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -160,6 +165,7 @@ export default class EntityGridFunctionPropTest extends Vue {
|
||||
customProjectsFunction = (
|
||||
entities: PlanData[],
|
||||
_entityType: string,
|
||||
_maxItems: number,
|
||||
): PlanData[] => {
|
||||
return entities.sort((a, b) => a.name.localeCompare(b.name)).slice(0, 3);
|
||||
};
|
||||
@@ -194,16 +200,16 @@ export default class EntityGridFunctionPropTest extends Vue {
|
||||
*/
|
||||
get displayedPeopleCount(): number {
|
||||
if (this.useCustomFunction) {
|
||||
return this.customPeopleFunction(this.people, "people").length;
|
||||
return this.customPeopleFunction(this.people, "people", 5).length;
|
||||
}
|
||||
return Math.min(10, this.people.length); // Initial batch size for infinite scroll
|
||||
return Math.min(5, this.people.length);
|
||||
}
|
||||
|
||||
get displayedProjectsCount(): number {
|
||||
if (this.useCustomFunction) {
|
||||
return this.customProjectsFunction(this.projects, "projects").length;
|
||||
return this.customProjectsFunction(this.projects, "projects", 3).length;
|
||||
}
|
||||
return Math.min(10, this.projects.length); // Initial batch size for infinite scroll
|
||||
return Math.min(7, this.projects.length);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user