• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
figleaf

figleaf

whenwasthelasttime

Public
Like
whenwasthelasttime
Home
Code
8
.claude
1
backend
2
frontend
5
public
4
.vtignore
README.md
VALTOWN_SQLITE_GUIDE.md
spec.md
Branches
1
Pull requests
Remixes
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
spec.md
Code
/
spec.md
Search
…
spec.md

"When Was The Last Time" Tracker - Technical Specification

1. Project Overview

1.1 Application Name

"When Was The Last Time" - A progressive web application for tracking occurrences of events, activities, and items.

1.2 Purpose

A single-user event tracker that records when specific items/events last occurred, displays time elapsed, maintains full historical data, and provides statistical analysis of occurrence patterns. No authentication required - data is stored locally in the browser's SQLite database.

1.3 Core Value Proposition

  • Flexible tracking: Can track personal activities ("I went to the gym") or general events ("Dog was groomed", "It rained")
  • Complete historical record with intervals between occurrences
  • Statistical insights (averages, longest/shortest intervals, frequency)
  • Offline-capable PWA accessible across devices
  • Category-based organization with search and filtering
  • No login required - simple, immediate access

2. Technical Stack

2.1 Platform

  • Deployment: Val Town (https://val.town)
  • Runtime: Deno/TypeScript on Val Town infrastructure
  • Application Type: Progressive Web App (PWA)

2.2 Backend

  • Framework: Hono (lightweight web framework for Deno)
  • Database: SQLite (Val Town's built-in SQLite via Turso)
    • Import: import { sqlite } from "https://esm.town/v/std/sqlite"
  • No Authentication: Single-user app with no login required

2.3 Frontend

  • Framework: React 18+ (client-side rendering)
  • Styling: Tailwind CSS (via CDN)
  • State Management: React hooks (useState, useEffect, useContext)
  • Build: No build step - direct ESM imports

2.4 PWA Requirements

  • Manifest: /manifest.json with app metadata
  • Service Worker: /sw.js for offline capability and caching
  • Icons: Multiple sizes (192x192, 256x256, 384x384, 512x512)
  • HTTPS: Required (Val Town provides this automatically)

3. System Architecture

3.1 Val Town Structure

username-lastTimeTracker/
├── backend/
│   ├── index.ts          # Main Hono server & routing
│   └── db.ts             # Database initialization & queries
├── frontend/
│   ├── index.html        # Main HTML entry point
│   ├── index.tsx         # React root component
│   ├── App.tsx           # Main React app component
│   ├── components/
│   │   ├── ItemList.tsx
│   │   ├── ItemDetail.tsx
│   │   ├── AddItem.tsx
│   │   ├── CategoryManager.tsx
│   │   └── ArchiveView.tsx
│   └── utils/
│       ├── api.ts        # API client functions
│       └── timeCalc.ts   # Time calculation utilities
├── public/
│   ├── manifest.json     # PWA manifest
│   ├── sw.js            # Service worker
│   └── icons/           # App icons (various sizes)
└── README.md

3.2 Request Flow

User (PWA) → HTTPS → Val Town Endpoint
                ↓
            Hono Router
                ↓
         Route Handler → SQLite (data operations)
                ↓
            JSON Response → React Frontend

3.3 Data Access

Single-User Architecture: This app has no authentication system. All data is stored in a single SQLite database on Val Town. Anyone with access to the Val Town URL can view and modify the data.

Privacy Note: Since there's no authentication:

  • Don't store sensitive personal information
  • The app is best suited for personal use or trusted environments
  • Consider the Val Town URL as private (don't share publicly)
  • Val Town Pro users can add custom domains with additional security if needed

4. Database Schema

4.1 Schema Design Principles

  • Normalized structure to minimize redundancy
  • Indexes on frequently queried fields (category_id, item_id)
  • Date-only storage (no time component) as per requirements
  • Soft delete for items (archived flag)
  • Single-user: No user table needed

4.2 Tables

categories

create table if not exists categories ( id text primary key, name text not null unique, created_at text not null )

Notes:

  • id: UUID generated via crypto.randomUUID()
  • created_at: ISO 8601 date string (YYYY-MM-DDTHH:mm:ss.sssZ)
  • Unique constraint on name prevents duplicates

items

create table if not exists items ( id text primary key, category_id text not null, name text not null, description text, created_at text not null, archived integer default 0, foreign key (category_id) references categories(id) )

Notes:

  • id: UUID
  • archived: 0 = active, 1 = archived (SQLite boolean as integer)
  • Foreign key to categories (no cascade delete - prevents accidental data loss)

occurrences

create table if not exists occurrences ( id text primary key, item_id text not null, occurrence_date text not null, note text, created_at text not null, foreign key (item_id) references items(id) on delete cascade )

Notes:

  • id: UUID
  • occurrence_date: ISO 8601 date (YYYY-MM-DD)
  • created_at: When this record was created
  • Cascade delete: If item deleted, its occurrences are also deleted

4.3 Indexes

-- Categories (no additional indexes needed - name is unique) -- Items create index if not exists idx_items_category_id on items(category_id); create index if not exists idx_items_archived on items(archived); -- Occurrences create index if not exists idx_occurrences_item_id on occurrences(item_id); create index if not exists idx_occurrences_date on occurrences(occurrence_date);

4.4 Key Relationships

  • One category → Many items
  • One item → Many occurrences

4.5 Data Integrity Rules

  1. Category deletion: Requires reassignment or deletion of items in that category
  2. Item "deletion": Sets archived = 1, preserves all data
  3. Occurrence deletion: Cascade when item is hard-deleted
  4. At least one occurrence should exist per item (enforced in application logic)

5. API Endpoints

Note: No authentication required. All endpoints are publicly accessible.

5.1 Category Endpoints

GET /api/categories

Purpose: Get all categories Response:

{ "categories": [ { "id": "cat-uuid", "name": "Health", "created_at": "2024-01-01T00:00:00Z" } ] }

POST /api/categories

Purpose: Create new category Body:

{ "name": "Health" }

Response:

{ "category": { "id": "cat-uuid", "name": "Health", "created_at": "2024-01-01T00:00:00Z" } }

PUT /api/categories/:id

Purpose: Update category name Body:

{ "name": "Updated Name" }

DELETE /api/categories/:id

Purpose: Delete category Query params: ?reassign_to=<CATEGORY_ID> (optional) Notes: If items exist in category, must provide reassign_to or it will fail

5.2 Item Endpoints

GET /api/items

Purpose: Get all items with their latest occurrence Query params:

  • ?category=<CATEGORY_ID> - Filter by category
  • ?search=<QUERY> - Search item names, descriptions, and notes
  • ?sort=<alpha|recent|oldest> - Sort order
  • ?archived=<true|false> - Include/exclude archived (default: false)

Response:

{ "items": [ { "id": "item-uuid", "name": "Went to the gym", "description": "Morning workout routine", "category_id": "cat-uuid", "category_name": "Health", "latest_occurrence": "2024-11-13", "days_since": 0, "months_since": 0, "years_since": 0, "total_occurrences": 5, "archived": false, "created_at": "2024-01-01T00:00:00Z" } ] }

POST /api/items

Purpose: Create new item Body:

{ "name": "Went to the gym", "description": "Morning workout", "category_id": "cat-uuid", "initial_date": "2024-11-13" // or "now" for today }

Response:

{ "item": { /* item object */ } }

Notes: Automatically creates first occurrence

GET /api/items/:id

Purpose: Get item details with all occurrences and statistics Response:

{ "item": { "id": "item-uuid", "name": "Went to the gym", "description": "Morning workout", "category_id": "cat-uuid", "category_name": "Health", "archived": false, "statistics": { "total_occurrences": 10, "average_interval_days": 3.5, "longest_interval_days": 7, "shortest_interval_days": 1, "occurrences_per_month": 8.6, "occurrences_per_year": 104 }, "occurrences": [ { "id": "occ-uuid", "occurrence_date": "2024-11-13", "note": "Great workout!", "days_since_previous": 3, "created_at": "2024-11-13T10:00:00Z" } ] } }

PUT /api/items/:id

Purpose: Update item details Body:

{ "name": "Updated name", "description": "Updated description", "category_id": "new-cat-uuid" }

DELETE /api/items/:id

Purpose: Archive item (soft delete) Response:

{ "success": true }

Notes: Sets archived = 1, keeps all data

POST /api/items/:id/restore

Purpose: Restore archived item

5.3 Occurrence Endpoints

POST /api/items/:id/occurrences

Purpose: Log new occurrence Body:

{ "occurrence_date": "2024-11-13", // or "now" for today "note": "Felt great today" }

Response:

{ "occurrence": { "id": "occ-uuid", "item_id": "item-uuid", "occurrence_date": "2024-11-13", "note": "Felt great today", "created_at": "2024-11-13T10:00:00Z" } }

PUT /api/occurrences/:id

Purpose: Update occurrence Body:

{ "occurrence_date": "2024-11-12", "note": "Updated note" }

DELETE /api/occurrences/:id

Purpose: Delete occurrence (hard delete) Notes: Cannot delete if it's the only occurrence for an item


6. Frontend Architecture

6.1 Component Hierarchy

App.tsx
├── Router (client-side)
│   ├── HomeView
│   │   ├── Header (with search, filter, sort controls)
│   │   ├── ItemList
│   │   │   └── ItemCard (shows time elapsed)
│   │   └── AddItemButton
│   ├── ItemDetailView
│   │   ├── ItemHeader (name, description, edit)
│   │   ├── StatisticsPanel
│   │   ├── OccurrenceList
│   │   │   └── OccurrenceCard (with edit/delete)
│   │   └── AddOccurrenceButton
│   ├── AddItemView
│   │   ├── ItemForm
│   │   └── CategorySelector (with create new)
│   ├── CategoryManagementView
│   │   └── CategoryList (edit/delete/create)
│   └── ArchiveView
│       └── ArchivedItemsList (with restore option)

6.2 State Management

Global State (Context API):

  • Categories list (cached)
  • App settings (theme, sort preferences)

Local Component State:

  • UI state (modals, loading, errors)
  • Form inputs
  • Search/filter/sort parameters
  • Item and occurrence data (fetched per view)

6.3 Client-Side Routing

Use React Router or similar library for single-page app routing:

  • / - Main item list (home)
  • /items/:id - Item detail view
  • /items/new - Add new item
  • /categories - Category management
  • /archive - Archived items

6.4 Offline Behavior

  • Service worker caches app shell (HTML, CSS, JS)
  • API requests queue when offline, sync when online
  • Show offline indicator in UI
  • Optimistic UI updates with rollback on sync failure

7. Time Calculation Logic

7.1 Display Format

Breakdown format: X years, Y months, Z days

7.2 Calculation Algorithm

function calculateElapsedTime(occurrenceDate: string, currentDate: string) { const start = new Date(occurrenceDate); const end = new Date(currentDate); let years = end.getFullYear() - start.getFullYear(); let months = end.getMonth() - start.getMonth(); let days = end.getDate() - start.getDate(); // Adjust for negative days if (days < 0) { months--; const prevMonth = new Date(end.getFullYear(), end.getMonth(), 0); days += prevMonth.getDate(); } // Adjust for negative months if (months < 0) { years--; months += 12; } return { years, months, days }; }

7.3 Display Rules

  • Show all three units: "1 year, 2 months, 5 days"
  • Zero values included: "0 years, 0 months, 3 days"
  • Singular/plural: "1 year" vs "2 years"
  • Manual refresh: User can manually update times (no auto-refresh)

7.4 Interval Calculation (for statistics)

// Between two occurrences function calculateInterval(date1: string, date2: string) { const d1 = new Date(date1); const d2 = new Date(date2); const diffMs = Math.abs(d2.getTime() - d1.getTime()); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); return diffDays; } // Average interval function calculateAverageInterval(occurrences: Occurrence[]) { if (occurrences.length < 2) return null; const sortedDates = occurrences .map(o => o.occurrence_date) .sort(); let totalDays = 0; for (let i = 1; i < sortedDates.length; i++) { totalDays += calculateInterval(sortedDates[i-1], sortedDates[i]); } return totalDays / (sortedDates.length - 1); }

7.5 Frequency Calculation

function calculateFrequency(occurrences: Occurrence[]) { if (occurrences.length < 2) return { perMonth: 0, perYear: 0 }; const sortedDates = occurrences .map(o => new Date(o.occurrence_date)) .sort((a, b) => a.getTime() - b.getTime()); const firstDate = sortedDates[0]; const lastDate = sortedDates[sortedDates.length - 1]; const totalDays = calculateInterval( firstDate.toISOString().split('T')[0], lastDate.toISOString().split('T')[0] ); const totalMonths = totalDays / 30.44; // Average days per month const totalYears = totalDays / 365.25; // Account for leap years return { perMonth: occurrences.length / totalMonths, perYear: occurrences.length / totalYears }; }

8. PWA Configuration

8.1 Manifest File (/public/manifest.json)

{ "name": "When Was The Last Time", "short_name": "Last Time", "description": "Track when you last did things", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#4f46e5", "orientation": "portrait-primary", "icons": [ { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-256x256.png", "sizes": "256x256", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-512x512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "categories": ["productivity", "utilities"], "lang": "en-US" }

8.2 Service Worker (/public/sw.js)

Caching Strategy:

  • App Shell (HTML, CSS, JS): Cache-first with network fallback
  • API Requests: Network-first with cache fallback
  • Static Assets (icons, fonts): Cache-first

Basic Implementation:

const CACHE_NAME = 'lasttime-v1'; const APP_SHELL = [ '/', '/index.html', '/index.js', '/styles.css', '/icons/icon-192x192.png', '/manifest.json' ]; // Install event - cache app shell self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(APP_SHELL); }) ); self.skipWaiting(); }); // Fetch event - serve from cache or network self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // API requests: network-first if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request) .then(response => { const responseClone = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(request, responseClone); }); return response; }) .catch(() => caches.match(request)) ); } else { // App shell: cache-first event.respondWith( caches.match(request).then(response => { return response || fetch(request); }) ); } }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then(keys => { return Promise.all( keys.filter(key => key !== CACHE_NAME) .map(key => caches.delete(key)) ); }) ); self.clients.claim(); });

8.3 Service Worker Registration (in index.html)

if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered:', reg)) .catch(err => console.log('SW registration failed:', err)); }); }

8.4 iOS-Specific Meta Tags (in index.html)

<!-- iOS-specific meta tags --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-title" content="Last Time"> <link rel="apple-touch-icon" href="/icons/icon-192x192.png"> <!-- Android/Chrome --> <meta name="mobile-web-app-capable" content="yes"> <meta name="theme-color" content="#4f46e5">

9. Security Considerations

9.1 No Authentication

Important: This app has no authentication system. Consider it a personal, private app.

Security Implications:

  • Anyone with the Val Town URL can access and modify data
  • Treat the URL as a secret (like a password)
  • Don't share the URL publicly
  • Don't store sensitive personal information
  • Best for personal use or trusted environments only

9.2 Input Validation

  • Dates: ISO 8601 format (YYYY-MM-DD) validation
  • Text fields: Sanitize HTML, max lengths enforced
  • Category/item names: 100 character limit
  • Notes: 1000 character limit
  • No special characters that could break SQL

9.3 SQL Injection Prevention

  • Use parameterized queries exclusively
  • Val Town's sqlite library handles this automatically:
await sqlite.execute({ sql: 'select * from items where category_id = ?', args: [categoryId] });

9.4 Rate Limiting

  • API requests: Max 100 per minute per IP (Val Town handles this)
  • Consider adding client-side throttling for expensive operations

9.5 CORS Policy

  • Allow only same-origin requests by default
  • Val Town handles this automatically for Val endpoints

9.6 Data Privacy

Since there's no authentication:

  • Don't store: passwords, SSN, credit cards, medical info
  • Safe to store: general activity tracking, non-sensitive notes
  • Consider: who has physical access to devices using this app

10. User Experience Details

10.1 Home Screen (Entry Point)

Layout:

┌─────────────────────────────┐
│ [≡] Last Time      [+] [⚙] │  ← Header
├─────────────────────────────┤
│ 🔍 Search...                │  ← Search bar
│ [All Categories ▼] [Sort ▼]│  ← Filters
├─────────────────────────────┤
│ ┌─────────────────────────┐ │
│ │ Went to the gym         │ │
│ │ Health                  │ │
│ │ 0 years, 0 months, 3 days│ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ Watered plants          │ │
│ │ Home                    │ │
│ │ 0 years, 0 months, 7 days│ │
│ └─────────────────────────┘ │
│                             │
└─────────────────────────────┘

Interactions:

  • Tap item → View detail
  • Swipe item → Quick "Log now" action
  • Pull to refresh → Update elapsed times
  • Tap [+] → Add new item
  • Tap [≡] → Open menu (Categories, Archive)

10.2 Item Detail Screen

Layout:

┌─────────────────────────────┐
│ [←] Went to the gym    [⋮]  │
├─────────────────────────────┤
│ Health | Edit               │
│ Morning workout routine     │
├─────────────────────────────┤
│ 📊 STATISTICS               │
│ Average: 3.5 days           │
│ Longest: 7 days             │
│ Shortest: 1 day             │
│ Per month: 8.6              │
│ Per year: 104               │
├─────────────────────────────┤
│ 📅 HISTORY                  │
│ ┌─────────────────────────┐ │
│ │ Nov 13, 2024 (3 days)   │ │
│ │ Great workout!          │ │
│ │                    [✏][🗑]│ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ Nov 10, 2024 (2 days)   │ │
│ │                    [✏][🗑]│ │
│ └─────────────────────────┘ │
├─────────────────────────────┤
│ [✓ Log it now]              │
└─────────────────────────────┘

Interactions:

  • Tap [✓ Log it now] → Log occurrence with today's date
  • Tap [✏] on occurrence → Edit date/note
  • Tap [🗑] on occurrence → Confirm delete
  • Tap [⋮] → Options (Edit item, Archive, Delete)

10.3 Add Item Flow

Screen 1: Basic Info

┌─────────────────────────────┐
│ [×] Add New Item            │
├─────────────────────────────┤
│ When was the last time...   │
│ ┌─────────────────────────┐ │
│ │ I went to the gym       │ │
│ └─────────────────────────┘ │
│                             │
│ Category                    │
│ ┌─────────────────────────┐ │
│ │ Health              [▼] │ │
│ └─────────────────────────┘ │
│ [+ Create new category]     │
│                             │
│ Description (optional)      │
│ ┌─────────────────────────┐ │
│ │ Morning workout routine │ │
│ └─────────────────────────┘ │
│                             │
│ [Next]                      │
└─────────────────────────────┘

Screen 2: First Occurrence

┌─────────────────────────────┐
│ [←] Add First Occurrence    │
├─────────────────────────────┤
│ When did you last:          │
│ "Went to the gym"           │
│                             │
│ ○ Today                     │
│ ○ Set a date                │
│                             │
│ [Date picker]               │
│                             │
│ Note (optional)             │
│ ┌─────────────────────────┐ │
│ │                         │ │
│ └─────────────────────────┘ │
│                             │
│ [Create Item]               │
└─────────────────────────────┘

10.4 Search & Filter

Search:

  • Searches item names AND descriptions AND notes
  • Real-time filtering as user types
  • Show count: "5 results for 'gym'"
  • Clear button (×) to reset search

Category Filter:

  • Dropdown with all categories
  • "All Categories" option to show everything
  • Show item count per category in dropdown

Sort Options:

  • Alphabetical (A-Z)
  • Most recent (shortest time since last occurrence)
  • Least recent (longest time since last occurrence)
  • Persist sort preference in localStorage

11. Implementation Checklist

Phase 1: Infrastructure (Days 1-2)

  • Research complete
  • Set up Val Town account
  • Create Val with folder structure
  • Initialize SQLite database with schema
  • Create database initialization script
  • Test basic CRUD operations

Phase 2: Backend API (Days 3-5)

  • Create Hono server with routing
  • Implement category endpoints
  • Implement item endpoints
  • Implement occurrence endpoints
  • Add error handling and validation
  • Test all endpoints with Postman/curl

Phase 3: Frontend Core (Days 6-10)

  • Set up React app structure
  • Create routing system
  • Build home screen with item list
  • Implement time calculation utilities
  • Create item detail view
  • Build add item flow
  • Implement search and filtering

Phase 4: Advanced Features (Days 11-13)

  • Category management UI
  • Archive functionality
  • Edit item/occurrence forms
  • Statistics calculations
  • Error handling and loading states
  • Responsive design refinement

Phase 5: PWA Setup (Days 14-15)

  • Create manifest.json
  • Generate app icons (all sizes)
  • Implement service worker
  • Test offline functionality
  • Test installation on iOS and Android

Phase 6: Polish & Testing (Days 16-18)

  • UI/UX refinement based on usage
  • Performance optimization
  • Cross-browser testing
  • iOS PWA specific testing
  • Documentation
  • Deployment to production Val

Phase 7: Enhancements (Future)

  • Export data feature (CSV/JSON)
  • Import data feature
  • Dark mode
  • Multiple themes
  • Data visualization (charts/graphs)
  • Tags in addition to categories
  • Bulk operations

12. Key Technical Decisions & Rationale

12.1 Why Val Town?

  • Zero infrastructure management
  • Built-in SQLite
  • Instant HTTPS deployment
  • Automatic scaling
  • Perfect for single-user PWAs

12.2 Why No Authentication?

  • Simplicity: Immediate access, no signup friction
  • Personal use: Designed for individual tracking
  • Privacy: No email collection or user data
  • Speed: Faster development and simpler codebase
  • Note: URL acts as the security - keep it private

12.3 Why Date-Only Storage?

  • Requirement simplification per user specs
  • Easier calculations (no timezone complexity)
  • Sufficient granularity for use case
  • Reduces data size

12.4 Why Soft Delete (Archive)?

  • Preserves historical data for statistics
  • Allows users to restore accidentally deleted items
  • "Deleted" items can still contribute to overall insights
  • User requested this feature explicitly

12.5 Why Categories Required?

  • Simplifies UI/UX (no "uncategorized" state)
  • Ensures data organization from start
  • Easy to create categories during item creation
  • Per user request for simplicity

13. Performance Considerations

13.1 Database Optimization

  • Indexes on all foreign keys and frequently queried fields
  • Batch operations where possible
  • Limit query results (pagination if needed)
  • Use SQLite's EXPLAIN QUERY PLAN to optimize queries

13.2 Frontend Optimization

  • Lazy load components with React.lazy()
  • Memoize expensive calculations (React.useMemo)
  • Debounce search input (300ms delay)
  • Virtual scrolling for long item lists (if >100 items)
  • Optimize re-renders with React.memo()

13.3 Caching Strategy

  • Cache categories in memory (they change rarely)
  • Cache item list with timestamp, refresh on manual pull
  • Invalidate cache after mutations
  • Service worker caches static assets aggressively

13.4 Network Efficiency

  • Minimize API calls by combining data in responses
  • Use conditional requests (If-None-Match headers)
  • Compress responses (Val Town handles this)
  • Batch operations where appropriate

14. Error Handling Strategy

14.1 User-Facing Errors

  • Friendly error messages, no technical jargon
  • Actionable guidance ("Check your internet connection")
  • Toast notifications for transient errors
  • Modal dialogs for critical errors
  • Retry buttons where appropriate

14.2 Technical Error Logging

  • Console.error() for client-side errors
  • Server-side logging to Val Town logs
  • Include context (user ID, action attempted, timestamp)
  • Never log sensitive data (emails, tokens)

14.3 Offline Handling

  • Show offline indicator when no connection
  • Queue mutations for later sync
  • Show optimistic UI updates
  • Explain sync status to user

15. Testing Strategy

15.1 Manual Testing Priority

  1. Add item → Log occurrence → View statistics
  2. Search and filter functionality
  3. Category management
  4. Archive and restore
  5. Offline functionality
  6. Cross-browser compatibility
  7. iOS PWA installation and usage

15.2 Edge Cases to Test

  • Empty states (no items, no categories)
  • Single occurrence (statistics should handle gracefully)
  • Same-day multiple occurrences
  • Date edge cases (leap years, month boundaries)
  • Very old occurrences (years ago)
  • Very large text in notes
  • Network interruption during submission
  • Very large numbers of items (100+)

16. Deployment Notes

16.1 Val Town Deployment

  • Val automatically deploys on save
  • URL format: https://username-valname.val.run
  • Environment variables: Set in Val Town UI
  • Monitoring: Available in Val Town dashboard

16.2 Custom Domain (Optional)

  • Can add custom domain in Val Town Pro
  • Requires DNS configuration
  • SSL automatically provisioned

16.3 Versioning Strategy

  • Use Git-style branches in Val Town
  • Tag stable versions
  • Keep changelog in README.md

17. Future Considerations

17.1 Multi-User Support

Current design is single-user, but could scale:

  • Add team/family sharing features
  • Shared item ownership
  • Permission system (view-only vs edit)
  • User profiles

17.2 Analytics & Insights

  • Trends over time (am I doing X more or less?)
  • Correlations (when I do X, I also do Y)
  • Recommendations (you haven't done X in a while)
  • Goal setting and tracking

17.3 Integrations

  • Google Calendar sync
  • IFTTT/Zapier webhooks
  • Health app data import
  • SMS logging interface

End of Specification

This document should be sufficient to begin implementation with Claude Code or another AI coding assistant. The specification prioritizes clarity, completeness, and adherence to the user's requirements while incorporating best practices discovered through research.

Next Steps:

  1. User provides UI mockups (optional but helpful)
  2. Begin Phase 1 implementation
  3. Iterate based on user feedback during development
FeaturesVersion controlCode intelligenceCLI
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.