feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)
## Phase 4: Web Interface Implementation Implemented complete web interface with public and admin routes, templates, CSS, and development authentication. ### Core Features **Public Routes**: - Homepage with recent published notes - Note permalinks with microformats2 - Server-side rendering (Jinja2) **Admin Routes**: - Login via IndieLogin - Dashboard with note management - Create, edit, delete notes - Protected with @require_auth decorator **Development Authentication**: - Dev login bypass for local testing (DEV_MODE only) - Security safeguards per ADR-011 - Returns 404 when disabled **Templates & Frontend**: - Base layouts (public + admin) - 8 HTML templates with microformats2 - Custom responsive CSS (114 lines) - Error pages (404, 500) ### Bugfixes (v0.5.1 → v0.5.2) 1. **Cookie collision fix (v0.5.1)**: - Renamed auth cookie from "session" to "starpunk_session" - Fixed redirect loop between dev login and admin dashboard - Flask's session cookie no longer conflicts with auth 2. **HTTP 404 error handling (v0.5.1)**: - Update route now returns 404 for nonexistent notes - Delete route now returns 404 for nonexistent notes - Follows ADR-012 HTTP Error Handling Policy - Pattern consistency across all admin routes 3. **Note model enhancement (v0.5.2)**: - Exposed deleted_at field from database schema - Enables soft deletion verification in tests - Follows ADR-013 transparency principle ### Architecture **New ADRs**: - ADR-011: Development Authentication Mechanism - ADR-012: HTTP Error Handling Policy - ADR-013: Expose deleted_at Field in Note Model **Standards Compliance**: - Uses uv for Python environment - Black formatted, Flake8 clean - Follows git branching strategy - Version incremented per versioning strategy ### Test Results - 405/406 tests passing (99.75%) - 87% code coverage - All security tests passing - Manual testing confirmed working ### Documentation - Complete implementation reports in docs/reports/ - Architecture reviews in docs/reviews/ - Design documents in docs/design/ - CHANGELOG updated for v0.5.2 ### Files Changed **New Modules**: - starpunk/dev_auth.py - starpunk/routes/ (public, admin, auth, dev_auth) **Templates**: 10 files (base, pages, admin, errors) **Static**: CSS and optional JavaScript **Tests**: 4 test files for routes and templates **Docs**: 20+ architectural and implementation documents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
521
docs/decisions/ADR-011-development-authentication-mechanism.md
Normal file
521
docs/decisions/ADR-011-development-authentication-mechanism.md
Normal file
@@ -0,0 +1,521 @@
|
||||
# ADR-011: Development Authentication Mechanism
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
During Phase 4 development (Web Interface), the team needs to test authentication-protected routes locally. However, IndieLogin.com requires:
|
||||
- A publicly accessible callback URL (HTTPS)
|
||||
- A real domain with valid DNS
|
||||
- External network connectivity
|
||||
|
||||
This creates friction for local development:
|
||||
- Cannot test protected routes without deploying
|
||||
- Cannot run tests without network access
|
||||
- Cannot develop offline
|
||||
- Slow iteration cycle (deploy to test auth flows)
|
||||
|
||||
The question: Should we implement a development-only authentication mechanism?
|
||||
|
||||
### Requirements for Dev Auth (if implemented)
|
||||
|
||||
1. **Must work for local testing** - Allow developers to authenticate locally
|
||||
2. **Must be easy to use** - Minimal configuration required
|
||||
3. **Must NEVER exist in production** - Critical security requirement
|
||||
4. **Must integrate seamlessly** - Work with existing auth module
|
||||
5. **Must allow protected route testing** - Enable full workflow testing
|
||||
6. **Must not compromise security** - No backdoors in production code
|
||||
|
||||
### Security Criticality
|
||||
|
||||
This is an extremely sensitive decision. Implemented incorrectly, a dev auth mechanism could:
|
||||
- Create a production authentication bypass
|
||||
- Expose admin functionality to attackers
|
||||
- Violate IndieWeb authentication principles
|
||||
- Undermine the entire security model
|
||||
|
||||
## Decision
|
||||
|
||||
**YES - Implement a development authentication mechanism with strict safeguards**
|
||||
|
||||
### Approach: Environment-Based Toggle with Explicit Configuration
|
||||
|
||||
We will implement a **separate development authentication pathway** that:
|
||||
1. Only activates when explicitly configured
|
||||
2. Uses a different route from production auth
|
||||
3. Clearly indicates development mode
|
||||
4. Requires explicit opt-in via environment variable
|
||||
5. Logs prominent warnings when active
|
||||
6. Cannot coexist with production configuration
|
||||
|
||||
### Implementation Design
|
||||
|
||||
#### Configuration
|
||||
|
||||
```bash
|
||||
# Development mode (mutually exclusive)
|
||||
DEV_MODE=true
|
||||
DEV_ADMIN_ME=https://yoursite.com # Identity to simulate
|
||||
|
||||
# Production mode
|
||||
DEV_MODE=false # or unset
|
||||
ADMIN_ME=https://yoursite.com
|
||||
SITE_URL=https://production.example.com
|
||||
```
|
||||
|
||||
#### Route Structure
|
||||
|
||||
```python
|
||||
# Production authentication (always available)
|
||||
GET /admin/login # IndieLogin flow
|
||||
POST /admin/login # Initiate IndieLogin
|
||||
GET /auth/callback # IndieLogin callback
|
||||
POST /admin/logout # Logout
|
||||
|
||||
# Development authentication (DEV_MODE only)
|
||||
GET /dev/login # Development login form
|
||||
POST /dev/login # Instant login (no external service)
|
||||
```
|
||||
|
||||
#### Dev Auth Flow
|
||||
|
||||
```python
|
||||
# /dev/login (GET)
|
||||
def dev_login_form():
|
||||
# Check DEV_MODE is enabled
|
||||
if not current_app.config.get('DEV_MODE'):
|
||||
abort(404) # Route doesn't exist in production
|
||||
|
||||
# Render simple form or auto-login
|
||||
return render_template('dev/login.html')
|
||||
|
||||
# /dev/login (POST)
|
||||
def dev_login():
|
||||
# Check DEV_MODE is enabled
|
||||
if not current_app.config.get('DEV_MODE'):
|
||||
abort(404)
|
||||
|
||||
# Get configured dev admin identity
|
||||
me = current_app.config.get('DEV_ADMIN_ME')
|
||||
|
||||
# Create session directly (bypass IndieLogin)
|
||||
session_token = create_session(me)
|
||||
|
||||
# Log warning
|
||||
current_app.logger.warning(
|
||||
f"DEV MODE: Created session for {me} without authentication"
|
||||
)
|
||||
|
||||
# Set cookie and redirect
|
||||
response = redirect('/admin')
|
||||
response.set_cookie('session', session_token,
|
||||
httponly=True, secure=False)
|
||||
return response
|
||||
```
|
||||
|
||||
#### Safeguards
|
||||
|
||||
**1. Route Registration Protection**
|
||||
```python
|
||||
# In app.py or routes module
|
||||
def register_routes(app):
|
||||
# Always register production routes
|
||||
register_production_auth_routes(app)
|
||||
|
||||
# Only register dev routes if DEV_MODE enabled
|
||||
if app.config.get('DEV_MODE'):
|
||||
app.logger.warning(
|
||||
"=" * 60 + "\n"
|
||||
"WARNING: Development authentication enabled!\n"
|
||||
"This should NEVER be used in production.\n"
|
||||
"Set DEV_MODE=false for production deployments.\n" +
|
||||
"=" * 60
|
||||
)
|
||||
register_dev_auth_routes(app)
|
||||
```
|
||||
|
||||
**2. Configuration Validation**
|
||||
```python
|
||||
def validate_config(app):
|
||||
dev_mode = app.config.get('DEV_MODE', False)
|
||||
|
||||
if dev_mode:
|
||||
# Require DEV_ADMIN_ME
|
||||
if not app.config.get('DEV_ADMIN_ME'):
|
||||
raise ConfigError("DEV_MODE requires DEV_ADMIN_ME")
|
||||
|
||||
# Prevent production config in dev mode
|
||||
if app.config.get('SITE_URL', '').startswith('https://'):
|
||||
app.logger.error(
|
||||
"WARNING: DEV_MODE with production SITE_URL detected"
|
||||
)
|
||||
else:
|
||||
# Require production config
|
||||
if not app.config.get('ADMIN_ME'):
|
||||
raise ConfigError("Production mode requires ADMIN_ME")
|
||||
```
|
||||
|
||||
**3. Visual Indicators**
|
||||
```html
|
||||
<!-- base.html template -->
|
||||
{% if config.DEV_MODE %}
|
||||
<div style="background: red; color: white; padding: 10px; text-align: center;">
|
||||
⚠️ DEVELOPMENT MODE - Authentication bypassed
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**4. Test Detection**
|
||||
```python
|
||||
# In tests/conftest.py
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = create_app()
|
||||
app.config['DEV_MODE'] = True
|
||||
app.config['DEV_ADMIN_ME'] = 'https://test.example.com'
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
```
|
||||
|
||||
### File Organization
|
||||
|
||||
```
|
||||
starpunk/
|
||||
├── auth.py # Production auth functions (unchanged)
|
||||
├── dev_auth.py # Development auth functions (new)
|
||||
└── routes/
|
||||
├── auth.py # Production auth routes
|
||||
└── dev_auth.py # Dev auth routes (conditional registration)
|
||||
|
||||
templates/
|
||||
└── dev/
|
||||
└── login.html # Simple dev login form
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Implement Dev Auth?
|
||||
|
||||
**Development Velocity**: 10/10
|
||||
- Test protected routes instantly
|
||||
- No deployment required for auth testing
|
||||
- Faster iteration cycle
|
||||
- Enable offline development
|
||||
- Simplify CI/CD testing
|
||||
|
||||
**Developer Experience**: 10/10
|
||||
- Remove friction from local development
|
||||
- Make onboarding easier
|
||||
- Enable rapid prototyping
|
||||
- Reduce cognitive load
|
||||
|
||||
**Testing Benefits**: 10/10
|
||||
- Test auth flows without network
|
||||
- Deterministic test behavior
|
||||
- Faster test execution
|
||||
- Enable integration tests
|
||||
- Mock external dependencies
|
||||
|
||||
### Why This Specific Approach?
|
||||
|
||||
**Separate Routes** (vs modifying production routes):
|
||||
- Clear separation of concerns
|
||||
- No conditional logic in production code
|
||||
- Easy to audit security
|
||||
- Impossible to accidentally enable in production
|
||||
|
||||
**Explicit DEV_MODE** (vs detecting localhost):
|
||||
- Explicit is better than implicit
|
||||
- Prevents accidental activation
|
||||
- Clear intent in configuration
|
||||
- Works in any environment
|
||||
|
||||
**Separate Configuration Variables** (vs reusing ADMIN_ME):
|
||||
- Prevents production config confusion
|
||||
- Makes dev mode obvious
|
||||
- Enables validation logic
|
||||
- Clear intent
|
||||
|
||||
**Module Separation** (vs mixing in auth.py):
|
||||
- Production auth code stays clean
|
||||
- Easy to review for security
|
||||
- Can exclude from production builds
|
||||
- Clear architectural boundary
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Faster Development** - Test auth flows without deployment
|
||||
2. **Better Testing** - Comprehensive test coverage possible
|
||||
3. **Offline Development** - No network dependency
|
||||
4. **Simpler Onboarding** - New developers can start immediately
|
||||
5. **CI/CD Friendly** - Tests run without external services
|
||||
6. **Clear Separation** - Dev code isolated from production
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Additional Code** - ~100 lines of dev-specific code
|
||||
2. **Maintenance Burden** - Another code path to maintain
|
||||
3. **Potential Misuse** - Could be accidentally enabled
|
||||
4. **Security Risk** - If misconfigured, creates vulnerability
|
||||
|
||||
### Mitigations
|
||||
|
||||
**For Accidental Activation**:
|
||||
- Startup warnings if DEV_MODE enabled
|
||||
- Configuration validation
|
||||
- Visual indicators in UI
|
||||
- Documentation emphasizing risk
|
||||
|
||||
**For Security**:
|
||||
- Separate routes (not modifying production)
|
||||
- Explicit configuration required
|
||||
- 404 if DEV_MODE disabled
|
||||
- Logging all dev auth usage
|
||||
- Code review checklist
|
||||
|
||||
**For Maintenance**:
|
||||
- Keep dev auth code simple
|
||||
- Document clearly
|
||||
- Include in test coverage
|
||||
- Regular security audits
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. No Dev Auth - Always Use IndieLogin (Rejected)
|
||||
|
||||
**Approach**: Require deployment for auth testing
|
||||
|
||||
**Pros**:
|
||||
- No security risk
|
||||
- No additional code
|
||||
- Forces realistic testing
|
||||
|
||||
**Cons**:
|
||||
- Slow development cycle
|
||||
- Cannot test offline
|
||||
- Requires deployment infrastructure
|
||||
- Painful onboarding
|
||||
|
||||
**Verdict**: Rejected - Too much friction for development
|
||||
|
||||
---
|
||||
|
||||
### 2. Mock IndieLogin in Tests Only (Rejected)
|
||||
|
||||
**Approach**: Mock httpx responses in tests, no dev mode
|
||||
|
||||
**Pros**:
|
||||
- Works for tests
|
||||
- No production risk
|
||||
- Simple implementation
|
||||
|
||||
**Cons**:
|
||||
- Doesn't help manual testing
|
||||
- Cannot test in browser
|
||||
- Doesn't solve local development
|
||||
- Still requires deployment for UI testing
|
||||
|
||||
**Verdict**: Rejected - Solves tests but not development workflow
|
||||
|
||||
---
|
||||
|
||||
### 3. Localhost Detection (Rejected)
|
||||
|
||||
**Approach**: Auto-enable dev auth if running on localhost
|
||||
|
||||
**Pros**:
|
||||
- No configuration needed
|
||||
- Automatic
|
||||
|
||||
**Cons**:
|
||||
- Implicit behavior (dangerous)
|
||||
- Could run production on localhost
|
||||
- Hard to disable
|
||||
- Security through obscurity
|
||||
|
||||
**Verdict**: Rejected - Too implicit, risky
|
||||
|
||||
---
|
||||
|
||||
### 4. Special Password (Rejected)
|
||||
|
||||
**Approach**: Accept a special dev password for local auth
|
||||
|
||||
**Pros**:
|
||||
- Familiar pattern
|
||||
- Easy to implement
|
||||
|
||||
**Cons**:
|
||||
- Password in code or config
|
||||
- Could leak to production
|
||||
- Not IndieWeb-compatible
|
||||
- Defeats purpose of IndieLogin
|
||||
|
||||
**Verdict**: Rejected - Undermines authentication model
|
||||
|
||||
---
|
||||
|
||||
### 5. Self-Hosted IndieAuth Server (Rejected)
|
||||
|
||||
**Approach**: Run local IndieAuth server for development
|
||||
|
||||
**Pros**:
|
||||
- Realistic auth flow
|
||||
- No dev auth code needed
|
||||
- Tests full integration
|
||||
|
||||
**Cons**:
|
||||
- Complex setup
|
||||
- Additional service to run
|
||||
- Doesn't work offline
|
||||
- Violates simplicity principle
|
||||
|
||||
**Verdict**: Rejected - Too complex for V1
|
||||
|
||||
---
|
||||
|
||||
### 6. Session Injection via CLI (Considered)
|
||||
|
||||
**Approach**: Command-line tool to create dev sessions directly in DB
|
||||
|
||||
```bash
|
||||
python -m starpunk dev-login --me https://test.com
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- No web routes needed
|
||||
- Very explicit
|
||||
- Hard to misuse
|
||||
- Clean separation
|
||||
|
||||
**Cons**:
|
||||
- Less convenient than web UI
|
||||
- Doesn't test login flow
|
||||
- Requires DB access
|
||||
- Extra tooling
|
||||
|
||||
**Verdict**: Good alternative, but web route is more ergonomic
|
||||
|
||||
---
|
||||
|
||||
### 7. Separate Dev Auth Endpoint with Token (Considered)
|
||||
|
||||
**Approach**: `/dev/auth?token=SECRET` route with shared secret
|
||||
|
||||
**Pros**:
|
||||
- Prevents accidental use
|
||||
- Simple implementation
|
||||
- Works in browser
|
||||
|
||||
**Cons**:
|
||||
- Secret in URL (logs)
|
||||
- Still a backdoor
|
||||
- Not much better than env var
|
||||
|
||||
**Verdict**: Similar risk profile, less clear
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Dev Auth (Phase 4)
|
||||
- Implement dev_auth.py module
|
||||
- Add DEV_MODE configuration
|
||||
- Create /dev/login routes
|
||||
- Add configuration validation
|
||||
- Update documentation
|
||||
|
||||
### Phase 2: Developer Experience (Phase 4)
|
||||
- Visual dev mode indicators
|
||||
- Startup warnings
|
||||
- Better error messages
|
||||
- Quick-start guide
|
||||
|
||||
### Phase 3: Security Hardening (Before v1.0)
|
||||
- Security audit of dev auth
|
||||
- Penetration testing
|
||||
- Code review checklist
|
||||
- Production deployment guide
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before v1.0 release:
|
||||
|
||||
- [ ] DEV_MODE defaults to false
|
||||
- [ ] Production docs emphasize security
|
||||
- [ ] Deployment guide includes check for DEV_MODE=false
|
||||
- [ ] Startup warnings are prominent
|
||||
- [ ] Routes return 404 when DEV_MODE=false
|
||||
- [ ] No way to enable DEV_MODE in production config
|
||||
- [ ] Security audit completed
|
||||
- [ ] Code review of dev auth implementation
|
||||
- [ ] Test that production build doesn't include dev routes
|
||||
- [ ] Documentation warns about risks
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test dev auth functions in isolation
|
||||
- Test configuration validation
|
||||
- Test route registration logic
|
||||
- Test DEV_MODE toggle behavior
|
||||
|
||||
### Integration Tests
|
||||
- Test full dev auth flow
|
||||
- Test production auth still works
|
||||
- Test DEV_MODE disabled blocks dev routes
|
||||
- Test visual indicators appear
|
||||
|
||||
### Security Tests
|
||||
- Test dev routes return 404 in production mode
|
||||
- Test configuration validation catches mistakes
|
||||
- Test cannot enable with production URL
|
||||
- Test logging captures dev auth usage
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
### Developer Guide
|
||||
- How to enable DEV_MODE for local development
|
||||
- Clear warnings about production use
|
||||
- Explanation of security model
|
||||
- Troubleshooting guide
|
||||
|
||||
### Production Deployment Guide
|
||||
- Checklist to verify DEV_MODE=false
|
||||
- How to validate production configuration
|
||||
- What to check before deployment
|
||||
|
||||
### Security Documentation
|
||||
- Threat model for dev auth
|
||||
- Security trade-offs
|
||||
- Mitigation strategies
|
||||
- Incident response if misconfigured
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Dev auth implementation is successful if:
|
||||
|
||||
1. ✓ Developers can test protected routes locally
|
||||
2. ✓ No production deployment needed for auth testing
|
||||
3. ✓ Tests run without network dependencies
|
||||
4. ✓ DEV_MODE cannot be accidentally enabled in production
|
||||
5. ✓ Clear visual/log indicators when active
|
||||
6. ✓ Production auth code remains unchanged
|
||||
7. ✓ Security audit passes
|
||||
8. ✓ Documentation is comprehensive
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-005: IndieLogin Authentication](/home/phil/Projects/starpunk/docs/decisions/ADR-005-indielogin-authentication.md)
|
||||
- [ADR-010: Authentication Module Design](/home/phil/Projects/starpunk/docs/decisions/ADR-010-authentication-module-design.md)
|
||||
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||
- [The Twelve-Factor App - Dev/Prod Parity](https://12factor.net/dev-prod-parity)
|
||||
|
||||
---
|
||||
|
||||
**ADR**: 011
|
||||
**Date**: 2025-11-18
|
||||
**Status**: Accepted
|
||||
**Decision**: Implement environment-based development authentication with strict safeguards
|
||||
**Impact**: Development workflow, testing, security architecture
|
||||
299
docs/decisions/ADR-012-http-error-handling-policy.md
Normal file
299
docs/decisions/ADR-012-http-error-handling-policy.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# ADR-012: HTTP Error Handling Policy
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
During Phase 4 (Web Interface) implementation, a test failure revealed inconsistent error handling between GET and POST routes when accessing nonexistent resources:
|
||||
|
||||
- `GET /admin/edit/99999` returns HTTP 404 (correct)
|
||||
- `POST /admin/edit/99999` returns HTTP 302 redirect (incorrect)
|
||||
|
||||
This inconsistency creates several problems:
|
||||
|
||||
1. **Semantic confusion**: HTTP 404 means "Not Found", but we were redirecting instead
|
||||
2. **API incompatibility**: Future Micropub API implementation requires proper HTTP status codes
|
||||
3. **Debugging difficulty**: Hard to distinguish between "note doesn't exist" and "operation failed"
|
||||
4. **Test suite inconsistency**: Tests expect 404, implementation returns 302
|
||||
|
||||
### Traditional Web App Pattern
|
||||
|
||||
Many traditional web applications use:
|
||||
- **404 for GET**: Can't render a form for nonexistent resource
|
||||
- **302 redirect for POST**: Show user-friendly error message via flash
|
||||
|
||||
This provides good UX but sacrifices HTTP semantic correctness.
|
||||
|
||||
### REST/API Pattern
|
||||
|
||||
REST APIs consistently use:
|
||||
- **404 for all operations** on nonexistent resources
|
||||
- Applies to GET, POST, PUT, DELETE, etc.
|
||||
|
||||
This provides semantic correctness and API compatibility.
|
||||
|
||||
### StarPunk's Requirements
|
||||
|
||||
1. Human-facing web interface (Phase 4)
|
||||
2. Future Micropub API endpoint (Phase 5)
|
||||
3. Single-user system (simpler error handling needs)
|
||||
4. Standards compliance (IndieWeb specs require proper HTTP)
|
||||
|
||||
## Decision
|
||||
|
||||
**StarPunk will use REST-style error handling for all routes**, returning HTTP 404 for any operation on a nonexistent resource, regardless of HTTP method.
|
||||
|
||||
### Specific Rules
|
||||
|
||||
1. **All routes MUST return 404** when the target resource does not exist
|
||||
2. **All routes SHOULD check resource existence** before processing the request
|
||||
3. **404 responses MAY include user-friendly flash messages** for web routes
|
||||
4. **404 responses MAY redirect** to a safe location (e.g., dashboard) while still returning 404 status
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```python
|
||||
@bp.route("/operation/<int:resource_id>", methods=["GET", "POST"])
|
||||
@require_auth
|
||||
def operation(resource_id: int):
|
||||
# 1. CHECK EXISTENCE FIRST
|
||||
resource = get_resource(id=resource_id)
|
||||
if not resource:
|
||||
flash("Resource not found", "error")
|
||||
return redirect(url_for("admin.dashboard")), 404 # ← MUST return 404
|
||||
|
||||
# 2. VALIDATE INPUT (for POST/PUT)
|
||||
# ...
|
||||
|
||||
# 3. PERFORM OPERATION
|
||||
# ...
|
||||
|
||||
# 4. RETURN SUCCESS
|
||||
# ...
|
||||
```
|
||||
|
||||
### Status Code Policy
|
||||
|
||||
| Scenario | Status Code | Response Type | Flash Message |
|
||||
|----------|-------------|---------------|---------------|
|
||||
| Resource not found | 404 | Redirect to dashboard | "Resource not found" |
|
||||
| Validation failed | 302 | Redirect to form | "Invalid data: {details}" |
|
||||
| Operation succeeded | 302 | Redirect to dashboard | "Success: {details}" |
|
||||
| System error | 500 | Error page | "System error occurred" |
|
||||
| Unauthorized | 302 | Redirect to login | "Authentication required" |
|
||||
|
||||
### Flask Pattern for 404 with Redirect
|
||||
|
||||
Flask allows returning a tuple `(response, status_code)`:
|
||||
|
||||
```python
|
||||
return redirect(url_for("admin.dashboard")), 404
|
||||
```
|
||||
|
||||
This sends:
|
||||
- HTTP 404 status code
|
||||
- Location header pointing to dashboard
|
||||
- Flash message in session
|
||||
|
||||
The client receives 404 but can follow the redirect to see the error message.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why REST-Style Over Web-Form-Style?
|
||||
|
||||
1. **Future API Compatibility**: Micropub (Phase 5) requires proper HTTP semantics
|
||||
2. **Standards Compliance**: IndieWeb specs expect REST-like behavior
|
||||
3. **Semantic Correctness**: 404 means "not found" - this is universally understood
|
||||
4. **Consistency**: Simpler mental model - all operations follow same rules
|
||||
5. **Debugging**: Clear distinction between error types
|
||||
6. **Test Intent**: Test suite already expects this behavior
|
||||
|
||||
### UX Considerations
|
||||
|
||||
**Concern**: Won't users see ugly error pages?
|
||||
|
||||
**Mitigation**:
|
||||
1. Flash messages provide context ("Note not found")
|
||||
2. 404 response includes redirect to dashboard
|
||||
3. Can implement custom 404 error handler with navigation
|
||||
4. Single-user system = developer is the user (understands HTTP)
|
||||
|
||||
### Comparison to Delete Operation
|
||||
|
||||
The `delete_note()` function is idempotent - it succeeds even if the note doesn't exist. This is correct for delete operations (common REST pattern). However, the route should still check existence and return 404 for consistency:
|
||||
|
||||
- Idempotent implementation: Good (delete succeeds either way)
|
||||
- Explicit existence check in route: Better (clear 404 for user)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Consistent behavior** across all routes (GET, POST, DELETE)
|
||||
2. **API-ready**: Micropub implementation will work correctly
|
||||
3. **Standards compliance**: Meets IndieWeb/REST expectations
|
||||
4. **Better testing**: Status codes clearly indicate error types
|
||||
5. **Clearer debugging**: Know immediately if resource doesn't exist
|
||||
6. **Simpler code**: One pattern to follow everywhere
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Requires existence checks**: Every route must check before operating
|
||||
2. **Slight performance cost**: Extra database query per request (minimal for SQLite)
|
||||
3. **Different from some web apps**: Traditional web apps often use redirects for all POST errors
|
||||
|
||||
### Neutral
|
||||
|
||||
1. **Custom 404 handler needed**: For good UX (but we'd want this anyway)
|
||||
2. **Test suite updates**: Some tests may need adjustment (but most already expect 404)
|
||||
3. **Documentation**: Need to document this pattern (but good practice anyway)
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Immediate (Phase 4 Fix)
|
||||
|
||||
- [ ] Fix `POST /admin/edit/<id>` to return 404 for nonexistent notes
|
||||
- [ ] Verify `GET /admin/edit/<id>` still returns 404 (already correct)
|
||||
- [ ] Update `POST /admin/delete/<id>` to return 404 (optional, but recommended)
|
||||
- [ ] Update test `test_delete_nonexistent_note_shows_error` if delete route changed
|
||||
|
||||
### Future (Phase 4 Completion)
|
||||
|
||||
- [ ] Create custom 404 error handler with navigation
|
||||
- [ ] Document pattern in `/home/phil/Projects/starpunk/docs/standards/http-error-handling.md`
|
||||
- [ ] Review all routes for consistency
|
||||
- [ ] Add error handling section to coding standards
|
||||
|
||||
### Phase 5 (Micropub API)
|
||||
|
||||
- [ ] Verify Micropub routes follow this pattern
|
||||
- [ ] Return JSON error responses for API routes
|
||||
- [ ] Maintain 404 status codes for missing resources
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Example: Edit Note Form (GET)
|
||||
|
||||
```python
|
||||
@bp.route("/edit/<int:note_id>", methods=["GET"])
|
||||
@require_auth
|
||||
def edit_note_form(note_id: int):
|
||||
note = get_note(id=note_id)
|
||||
|
||||
if not note:
|
||||
flash("Note not found", "error")
|
||||
return redirect(url_for("admin.dashboard")), 404 # ✓ CORRECT
|
||||
|
||||
return render_template("admin/edit.html", note=note)
|
||||
```
|
||||
|
||||
**Status**: Currently implemented correctly
|
||||
|
||||
### Bad Example: Update Note (POST) - Before Fix
|
||||
|
||||
```python
|
||||
@bp.route("/edit/<int:note_id>", methods=["POST"])
|
||||
@require_auth
|
||||
def update_note_submit(note_id: int):
|
||||
# ✗ NO EXISTENCE CHECK
|
||||
|
||||
try:
|
||||
note = update_note(id=note_id, content=content, published=published)
|
||||
# ...
|
||||
except Exception as e:
|
||||
flash(f"Error: {e}", "error")
|
||||
return redirect(url_for("admin.edit_note_form", note_id=note_id)) # ✗ Returns 302
|
||||
```
|
||||
|
||||
**Problem**: Returns 302 redirect, not 404
|
||||
|
||||
### Good Example: Update Note (POST) - After Fix
|
||||
|
||||
```python
|
||||
@bp.route("/edit/<int:note_id>", methods=["POST"])
|
||||
@require_auth
|
||||
def update_note_submit(note_id: int):
|
||||
# ✓ CHECK EXISTENCE FIRST
|
||||
existing_note = get_note(id=note_id, load_content=False)
|
||||
if not existing_note:
|
||||
flash("Note not found", "error")
|
||||
return redirect(url_for("admin.dashboard")), 404 # ✓ CORRECT
|
||||
|
||||
# Process the update
|
||||
# ...
|
||||
```
|
||||
|
||||
**Status**: Needs implementation
|
||||
|
||||
## References
|
||||
|
||||
- Test failure: `test_update_nonexistent_note_404` in `tests/test_routes_admin.py:386`
|
||||
- Architectural review: `/home/phil/Projects/starpunk/docs/reviews/error-handling-rest-vs-web-patterns.md`
|
||||
- RFC 7231 Section 6.5.4 (404 Not Found): https://tools.ietf.org/html/rfc7231#section-6.5.4
|
||||
- IndieWeb Micropub spec: https://micropub.spec.indieweb.org/
|
||||
- Flask documentation on status codes: https://flask.palletsprojects.com/en/latest/quickstart/#about-responses
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: Web-Form Pattern (Redirect All POST Errors)
|
||||
|
||||
**Rejected** because:
|
||||
- Breaks semantic HTTP (404 means "not found")
|
||||
- Incompatible with future Micropub API
|
||||
- Makes debugging harder (can't distinguish error types by status code)
|
||||
- Test suite already expects 404
|
||||
|
||||
### Alternative 2: Hybrid Approach (404 for API, 302 for Web)
|
||||
|
||||
**Rejected** because:
|
||||
- Adds complexity (need to detect context)
|
||||
- Inconsistent behavior confuses developers
|
||||
- Same route may serve both web and API clients
|
||||
- Flask blueprint structure makes this awkward
|
||||
|
||||
### Alternative 3: Exception-Based (Let Exceptions Propagate to Error Handler)
|
||||
|
||||
**Rejected** because:
|
||||
- Less explicit (harder to understand flow)
|
||||
- Error handlers are global (less flexibility per route)
|
||||
- Flash messages harder to customize per route
|
||||
- Lose ability to redirect to different locations per route
|
||||
|
||||
## Notes
|
||||
|
||||
### Performance Consideration
|
||||
|
||||
The existence check adds one database query per request:
|
||||
|
||||
```python
|
||||
existing_note = get_note(id=note_id, load_content=False) # SELECT query
|
||||
```
|
||||
|
||||
With `load_content=False`, this is just a metadata query (no file I/O):
|
||||
- SQLite query: ~0.1ms for indexed lookup
|
||||
- Negligible overhead for single-user system
|
||||
- Could be optimized later if needed (caching, etc.)
|
||||
|
||||
### Future Enhancement: Error Handler
|
||||
|
||||
Custom 404 error handler can improve UX:
|
||||
|
||||
```python
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
"""Custom 404 page with navigation"""
|
||||
# Check if there's a flash message (from routes)
|
||||
# Render custom template with link to dashboard
|
||||
# Or redirect to dashboard for admin routes
|
||||
return render_template('errors/404.html'), 404
|
||||
```
|
||||
|
||||
This is optional but recommended for Phase 4 completion.
|
||||
|
||||
## Revision History
|
||||
|
||||
- 2025-11-18: Initial decision (v0.4.0 development)
|
||||
- Status: Accepted
|
||||
- Supersedes: None
|
||||
- Related: ADR-003 (Frontend Technology), Phase 4 Design
|
||||
383
docs/decisions/ADR-013-expose-deleted-at-in-note-model.md
Normal file
383
docs/decisions/ADR-013-expose-deleted-at-in-note-model.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# ADR-013: Expose deleted_at Field in Note Model
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The StarPunk application implements soft deletion for notes, using a `deleted_at` timestamp in the database to mark notes as deleted without physically removing them. However, there is a **model-schema mismatch**: the `deleted_at` column exists in the database schema but is not exposed as a field in the `Note` dataclass.
|
||||
|
||||
### Current State
|
||||
|
||||
**Database Schema** (`starpunk/database.py`):
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
file_path TEXT UNIQUE NOT NULL,
|
||||
published BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP, -- Column exists
|
||||
content_hash TEXT
|
||||
);
|
||||
```
|
||||
|
||||
**Note Model** (`starpunk/models.py`):
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Note:
|
||||
# Core fields from database
|
||||
id: int
|
||||
slug: str
|
||||
file_path: str
|
||||
published: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
# deleted_at: MISSING
|
||||
```
|
||||
|
||||
**Notes Module** (`starpunk/notes.py`):
|
||||
- Uses `deleted_at` in queries (`WHERE deleted_at IS NULL`)
|
||||
- Sets `deleted_at` during soft deletion (`UPDATE notes SET deleted_at = ?`)
|
||||
- Never exposes the value through the model layer
|
||||
|
||||
### Problem
|
||||
|
||||
This architecture creates several issues:
|
||||
|
||||
1. **Testability Gap**: Tests cannot verify soft-deletion status because `note.deleted_at` doesn't exist
|
||||
2. **Information Hiding**: The model hides database state from consumers
|
||||
3. **Principle Violation**: Data models should faithfully represent database schema
|
||||
4. **Future Limitations**: Admin UIs, debugging tools, and backup utilities cannot access deletion timestamps
|
||||
|
||||
### Immediate Trigger
|
||||
|
||||
Test `test_delete_without_confirmation_cancels` fails with:
|
||||
```
|
||||
AttributeError: 'Note' object has no attribute 'deleted_at'
|
||||
```
|
||||
|
||||
The test attempts to verify that a cancelled deletion does NOT set `deleted_at`:
|
||||
```python
|
||||
note = get_note(id=note_id)
|
||||
assert note is not None
|
||||
assert note.deleted_at is None # ← Fails here
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
**We will add `deleted_at: Optional[datetime]` as a field in the Note dataclass.**
|
||||
|
||||
The field will be:
|
||||
- **Nullable**: `Optional[datetime] = None`
|
||||
- **Extracted** from database rows in `Note.from_row()`
|
||||
- **Documented** in the Note docstring
|
||||
- **Optionally serialized** in `Note.to_dict()` when present
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Add the Field
|
||||
|
||||
1. **Transparency Over Encapsulation**
|
||||
- For data models, transparency should win
|
||||
- Developers expect to access any database column through the model
|
||||
- Hiding fields creates semantic mismatches
|
||||
|
||||
2. **Testability**
|
||||
- Tests must be able to verify soft-deletion behavior
|
||||
- Current design makes deletion status verification impossible
|
||||
- Exposing the field enables proper test coverage
|
||||
|
||||
3. **Principle of Least Surprise**
|
||||
- If a database column exists, it should be accessible
|
||||
- Other models (Session, Token, AuthState) expose all their fields
|
||||
- Consistency across the codebase
|
||||
|
||||
4. **Future Flexibility**
|
||||
- Admin interfaces may need to show when notes were deleted
|
||||
- Data export/backup tools need complete state
|
||||
- Debugging requires visibility into deletion status
|
||||
|
||||
5. **Low Complexity Cost**
|
||||
- Adding one optional field is minimal complexity
|
||||
- No performance impact (no additional queries)
|
||||
- Backwards compatible (existing code won't break)
|
||||
|
||||
### Why NOT Use Alternative Approaches
|
||||
|
||||
**Alternative 1: Fix the Test Only**
|
||||
- Weakens test coverage (can't verify deletion status)
|
||||
- Doesn't solve root problem (future code will hit same issue)
|
||||
- Rejected
|
||||
|
||||
**Alternative 2: Add Helper Property (`is_deleted`)**
|
||||
- Loses information (can't see deletion timestamp)
|
||||
- Adds complexity (two fields instead of one)
|
||||
- Inconsistent with other models
|
||||
- Rejected
|
||||
|
||||
**Alternative 3: Separate Model Class for Deleted Notes**
|
||||
- Massive complexity increase
|
||||
- Violates simplicity principle
|
||||
- Breaks existing code
|
||||
- Rejected
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
1. **Test Suite Passes**: `test_delete_without_confirmation_cancels` will pass
|
||||
2. **Complete Model**: Note model accurately reflects database schema
|
||||
3. **Better Testability**: All tests can verify soft-deletion state
|
||||
4. **Future-Proof**: Admin UIs and debugging tools have access to deletion data
|
||||
5. **Consistency**: All models expose their database fields
|
||||
|
||||
### Negative Consequences
|
||||
|
||||
1. **Loss of Encapsulation**: Consumers now see `deleted_at` and must understand soft deletion
|
||||
- **Mitigation**: Document the field clearly in docstring
|
||||
- **Impact**: Minimal - developers working with notes should understand deletion
|
||||
|
||||
2. **Slight Complexity Increase**: Model has one more field
|
||||
- **Impact**: One line of code, negligible complexity
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**None** - The field is optional and nullable, so:
|
||||
- Existing code that doesn't use `deleted_at` continues to work
|
||||
- `Note.from_row()` sets it to `None` for active notes
|
||||
- Serialization is optional
|
||||
|
||||
## Implementation Guidance
|
||||
|
||||
### File: `starpunk/models.py`
|
||||
|
||||
#### Change 1: Add Field to Dataclass
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Note:
|
||||
"""Represents a note/post"""
|
||||
|
||||
# Core fields from database
|
||||
id: int
|
||||
slug: str
|
||||
file_path: str
|
||||
published: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: Optional[datetime] = None # ← ADD THIS LINE
|
||||
|
||||
# Internal fields (not from database)
|
||||
_data_dir: Path = field(repr=False, compare=False)
|
||||
|
||||
# Optional fields
|
||||
content_hash: Optional[str] = None
|
||||
```
|
||||
|
||||
#### Change 2: Update from_row() Method
|
||||
|
||||
Add timestamp conversion for `deleted_at`:
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def from_row(cls, row: sqlite3.Row | dict[str, Any], data_dir: Path) -> "Note":
|
||||
# ... existing code ...
|
||||
|
||||
# Convert timestamps if they are strings
|
||||
created_at = data["created_at"]
|
||||
if isinstance(created_at, str):
|
||||
created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
||||
|
||||
updated_at = data["updated_at"]
|
||||
if isinstance(updated_at, str):
|
||||
updated_at = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
||||
|
||||
# ← ADD THIS BLOCK
|
||||
deleted_at = data.get("deleted_at")
|
||||
if deleted_at and isinstance(deleted_at, str):
|
||||
deleted_at = datetime.fromisoformat(deleted_at.replace("Z", "+00:00"))
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
slug=data["slug"],
|
||||
file_path=data["file_path"],
|
||||
published=bool(data["published"]),
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
deleted_at=deleted_at, # ← ADD THIS LINE
|
||||
_data_dir=data_dir,
|
||||
content_hash=data.get("content_hash"),
|
||||
)
|
||||
```
|
||||
|
||||
#### Change 3: Update Docstring
|
||||
|
||||
Add documentation for `deleted_at`:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Note:
|
||||
"""
|
||||
Represents a note/post
|
||||
|
||||
Attributes:
|
||||
id: Database ID (primary key)
|
||||
slug: URL-safe slug (unique)
|
||||
file_path: Path to markdown file (relative to data directory)
|
||||
published: Whether note is published (visible publicly)
|
||||
created_at: Creation timestamp (UTC)
|
||||
updated_at: Last update timestamp (UTC)
|
||||
deleted_at: Soft deletion timestamp (UTC, None if not deleted) # ← ADD THIS LINE
|
||||
content_hash: SHA-256 hash of content (for integrity checking)
|
||||
# ... rest of docstring ...
|
||||
"""
|
||||
```
|
||||
|
||||
#### Change 4 (Optional): Update to_dict() Method
|
||||
|
||||
Add `deleted_at` to serialization when present:
|
||||
|
||||
```python
|
||||
def to_dict(
|
||||
self, include_content: bool = False, include_html: bool = False
|
||||
) -> dict[str, Any]:
|
||||
data = {
|
||||
"id": self.id,
|
||||
"slug": self.slug,
|
||||
"title": self.title,
|
||||
"published": self.published,
|
||||
"created_at": self.created_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"updated_at": self.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"permalink": self.permalink,
|
||||
"excerpt": self.excerpt,
|
||||
}
|
||||
|
||||
# ← ADD THIS BLOCK (optional)
|
||||
if self.deleted_at is not None:
|
||||
data["deleted_at"] = self.deleted_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
if include_content:
|
||||
data["content"] = self.content
|
||||
|
||||
if include_html:
|
||||
data["html"] = self.html
|
||||
|
||||
return data
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
#### Verification Steps
|
||||
|
||||
1. **Run Failing Test**:
|
||||
```bash
|
||||
uv run pytest tests/test_routes_admin.py::TestDeleteRoute::test_delete_without_confirmation_cancels -v
|
||||
```
|
||||
Should pass after changes.
|
||||
|
||||
2. **Run Full Test Suite**:
|
||||
```bash
|
||||
uv run pytest
|
||||
```
|
||||
Should pass with no regressions.
|
||||
|
||||
3. **Manual Verification**:
|
||||
```python
|
||||
# Active note should have deleted_at = None
|
||||
note = get_note(slug="active-note")
|
||||
assert note.deleted_at is None
|
||||
|
||||
# Soft-deleted note should have deleted_at set
|
||||
delete_note(slug="test-note", soft=True)
|
||||
# Note: get_note() filters out soft-deleted notes
|
||||
# To verify, query database directly or use admin interface
|
||||
```
|
||||
|
||||
#### Expected Test Coverage
|
||||
|
||||
- `deleted_at` is `None` for active notes
|
||||
- `deleted_at` is `None` for newly created notes
|
||||
- `deleted_at` is set after soft deletion (verify via database query)
|
||||
- `get_note()` returns `None` for soft-deleted notes (existing behavior)
|
||||
- `list_notes()` excludes soft-deleted notes (existing behavior)
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [ ] `deleted_at` field added to Note dataclass
|
||||
- [ ] `from_row()` extracts and parses `deleted_at` from database rows
|
||||
- [ ] `from_row()` handles `deleted_at` as ISO string
|
||||
- [ ] `from_row()` handles `deleted_at` as None (active notes)
|
||||
- [ ] Docstring updated to document `deleted_at`
|
||||
- [ ] Test `test_delete_without_confirmation_cancels` passes
|
||||
- [ ] Full test suite passes with no regressions
|
||||
- [ ] Optional: `to_dict()` includes `deleted_at` when present
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Update Test to Remove deleted_at Check
|
||||
|
||||
**Approach**: Modify test to not verify deletion status
|
||||
|
||||
**Pros**:
|
||||
- One line change
|
||||
- Maintains current encapsulation
|
||||
|
||||
**Cons**:
|
||||
- Weakens test coverage
|
||||
- Doesn't solve root problem
|
||||
- Violates test intent
|
||||
|
||||
**Decision**: Rejected - Band-aid solution
|
||||
|
||||
### 2. Add Helper Property Instead of Raw Field
|
||||
|
||||
**Approach**: Expose `is_deleted` boolean property, hide timestamp
|
||||
|
||||
**Pros**:
|
||||
- Encapsulates implementation
|
||||
- Simple boolean interface
|
||||
|
||||
**Cons**:
|
||||
- Loses deletion timestamp information
|
||||
- Inconsistent with other models
|
||||
- More complex than exposing field directly
|
||||
|
||||
**Decision**: Rejected - Adds complexity without clear benefit
|
||||
|
||||
### 3. Create Separate SoftDeletedNote Model
|
||||
|
||||
**Approach**: Use different classes for active vs deleted notes
|
||||
|
||||
**Pros**:
|
||||
- Type safety
|
||||
- Clear separation
|
||||
|
||||
**Cons**:
|
||||
- Massive complexity increase
|
||||
- Violates simplicity principle
|
||||
- Breaks existing code
|
||||
|
||||
**Decision**: Rejected - Over-engineered for V1
|
||||
|
||||
## References
|
||||
|
||||
- **Test Failure Analysis**: `/home/phil/Projects/starpunk/docs/reports/test-failure-analysis-deleted-at-attribute.md`
|
||||
- **Database Schema**: `starpunk/database.py:11-27`
|
||||
- **Note Model**: `starpunk/models.py:44-440`
|
||||
- **Notes Module**: `starpunk/notes.py:685-849`
|
||||
- **Failing Test**: `tests/test_routes_admin.py:435-441`
|
||||
- **ADR-004**: File-Based Note Storage (discusses soft deletion design)
|
||||
|
||||
## Related Standards
|
||||
|
||||
- **Data Model Design**: Models should faithfully represent database schema
|
||||
- **Testability Principle**: All business logic must be testable
|
||||
- **Principle of Least Surprise**: Developers expect database columns to be accessible
|
||||
- **Transparency vs Encapsulation**: For data models, transparency wins
|
||||
|
||||
---
|
||||
|
||||
**Date**: 2025-11-18
|
||||
**Author**: StarPunk Architect Agent
|
||||
**Status**: Accepted
|
||||
Reference in New Issue
Block a user